Smart Life US

← 목록 · 2026-08-14 · 엑셀·업무 자동화

구글시트 예약 운영시간 차단 설정: 구글시트 예약 시스템 만들기

구글시트 예약 운영시간 차단 설정: 구글시트 예약 시스템 만들기

도입: 창고 입고 예약, 운영시간 밖 예약을 어떻게 막을까

창고·물류 현장에서 구글시트로 입고 예약 시스템을 운영하다 보면 가장 먼저 부딪히는 문제가 있습니다. 담당자가 예약을 받아 두고 나중에 확인해 보니 운영시간 밖이거나 휴일인 경우입니다. 이때는 전화를 해서 시간을 다시 조정하고, 메일로 확인서를 다시 보내야 하므로 담당자와 기사 모두 불편이 커집니다. 이런 일을 줄이기 위해, 이 글에서는 구글시트 예약 운영시간 차단 설정을 적용해 요일별 운영시간과 휴일을 시트에 등록해 두고, 그 외 시간대에는 예약이 자동으로 막히도록 만드는 방법을 정리합니다. 핵심은 getDateBlockInfo(date) 함수로 날짜별 운영 가능 시간과 차단 구간을 계산해, 예약 생성 전에 검증 단계에서 거절하는 구조입니다.

예약 운영시간 차단 흐름

이 글은 실제 창고에서 사용 중인 구글시트 입고 예약 자동화 설계 경험을 바탕으로 한 “예약 등록 1단계”에 해당합니다. 앞선 글에서 SETTINGS 시트와 APPT_MAIN 등 기본 구조와 공통 설정 함수를 만들었다면, 이번 글에서는 그 위에 운영시간·휴일 차단 규칙을 얹어 예약 가능한 시간대만 열어 주는 캘린더 설정을 구현합니다. 여기서 만든 함수들은 이후 Web App·폼 예약 등과 바로 연결할 수 있도록 구성합니다.


CALENDAR_CONFIG·HOLIDAYS 시트 구조 설계

운영시간 차단을 구글 앱스스크립트로 구현하려면 먼저 사람이 보기 쉬운 시트 구조가 필요합니다. 실제 창고·물류 환경에서 유지보수하기 가장 편했던 방식은 CALENDAR_CONFIG, HOLIDAYS 두 시트를 나눠 두는 것입니다. 이 두 시트만 관리하면 운영시간 정책을 바꿀 수 있도록 설계합니다.

첫 번째는 CALENDAR_CONFIG 시트입니다. 요일별 기본 규칙을 한눈에 볼 수 있어야 하므로, MON~SUN 요일별로 한 줄씩만 두고 다음과 같이 열을 구성합니다.

  • DAY_OF_WEEK : 요일 코드(예: MON, TUE, SAT, SUN). 세 글자로 통일하면 코드에서 다루기 편합니다.
  • OPEN_TIME / CLOSE_TIME : 그 요일의 기본 운영 시작·종료 시간입니다. 형식은 HH:MM 으로 고정합니다.
  • LUNCH_START / LUNCH_END : 점심시간 등 기본적으로 막아야 하는 연속 구간입니다.
  • BLOCKED_SLOTS : 특정 요일에 추가로 막을 시간이 있으면 "10:00-11:00;15:00-15:30" 처럼 세미콜론으로 나열합니다.

두 번째는 HOLIDAYS 시트입니다. 여기에는 특정 날짜에 대한 예외 규칙을 적습니다. 공휴일, 재고조사, 설비 점검 등 실제 운영에서 자주 발생하는 휴무를 모두 이 시트 하나로 관리합니다. 기본 열 구조는 다음과 같습니다.

  • DATE : 구글시트 날짜 형식으로 입력합니다. 텍스트가 아닌 “날짜” 데이터여야 합니다.
  • TYPE : FULL(종일 휴무), PARTIAL(부분 휴무) 등 구분 코드입니다. 나중에 코드에서 이 값으로 분기합니다.
  • OPEN_TIME / CLOSE_TIME : TYPE이 PARTIAL인 경우에만 사용하는 운영 시간입니다.
  • NOTE : “추석 연휴”, “재고조사” 등 사유 메모를 남겨 나중에 문의 응대에 활용합니다.

