구글시트 체크인 GPS 거리 검증: 현장 밖 체크인 차단하기
도입 – 현장 밖에서 눌러 버리는 체크인, 어떻게 막을까
구글시트 체크인 GPS 거리 검증을 찾는 분들은 대부분 같은 고민을 갖고 있습니다. 기사나 외주 인력이 창고에 도착하기도 전에, 집이나 도로 위에서 미리 체크인 버튼을 눌러 버리는 상황을 막고 싶어 합니다. 시트에는 도착 시각처럼 보이는 체크인 시간이 찍히지만 실제 차량은 아직 오지 않아, 현장 운영과 KPI가 어긋나기 때문입니다.
앞 글인 구글시트 컨테이너 번호 체크인: 기사 도착 예약 조회 만들기에서는 구글 Apps Script로 체크인 웹앱 화면을 만들고, 컨테이너 번호로 예약 정보를 불러오는 부분까지 구현했습니다. 이번 글에서는 여기에 “구글시트 GPS 체크인 방법”을 얹어, 정해 둔 반경 밖에서는 체크인이 아예 저장되지 않도록 차단하는 로직을 완성합니다. 핵심은 기준 좌표와 허용 반경을 설정하고, Apps Script에서 하버사인으로 거리를 계산해 반경 안/밖을 판별하는 구조입니다.
이 글을 따라 하시면, 1) 창고 기준 좌표와 허용 반경(예: 150m)을 설정값으로 관리하고, 2) 기사 휴대폰에서 전송된 GPS 좌표가 이 범위 안에 있을 때만 시트에 체크인을 저장하는 “현장 체크인 위치 검증” 흐름을 구현할 수 있습니다.
체크인 GPS 검증 구조 이해하기
구글시트 현장 체크인 제한을 설계할 때 가장 먼저 정해야 할 것은 “어디에서 검증할 것인가”입니다. 선택지는 보통 두 가지입니다. 브라우저(클라이언트)에서만 거리 검증을 하거나, Apps Script 서버에서 최종 검증을 하는 방식입니다. 운영 경험상 두 번째, 서버 쪽에서 최종 판단을 하는 구조가 낫습니다.
다만 이 방식이 무엇을 막고 무엇을 못 막는지는 처음부터 분명히 해 두는 편이 좋습니다. 서버 검증은 화면 스크립트를 고쳐 검사를 건너뛰는 단순한 우회를 줄여 줍니다. 반경 판정이 서버에 고정돼 있으니, 화면 코드가 망가지거나 조작돼도 저장되는 데이터의 기준은 유지됩니다.
하지만 서버로 올라오는 위도·경도 자체가 브라우저가 보낸 값입니다. 요청을 직접 만들어 창고 좌표를 그대로 실어 보내면 서버는 그것을 진짜 위치와 구분할 수 없습니다. 즉 이 구조는 운영 보조 장치이지, 강한 출입 인증 수단이 아닙니다. 실수나 습관적인 조기 체크인은 상당 부분 걸러 주지만, 작정하고 위조하는 경우까지 막지는 못합니다. 정말 강하게 통제해야 하는 현장이라면 게이트 단말·QR·비콘처럼 사람이 그 자리에 있어야만 얻을 수 있는 신호를 같이 써야 합니다.
이 글에서는 클라이언트가 좌표와 정확도를 읽어 전달하고, 서버의 CHK2_validateCheckinData_() 함수가 최종 판정을 내리는 구조로 설명합니다.
흐름을 정리하면 다음과 같습니다.
1) 브라우저에서 체크인 버튼을 누를 때 navigator.geolocation 으로 위도·경도와 정확도를 읽어 formData.lat·formData.lng·formData.accuracyM(= position.coords.accuracy) 으로 담습니다.
2) 이 객체를 google.script.run.CHK2_checkIn 같은 Apps Script 서버 함수에 넘깁니다.
3) 서버 함수 안에서 CHK2_validateCheckinData_() 를 호출해 컨테이너·예약 상태·GPS까지 모두 검증합니다.
4) CHK2_validateCheckinData_() 는 내부에서 CHK2_distanceMeters_() 로 기준 좌표와의 거리를 계산하고, 허용 반경보다 크면 실패 메시지를 돌려주어 시트 저장을 생략합니다.
5) 반경 안이면 ok: true·정제된 데이터(cleaned)를 반환하고, 저장 함수가 이 값을 사용해 체크인 행을 추가합니다.
이 구조를 한 번 잡아 두면, 나중에 창고를 옮기거나, 허용 반경·GPS 필수 여부 정책을 바꿀 때도 설정 상수만 수정하면 되어 유지보수가 수월합니다.
기준 좌표와 반경 설정 – 체크인 전용 설정 상수 만들기
GPS 검증의 기준이 되는 것은 “어디를 중심으로 얼마까지 허용할 것인가”입니다. 실무에서는 대개 창고 정문, 경비실, 도크 도어 중앙 등 한 지점을 기준점으로 정합니다. 이 좌표와 허용 반경은 소스 곳곳에 흩뿌리기보다, 한 객체에 모아 두고 필요하면 한 군데만 수정할 수 있게 만드는 것이 좋습니다.
이 시리즈의 다른 글 코드와 한 Apps Script 프로젝트에 합쳐 쓰더라도 충돌이 나지 않도록, 이번 편에서 사용하는 상수와 함수에는 CHK2_ 접두어를 붙입니다. 앞 글에서 이미 CHK_ 이름 공간을 쓰고 있기 때문에, 이번 글은 모두 CHK2_ 로 분리해 “Identifier has already been declared” 오류를 원천 차단합니다. 구글시트 Apps Script 프로젝트에 아래 상수를 추가해 두면 “창고 좌표”와 “허용 반경”을 한눈에 관리할 수 있습니다.
1단계 — 체크인 기준 좌표·반경 설정 상수 만들기
- 하는 일: 체크인 허용 기준점의 위도·경도와 허용 반경(미터), GPS 필수 여부를 상수로 정의합니다.
- 붙여넣을 위치: 구글시트 → 확장 프로그램 → Apps Script → 체크인(GPS) 관련 파일 맨 위.
- 붙여넣은 뒤 할 일: 실제 창고 좌표와 정책에 맞게 값들을 수정한 뒤 저장합니다.
const CHK2_CONFIG = { // → 체크인(GPS) 전용 설정 모음
BASE_LAT: 37.123456, // → 창고 기준 위도(여기만 수정)
BASE_LNG: -122.123456, // → 창고 기준 경도(여기만 수정)
MAX_DISTANCE_M: 150, // → 허용 반경(미터 단위)
MAX_ACCURACY_M: 100, // → 이보다 부정확한 좌표는 판정하지 않는다
REQUIRE_GPS: true // → 위치 없으면 거부할지 여부
}; 제대로 됐는지 확인하는 법: 저장 시 에러가 없고, 다른 파일에서 CHK2_CONFIG 를 참조했을 때 자동 완성 목록에 보이면 정상입니다. 기준 좌표는 Google Maps에서 창고 위치를 우클릭해 위도·경도를 복사해 넣으면 됩니다.
하버사인으로 거리 계산 – 두 좌표 사이 거리 구하기
다음으로 필요한 것은 두 GPS 좌표 사이 거리를 계산하는 함수입니다. Apps Script 하버사인 거리 계산을 써서 지구 표면상의 최단 거리를 미터 단위로 구합니다. 이때 한 가지 중요한 점은 “숫자로 변환되지 않는 값이 들어왔을 때 바로 거절할 것”입니다. 그렇지 않으면 NaN 이 조용히 퍼져 검증이 항상 실패하거나, 이상한 값으로 통과하는 문제가 생깁니다.
현장에서 보면 브라우저·단말기 환경에 따라 위도·경도가 숫자형이 아니라 문자열 "37.123456" 형태로 넘어오는 경우가 있습니다. 따라서 단순히 Number.isFinite(lat) 같은 검사만 하면 문자열이 모두 실패 처리됩니다. 이 글에서는 먼저 Number() 로 변환한 뒤, 변환 결과가 유한한 숫자인지를 다시 확인하는 방식을 씁니다.
2단계 — CHK2_distanceMeters_ 함수 만들기
- 하는 일: 두 좌표(위도·경도) 사이의 거리를 미터 단위 정수로 계산합니다.
- 붙여넣을 위치: 같은 Apps Script 파일에서
CHK2_CONFIG바로 아래쪽. - 붙여넣은 뒤 할 일: 아래 테스트 함수를 추가해 실행 로그에서 거리가 정상적으로 찍히는지 확인합니다.
function CHK2_distanceMeters_(lat1Raw, lng1Raw, lat2Raw, lng2Raw) { // → 두 점 거리 계산
const lat1 = Number(lat1Raw); // → 첫 번째 위도 숫자 변환
const lng1 = Number(lng1Raw); // → 첫 번째 경도 숫자 변환
const lat2 = Number(lat2Raw); // → 두 번째 위도 숫자 변환
const lng2 = Number(lng2Raw); // → 두 번째 경도 숫자 변환
if (!Number.isFinite(lat1) || !Number.isFinite(lng1) || // → 변환 후 검증
!Number.isFinite(lat2) || !Number.isFinite(lng2)) { // → 네 값 모두 확인
throw new Error('좌표 값이 올바르지 않습니다'); // → 잘못된 입력 막기
}
if (lat1 < -90 || lat1 > 90 || // → 위도 허용 범위 검사
lat2 < -90 || lat2 > 90 || // → 두 좌표 모두 확인
lng1 < -180 || lng1 > 180 || // → 경도 허용 범위 검사
lng2 < -180 || lng2 > 180) { // → 오타·이상값 차단
throw new Error('좌표 범위가 유효하지 않습니다'); // → 범위 오류 메시지
}
const R = 6371000; // → 지구 반지름(미터)
const toRad = Math.PI / 180; // → 도 → 라디안 변환 상수
const φ1 = lat1 * toRad; // → 첫 번째 위도 라디안
const φ2 = lat2 * toRad; // → 두 번째 위도 라디안
const Δφ = (lat2 - lat1) * toRad; // → 위도 차이 라디안
const Δλ = (lng2 - lng1) * toRad; // → 경도 차이 라디안
const sinHalfDLat = Math.sin(Δφ / 2); // → 위도 절반 사인
const sinHalfDLng = Math.sin(Δλ / 2); // → 경도 절반 사인
const a = sinHalfDLat * sinHalfDLat + // → 하버사인 공식 중간값
Math.cos(φ1) * Math.cos(φ2) * // → 위도 보정 항
sinHalfDLng * sinHalfDLng; // → 경도 부분 제곱
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); // → 중심각 계산
const d = R * c; // → 거리(미터)
return Math.round(d); // → 반올림한 정수 반환
} 제대로 됐는지 확인하는 법: 아래와 같이 테스트 함수를 추가해 실행 후 로그를 보면 됩니다.
function CHK2_testDistance_() { // → 거리 테스트 함수
const baseLat = CHK2_CONFIG.BASE_LAT; // → 기준 위도
const baseLng = CHK2_CONFIG.BASE_LNG; // → 기준 경도
const dist = CHK2_distanceMeters_( // → 거리 계산 실행
baseLat, baseLng, baseLat, baseLng + 0.001 // → 경도 약간 이동
);
Logger.log('거리(m): ' + dist); // → 결과 로그로 출력
} 실행 후 실행 로그에 수십~수백 미터 정도 값이 찍히면 정상입니다. 값 자체는 기준 좌표에 따라 달라집니다.
체크인 데이터 검증에 GPS 거리 제한 붙이기
이제 구글시트 현장 체크인 제한의 핵심인 CHK2_validateCheckinData_() 를 완성합니다. 이 함수는 클라이언트에서 온 입력을 모아 검증하고, 문제 없을 때만 ok: true 와 함께 정제된 데이터를 반환하는 역할을 합니다. 여기서 GPS 좌표는 선택적으로 들어올 수 있다는 점을 고려해, CHK2_CONFIG.REQUIRE_GPS 에 따라 분기 처리를 합니다.
정리하면 다음과 같은 규칙입니다.
REQUIRE_GPS === true인 경우- 위도·경도가 없거나 숫자로 변환되지 않으면 “위치 권한을 허용해야 체크인할 수 있습니다” 메시지로 실패 처리합니다.
- 좌표는 있으나 기준 반경(
MAX_DISTANCE_M) 밖이면 “현장에서만 체크인할 수 있습니다 (기준 반경 Xm)” 메시지로 실패 처리합니다. - 반경 안이면 통과시키고,
cleaned데이터에lat·lng·distanceM를 함께 넣어 이후 저장 로직에서 활용할 수 있게 합니다. REQUIRE_GPS === false인 경우- 좌표가 없으면 GPS 검증을 건너뛰고 다른 필드만 검사합니다.
- 좌표가 들어온 경우에는 거리만 계산해 반경과 함께 기록하되, 반경 밖이라고 해서 체크인을 막지는 않습니다(로그용·전환기 운영에 유용합니다).
3단계 — CHK2_validateCheckinData_ 안에 GPS 검증 완성하기
- 하는 일: 컨테이너·예약 검증 흐름에 GPS 거리 검증을 통합해, 반경 밖이면 체크인을 차단합니다.
- 붙여넣을 위치: 기존 체크인 검증 함수 대신 사용하거나, 기존
validateCheckinData_내용을 이 구조에 옮겨 담습니다. 다른 편 코드와 합칠 때는 함수 이름이 겹치지 않도록 반드시CHK2_validateCheckinData_이름을 그대로 사용합니다. - 붙여넣은 뒤 할 일: 웹앱에서 실제로 체크인 시도를 해 보고, 위치 권한 꺼짐·반경 밖·반경 안 세 가지 상황에서 메시지와 시트 저장 결과가 의도대로 나오는지 확인합니다.
function CHK2_validateCheckinData_(formData) { // → 체크인 입력 검증
const result = { // → 검증 결과 기본값
ok: false, // → 성공 여부
message: '', // → 사용자용 메시지
cleaned: null // → 정제된 데이터
};
if (!formData) { // → 입력 자체가 없을 때
result.message = '데이터가 전송되지 않았습니다'; // → 오류 메시지
return result; // → 즉시 종료
}
const containerNo = (formData.containerNo || '') // → 컨테이너 번호 꺼내기
.toString() // → 문자열로 변환
.trim(); // → 공백 제거
if (!containerNo) { // → 컨테이너 비었으면
result.message = '컨테이너 번호를 입력해 주세요'; // → 안내 메시지
return result; // → 실패 반환
}
// ---- GPS 위치 검증 시작 ----
// 공백만 든 문자열(' ')은 빈 문자열과 다르지만 Number(' ') 는 **0** 이다.
// 그대로 두면 위도·경도 0(대서양 한가운데)으로 거리를 재게 된다. 먼저 다듬는다.
const latRaw = (formData.lat == null) ? '' : String(formData.lat).trim();
const lngRaw = (formData.lng == null) ? '' : String(formData.lng).trim();
const accRaw = (formData.accuracyM == null) ? '' : String(formData.accuracyM).trim();
let latNum = null; // → 숫자형 위도 저장
let lngNum = null; // → 숫자형 경도 저장
let distanceM = null; // → 기준점과 거리 저장
let accuracyM = null; // → 좌표 정확도(미터)
if (latRaw !== '' && lngRaw !== '') { // → 두 값이 실제로 있을 때
try { // → 변환·계산 중 예외 대비
distanceM = CHK2_distanceMeters_( // → 기준점과 거리 계산
latRaw, // → 현재 위도 원본
lngRaw, // → 현재 경도 원본
CHK2_CONFIG.BASE_LAT, // → 기준 위도
CHK2_CONFIG.BASE_LNG // → 기준 경도
);
latNum = Number(latRaw); // → 숫자형 위도 보관
lngNum = Number(lngRaw); // → 숫자형 경도 보관
accuracyM = (accRaw === '') ? null : Number(accRaw); // → 정확도(없으면 null)
} catch (e) { // → 좌표·범위 오류 등
result.message = '위치 정보를 읽는 중 오류가 발생했습니다'; // → 일반 오류 메시지
return result; // → 실패 반환
}
}
if (CHK2_CONFIG.REQUIRE_GPS) { // → 위치 필수인 경우
if (latNum === null || lngNum === null) { // → 좌표가 없을 때
result.message = '위치 권한을 허용해야 체크인할 수 있습니다'; // → 권한 안내
return result; // → 실패 반환
}
// 정확도가 반경보다 크면 '안에 있다/밖에 있다'를 말할 수 없다. 정확도 500m 짜리
// 좌표로 150m 반경을 판정하면, 사실은 아무것도 판정하지 않은 것과 같다.
if (!Number.isFinite(accuracyM) || // → 정확도를 안 보냈거나
accuracyM > CHK2_CONFIG.MAX_ACCURACY_M) { // → 너무 부정확하면
result.message =
'GPS 정확도가 낮습니다(오차 ' + // → 사용자 안내
(Number.isFinite(accuracyM) ? Math.round(accuracyM) + 'm' : '알 수 없음') +
'). 실외에서 잠시 후 다시 시도해 주세요';
return result; // → 판정 보류
}
if (distanceM > CHK2_CONFIG.MAX_DISTANCE_M) { // → 허용 반경 초과
result.message =
'현장에서만 체크인할 수 있습니다 (기준 ' + // → 반경 밖 안내
CHK2_CONFIG.MAX_DISTANCE_M + 'm 이내)'; // → 허용 반경 표시
return result; // → 실패 반환
}
} else { // → 위치가 선택인 경우
// 좌표가 있을 때만 거리·좌표를 참고용으로 남기고, 반경 밖이라도 막지 않습니다.
// 좌표가 없다면 추가 검증 없이 통과시킵니다.
}
// ---- GPS 위치 검증 끝 ----
// ---- 여기 이후로는 예약 조회·상태 검증·최종 정제 로직을 넣습니다 ----
// 예: const apptInfo = getApptInfoByCntr(containerNo); 등 (1편 조회 함수)
// 이 글에서는 GPS 검증 흐름에 집중하기 위해 최소 예시만 포함합니다.
result.ok = true; // → 모든 검증 통과(예시)
result.message = '체크인이 가능합니다'; // → 성공 메시지
result.cleaned = { // → 정제된 데이터 예시
containerNo: containerNo, // → 컨테이너 번호
lat: latNum, // → 위도(없으면 null)
lng: lngNum, // → 경도(없으면 null)
accuracyM: accuracyM, // → 좌표 정확도(없으면 null)
distanceM: distanceM // → 기준점과 거리(없으면 null)
};
return result; // → 최종 반환
} 제대로 됐는지 확인하는 법: 웹앱에서
1) 위치 권한을 끈 상태로 체크인을 시도했을 때 “위치 권한을 허용해야 체크인할 수 있습니다” 메시지가 나오는지,
2) 기준 좌표에서 충분히 멀리 떨어진 곳에서 테스트 좌표를 강제로 넣었을 때 “현장에서만 체크인할 수 있습니다 (기준 Xm 이내)” 메시지가 뜨고 시트에 행이 생기지 않는지,
3) 기준점 근처 좌표로 테스트했을 때에는 “체크인이 가능합니다”가 뜨고 실제로 시트에 체크인 행이 추가되는지 확인합니다.
실무 팁 – 반경 값과 정책, 오류 상황 다루기
현장에서 구글시트 GPS 체크인 방법을 실제로 돌려 보면, 코드보다 정책과 예외 상황에서 더 많은 논의가 필요합니다. 운영 경험을 바탕으로 몇 가지를 정리합니다.
- 허용 반경은 “GPS 오차 + 대기 위치”를 합쳐 잡기
실외 환경에서 GPS 오차는 10~30m 정도까지 자주 발생합니다. 게다가 많은 센터가 도크 바로 앞이 아니라 입구 대기열에서 미리 체크인을 받습니다. 기준점과 대기 구역 사이가 50~70m 정도인 곳에서는 최소 100m 이상, 여유 있게는 150~200m 정도로 잡아야 기사들이 불필요하게 거절되지 않습니다.
- REQUIRE_GPS 를 바로 true 로 두지 말고, 전환기를 두기
일부 구형 단말기나 사내 정책 때문에 위치 서비스를 상시 켜기 어려운 경우가 있습니다. 이런 곳에서는 초기에 REQUIRE_GPS: false 로 두고, 위치가 오면 기록만 하고, 없는 경우에도 체크인을 허용하는 “완화 모드”로 운영하는 게 안전합니다. 이후 단말기 교체나 정책 정비가 마무리되면 true 로 올리는 식으로 전환하면 혼선이 적습니다.
- GPS 검증과 별개로, 저장 단계에 LockService 적용
같은 컨테이너로 여러 사람이 거의 동시에 체크인 버튼을 누르는 경우, 거리 검증은 통과해도 시트에 이중 저장이 생길 수 있습니다. 이 부분은 구글시트 입고 예약 중복 방지 방법: LockService로 안전하게 저장하기에서 다룬 패턴을 그대로 재사용하면 됩니다.
GPS 검증을 통과한 뒤 LockService.getScriptLock() 으로 잠그고, 예약 상태를 다시 한 번 확인한 후 저장한 뒤 finally 블록에서 잠금을 해제하는 구조가 실무에서 가장 안전했습니다.
- 정책 변경 시 충분한 안내와 예외 기준 정하기
“이제부터는 창고 반경 Xm 밖에서는 체크인이 막힌다”는 사실을 공유하지 않으면, 기사들은 시스템 오류로 오해하기 쉽습니다. 공지에 “위치 권한 허용”과 “기준 반경”을 명시하고, 첫 운영 주에는 예외적으로 수기 체크인을 허용할 기준(예: 첫 주에는 시스템 거절 시 현장 관리자 승인 후 수기 입력 허용)을 함께 정해 두면 혼선을 줄일 수 있습니다.
맺음말
구글시트 체크인 GPS 거리 검증을 Apps Script로 구현해 두면, “현장 밖에서 미리 눌러 둔 체크인”을 상당 부분 줄일 수 있습니다. 앞에서 짚었듯 좌표 자체의 위조까지 막지는 못하므로, 부정 방지 장치라기보다 운영 품질을 지키는 보조 장치로 보는 편이 정확합니다. 이번 글에서는 앞 편 코드와 한 프로젝트에서 함께 돌아가도록 CHK2_ 접두어로 시리즈를 분리해, Identifier 'CHK_CONFIG' has already been declared 같은 충돌 없이 합본할 수 있게 정리했습니다.
핵심은 세 가지입니다.
1) CHK2_CONFIG 로 기준 좌표와 반경·정책을 한곳에서 관리하고,
2) CHK2_distanceMeters_() 로 좌표 사이 거리를 안정적으로 계산하며,
3) CHK2_validateCheckinData_() 에서 정확도를 먼저 확인한 뒤, 반경 밖 체크인을 서버에서 일관되게 거부하는 것.
오늘 바로 할 수 있는 한 가지는, Apps Script 편집기를 열어 CHK2_CONFIG 와 CHK2_distanceMeters_()·CHK2_validateCheckinData_() 세 블록을 그대로 붙여 넣고, 테스트 함수와 웹앱에서 반경 안/밖·위치 권한 꺼짐·정확도 낮음 네 가지 상황을 각각 시뮬레이션해 보는 일입니다. 특히 실내에서 한 번 눌러 보시면 정확도 경고가 왜 필요한지 바로 보입니다.