구글시트 컨테이너 번호 체크인: 기사 도착 예약 조회 만들기
도입: 전화·무전기 대신 컨테이너 번호로 바로 찾기
구글시트 기사 체크인 화면 만들기를 찾고 있다면, 이미 입고 예약은 어느 정도 자동화했지만 기사 도착 시점만큼은 아직 전화와 무전기에 의존하고 있을 가능성이 큽니다. “컨테이너 몇 번이요?”, “예약 몇 시죠?”를 계속 묻다 보면 줄은 길어지고, 데스크 직원은 예약 시트를 열어 검색창에 번호를 치는 일을 하루 종일 반복하게 됩니다.
이 글에서는 그 문제를 줄이기 위해, 구글시트·Apps Script로 컨테이너 번호를 입력하면 오늘 예약을 자동으로 찾아주는 백엔드 조회 기능을 구현합니다. 즉, 기사 휴대폰에서 돌아갈 도착 체크인 화면의 심장부 로직을 먼저 만드는 단계입니다. 코드는 그대로 복사해 붙여넣으면 동작하도록 구성하되, 실제로는 어디에 붙이고 어떻게 테스트해야 하는지까지 현실적인 흐름을 기준으로 설명합니다.
이 글은 이미 입고 예약 기본 구조(예약 시트, SETTINGS, APPT_ymd_, APPT_hm_, loadSettings_ 등)를 만들어 두었다는 전제에서 이어집니다. 아직이라면 먼저 구글시트 입고 예약 시스템 시트 구조 만들기 — 기본 1편을 세팅한 뒤 따라오는 편이 안전합니다.
체크인 조회 기능이 해야 할 일 정리
구글시트로 창고 체크인 시스템을 만들 때, 기사 입장에서 필요한 행동은 단순합니다. 컨테이너 번호를 입력하고 [조회]를 누른다. 그 뒤는 시스템이 해야 할 영역입니다. 이번 글에서 만드는 Apps Script 체크인 조회 로직은 다음 역할을 합니다.
- 오늘 날짜 기준으로 예약 시트(APPT_MAIN)를 읽습니다.
- 입력된 컨테이너 번호 형식을 1차로 검증합니다. (빈 값, 지나치게 짧은 값 등)
- 오늘 예약 중에서 해당 번호를 가진 행을 찾습니다.
- 결과에 따라 아래와 같이 구분해 돌려줍니다.
- 정확히 1건: 시간, 도어/슬롯, 장비 유형, 운송사, 고객사를 묶어서 반환
- 0건: “오늘 이 번호 예약 없음” 사유와 메시지
- 2건 이상: “중복 예약” 상태와 메시지
- 번호는 찾았지만 날짜 칸이 깨진 경우: “데이터 오류” 사유와 메시지 (없는 예약으로 잘못 안내하지 않기 위해)
또한 체크인 화면이 처음 열릴 때 필요한 초기 정보(오늘 날짜, 안내문, 운영시간, 타임존) 를 한 번에 내려주는 getInitialDataForCheckIn() 도 함께 만듭니다. 실제 웹앱 화면은 별도의 HTML·클라이언트 코드에서 구현하게 되며, 이 글의 초점은 그 화면이 호출할 백엔드 API 역할의 Apps Script 함수입니다.
체크인용 설정 묶기: 시트·열 위치 상수화
실무에서 가장 자주 바뀌는 것은 스크립트 로직보다 시트 구조와 안내문입니다. 열을 한 칸 옮기거나, 시트 이름을 바꾸거나, 안내문을 새로 정할 때마다 코드 곳곳을 고치다 보면 오류가 생기기 쉽습니다. 그래서 이번 글에서는 체크인 기능에서 쓸 값들을 CHK_CONFIG라는 상수 객체에 모아 둡니다.
이렇게 해 두면 나중에 창고가 늘어나거나 시트 구조를 바꿔도 위쪽 설정 몇 줄만 바꾸면 전체 조회 로직이 그대로 따라오도록 만들 수 있습니다.
1단계 — 체크인 설정 상수 정의
이 코드는 체크인에서 사용할 시트 이름, 열 번호, SETTINGS 키, 타임존을 한 곳에 모아 두는 역할을 합니다.
붙여넣을 위치는 구글시트 → 확장 프로그램 → Apps Script → Code.gs 파일 맨 위, 기존 상수들 아래입니다.
붙여넣은 뒤에는 저장(⌘S 또는 Ctrl+S)만 해 두면 되고, 별도 실행은 필요하지 않습니다.
// 열 번호는 **입고예약 시리즈가 확정한 APPT_MAIN 구조**를 그대로 따른다.
// A 날짜 · B 시작시간 · C 장비유형 · D 도어 · E 컨테이너 · F 운송사 · G 고객사 · H 비고
// I 종료시간 · J 생성일시 · K 수량 · L 팔레트 · M 예약ID — **체크인 상태 열은 아직 없다.**
// 여기서 한 칸이라도 어긋나면 컨테이너 번호를 장비 유형 칸에서 찾게 되어 아무것도 못 찾는다.
const CHK_CONFIG = { // → 체크인 설정 모음
APPT_SHEET_NAME: 'APPT_MAIN', // → 예약 메인 시트 이름
COL_DATE: 1, // → A열: 예약일자
COL_TIME: 2, // → B열: 시작 시간
COL_EQUIP_TYPE: 3, // → C열: 장비 유형
COL_DOOR: 4, // → D열: 도어/야드 슬롯
COL_CNTR: 5, // → E열: 컨테이너 번호
COL_CARRIER: 6, // → F열: 운송사
COL_CLIENT: 7, // → G열: 고객사
SETTINGS_CHECKIN_NOTICE_KEY: 'CHECKIN_NOTICE', // → 안내문 설정 키
SETTINGS_BUSINESS_HOURS_KEY: 'CHECKIN_HOURS' // → 운영시간 설정 키
}; //
// 시간대는 **여기서 문자열로 적지 않는다.** 기본 5편이 만든 `APPT_TZ`(= 프로젝트 시간대)를
// 그대로 쓴다. 화면에는 엉뚱한 지역 시간대를 보내면서 날짜는 프로젝트 시간대로 계산하는
// 어긋남이 이렇게 생긴다. 프로젝트 시간대는 Apps Script 설정에서 America/New_York 로 둔다.제대로 됐는지 확인하는 법: APPT_MAIN 시트와 열 배치가 실제 구조와 맞는지 눈으로 한 번 비교해 보고, 나머지 함수에서 이 상수를 참조할 때 오류가 나지 않으면 정상입니다.
초기 정보 보내기: getInitialDataForCheckIn
기사 체크인 화면이 처음 열릴 때마다 오늘 날짜와 안내문을 일일이 계산해서 그때그때 구하는 것보다, 서버에서 한 번에 받아 두고 화면에서 재사용하는 구조가 유지보수에 더 유리합니다. 이번 단계에서는 그 역할을 하는 getInitialDataForCheckIn() 함수를 만듭니다.
이 함수는 다음 네 가지를 한 번에 내려줍니다.
todayYmd:APPT_ymd_()로 포맷한 오늘 날짜(YYYY-MM-DD)notice: SETTINGS 시트에서 읽은 체크인 안내문 (없으면 빈 문자열)businessHours: 체크인 운영시간 문자열 (예:09:00~18:00, 없으면 빈 문자열)timezone: 체크인에서 사용할 타임존 — 기본 5편APPT_TZ값(프로젝트 시간대,America/New_York)을 그대로 내려보냅니다.
휴무일 여부 같은 세밀한 로직은 앞선 예약·캘린더 파트에서 다루는 것이 자연스럽고, 이번 편에서는 화면 상단에 띄울 기본 정보 정도까지만 책임집니다.
2단계 — 초기 데이터 제공 함수
이 코드는 체크인 웹앱이 처음 열릴 때 필요한 날짜·안내문·운영시간 정보를 한 번에 반환하는 역할을 합니다.
붙여넣을 위치는 Code.gs 파일 맨 아래, 기존 함수들 다음입니다.
붙여넣은 뒤에는 저장 후, 상단 함수 목록에서 getInitialDataForCheckIn 을 선택해 한 번 실행하고 권한을 허용합니다.
function getInitialDataForCheckIn() { // → 체크인 초기 데이터 제공
const tz = APPT_TZ; // → 기본 5편 프로젝트 시간대
const now = new Date(); // → 현재 시각
const todayYmd = APPT_ymd_(now); // → 'YYYY-MM-DD' 변환
const settings = loadSettings_(); // → SETTINGS 시트 읽기
const noticeKey = CHK_CONFIG.SETTINGS_CHECKIN_NOTICE_KEY; // → 안내문 키
const hoursKey = CHK_CONFIG.SETTINGS_BUSINESS_HOURS_KEY; // → 운영시간 키
const notice = settings[noticeKey] || ''; // → 안내문 없으면 빈 문자열
const hours = settings[hoursKey] || ''; // → 운영시간 없으면 빈 문자열
const result = { // → 화면에 보낼 객체
todayYmd: todayYmd, // → 오늘 날짜
notice: notice, // → 안내문
businessHours: hours, // → 운영시간 문자열
timezone: tz // → 타임존 정보
};
return result; // → 웹앱으로 전달
}제대로 됐는지 확인하는 법: Apps Script 편집기에서 getInitialDataForCheckIn 을 선택해 실행하고, 오류 없이 끝나면 1차로 정상입니다. 더 확실히 보려면 아래 테스트 함수를 함께 붙여 실행합니다.
function CHK_testInitialData() { // → 초기 데이터 테스트
const data = getInitialDataForCheckIn(); // → 함수 호출
Logger.log(JSON.stringify(data)); // → 내용 출력
}CHK_testInitialData 를 실행한 뒤 실행 로그에서 {"todayYmd":"2026-08-16","notice":...} 처럼 JSON 문자열이 보이면 의도대로 동작하고 있는 것입니다.
컨테이너 번호로 오늘 예약 찾기: getApptInfoByCntr
이제 구글시트 도착 체크인 Apps Script의 핵심인 조회 함수를 만듭니다. 이 함수는 기사나 현장 직원이 입력한 컨테이너 번호를 받아, APPT_MAIN 시트에서 오늘 예약을 찾아 정보를 돌려줍니다.
로직은 다음과 같습니다.
- 입력값을 공백 제거·대문자 변환하여 정리합니다.
- 빈 값이거나 너무 짧은 값이면 형식 오류로 바로 반환합니다.
- 예약 시트를 한 번에 읽고, 2행부터 마지막 행까지 순회합니다.
- 날짜 셀은 Date·시리얼·문자열 어느 쪽이든
APPT_ymd_()로YYYY-MM-DD형식으로 통일해 비교합니다. - 오늘 날짜에 해당하는 행 중에서 컨테이너 번호가 일치하는 행을 모두 모읍니다.
- 결과 건수에 따라 세 가지로 나눠 처리합니다.
- 0건:
NOT_FOUND_TODAY(다른 날짜에만 있으면OTHER_DATE) - 1건:
OK+ 상세 정보 - 2건 이상:
DUPLICATE_CNTR - 번호는 맞는데 그 행의 날짜를 읽을 수 없으면:
DATA_ERROR
실무에서는 번호 패턴(예: 4글자+7숫자)을 정규식으로 더 엄격하게 검증하는 경우도 많습니다. 이 글에서는 기본 뼈대만 먼저 잡고, 현장 규칙에 따라 확장할 수 있도록 길이 검증 정도까지 반영했습니다.
3단계 — 컨테이너 번호 예약 조회 함수
이 코드는 컨테이너 번호로 오늘 예약을 검색하고, 결과/사유를 구분해 객체로 돌려주는 역할을 합니다.
붙여넣을 위치는 getInitialDataForCheckIn 함수 바로 아래입니다.
붙여넣은 뒤에는 저장하고, 마지막에 나오는 테스트 함수 CHK_testGetApptInfoByCntr_로 실제 데이터를 확인합니다.
function getApptInfoByCntr(cntrRaw) { // → 컨테이너 번호로 오늘 예약 찾기
const ss = SpreadsheetApp.getActiveSpreadsheet(); // → 현재 스프레드시트
const sheet = ss.getSheetByName(CHK_CONFIG.APPT_SHEET_NAME);// → 예약 메인 시트
if (!sheet) { // → 시트 없을 때
throw new Error('예약 시트를 찾을 수 없습니다.'); // → 설정 오류
}
const todayYmd = APPT_ymd_(new Date()); // → 오늘 날짜 문자열
const cntr = String(cntrRaw || '').trim().toUpperCase(); // → 입력 정리
if (!cntr) { // → 빈 입력
return {
found: false,
reason: 'EMPTY_CNTR',
message: '컨테이너 번호를 입력해 주세요.'
};
}
if (cntr.length < 4) { // → 너무 짧은 번호
return {
found: false,
reason: 'INVALID_CNTR',
message: '컨테이너 번호 형식이 올바르지 않습니다.'
};
}
const lastRow = sheet.getLastRow(); // → 마지막 행
if (lastRow < 2) { // → 데이터 없음
return { // → 결과 객체
found: false, // → 미발견
reason: 'NO_DATA_TODAY', // → 오늘 데이터 없음
message: '오늘 등록된 예약이 없습니다.' // → 안내문
};
}
const lastCol = sheet.getLastColumn(); // → 마지막 열 번호
const rng = sheet.getRange(2, 1, lastRow - 1, lastCol); // → 2행부터 전체 열 읽기
const values = rng.getValues(); // → 값 2차원 배열
const matches = []; // → 오늘 일치 행 목록
let hasOtherDate = false; // → 다른 날짜에 존재 여부
const dataErrorRows = []; // → 날짜를 못 읽은 '이 컨테이너' 행
for (let i = 0; i < values.length; i++) { // → 각 행 반복
const row = values[i]; // → 한 행
const dateCell = row[CHK_CONFIG.COL_DATE - 1]; // → 날짜 셀
const cntrCell = String(row[CHK_CONFIG.COL_CNTR - 1] || '').trim().toUpperCase(); // → 컨테이너
if (!cntrCell) { // → 컨테이너 없는 행
continue; // → 볼 것 없다
}
// **번호부터 맞춰 본다.** 날짜를 먼저 읽고 실패했다고 넘겨 버리면, 정작 찾는
// 컨테이너의 날짜가 깨졌을 때 '오늘 예약 없음'으로 잘못 안내하게 된다.
if (cntrCell !== cntr) { // → 번호 불일치
continue; // → 건너뛰기
}
// 여기서부터는 **찾는 컨테이너의 행**이다. 날짜를 못 읽으면 조용히 넘기지 않는다.
let rowYmd; // → 행 날짜 문자열
try { // → 형식 통일 시도
rowYmd = (dateCell instanceof Date) // → Date·시리얼·문자열 모두
? APPT_ymd_(dateCell) // → 'YYYY-MM-DD'
: APPT_ymd_(String(dateCell).trim()); // → 공백·'2026-8-3' 도 통일
} catch (e) { // → 날짜로 못 읽음
dataErrorRows.push(i + 2); // → 사람이 봐야 할 행
continue; //
} //
if (rowYmd === todayYmd) { // → 오늘 날짜
matches.push({ // → 결과에 추가
rowIndex: i + 2, // → 실제 행 번호
row: row // → 전체 행 데이터
});
} else { // → 다른 날짜
hasOtherDate = true; // → 다른 날짜 있음 표시
}
}
// 오늘 예약을 정확히 한 건 찾았다면 그것으로 확정한다. 그 밖의 경우에 날짜가 깨진
// 행이 있었다면 '예약 없음' 이라고 말해서는 안 된다 — 데이터 문제라고 알려 준다.
if (matches.length !== 1 && dataErrorRows.length > 0) { // → 판단할 수 없음
return {
found: false,
reason: 'DATA_ERROR',
message: '해당 컨테이너 예약 행의 날짜를 읽을 수 없습니다(행 ' +
dataErrorRows.slice(0, 10).join(', ') + '). 사무실에 문의해 주세요.'
};
}
if (matches.length === 0) { // → 오늘 일치 예약 없음
if (hasOtherDate) { // → 다른 날짜에만 있음
return {
found: false,
reason: 'OTHER_DATE',
message: '해당 컨테이너 번호 예약이 다른 날짜에만 있습니다. 예약 날짜를 확인해 주세요.'
};
}
return {
found: false,
reason: 'NOT_FOUND_TODAY',
message: '입력한 컨테이너 번호로 오늘 예약이 없습니다.'
};
}
if (matches.length > 1) { // → 여러 건 발견
return {
found: false,
reason: 'DUPLICATE_CNTR',
message: '같은 컨테이너 번호로 오늘 예약이 여러 건 있습니다. 사무실에 문의해 주세요.'
};
}
const match = matches[0]; // → 유일한 행
const row = match.row; // → 행 데이터
const timeCell = row[CHK_CONFIG.COL_TIME - 1]; // → 시간 셀
const equipType = row[CHK_CONFIG.COL_EQUIP_TYPE - 1] || ''; // → 장비 유형
const door = row[CHK_CONFIG.COL_DOOR - 1] || ''; // → 도어/슬롯
const carrier = row[CHK_CONFIG.COL_CARRIER - 1] || ''; // → 운송사
const client = row[CHK_CONFIG.COL_CLIENT - 1] || ''; // → 고객사
let timeStr = ''; // → 시간 문자열
if (timeCell instanceof Date) { // → Date 인 경우
timeStr = APPT_hm_(timeCell); // → 'HH:mm' 변환
} else if (timeCell) { // → 값이 있는 경우
timeStr = String(timeCell); // → 문자열 처리
}
return { // → 성공 결과
found: true, // → 발견됨
reason: 'OK', // → 정상
message: '예약을 찾았습니다.', // → 안내
data: { // → 상세 정보
rowIndex: match.rowIndex, // → 행 번호
date: todayYmd, // → 날짜
time: timeStr, // → 시간
container: cntr, // → 컨테이너 번호
equipType: equipType, // → 장비 유형
door: door, // → 도어/슬롯
carrier: carrier, // → 운송사
client: client // → 고객사
}
};
}제대로 됐는지 확인하는 법: 아래 테스트 함수에서 실제 존재하는 컨테이너 번호와 존재하지 않는 번호를 하나씩 넣고 실행하여, 각 케이스에 맞는 reason 과 message 가 찍히는지 확인합니다.
function CHK_testGetApptInfoByCntr_() { // → 컨테이너 조회 테스트
const existingCntr = 'TESTCNTR1'; // → 실제 존재하는 번호로 교체
const missingCntr = 'NO_SUCH_CNTR'; // → 존재하지 않는 번호
const otherDateCntr = 'OTHER_DATE_CNTR'; // → 다른 날짜에만 있는 번호로 교체
const res1 = getApptInfoByCntr(existingCntr); // → 있는 번호 조회
const res2 = getApptInfoByCntr(missingCntr); // → 아예 없는 번호 조회
const res3 = getApptInfoByCntr(otherDateCntr);// → 다른 날짜만 있는 번호 조회
Logger.log('EXISTING: ' + JSON.stringify(res1)); // → 결과 출력
Logger.log('MISSING: ' + JSON.stringify(res2)); // → 결과 출력
Logger.log('OTHER_DATE: ' + JSON.stringify(res3));// → 결과 출력
}CHK_testGetApptInfoByCntr_ 를 실행한 뒤 실행 로그에서
EXISTING 줄은 found:true, reason:"OK",
MISSING 줄은 found:false, reason:"NOT_FOUND_TODAY",
OTHER_DATE 줄은 found:false, reason:"OTHER_DATE"
형태로 출력되면 의도대로 동작하고 있습니다.
실무에서 자주 틀리는 부분과 체크 포인트
실제 물류센터 몇 곳에 비슷한 구글시트 컨테이너 번호 조회 자동화를 적용해 보면서 반복적으로 나왔던 실수와 그 대책을 정리해 보겠습니다.
첫째, 날짜 조건을 빼먹는 경우입니다. 단순히 시트 전체에서 컨테이너 번호만 검색하면, 과거나 미래 예약까지 같이 걸려 들어와 “오늘은 오지 않아야 하는 트럭”까지 체크인된 것처럼 보일 수 있습니다. 이번 코드에서 rowYmd === todayYmd 인 것만 matches에 넣고, 그 외 동일 컨테이너는 hasOtherDate 만 표시하는 이유가 여기에 있습니다.
둘째, 컨테이너 번호 형식을 전혀 검증하지 않는 경우입니다. 빈 칸, A, 123 같은 입력도 다 조회를 시도하면, 결과가 안 나올 때마다 기사와 데스크가 서로 “한 번 더 불러 달라”는 대화를 반복하게 됩니다. 현장 규칙에 맞게 최소 길이, 알파벳+숫자 조합 등을 제한해 두면 이런 소모를 줄일 수 있습니다.
셋째, 날짜 형식 혼재 문제입니다. 사람이 직접 입력한 문자열 날짜와, 날짜 형식으로 입력된 셀이 섞여 있는 경우가 많습니다. 그래서 instanceof Date 로 먼저 분기한 뒤 APPT_ymd_() 를 적용하고, 문자열 경로에서도 최소한 todayYmd와 동일 포맷을 쓰도록 기존 예약 저장 로직을 조정해 두는 것이 좋습니다. 예약 저장 쪽 포맷 정리는 구글시트 날짜 형식 통일 방법: 입고 예약 시스템 기본 5편을 참고하면 연결이 수월합니다.
넷째, 중복 예약 처리 정책 부재입니다. 원칙상 컨테이너 하나에 예약은 한 건만 있어야 하므로, 저장 단계에서부터 중복을 막는 것이 이상적입니다. 이를 위해 LockService를 이용한 저장 로직과 중복 검사는 구글시트 입고 예약 중복 방지 방법: LockService로 안전하게 저장하기에 따로 정리해 두었습니다. 그럼에도 기존 데이터나 수기 예약 때문에 중복이 생길 수 있어, 조회 단계에서는 DUPLICATE_CNTR 로 명확하게 구분하여 현장 직원이 상황을 바로 파악할 수 있게 했습니다.
맺음말: 조회 로직부터 안정시키고 화면은 그다음
이번 글에서는 구글시트 컨테이너 번호 체크인을 위한 핵심 백엔드 두 가지,
getInitialDataForCheckIn() 과 getApptInfoByCntr() 를 Apps Script로 구현했습니다.
- 체크인 화면이 열릴 때 한 번만 호출해 오늘 날짜·안내문·운영시간을 받아 두는 함수,
- 기사나 직원이 입력한 컨테이너 번호로 오늘 예약을 찾아 결과·사유를 구분해 돌려주는 함수까지 준비되면, 그 위에 HTML·웹앱 UI를 얹는 일은 비교적 수월합니다.
다음 단계에서는 이 함수들을 doGet(e) 기반 웹앱과 연결하고, 체크인 완료 시간·담당자·상태를 저장하는 부분, 동시 입력을 LockService로 조절하는 부분까지 확장할 수 있습니다.
지금 당장 할 수 있는 행동 한 가지를 제안한다면, APPT_MAIN 시트에서 실제 컨테이너 번호 하나를 골라 CHK_testGetApptInfoByCntr_ 함수에 넣고 실행해 보시기 바랍니다. 로그에 시간·도어·운송사가 정확히 찍히는 것을 한 번 확인해 두면, 이후 웹 화면을 붙일 때도 안심하고 작업을 이어갈 수 있습니다.