이렇게 구조를 나눠 두면 “토요일 단축 운영을 없애고 휴무로 바꾼다” 같은 정책 변경이 생겨도 CALENDAR_CONFIG 한 줄만 수정하면 됩니다. 다음 해 공휴일을 미리 반영할 때도 HOLIDAYS 시트에 날짜와 TYPE만 추가하면 되고, 앱스스크립트 코드는 바뀌지 않습니다. 이처럼 구글시트 예약 가능시간 제한을 시트 기준으로 설계해 두면 운영팀이 직접 정책을 관리할 수 있어 실무 효율이 좋아집니다.


코드 1단계 — 캘린더 설정 시트 자동 생성 함수

먼저, CALENDAR_CONFIG·HOLIDAYS 시트를 자동으로 생성하는 함수를 만듭니다. 초기에 한 번만 실행하면 되지만, 새 파일에서 구조를 다시 만들 때 실수를 줄여 주기 때문에 실무에서 유용합니다.

이 코드는 “예약 도구” Apps Script 프로젝트의 Code.gs 맨 아래에 붙여넣습니다. 붙여넣은 뒤 스크립트 편집기에서 initCalendarConfig() 함수를 한 번 실행해 시트를 생성합니다.

이 코드는 다음 일을 합니다.

1) CALENDAR_CONFIG 시트가 없으면 만들고 헤더와 기본 요일 행을 채웁니다.

2) HOLIDAYS 시트가 없으면 만들고 헤더만 채웁니다.

3) 요일별 기본 운영시간 예시를 삽입해 수정만 하면 바로 쓸 수 있게 준비합니다.

Apps Script (JavaScript)
// 여기만 본인 환경에 맞게 바꾸세요
const BOOK_CAL_CONFIG_SHEET = 'CALENDAR_CONFIG';   // → 캘린더 설정 시트 이름
const BOOK_HOLIDAYS_SHEET   = 'HOLIDAYS';          // → 휴일 시트 이름

function initCalendarConfig() {                     // → 캘린더 설정 시트들을 만드는 함수
  const ss = SpreadsheetApp.getActive();           // → 현재 스프레드시트 가져오기

  // 기본 요일 코드와 예시 운영시간 설정
  const defaultRows = [                             // → 요일별 기본 설정 배열
    ['MON', '08:00', '17:00', '12:00', '13:00', ''], // → 월요일
    ['TUE', '08:00', '17:00', '12:00', '13:00', ''], // → 화요일
    ['WED', '08:00', '17:00', '12:00', '13:00', ''], // → 수요일
    ['THU', '08:00', '17:00', '12:00', '13:00', ''], // → 목요일
    ['FRI', '08:00', '17:00', '12:00', '13:00', ''], // → 금요일
    ['SAT', '08:00', '12:00', '',      '',      ''], // → 토요일(단축 운영 예시)
    ['SUN', '',      '',      '',      '',      'CLOSED'] // → 일요일 휴무 예시
  ];

  // CALENDAR_CONFIG 시트 생성 및 헤더 세팅
  let calSheet = ss.getSheetByName(BOOK_CAL_CONFIG_SHEET); // → 시트 존재 여부 확인
  if (!calSheet) {                               // → 없으면 새로 만듦
    calSheet = ss.insertSheet(BOOK_CAL_CONFIG_SHEET);      // → 시트 생성
    const headers = [                            // → 헤더 행 정의
      'DAY_OF_WEEK',                             // → 요일 코드(MON 등)
      'OPEN_TIME',                               // → 운영 시작
      'CLOSE_TIME',                              // → 운영 종료
      'LUNCH_START',                             // → 점심 시작
      'LUNCH_END',                               // → 점심 종료
      'BLOCKED_SLOTS'                            // → 추가 차단 구간
    ];
    calSheet.getRange(1, 1, 1, headers.length).setValues([headers]); // → 헤더 입력
    calSheet.getRange(2, 1, defaultRows.length, defaultRows[0].length)
      .setValues(defaultRows);                   // → 기본 요일 행 입력
    calSheet.setFrozenRows(1);                   // → 헤더 고정
  }

  // HOLIDAYS 시트 생성 및 헤더 세팅
  let holidaySheet = ss.getSheetByName(BOOK_HOLIDAYS_SHEET); // → 휴일 시트 확인
  if (!holidaySheet) {                          // → 없으면 생성
    holidaySheet = ss.insertSheet(BOOK_HOLIDAYS_SHEET);      // → 시트 생성
    const headers = [                           // → 휴일 헤더 정의
      'DATE',                                   // → 날짜
      'TYPE',                                   // → FULL / PARTIAL
      'OPEN_TIME',                              // → 부분 운영 시작
      'CLOSE_TIME',                             // → 부분 운영 종료
      'NOTE'                                    // → 비고
    ];
    holidaySheet.getRange(1, 1, 1, headers.length).setValues([headers]); // → 헤더 입력
    holidaySheet.setFrozenRows(1);              // → 헤더 고정
  }
}

