구글시트 일괄 예약 검증 방법: 붙여넣기 한 번에 검사하기 (완성본)
도입: 몇 번째 줄이 왜 안 되는지 한눈에 보고 싶을 때
구글시트에 예약을 여러 건 붙여넣다 보면, 어느 줄이 왜 안 되는지 찾다가 시간을 다 쓰는 경우가 많습니다. 특히 엑셀에서 복사해 온 입고 예약 목록을 한 번에 넣으려다가, 날짜 형식 하나 잘못된 줄 때문에 전체가 막히면 현장에서 바로 불만이 나옵니다. “3행이 문제인데 1~50행을 다시 손으로 보라”는 식의 안내는 실무에서는 거의 작동하지 않습니다.
이 글에서는 이 문제를 정면으로 다룹니다. 구글시트 일괄 예약 검증 방법을 Apps Script로 구현해, 여러 줄을 한 번에 붙여넣었을 때 줄마다 오류를 검사하고 “몇 행이 왜 안 되는지”를 바로 보여 주는 구조를 만드는 방법을 정리합니다. 엑셀에서 복사한 입고 예약 목록을 메모리에만 올려 두고, 실제 시트에 저장하기 전에 한 번에 검증하는 레이어를 만드는 것이 목표입니다.
현장에서 받은 예약 엑셀 파일을 통째로 붙여넣어야 할 때, 한 건씩 폼으로 입력하는 방식만으로는 계획 변경을 따라가기 어렵습니다. 잘못된 예약이 그대로 들어가면 도크와 인력 계획이 엉키고, 반대로 한 줄 오류 때문에 멀쩡한 줄까지 전부 다시 입력해야 하는 상황도 피하고 싶습니다. 그래서 “붙여넣고 나서 저장하기 전에, 시스템이 먼저 한 번 걸러 주는 구조”를 별도로 설계했습니다.
이번 편에서는 특히 다음 부분까지 구현합니다.
- 엑셀에서 복사한 여러 줄을 메모리 상의
rows배열로 받아 validateBulkSlots(rows)함수가- 필수값 누락
- 날짜·시간 형식
- 운영 시간 범위
- 정원(용량) 초과 여부
- 이번에 붙여넣은 줄들끼리의 컨테이너 중복
을 줄 단위로 검사하고,
- “몇 번째 줄이 왜 안 되는지”를
{ ok, errors[], cleanedRows[] }형태로 돌려주는 구조를 만듭니다.
실제 시트에 저장하고 LockService로 동시성을 제어하는 부분은 다음 글에서 다루고, 이번 글은 검증 레이어만 완결하는 데 집중합니다.
붙여넣기용 설정: 무엇을 어떻게 검증할지 먼저 정하기
일괄 검증을 만들기 전에 먼저 정해야 할 것이 있습니다. 한 줄이라도 오류가 있으면 전체를 막을지, 통과 가능한 줄만 저장할지에 대한 정책입니다. 물류센터마다 철학이 다르지만, 실제로 돌려 본 기준으로는 다음처럼 단계를 나누는 것이 현실적이었습니다.
- 검증 단계에서는
- 모든 줄을 끝까지 다 읽고
- 각 줄이 통과인지, 어떤 오류가 있는지를 리스트로 돌려줍니다.
이 단계에서는 아직 아무것도 시트에 쓰지 않습니다. 운영자는 이 결과를 보고 “에러 있는 줄을 고쳐서 다시 전체를 넣을지, 일단 통과된 줄만 먼저 반영할지”를 결정할 수 있습니다.
- 저장 단계에서는
- LockService로 잠근 뒤
- 정원 계산과 중복 체크를 다시 한 번 확인하고
- 예약 본문 시트에 실제로 행을 추가합니다.
여기에서야 비로소 “한 줄이라도 실패하면 전체 롤백할지”, “통과한 줄만 부분 저장할지”를 분기합니다.
이 구조를 코드로 옮기기 위해, 일괄 검증에 필요한 열 위치와 정책 값을 하나의 설정 객체로 모읍니다. 다른 갈래 코드와 이름이 겹치지 않게 하기 위해 BULK_ 접두어를 붙입니다. 예약 본문 시트의 실제 이름은 운영 환경마다 다를 수 있으므로, 여기서는 튜토리얼용 가상 명칭으로만 씁니다.
여기서 주의할 점이 하나 있습니다. 장비유형 허용 목록은 앞 편에서 쓰는 BOOK_CAPACITY_CONFIG.VALID_TYPES 기준을 그대로 따라야 합니다. 이 목록은 ['CONTAINER', 'TRAILER'] 두 가지뿐이고, 이 글에서 임의로 다른 값을 넣어 새 허용 목록을 만들면 앞 편의 정원 계산과 집계가 어긋나 버립니다. 그래서 이 글의 설정에서도 허용 목록은 그대로 유지합니다.
관련 용량·예약 로직은 다음 글들을 그대로 사용합니다.
해당 편에서 이미 만든 BOOK_getCapacityForType_(), BOOK_getSeqUsageForSlotCore_() 등의 규격을 이 글에서도 그대로 따릅니다.
1단계 — 일괄 검증 설정 상수 정의
이 코드는 일괄 검증에서 쓸 붙여넣기 열 매핑과 정책 값을 정의합니다.
붙여넣는 위치: 구글시트 → 확장 프로그램 → Apps Script → Code.gs (또는 기존 파일 맨 위쪽)
붙여넣은 뒤 할 일: 저장 후 별도 실행은 필요 없습니다.
// 일괄 검증 설정입니다. 필요하면 여기만 본인 환경에 맞게 바꾸세요.
const BULK_CONFIG = {
COLUMN_MAP: { // 붙여넣기 열 매핑 (0부터 시작, 배열 인덱스 기준)
date: 0, // 0번째: 날짜
startTime: 1, // 1번째: 시작시간
endTime: 2, // 2번째: 종료시간
type: 3, // 3번째: 장비유형
door: 4, // 4번째: 도어
containerNo: 5, // 5번째: 컨테이너번호
carrier: 6, // 6번째: 운송사
client: 7, // 7번째: 고객사
remark: 8, // 8번째: 비고
qty: 9, // 9번째: 수량
pallet: 10 // 10번째: 팔레트수
},
// 한 줄이라도 오류가 있으면 "저장" 단계에서 전체를 막을지 여부.
// 검증 단계에서는 이 값과 상관없이 모든 줄을 끝까지 검사해서 오류 목록을 돌려줍니다.
REQUIRE_ALL_OR_NOTHING: true,
// 장비유형 허용 목록 — 앞 편에서 사용하는 BOOK_CAPACITY_CONFIG.VALID_TYPES 와 동일해야 합니다.
// ['CONTAINER', 'TRAILER'] 외의 값을 추가 허용 목록으로 쓰지 마세요.
VALID_TYPES: ['CONTAINER', 'TRAILER']
};여기서는 예약 본문 시트 이름을 별도 상수로 두지 않았습니다. 이미 기본 시리즈에서 APPT_MAIN 시트를 기준으로 용량·예약 계산을 하기 때문에, 실제 저장 단계에서도 그 시트를 사용합니다. 이 편은 검증 전용 레이어이므로, 어디에 저장할지는 다음 편의 “일괄 저장” 함수에서 APPT_MAIN 을 기준으로 다루겠습니다.
제대로 됐는지 확인하는 법: 코드 저장 시 오류가 나지 않으면 정상입니다. 이후 함수에서 BULK_CONFIG 를 문제없이 참조할 수 있습니다.
핵심: validateBulkSlots로 줄마다 오류 이유 돌려받기
이제 이번 글의 핵심 함수인 validateBulkSlots(rows) 를 만듭니다. 이 함수는 외부에서 2차원 배열을 받아 검증만 수행하고, 실제로 시트에 쓰지는 않습니다.
이 함수의 역할은 다음과 같습니다.
rows배열(2차원, 각 행이 한 예약)을 받아- 각 행별로
- 필수값 유무
- 날짜·시간 형식
- 휴무일 여부·운영 시간 범위
- 정원(용량) 초과 여부
- 이번에 붙여넣은 줄끼리의 컨테이너 중복
을 검사합니다.
- 최종 결과를
{ ok, errors[], cleanedRows[] }형태로 돌려줍니다.
ok: 전체가 통과했는지 여부 (errors.length === 0기준)errors:{ rowIndex, message }배열cleanedRows: 검증을 통과한 줄만 정리한 배열 (다음 편에서 실제 저장에 사용)
이때 이미 앞 편들에서 만든 헬퍼 함수들을 그대로 가져와 쓰는 것이 중요합니다. 예를 들어:
APPT_ymd_(value)→'YYYY-MM-DD'문자열을 돌려줍니다.- 실재하지 않는 날짜·시각만 있는 값·1 미만 숫자는 예외를 던집니다.
- 빈 셀·빈 문자열도 예외를 던집니다.
- 정의는 입고예약 기본 5편에 이미 있습니다.
BOOK_timeToMinutes_(value)→'HH:mm'형식의 시간을 분(정수)로 바꿉니다.- 형식이 틀리면 예외가 아니라
null을 돌려줍니다. - 정의는 입고예약 등록 1편에 있습니다.
getDateBlockInfo(date)→ 해당 날짜의 운영·휴무 정보를{date, isHoliday, holidayType, open, close, blocks[]}형태로 돌려줍니다.- 정의와 사용 예시는 역시 입고예약 등록 1편을 참고합니다.
BOOK_getSeqUsageForSlotCore_(dateObj, timeObj, type)→{time, equipType, used, capacity, remaining}형태로 현재 사용량과 남은 자리를 계산합니다.- 정의는 입고예약 등록 3편에 있습니다.
이 규격을 다르게 가정하면 코드가 조용히 틀어지므로, 이미 발행된 앞 편 글의 정의와 정확히 일치하는지 반드시 확인해야 합니다. 이 글에서는 위 함수들을 다시 정의하지 않고, “앞 편에서 만든 함수를 그대로 쓴다”는 전제만 둡니다.
2단계 — validateBulkSlots 구현
이 코드는 붙여넣은 예약 줄들을 한 번에 검증하고, 줄별 오류 사유를 모은 뒤 통과한 줄만 정제해 반환합니다.
붙여넣는 위치: 구글시트 → 확장 프로그램 → Apps Script → Code.gs 맨 아래
붙여넣은 뒤 할 일: 저장 후, 아래에서 만드는 BULK_testValidateSlots_() 테스트 함수를 실행합니다.
/**
* 여러 줄 예약을 한 번에 검증합니다.
* @param {Array<Array<*>>} rows 2차원 배열(각 행이 한 예약)
* @return {{ok: boolean, errors: Array<{rowIndex: number|null, message: string}>, cleanedRows: Array<Object>}}
*/
function validateBulkSlots(rows) {
const result = { ok: true, errors: [], cleanedRows: [] };
if (!Array.isArray(rows) || rows.length === 0) {
result.ok = false;
result.errors.push({
rowIndex: null,
message: '검증할 행이 없습니다.'
});
return result;
}
const seenContainers = new Set(); // 이번 붙여넣기 안에서만 중복 체크
const col = BULK_CONFIG.COLUMN_MAP;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const displayRow = i + 1; // 사용자에게 보여줄 줄 번호(1부터)
try {
if (!row || row.length === 0) {
// 완전히 빈 행은 건너뜁니다. APPT_ymd_ 가 빈 값을 던지기 때문입니다.
continue;
}
// 1) 원시 값 꺼내기
const rawDate = row[col.date];
const rawStartTime = row[col.startTime];
const rawEndTime = row[col.endTime];
const rawType = row[col.type];
const rawDoor = row[col.door];
const rawCntr = row[col.containerNo];
const rawCarrier = row[col.carrier];
const rawClient = row[col.client];
const rawRemark = row[col.remark];
const rawQty = row[col.qty];
const rawPallet = row[col.pallet];
// 2) 필수값 검사
if (!rawDate) {
throw new Error(displayRow + '행: 날짜 없음');
}
if (!rawStartTime) {
throw new Error(displayRow + '행: 시작 시간 없음');
}
if (!rawEndTime) {
throw new Error(displayRow + '행: 종료 시간 없음');
}
if (!rawType) {
throw new Error(displayRow + '행: 장비유형 없음');
}
if (!rawCntr) {
throw new Error(displayRow + '행: 컨테이너 번호 없음');
}
// 3) 날짜·시간 형식 정리
// 앞 편 [입고예약 기본 5편] 의 APPT_ymd_ 를 그대로 사용합니다.
const ymd = APPT_ymd_(rawDate); // 'YYYY-MM-DD' 문자열
if (!ymd) {
// APPT_ymd_ 는 잘못된 날짜일 때 예외를 던지므로 이 분기는 거의 오지 않습니다.
throw new Error(displayRow + '행: 날짜 형식이 잘못되었습니다');
}
const startTimeStr = String(rawStartTime).trim();
const endTimeStr = String(rawEndTime).trim();
// 앞 편 [입고예약 등록 1편] 의 BOOK_timeToMinutes_ 를 그대로 사용합니다.
const startMin = BOOK_timeToMinutes_(startTimeStr);
if (!Number.isFinite(startMin)) {
throw new Error(displayRow + '행: 시작 시간 형식이 잘못되었습니다');
}
const endMin = BOOK_timeToMinutes_(endTimeStr);
if (!Number.isFinite(endMin)) {
throw new Error(displayRow + '행: 종료 시간 형식이 잘못되었습니다');
}
if (endMin <= startMin) {
throw new Error(displayRow + '행: 종료 시간이 시작 시간보다 이후여야 합니다');
}
// 4) 운영일·운영시간 검사
// 앞 편 [입고예약 등록 1편] 에서 만든 getDateBlockInfo 를 그대로 사용합니다.
const blockInfo = getDateBlockInfo(ymd);
if (!blockInfo) {
throw new Error(displayRow + '행: 운영정보가 없는 날짜입니다');
}
// FULL / CLOSED 는 휴무, PARTIAL 은 예약 가능
if (blockInfo.holidayType === 'FULL' || blockInfo.holidayType === 'CLOSED') {
throw new Error(displayRow + '행: 휴무일에는 예약할 수 없습니다');
}
if (!blockInfo.open || !blockInfo.close) {
throw new Error(displayRow + '행: 운영시간이 설정되지 않은 날짜입니다');
}
const openMin = BOOK_timeToMinutes_(blockInfo.open);
const closeMin = BOOK_timeToMinutes_(blockInfo.close);
if (!Number.isFinite(openMin) || !Number.isFinite(closeMin)) {
throw new Error(displayRow + '행: 운영시간 설정이 잘못되었습니다');
}
// 시작·종료 모두 운영시간 안에 있어야 합니다.
if (startMin < openMin || startMin >= closeMin) {
throw new Error(displayRow + '행: 시작 시간이 운영시간 밖입니다');
}
if (endMin <= openMin || endMin > closeMin) {
throw new Error(displayRow + '행: 종료 시간이 운영시간 밖입니다');
}
// 5) 장비유형 허용 목록 검사
const typeKey = String(rawType).trim().toUpperCase();
if (!BULK_CONFIG.VALID_TYPES.includes(typeKey)) {
throw new Error(displayRow + '행: 허용되지 않은 장비유형입니다');
}
// 6) 정원(용량) 검사
// 시간대 버그를 피하기 위해 'YYYY-MM-DD' 를 직접 year, month, day 로 쪼갭니다.
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
if (!m) {
throw new Error(displayRow + '행: 날짜 파싱 오류입니다');
}
const year = Number(m[1]);
const month = Number(m[2]); // 1~12
const day = Number(m[3]);
const dateObj = new Date(year, month - 1, day); // 로컬 기준 자정
// BOOK_getSeqUsageForSlotCore_ 는 "슬롯 단위" 기준 시각만 필요하므로 시작 시각 기준으로 계산합니다.
const timeObj = new Date(year, month - 1, day, Math.floor(startMin / 60), startMin % 60);
const usage = BOOK_getSeqUsageForSlotCore_(dateObj, timeObj, typeKey);
if (!usage || !Number.isFinite(usage.remaining)) {
throw new Error(displayRow + '행: 용량 정보를 불러오지 못했습니다');
}
if (usage.remaining <= 0) {
throw new Error(displayRow + '행: 정원 초과 슬롯입니다');
}
// 7) 이번 붙여넣기 내 중복 컨테이너 검사
const keyCntr = String(rawCntr).trim().toUpperCase();
if (seenContainers.has(keyCntr)) {
throw new Error(displayRow + '행: 붙여넣은 목록 안에서 컨테이너 중복입니다');
}
seenContainers.add(keyCntr);
// 8) 수량·팔레트 유효성 검사
const qtyNum = (rawQty === '' || rawQty == null) ? 1 : Number(rawQty);
if (!Number.isFinite(qtyNum) || qtyNum <= 0) {
throw new Error(displayRow + '행: 수량이 잘못되었습니다');
}
const palletNum = (rawPallet === '' || rawPallet == null) ? 0 : Number(rawPallet);
if (!Number.isFinite(palletNum) || palletNum < 0) {
throw new Error(displayRow + '행: 팔레트 수가 잘못되었습니다');
}
// 9) 통과한 행을 정리해서 저장
result.cleanedRows.push({
// 날짜·시간·유형·컨테이너는 검증/정규화된 값으로, 나머지는 트리밍해서 저장합니다.
date: ymd,
startTime: startTimeStr,
endTime: endTimeStr,
type: typeKey,
door: rawDoor ? String(rawDoor).trim() : '',
containerNo: keyCntr,
carrier: rawCarrier ? String(rawCarrier).trim() : '',
client: rawClient ? String(rawClient).trim() : '',
remark: rawRemark ? String(rawRemark).trim() : '',
qty: qtyNum,
pallet: palletNum
});
} catch (e) {
// 행 단위 오류만 잡아서 누적합니다. 다른 행 검사는 계속 진행합니다.
result.ok = false;
result.errors.push({
rowIndex: displayRow,
message: e && e.message ? e.message : String(e)
});
// REQUIRE_ALL_OR_NOTHING 가 true 라도, "검증" 단계에서는
// 어디가 어떻게 잘못됐는지 모두 보여주는 것이 목적이므로
// 여기서 반복을 멈추지 않습니다. 실제 "저장" 단계에서만 전체 차단 여부를 쓸 예정입니다.
}
}
result.ok = result.errors.length === 0;
return result;
}제대로 됐는지 확인하는 법: 아래 테스트 함수를 실행했을 때, 의도적으로 넣은 잘못된 값들이 정확히 “n행: …” 형식의 메시지로 errors 배열에 나오고, 정상이면 cleanedRows 에만 쌓이는지 확인합니다.
3단계 — 테스트 함수로 예외 케이스까지 눈으로 확인하기
검증 로직은 눈으로 읽을 때보다 실제 데이터를 넣어 보면서 확인하는 편이 훨씬 안전합니다. 특히 수량에 문자를 넣는 경우처럼 NaN 이 섞이는 케이스는 테스트를 하지 않으면 운영 중에야 드러납니다. 그래서 의도적으로 잘못된 값을 섞은 예제 배열을 만들어 validateBulkSlots 를 한 번에 돌려 보는 것이 좋습니다.
여기서는 다음 여섯 가지 케이스를 한 번에 넣어 보겠습니다.
- 1행: 정상
- 2행: 시작 시간 비움 → “2행: 시작 시간 없음”
- 3행: 종료 시간 형식 오류 → “3행: 종료 시간 형식이 잘못되었습니다”
- 4행: 종료 시간이 시작보다 앞섬 → “4행: 종료 시간이 시작 시간보다 이후여야 합니다”
- 5행: 허용되지 않은 장비유형 → “5행: 허용되지 않은 장비유형입니다”
- 6행: 수량에
'ABC'→ “6행: 수량이 잘못되었습니다”
이 정도만 해도 필수값·시간 형식·시간 순서·허용 목록·수량 형식 검사가 제대로 작동하는지 바로 확인할 수 있습니다.
붙여넣는 위치: validateBulkSlots 함수 바로 아래
붙여넣은 뒤 할 일: Apps Script 편집기 상단의 실행 함수 선택 메뉴에서 BULK_testValidateSlots_ 를 선택해 실행합니다.
/**
* validateBulkSlots 동작을 테스트합니다.
* - 1행: 정상
* - 2행: 시작 시간 누락 → "시작 시간 없음"
* - 3행: 종료 시간 형식 오류 → "종료 시간 형식이 잘못되었습니다"
* - 4행: 종료 시간이 시작보다 앞섬 → "종료 시간이 시작 시간보다 이후여야 합니다"
* - 5행: 잘못된 장비유형 → "허용되지 않은 장비유형입니다"
* - 6행: 수량 문자 → "수량이 잘못되었습니다"
*/
function BULK_testValidateSlots_() {
const sampleRows = [
// 날짜 시작 종료 유형 도어 컨테이너 운송사 고객사 비고 수량 팔레트
['2026-08-30', '09:00', '10:00', 'CONTAINER', 'D1', 'C001', 'CARRIER1', 'CLIENT1', '', 1, 0], // 1행 정상
['2026-08-30', '', '11:00', 'CONTAINER', 'D2', 'C002', 'CARRIER2', 'CLIENT2', '', 1, 0], // 2행 시작 시간 비움
['2026-08-30', '11:00', 'aa:bb', 'CONTAINER', 'D3', 'C003', 'CARRIER3', 'CLIENT3', '', 1, 0], // 3행 종료 시간 형식 오류
['2026-08-30', '13:00', '12:00', 'CONTAINER', 'D4', 'C004', 'CARRIER4', 'CLIENT4', '', 1, 0], // 4행 종료<시작
['2026-08-30', '14:00', '15:00', 'XYZ', 'D5', 'C005', 'CARRIER5', 'CLIENT5', '', 1, 0], // 5행 잘못된 유형
['2026-08-30', '16:00', '17:00', 'CONTAINER', 'D6', 'C006', 'CARRIER6', 'CLIENT6', '', 'ABC', 0] // 6행 수량 문자
];
const result = validateBulkSlots(sampleRows);
Logger.log(JSON.stringify(result, null, 2));
if (result.ok) {
Logger.log('전체 통과했습니다. 통과 행 수: ' + result.cleanedRows.length);
} else {
Logger.log('오류 행 수: ' + result.errors.length);
result.errors.forEach(function (err) {
Logger.log(err.rowIndex + '행 오류: ' + err.message);
});
}
}제대로 됐는지 확인하는 법: 실행 후 실행 로그를 열었을 때, 최소한 다음과 비슷한 메시지가 보이면 정상입니다.
2행 오류: 2행: 시작 시간 없음3행 오류: 3행: 종료 시간 형식이 잘못되었습니다4행 오류: 4행: 종료 시간이 시작 시간보다 이후여야 합니다5행 오류: 5행: 허용되지 않은 장비유형입니다6행 오류: 6행: 수량이 잘못되었습니다
또한 cleanedRows 에는 1행(정상 케이스)만 포함되어 있어야 합니다.
실무에서 써 본 일괄 검증 설계 팁
실제 현장에서 이 구조를 돌려 보며 얻은 교훈 몇 가지를 정리하면 다음과 같습니다.
- 검증과 저장을 반드시 분리하는 것이 좋습니다.
한 함수 안에서 동시에 검증·저장·롤백까지 처리하려고 하면, 어디에서 막혔는지가 운영자에게 보이지 않습니다. 지금 만든 validateBulkSlots 처럼 “메모리 상에서만 검사하고 결과를 돌려주는 함수”를 두고, 별도 함수에서 LockService로 잠근 뒤 실제 저장을 하는 구조가 설명하기도 쉽고, 장애 분석에도 유리했습니다.
- 정원 계산과 중복 검사는 검증 단계와 저장 단계 모두에서 한 번씩 보는 편이 안전했습니다.
검증 시점과 저장 시점 사이에 다른 사용자가 같은 슬롯에 예약을 넣을 수 있기 때문입니다. 검증 단계에서는 “지금 이 데이터가 상식적으로 말이 되는지”를 확인하고, 저장 단계에서는 “지금 시점에 실제로 자리가 남아 있는지”를 다시 확인하는 식입니다. 정원 계산 그 자체는 [입고예약 등록 3편]·[4편] 에서 이미 만든 BOOK_getSeqUsageForSlotCore_()·BOOK_checkCapacityAndSave_() 로 통일해 두고, 이 편에서는 그 함수를 그대로 호출만 합니다.
- 에러 메시지는 가능하면 줄 번호 + 한국어 사유 조합으로 통일하는 것을 권장합니다.
"5행: 휴무일에는 예약할 수 없습니다" 같이 남겨 두면, 현장 담당자가 스크린샷 한 장으로도 어느 줄을 어떻게 고쳐야 하는지 바로 알 수 있습니다. INVALID_TIME 같은 추상적인 코드는 디버깅에는 편할 수 있지만, 실제 운영자에게는 설명이 한 번 더 필요한 표현입니다.
자주 발생하는 오류와 해결 방법
일괄 검증 코드를 처음 붙였을 때 자주 나왔던 오류와 해결 방법을 몇 가지 정리합니다.
APPT_ymd_ is not defined오류
이 함수는 앞 갈래에서 이미 만든 날짜 정규화 헬퍼입니다. 이 프로젝트에 아직 해당 코드를 붙여 넣지 않았거나, 함수 이름을 다른 파일에서 바꾼 경우에 이런 오류가 납니다.
- [입고예약 기본 5편] 글의
APPT_ymd_정의를 현재 프로젝트에도 그대로 넣었는지, - 이름을 변경하지 않았는지
를 확인하면 해결됩니다.
- 모든 날짜에서 “휴무일에는 예약할 수 없습니다”가 뜨는 경우
대부분은 getDateBlockInfo() 가 기대하는 'YYYY-MM-DD' 형식과 다른 값을 받아서, 휴무일 기본값으로 처리되었을 가능성이 큽니다.
- 이 글의 코드처럼
APPT_ymd_(rawDate)결과를 그대로 넘기는지, - 중간에 다른 포맷으로 바꾸고 있지는 않은지
를 확인해야 합니다. 테스트 시에는 실제로 운영일로 설정된 날짜를 사용하는 것도 중요합니다.
- 수량이나 팔레트 칸에
ABC같은 문자를 넣었을 때 통과하는 문제
예전에는 단순히 Number(rawQty) <= 0 만 검사해 NaN 이 조용히 통과하는 문제가 생길 수 있습니다. 지금 코드에서는
!Number.isFinite(qtyNum) || qtyNum <= 0조건을 먼저 보도록 해, 문자·공백·혼합 형식을 모두 “잘못된 수량”으로 잡도록 했습니다. 테스트 함수에 이미 문자를 넣어 두었으니, 이 부분이 제대로 잡히는지 실행 로그로 확인해 보시면 됩니다.
- 시간대 관련 미묘한 버그
자주 보던 패턴이 new Date(ymd + 'T00:00:00') 같이 문자열에 T00:00:00 을 붙이는 방식입니다. 이 경우 브라우저나 실행 환경에 따라 UTC 기준으로 해석되면서 하루가 어긋나는 문제가 생길 수 있습니다. 이 글에서는 반드시
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
const dateObj = new Date(year, month - 1, day);
const timeObj = new Date(year, month - 1, day, hour, minute);같이 연·월·일을 숫자로 쪼개서 new Date(year, month-1, day, ...) 생성자를 쓰는 방식으로 통일했습니다.
정리
이번 글에서는 구글시트 일괄 예약 검증 방법의 첫 단계로, 엑셀에서 복사해 온 여러 예약 행을 구글시트에 붙여넣기 전에 Apps Script에서 한 번에 검증하는 validateBulkSlots(rows) 구조를 완성했습니다.
- 필수값
- 날짜·시간 형식
- 시작·종료 시간 관계
- 운영 시간
- 정원(용량)
- 붙여넣기 내 컨테이너 중복
- 수량·팔레트 유효성
까지 줄 단위로 확인하고, “몇 번째 줄이 왜 안 되는지”를 바로 보여 주는 뼈대를 갖춘 셈입니다.
지금 당장 할 수 있는 행동은 한 가지입니다.
- 위의
BULK_CONFIG,validateBulkSlots,BULK_testValidateSlots_세 블록을 자신의 Apps Script 프로젝트에 그대로 붙여넣고 - 테스트 함수를 실행해
- 실행 로그에서
- “2행: 시작 시간 없음”
- “3행: 종료 시간 형식이 잘못되었습니다”
- “4행: 종료 시간이 시작 시간보다 이후여야 합니다”
- “5행: 허용되지 않은 장비유형입니다”
- “6행: 수량이 잘못되었습니다”
같은 메시지가 기대한 대로 보이는지 확인해 보세요.
여기까지 통과했다면, 그다음 단계인 “통과한 줄만 예약 본문 시트(APPT_MAIN)에 실제로 저장하는 일괄 등록 함수”를 붙일 준비가 된 것입니다. 이 저장 단계와 LockService 적용, 실제 예약 본문 시트와의 연결은 다음 글에서 이어서 다루겠습니다.