제대로 됐는지 확인하는 법: 스크립트 편집기에서 initCalendarConfig 를 실행한 뒤 시트 탭에 CALENDAR_CONFIG, HOLIDAYS 가 생기고, 각각 헤더와 요일 기본값이 들어가 있으면 성공입니다.


코드 2단계 — 캘린더 설정 읽기·캐시하기

이제 CALENDAR_CONFIG·HOLIDAYS에 적힌 내용을 스크립트에서 읽어 객체 형태로 보관하는 함수를 만듭니다. 예약 검증 로직에서는 매번 시트를 읽지 않고 메모리에 올려둔 설정을 재사용해야 성능이 안정적입니다. 이 글에서는 스크립트 실행 동안 재사용하는 “메모리 캐시” 방식으로 구현합니다.

이 코드는 같은 Apps Script 프로젝트의 Code.gs 에 바로 이어서 붙여넣습니다. 붙여넣은 뒤에는 BOOK_testLoadCalendarConfig_() 를 실행해 읽힌 구조를 실행 로그에서 확인합니다.

이 코드는 다음 작업을 합니다.

1) CALENDAR_CONFIG 시트를 읽어 요일 코드별로 운영시간, 점심시간, BLOCKED_SLOTS를 정리합니다.

2) HOLIDAYS 시트를 읽어 날짜(yyyy-MM-dd)를 키로 하는 객체에 TYPE·OPEN_TIME·CLOSE_TIME·NOTE를 저장합니다.

3) 한 번 읽은 결과를 BOOK_calendarCache 변수에 넣어 같은 실행 내에서 재사용합니다.

Apps Script (JavaScript)
// 캘린더 설정을 메모리에 보관할 캐시 변수
let BOOK_calendarCache = null;                   // → 한 번 읽은 설정 재사용

function loadCalendarConfig_() {                 // → 시트에서 운영시간·휴일 설정 읽기
  if (BOOK_calendarCache) {                      // → 이미 캐시가 있으면
    return BOOK_calendarCache;                   // → 그대로 반환
  }

  const ss = SpreadsheetApp.getActive();         // → 현재 스프레드시트
  const calSheet = ss.getSheetByName(BOOK_CAL_CONFIG_SHEET);  // → 캘린더 시트
  const holidaySheet = ss.getSheetByName(BOOK_HOLIDAYS_SHEET); // → 휴일 시트

  if (!calSheet) {                               // → 시트 없으면
    throw new Error('CALENDAR_CONFIG 시트가 없습니다. initCalendarConfig()를 먼저 실행하세요.'); // → 오류 알림
  }
  if (!holidaySheet) {                           // → 시트 없으면
    throw new Error('HOLIDAYS 시트가 없습니다. initCalendarConfig()를 먼저 실행하세요.');      // → 오류 알림
  }

  // CALENDAR_CONFIG 읽기
  const calValues = calSheet.getDataRange().getValues(); // → 전체 범위 읽기
  const calHeaders = calValues[0];              // → 첫 행은 헤더
  const dayConfigMap = {};                      // → 요일별 설정 보관 객체

  for (let i = 1; i < calValues.length; i++) {  // → 데이터 행 반복
    const row = calValues[i];                   // → 현재 행
    const day = String(row[0]).trim();          // → DAY_OF_WEEK 값
    if (!day) {                                 // → 비어 있으면
      continue;                                 // → 건너뜀
    }
    dayConfigMap[day] = {                       // → 요일별 설정 저장
      open: String(row[1] || '').trim(),        // → OPEN_TIME
      close: String(row[2] || '').trim(),       // → CLOSE_TIME
      lunchStart: String(row[3] || '').trim(),  // → LUNCH_START
      lunchEnd: String(row[4] || '').trim(),    // → LUNCH_END
      blockedSlots: String(row[5] || '').trim() // → BLOCKED_SLOTS
    };
  }

  // HOLIDAYS 읽기
  const holidayValues = holidaySheet.getDataRange().getValues(); // → 전체 범위
  const holidaysMap = {};                         // → 날짜별 휴일 정보 보관

  for (let i = 1; i < holidayValues.length; i++) { // → 데이터 행 반복
    const row = holidayValues[i];                // → 현재 행
    const dateVal = row[0];                      // → DATE 값
    if (!dateVal) {                              // → 비어 있으면
      continue;                                  // → 건너뜀
    }
    const jsDate = new Date(dateVal);            // → JS Date로 변환
    if (isNaN(jsDate.getTime())) {               // → 잘못된 날짜면
      continue;                                  // → 건너뜀
    }
    const ymd = Utilities.formatDate(jsDate, Session.getScriptTimeZone(), 'yyyy-MM-dd'); // → 기준 포맷
    holidaysMap[ymd] = {                         // → 날짜별 휴일 설정 저장
      type: String(row[1] || '').trim(),         // → TYPE
      open: String(row[2] || '').trim(),         // → OPEN_TIME
      close: String(row[3] || '').trim(),        // → CLOSE_TIME
      note: String(row[4] || '').trim()          // → NOTE
    };
  }

  BOOK_calendarCache = {                         // → 캐시에 묶어서 저장
    days: dayConfigMap,                          // → 요일별 기본 설정
    holidays: holidaysMap                        // → 날짜별 휴일 설정
  };

  return BOOK_calendarCache;                     // → 캐시 반환
}

// 테스트용: 설정이 제대로 읽히는지 로그로 확인
function BOOK_testLoadCalendarConfig_() {        // → 테스트 함수
  const cfg = loadCalendarConfig_();             // → 설정 읽기
  Logger.log(JSON.stringify(cfg, null, 2));      // → 내용 로그 출력
}

제대로 됐는지 확인하는 법: 편집기에서 BOOK_testLoadCalendarConfig_ 를 실행한 뒤 “실행 로그”를 열어 보면 daysholidays 구조가 JSON으로 출력됩니다. 요일별 OPEN_TIME 등이 보이고, HOLIDAYS에 입력한 날짜·TYPE이 보이면 정상입니다.


코드 3단계 — 시간 문자열 비교 유틸리티

이제 요일별 운영시간과 휴일을 활용하려면 '09:00' 같은 시간 문자열을 정확히 비교할 수 있어야 합니다. 단순 문자열 비교는 "10:00""9:30" 을 제대로 처리하지 못하므로, “분 단위 숫자”로 변환한 뒤 비교하는 보조 함수를 별도로 두는 것이 안전합니다.

이 코드는 Code.gs 에 계속 이어서 붙여넣습니다. 이 단계는 예약 저장을 건드리지 않으므로 LockService는 사용하지 않고, 입력값 검증을 통해 잘못된 형식이 들어왔을 때는 즉시 오류를 던지도록 합니다.

Apps Script (JavaScript)
// 'HH:MM' 문자열을 분 단위 숫자로 변환
function BOOK_timeToMinutes_(timeStr) {          // → 시간 문자열을 숫자로 변환
  const t = String(timeStr || '').trim();        // → 문자열 정리
  if (!t) {                                      // → 비어 있으면
    return null;                                 // → null 반환
  }
  const parts = t.split(':');                    // → 시와 분 분리
  if (parts.length !== 2) {                      // → 형식 다르면
    return null;                                 // → null 반환
  }
  const h = Number(parts[0]);                    // → 시 부분 숫자 변환
  const m = Number(parts[1]);                    // → 분 부분 숫자 변환
  if (!Number.isFinite(h) || !Number.isFinite(m)) { // → 숫자 검사
    return null;                                 // → 잘못된 값
  }
  return h * 60 + m;                             // → 전체 분 수
}

// 두 시간 문자열 비교: a < b 이면 음수, 같으면 0, 크면 양수
function compareTime_(a, b) {                    // → 시간 비교
  const ma = BOOK_timeToMinutes_(a);             // → a 변환
  const mb = BOOK_timeToMinutes_(b);             // → b 변환
  if (ma === null || mb === null) {              // → 변환 실패 시
    throw new Error('시간 형식이 잘못되었습니다: ' + a + ', ' + b); // → 오류
  }
  return ma - mb;                                // → 차이 반환
}

// 특정 시간이 구간 안에 있는지 확인
function isTimeInRange_(time, start, end) {      // → 구간 포함 여부
  const mt = BOOK_timeToMinutes_(time);          // → 기준 시간
  const ms = BOOK_timeToMinutes_(start);         // → 시작 시간
  const me = BOOK_timeToMinutes_(end);           // → 종료 시간
  if (mt === null || ms === null || me === null) { // → 변환 실패 시
    throw new Error('시간 형식이 잘못되었습니다: ' + time + ', ' + start + ', ' + end); // → 오류
  }
  return mt >= ms && mt < me;                    // → 시작 이상, 종료 미만이면 true
}

아래 테스트 함수에서 기대값 검증까지 포함해 자동으로 확인할 수 있게 구성합니다.

Apps Script (JavaScript)
function BOOK_testCompareTime_() {               // → 시간 비교 테스트
  const test1 = compareTime_('09:00', '10:00');  // → 음수 기대
  if (test1 >= 0) {
    throw new Error('FAIL: compareTime음수기대 got ' + test1);
  }

  const test2 = isTimeInRange_('09:30', '09:00', '10:00'); // → true 기대
  if (test2 !== true) {
    throw new Error('FAIL: isTimeInRange true기대 got ' + test2);
  }

  Logger.log('✓ 테스트 통과');                  // → 기대대로 동작
}

BOOK_testCompareTime_ 를 실행했을 때 에러 없이 완료되고 로그에 ✓ 테스트 통과 가 찍히면 유틸리티가 의도대로 동작하는 것입니다.


코드 4단계 — 날짜별 운영시간·휴일 정보 반환 함수

이제 이 글의 핵심 함수인 getDateBlockInfo(date) 를 구현합니다. 이 함수는 Web App이나 예약 생성 로직에서 “이 날짜에 이 시간으로 예약이 가능한가”를 판단하기 위한 기준 정보를 제공합니다. 구조적으로는 운영시간 범위와 차단 구간 목록을 함께 돌려주는 구조로 설계합니다.

이 함수는 다음 순서로 동작합니다.

1) 인자로 받은 Date 객체를 스크립트 타임존 기준 yyyy-MM-dd 문자열로 변환합니다.

2) loadCalendarConfig_() 를 호출해 요일별 기본 설정(dayConfigMap)과 HOLIDAYS 정보(holidaysMap)를 가져옵니다.

3) 먼저 HOLIDAYS에서 해당 날짜를 찾고, TYPE이 FULL 이면 종일 휴무로 처리합니다.

  • 이때 {open: null, close: null, reason: NOTE} 구조의 의미를 살려, blocks 배열에 00:00~24:00 전체 차단 구간과 휴무 사유를 담아 반환합니다.

4) FULL 휴무가 아니면 요일 코드(SUN~SAT)를 기준으로 CALENDAR_CONFIG 행을 가져옵니다. OPEN_TIME/CLOSE_TIME가 비어 있으면 요일 휴무로 간주하고, 마찬가지로 00:00~24:00 차단으로 돌려줍니다.

5) 점심시간과 BLOCKED_SLOTS를 “차단 구간 배열”에 넣어 반환합니다.

6) HOLIDAYS가 PARTIAL이면 open·close 값을 HOLIDAYS의 값을 우선해 덮어씁니다.

코드는 같은 Code.gs 에 이어 작성합니다.

Apps Script (JavaScript)
// 날짜별 운영시간·차단 구간 정보를 돌려줍니다.
function getDateBlockInfo(date) {                // → 날짜별 예약 가능 정보 조회
  if (!(date instanceof Date)) {                 // → Date 타입 검사
    throw new Error('getDateBlockInfo에는 Date 객체를 넘겨야 합니다.'); // → 잘못된 인자
  }

  const tz = Session.getScriptTimeZone();        // → 스크립트 타임존
  const ymd = Utilities.formatDate(date, tz, 'yyyy-MM-dd'); // → 기준 날짜 문자열
  const cfg = loadCalendarConfig_();             // → 캐시된 설정 가져오기
  const dayConfigMap = cfg.days;                 // → 요일별 기본 설정 맵
  const holidaysMap = cfg.holidays;              // → 날짜별 휴일 설정 맵

  // 1. 휴일 여부 확인
  const holiday = holidaysMap[ymd];              // → 해당 날짜 휴일 설정
  if (holiday && holiday.type === 'FULL') {      // → 종일 휴무
    return {                                     // → 정보 반환
      date: ymd,                                 // → 날짜
      isHoliday: true,                           // → 휴일 여부
      holidayType: 'FULL',                       // → 휴일 타입
      open: null,                                // → 운영 없음
      close: null,                               // → 운영 없음
      blocks: [{                                 // → 종일 차단 구간
        start: '00:00',                          // → 시작
        end: '24:00',                            // → 끝
        reason: holiday.note || '휴무'           // → 사유 (NOTE 기반)
      }]
    };
  }

  // 2. 요일별 기본 설정 가져오기
  const dayIndex = date.getDay();                // → 0(일)~6(토)
  const dayCodes = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']; // → 코드 배열
  const dayCode = dayCodes[dayIndex];            // → 현재 요일 코드
  const base = dayConfigMap[dayCode];            // → 요일별 기본 설정

  // 요일 휴무(운영시간이 비어 있는 경우) 처리
  if (!base || !base.open || !base.close) {      // → 운영시간이 비어 있으면
    return {                                     // → 운영 없음으로 처리
      date: ymd,
      isHoliday: true,                           // → 실질적으로 휴무
      holidayType: 'CLOSED',                     // → 요일 휴무
      open: null,
      close: null,
      blocks: [{
        start: '00:00',
        end: '24:00',
        reason: '요일 휴무'
      }]
    };
  }

  let open = base.open;                          // → 기본 운영 시작
  let close = base.close;                        // → 기본 운영 종료
  const blocks = [];                             // → 차단 구간 배열

  // 3. 점심시간을 차단 구간으로 추가
  if (base.lunchStart && base.lunchEnd) {        // → 점심 구간이 있으면
    blocks.push({                                // → 차단 리스트에 추가
      start: base.lunchStart,                    // → 점심 시작
      end: base.lunchEnd,                        // → 점심 종료
      reason: '점심'                              // → 사유
    });
  }

  // 4. BLOCKED_SLOTS 파싱 (예: "10:00-11:00;15:00-15:30")
  if (base.blockedSlots) {                       // → 추가 차단 구간이 있으면
    const parts = base.blockedSlots.split(';');  // → 세미콜론 기준 분할
    parts.forEach(part => {                      // → 각 구간 처리
      const p = part.trim();                     // → 공백 제거
      if (!p) return;                            // → 비어 있으면 건너뜀
      const range = p.split('-');                // → 시작-끝 분리
      if (range.length !== 2) return;            // → 형식 안 맞으면 건너뜀
      blocks.push({                              // → 차단 구간 추가
        start: range[0].trim(),                  // → 시작
        end: range[1].trim(),                    // → 종료
        reason: '추가 차단'                       // → 사유
      });
    });
  }

  // 5. 부분 휴무일이면 운영시간을 덮어씀
  if (holiday && holiday.type === 'PARTIAL') {   // → 부분 휴무 설정 있으면
    if (holiday.open && holiday.close) {         // → 시간이 들어 있으면
      open = holiday.open;                       // → 운영 시작 재설정
      close = holiday.close;                     // → 운영 종료 재설정
    }
  }

  return {                                       // → 최종 정보 반환
    date: ymd,                                   // → 날짜
    isHoliday: Boolean(holiday),                 // → 휴일 여부
    holidayType: holiday ? holiday.type : null,  // → 휴일 타입
    open: open,                                  // → 운영 시작
    close: close,                                // → 운영 종료
    blocks: blocks                               // → 차단 구간 목록
  };
}

// 테스트: 오늘 날짜에 대한 운영정보 확인
function BOOK_testGetDateBlockInfo() {           // → 테스트 함수
  const today = new Date();                      // → 오늘 날짜
  const info = getDateBlockInfo(today);          // → 정보 조회
  Logger.log(JSON.stringify(info, null, 2));     // → 로그 출력
}

제대로 됐는지 확인하는 법은 다음과 같습니다.

1) HOLIDAYS 시트에 오늘 날짜를 DATE 열에 입력하고 TYPE을 FULL 로 적습니다.

2) 스크립트 편집기에서 BOOK_testGetDateBlockInfo 를 실행합니다.

3) 실행 로그에서 isHoliday: true, holidayType: "FULL", open: null, close: null, blocks00:00~24:00 구간과 NOTE 에 적은 사유가 보이면 운영시간 밖 예약이 막히는 조건이 정상적으로 계산된 것입니다.

4) HOLIDAYS 해당 행을 지우고 다시 테스트하면, 요일별 CALENDAR_CONFIG에 설정한 운영시간과 점심시간·BLOCKED_SLOTS가 반영된 구조가 출력됩니다.

5) HOLIDAYS TYPE을 PARTIAL 로 두고 OPEN_TIME / CLOSE_TIME 에 다른 시간을 넣은 뒤 다시 실행하면, 반환된 open / close 값이 PARTIAL 설정으로 덮어써지는 것을 확인할 수 있습니다.

이제 Web App이나 예약 저장 함수에서 getDateBlockInfo() 를 호출해, 요청된 시간대가 open~close 사이인지, blocks 목록에 걸리지 않는지 검사한 뒤에만 예약을 저장하면 Apps Script 운영시간 설정 방법을 스크립트 전체에 일관되게 적용할 수 있습니다.


실무 팁: 운영시간·휴일 차단 규칙을 운용하며 느낀 점

창고 입고 예약 자동화를 실제 돌리면서 몇 가지 공통된 패턴이 보였습니다. 구글 앱스스크립트 휴일 차단 설정을 도입할 때 함께 고려하면 도움이 되는 부분을 정리합니다.

  1. BLOCKED_SLOTS에는 “자주 바뀌지 않는 규칙”만

CALENDAR_CONFIG의 BLOCKED_SLOTS에는 점심 외에 상·하차 피크 시간대 등 요일 공통으로 거의 고정된 차단 구간만 넣는 편이 좋습니다. 특정 고객사나 특정 운송사에만 적용되는 예외 시간은 영업 정책에 따라 자주 바뀌는 영역이라, 여기까지 섞어 두면 나중에 “이 구간을 왜 막아놨지?”를 추적하기 어렵습니다. 고객사별 예외는 이후 예약 검증 로직에서 별도 조건으로 분리하는 것이 관리에 유리합니다.

  1. HOLIDAYS TYPE 코드는 조직 내에서 합의된 값만 사용

코드에서는 FULL / PARTIAL 두 가지만 전제로 분기를 짰습니다. 실무에서도 이 두 코드만 쓰도록 룰을 정해 두는 것이 좋습니다. 누군가 full, half, partial_am 등을 임의로 쓰기 시작하면 시트와 코드의 정의가 어긋나고, 일부 날짜는 의도와 다르게 “일반 요일 규칙”으로 예약이 열릴 수 있습니다.

  1. 날짜·시간 형식은 데이터 유효성 검사로 강제

구글시트는 눈으로 볼 때는 날짜처럼 보여도 내부적으로는 텍스트인 경우가 많습니다. 특히 CSV 업로드나 복붙이 잦은 환경에서는 더 그렇습니다. HOLIDAYS 시트의 DATE 열에는 “날짜만 허용” 유효성 검사를 걸고, OPEN_TIME/CLOSE_TIME에는 HH:MM 패턴만 허용하도록 데이터 유효성 검사를 추가해 두면 코드에서 에러가 훨씬 줄어듭니다.

위 코드에서는 잘못된 DATE 값을 건너뛰도록 했지만, 운영팀 입장에서는 “휴일을 넣었는데 실제로는 안 먹히는” 상황이 생길 수 있으니, 주기적으로 시트의 경고 표시를 점검하는 절차를 두는 것이 좋습니다.

  1. 운영시간·휴일 판단 로직은 예약 로직과 분리

이번 편에서 만든 loadCalendarConfig_, getDateBlockInfo, 시간 비교 유틸은 “캘린더 규칙”에만 집중합니다. 실제 예약 저장 단계에서는 여기에 더해 LockService로 동시 실행을 막고, 동일 시간대 중복 예약, 고객사별 슬롯 제한 등의 로직이 붙게 됩니다. 이때 캘린더 관련 판단 코드를 별도로 모듈화해 두면, 내년에 휴일 정책이 크게 바뀌더라도 이 부분만 수정하면 되어 유지보수가 수월합니다.


맺음말

이번 글에서는 구글시트 예약 시스템 만들기에서 가장 먼저 갖춰야 할 기반인 운영시간·휴일 차단 설정을 정리했습니다. CALENDAR_CONFIG·HOLIDAYS 시트를 만들어 요일별 운영시간과 특정 날짜 휴일 정보를 관리하고, Apps Script에서 loadCalendarConfig_()·getDateBlockInfo() 로 이 정보를 불러와 날짜별 운영 가능 구간과 차단 구간을 계산하는 구조입니다. 또한 compareTime_, isTimeInRange_ 같은 유틸과 테스트 함수까지 함께 구현해, 코드가 의도대로 동작하는지 매번 자동으로 확인할 수 있게 했습니다.

이 구조를 한 번 만들어 두면 운영 정책 변경 시 코드가 아니라 시트만 수정하면 되기 때문에, 현장 팀이 스스로 운영을 조정할 수 있다는 점이 큰 장점입니다.

지금 바로 할 수 있는 행동은 하나입니다. Apps Script 편집기에서 initCalendarConfig() 를 실행해 CALENDAR_CONFIG·HOLIDAYS 시트를 생성하고, 실제 창고 운영시간과 휴일 일정을 입력해 보시기 바랍니다. 그다음 BOOK_testGetDateBlockInfo()BOOK_testCompareTime_() 를 실행해 오늘 날짜 기준 운영시간·차단 구간 그리고 시간 비교 유틸이 어떻게 동작하는지 확인해 보면, 다음 단계인 “예약 생성 시 운영시간 밖·휴일 자동 차단” 로직을 구현할 준비가 끝납니다.