구글시트 예약 현황 자동화 방법: 오늘 시트 만들기 — 대시보드 1편
도입: 오늘 예약만 한눈에 보고 싶은데, 매번 필터를 다시 거시나요
구글시트로 입고 예약을 관리하다 보면, 오늘 들어올 차량만 따로 모아서 보고 싶은 순간이 계속 생깁니다. 그때마다 날짜 필터를 다시 걸고, 시작시간으로 정렬을 바꾸고, 어제·내일 예약을 숨기는 작업을 반복하면 생각보다 많은 시간이 소모됩니다. 운영 현장에서 가장 자주 보는 화면인데, 손이 너무 많이 가는 구조입니다.
이 글은 제가 실제 물류센터에서 돌리고 있는 구글시트 Apps Script 예약 현황 자동화 방법 가운데, 첫 단계인 “오늘 예약 현황 시트 자동 생성” 부분만 떼어 정리한 것입니다. 원본 예약 시트(APPT_MAIN)는 그대로 두고, 오늘 예약만 시간순으로 정리한 읽기 전용 뷰 시트(TODAY 같은 이름)를 Apps Script로 자동 갱신하는 구조를 만듭니다. 한 번 설정해 두면 메뉴에서 버튼 한 번 눌러서 “오늘 뭐 들어오는지” 화면을 항상 최신 상태로 맞출 수 있습니다.
이 글은 입고 예약 시스템 대시보드 1편입니다. 전체 구조와 배경이 궁금하다면 먼저 구글시트 입고 예약 시스템 전체 구조: 왜 만들었고 어떤 순서로 이어지는가를 읽어 보셔도 좋습니다. 이 편에서는 그중에서도 “오늘 예약 현황” 시트를 만드는 실무 구현을 다룹니다.
왜 오늘 예약 전용 뷰 시트가 필요한가
입고 예약을 조금만 써 보면, 예약 입력 화면과 운영 현장에서 필요한 화면이 다르다는 점을 금방 알 수 있습니다. 예약 입력용 APPT_MAIN 시트에는 날짜, 시간, 장비 유형, 도어, 컨테이너, 운송사, 고객사, 메모, 예약ID까지 여러 열이 들어가 기록·조회에는 좋습니다. 하지만 운영자는 그중에서도 “오늘”, “지금”, “어느 도어”라는 정보만 빨리 보고 싶어 합니다.
실제로 운영자가 자주 던지는 질문은 다음과 비슷합니다.
- 오늘 컨테이너가 몇 대 예정되어 있는지
- 어느 시간대에 물량이 몰려 있는지
- 어떤 도어로, 어떤 운송사가 들어오는지
이때 APPT_MAIN에서 직접 필터와 정렬을 계속 바꾸다 보면, 여러 명이 동시에 사용할 때 순서가 꼬이거나 숨기지 말아야 할 열을 실수로 숨기는 일이 생깁니다. 이런 문제를 막으려면, 원본 시트는 데이터베이스처럼 두고, 오늘 것만 보여 주는 “전용 현황 시트”를 자동으로 생성해서 쓰는 편이 훨씬 안정적입니다.
저는 이 구조로 바꾼 뒤부터 현장 모니터에는 항상 오늘 뷰 시트만 띄워 두고, 예약 등록이나 수정은 APPT_MAIN에서만 하도록 분리했습니다. 데이터 수정은 한 곳에서만, 조회·모니터링은 여러 뷰 시트와 대시보드에서 하는 구조가 운영·교육·권한 관리까지 모두 수월했습니다.
이번 글에서 구현하는 핵심 함수는 두 가지입니다.
DASH_getApptDailyViewSheet_(): 오늘 뷰 시트를 준비하고 안내 문구·헤더·서식을 맞춰 둡니다.DASH_updateApptDailyView(): APPT_MAIN에서 오늘 예약만 골라 시작시간 기준으로 정렬한 뒤 뷰 시트에 한 번에 setValues 합니다.
이 두 가지만 갖추면, 추후 구글시트 웹앱 암호 보호 방법: 대시보드 5편처럼 웹 대시보드를 붙일 때도 같은 데이터를 그대로 재사용할 수 있습니다.
오늘 예약 뷰 시트 구조 설계: 최소 정보만, 읽기 전용으로
먼저 오늘 예약 현황 시트에 어떤 정보를 보여 줄지부터 정리합니다. 예시로 APPT_MAIN 열 구조를 다음과 같이 가정하겠습니다.
- A 날짜
- B 시작시간
- C 장비유형
- D 도어
- E 컨테이너
- F 운송사
- G 고객사
- H 비고
- I 종료시간
- J 생성일시
- K 수량
- L 팔레트
- M 예약ID
이 중 오늘 뷰 시트에서는 운영 관점에서 자주 보는 필드만 골라 다음 정도로 구성하는 방식을 권장합니다.
- 날짜
- 시작시간
- 종료시간
- 도어
- 컨테이너
- 운송사
- 고객사
- 수량
- 팔레트
- 예약ID
여기서 중요한 원칙이 두 가지 있습니다.
첫째, 뷰 시트는 읽기 전용 결과물이어야 합니다. TODAY 시트에서 직접 값을 고치면, 다음 갱신 시 APPT_MAIN 기준으로 다시 덮어써서 수정 내용이 사라집니다. 그래서 시트 상단 A1에 “이 시트는 자동 생성된 오늘 예약 현황이며, 직접 수정해도 다음 갱신 시 예약 시트 기준으로 덮어씁니다”라는 안내 문구를 넣어 혼선을 줄입니다.
둘째, 정렬 기준을 일관되게 정해 둡니다. 오늘 뷰 시트는 기본적으로 오늘만 보기 때문에, 정렬 기준은 대부분 B열 시작시간입니다. 이때 시작시간은 셀에 Date/Time 형식으로 저장해 두고, 구글시트에서는 표시 형식만 HH:mm으로 맞추는 방식을 추천합니다. 문자열로 시간을 저장해 두면 나중에 Apps Script에서 정렬하거나 계산할 때 오류를 찾기 어렵습니다.
이 구조를 바탕으로, 이제 구글시트에서 오늘 예약 현황 자동화 방법을 코드로 구현해 보겠습니다.
1단계 — 오늘 뷰 시트 준비 함수: DASH_getApptDailyViewSheet_()
이 단계에서는 오늘 예약 현황을 담을 시트(TODAY)를 준비합니다. 시트가 없으면 만들고, 있으면 내용을 초기화한 뒤 안내 문구와 헤더·서식을 다시 세팅합니다. 이렇게 하면 나중에 DASH_updateApptDailyView()가 여러 번 실행되더라도 항상 같은 구조로 유지됩니다.
1) 이 코드가 하는 일
- 현재 스프레드시트에서
TODAY시트를 찾거나 새로 만들고, 첫 줄에 안내 문구, 둘째 줄에 헤더와 서식을 세팅합니다.
2) 붙여넣을 위치
- 구글시트 →
확장 프로그램→Apps Script→ 기존 프로젝트의Code.gs맨 아래에 붙여넣습니다. - 이 글의 다른 코드와 이름이 겹치지 않도록, 상수·함수 이름에는
DASH_접두어를 사용합니다.
3) 붙여넣은 뒤 할 일
- 저장(Ctrl+S 또는 ⌘S) 후, 편집기 상단 함수 목록에서
DASH_getApptDailyViewSheet_를 선택해 한 번 실행하고 권한을 허용합니다.
// 오늘 예약 대시보드 설정 → 여기만 본인 시트 구조에 맞게 바꾸세요
const DASH_VIEW_SHEET_NAME = 'TODAY'; // → 오늘 현황 시트 이름
const DASH_TZ = 'America/New_York'; // → 시간대 고정
const DASH_APPT_MAIN_SHEET_NAME = 'APPT_MAIN'; // → 예약 원본 시트 이름
function DASH_getApptDailyViewSheet_() { // → 오늘 뷰 시트 준비 함수
const ss = SpreadsheetApp.getActiveSpreadsheet(); // → 현재 스프레드시트 가져오기
let sheet = ss.getSheetByName(DASH_VIEW_SHEET_NAME); // → TODAY 시트 찾기
if (!sheet) { // → 없으면
sheet = ss.insertSheet(DASH_VIEW_SHEET_NAME); // → 새 시트 만들기
}
sheet.clear(); // → 기존 내용 모두 지우기
// 안내 문구와 헤더 작성
const headerValues = [ // → 첫 두 줄 값 지정
[
'※ 이 시트는 오늘 입고 예약 현황을 자동으로 생성합니다. ' +
'직접 수정해도 다음 갱신 시 예약 시트(APPT_MAIN) 기준으로 덮어씁니다.'
],
[
'날짜', '시작시간', '종료시간',
'도어', '컨테이너', '운송사', '고객사',
'수량', '팔레트', '예약ID'
]
];
sheet.getRange(1, 1, headerValues.length, headerValues[1].length) // → A1~ 헤더 범위
.setValues(headerValues); // → 안내·헤더 입력
sheet.getRange('A1').setFontColor('#888888'); // → 안내 문구 회색 처리
sheet.getRange('A2:J2').setFontWeight('bold'); // → 헤더 굵게
sheet.setFrozenRows(2); // → 상단 두 줄 고정
// 열 너비·형식 지정
sheet.setColumnWidths(1, 3, 90); // → 날짜·시간 열 너비
sheet.setColumnWidth(4, 80); // → 도어 열 너비
sheet.setColumnWidths(5, 3, 120); // → 컨테이너~고객사 열 너비
sheet.setColumnWidths(8, 2, 70); // → 수량·팔레트 열 너비
sheet.setColumnWidth(10, 100); // → 예약ID 열 너비
sheet.getRange('A3:A').setNumberFormat('yyyy-mm-dd'); // → 날짜 표시 형식
sheet.getRange('B3:C').setNumberFormat('HH:mm'); // → 시간 표시 형식
return sheet; // → 준비된 시트 반환
}제대로 됐는지 확인하는 법: 스크립트 편집기에서 DASH_getApptDailyViewSheet_를 실행한 뒤 스프레드시트를 다시 열어 TODAY 시트가 생기고, A1에 안내 문구·A2:J2에 헤더가 보이면 성공입니다.
2단계 — 오늘 예약만 골라 그리기: DASH_updateApptDailyView()
이제 핵심인 오늘 예약만 읽어 와서 TODAY 시트를 다시 그리는 함수를 만듭니다. 실무에서 중요한 부분은 “한 줄씩 복사/삭제”를 반복하지 않고, 배열 단위로 필터링·정렬한 뒤 setValues로 한 번에 쓰는 구조로 만드는 것입니다. 이렇게 해야 속도가 빠르고, 여러 사람이 동시에 눌렀을 때도 동작이 안정적입니다.
이 함수는 다음과 같이 동작합니다.
- APPT_MAIN의 데이터 범위를 한 번에 읽습니다.
- 날짜가 “오늘”인 행만 필터링합니다.
- 시작시간이 비어 있는 행은 오늘 현황에서 제외합니다.
- 시작시간 기준으로 정렬합니다.
- TODAY 시트 3행부터 데이터를 덮어씁니다.
- 중간에 오류가 나더라도 LockService로 건 잠금은 마지막에 항상 해제합니다.
1) 이 코드가 하는 일
- APPT_MAIN에서 오늘 날짜의 예약만 골라 시작시간 순으로 정렬하고, TODAY 시트에 덮어씁니다.
2) 붙여넣을 위치
- 방금 만든
DASH_getApptDailyViewSheet_()함수 바로 아래에 이어서 붙여넣습니다.
3) 붙여넣은 뒤 할 일
- 저장 후, 스크립트 편집기에서
DASH_updateApptDailyView를 선택해 실행하고 권한을 허용합니다.
function DASH_updateApptDailyView() { // → 오늘 뷰 전체 갱신
const lock = LockService.getScriptLock(); // → 스크립트 잠금 객체
lock.waitLock(30000); // → 최대 30초 대기
try { // → 예외 처리 시작
const ss = SpreadsheetApp.getActiveSpreadsheet();// → 스프레드시트
const apptSheet = ss.getSheetByName(DASH_APPT_MAIN_SHEET_NAME); // → APPT_MAIN 시트
if (!apptSheet) { // → 시트 없으면
throw new Error('예약 시트(APPT_MAIN)를 찾을 수 없습니다.'); // → 오류 던지기
}
const lastRow = apptSheet.getLastRow(); // → 마지막 데이터 행
if (lastRow < 2) { // → 데이터가 없으면
const viewSheet = DASH_getApptDailyViewSheet_(); // → 뷰 시트 초기화
viewSheet.getRange('A3:J').clearContent(); // → 데이터 영역 지우기
return; // → 종료
}
const range = apptSheet.getRange(2, 1, lastRow - 1, 13); // → A2:M 범위
const values = range.getValues(); // → 모든 예약 데이터 가져오기
const today = new Date(); // → 현재 시각
const todayYmd = Utilities.formatDate(today, DASH_TZ, 'yyyy-MM-dd'); // → 오늘 ymd
const filtered = []; // → 오늘 예약을 담을 배열
for (let i = 0; i < values.length; i++) { // → 각 행 반복
const row = values[i]; // → 한 행 데이터
const dateVal = row[0]; // → A열 날짜
const startTime = row[1]; // → B열 시작시간
const endTime = row[8]; // → I열 종료시간
const type = row[2]; // → C열 장비유형
const door = row[3]; // → D열 도어
const cntr = row[4]; // → E열 컨테이너
const carrier = row[5]; // → F열 운송사
const client = row[6]; // → G열 고객사
const qty = row[10]; // → K열 수량
const pallet = row[11]; // → L열 팔레트
const apptId = row[12]; // → M열 예약ID
if (!dateVal) { // → 날짜가 비어 있으면
continue; // → 건너뛰기
}
// 날짜를 'yyyy-MM-dd'로 안전하게 비교
let rowYmd;
try { // → 날짜 변환 시도
rowYmd = APPT_ymd_(dateVal); // → 앞 편에서 만든 헬퍼 사용
} catch (e) { // → 변환 실패 시
continue; // → 그 행은 건너뛰기
}
if (rowYmd !== todayYmd) { // → 오늘이 아니면
continue; // → 건너뛰기
}
// 시작시간이 없는 행은 현황에서 제외
if (!startTime) { // → 시작시간 없으면
continue; // → 건너뛰기
}
// 수량·팔레트는 숫자인지 확인 후 아니면 빈 값 처리
const safeQty = Number.isFinite(Number(qty)) ? Number(qty) : ''; // → 수량
const safePallet = Number.isFinite(Number(pallet)) ? Number(pallet) : ''; // → 팔레트
filtered.push([
dateVal, // 날짜
startTime, // 시작시간
endTime, // 종료시간
door, // 도어
cntr, // 컨테이너
carrier, // 운송사
client, // 고객사
safeQty, // 수량
safePallet, // 팔레트
apptId // 예약ID
]);
}
// 시작시간 기준 정렬
filtered.sort(function (a, b) { // → 정렬 함수
const t1 = a[1]; // → a의 시작시간
const t2 = b[1]; // → b의 시작시간
// 시간 값이 Date 형식이면 getTime 기준으로 정렬
if (t1 instanceof Date && t2 instanceof Date) { // → 둘 다 Date이면
return t1.getTime() - t2.getTime(); // → 이른 시간 먼저
}
// 문자열인 경우 'HH:mm' 기준 비교
const s1 = String(t1); // → 문자열 변환
const s2 = String(t2); // → 문자열 변환
if (s1 < s2) return -1; // → s1이 더 이르면
if (s1 > s2) return 1; // → s2가 더 이르면
return 0; // → 같으면 0
});
const viewSheet = DASH_getApptDailyViewSheet_(); // → 뷰 시트 준비
const dataRange = viewSheet.getRange(3, 1, Math.max(filtered.length, 1), 10); // → A3:J
if (filtered.length === 0) { // → 오늘 예약이 없으면
dataRange.clearContent(); // → 데이터 영역 비우기
return; // → 종료
}
dataRange.setValues(filtered); // → 한 번에 값 쓰기
} finally { // → 예외 여부와 무관하게
lock.releaseLock(); // → 잠금 해제
}
}제대로 됐는지 확인하는 법: APPT_MAIN에 오늘 날짜로 테스트 예약을 1~2건 입력한 뒤 DASH_updateApptDailyView를 실행하고 TODAY 시트를 보면, 3행부터 오늘 예약만 시작시간 순으로 정렬되어 나오면 됩니다. 어제·내일 날짜는 보이지 않아야 합니다.
위 코드에서 사용하는 APPT_ymd_()는 날짜를 'YYYY-MM-DD' 형식으로 통일하는 헬퍼 함수입니다. 앞 편에서 이미 만들어 두었다면 그대로 재사용하면 되고, 이 글만 따로 쓰고 있다면 같은 규격으로 간단히 추가해 두면 좋습니다. 날짜 형식을 통일해 두면 “구글시트 오늘 예약 현황 시트 만들기”뿐 아니라 이후 통계·지표 계산에도 도움이 됩니다.
3단계 — 메뉴에 버튼 달기: onOpen과 대시보드 메뉴
현장에서는 스크립트 편집기에서 함수를 직접 실행하기보다는, 시트 상단 메뉴에 버튼을 달아 두고 쓰는 방식이 훨씬 편합니다. 이 단계에서는 “예약 도구”라는 메뉴를 만들고, 그 안에 “[오늘 예약 갱신]” 항목을 추가하겠습니다.
이 프로젝트에서 다른 글과 합쳐 쓸 가능성을 고려해, onOpen() 하나에 여러 도우미 함수를 부르는 구조를 사용합니다. 이번 편에서는 DASH_addDashboardMenu_(menu)라는 도우미만 추가하고, 나중에 다른 편에서 다른 도우미를 붙이는 방식으로 확장하면 됩니다.
1) 이 코드가 하는 일
- 스프레드시트를 열 때 상단에 “예약 도구” 메뉴를 만들고, 그 안에 “[오늘 예약 갱신]” 버튼을 추가합니다. 클릭하면
DASH_updateApptDailyView()가 실행됩니다.
2) 붙여넣을 위치
- 앞 단계 함수들 아래에 이어서 붙여넣습니다.
- 이미 다른 글에서
onOpen()을 사용 중이라면, 아래 전체를 넣지 말고 기존 onOpen 안에DASH_addDashboardMenu_(menu);한 줄만 추가합니다.
3) 붙여넣은 뒤 할 일
- 저장 후 스프레드시트를 새로고침합니다.
function onOpen() { // → 시트를 열 때 자동 실행
const ui = SpreadsheetApp.getUi(); // → UI 객체
const menu = ui.createMenu('예약 도구'); // → 상단 메뉴 생성
// 다른 편 도우미가 있다면 여기에서 함께 호출합니다.
// 예: addApptBaseMenu_(menu); APPT_addSettingsMenu_(menu); 등
DASH_addDashboardMenu_(menu); // → 이 편 대시보드 메뉴 추가
menu.addToUi(); // → 메뉴를 UI에 붙이기
}
function DASH_addDashboardMenu_(menu) { // → 대시보드 메뉴 도우미
menu.addItem('오늘 예약 갱신', 'DASH_updateApptDailyView'); // → 클릭 시 실행 함수
}제대로 됐는지 확인하는 법: 스프레드시트를 다시 열었을 때 상단 메뉴에 “예약 도구”가 생기고, 그 안에 “오늘 예약 갱신” 항목이 보이면 성공입니다. 이 메뉴를 누르면 TODAY 시트가 갱신되고, 오늘 예약이 3행부터 깔끔하게 다시 그려지는지 확인합니다.
실무에서 자주 생기는 문제와 예방 팁
날짜·시간 형식이 제각각인 데이터 다루기
실무에서는 예약을 수기로 입력하거나 여러 시스템에서 복사해 오다 보니, 날짜 형식이 2026-08-21, 8/21/2026, 텍스트 등 섞여 있는 경우가 많습니다. 이 상태에서 단순 비교를 하면 오늘 예약이 일부 누락되는 문제가 자주 발생합니다. APPT_ymd_()처럼 날짜를 'YYYY-MM-DD' 형식으로 강제 변환해 쓰면 이런 문제를 크게 줄일 수 있습니다. 이 글에서는 변환에 실패한 행은 조용히 건너뛰도록 설계해, 잘못된 날짜 하나 때문에 전체 갱신이 멈추지 않도록 했습니다.
오늘 예약이 없을 때도 오류 없이 빈 화면 유지하기
실제 운영에서 특정 날짜에는 예약이 한 건도 없을 수 있습니다. 이때 빈 배열을 setValues에 그대로 넘기면 오류가 납니다. 그래서 위 코드에서는 Math.max(filtered.length, 1)로 최소 1행 범위를 잡고, filtered.length === 0이면 clearContent()만 실행하고 바로 종료하도록 했습니다. TODAY 시트는 항상 구조가 유지되고, 데이터 영역만 깨끗하게 비워지기 때문에 사용자는 자연스럽게 “오늘 예약 없음” 상태를 인식할 수 있습니다.
여러 사람이 동시에 갱신할 때를 고려한 LockService 사용
대시보드 시트는 보통 모니터에 띄워 두고, 교대 시간마다 여러 사람이 “오늘 예약 갱신” 버튼을 누르는 경우가 많습니다. 이때 LockService를 사용하지 않으면 두 번의 clear()와 setValues()가 엇갈리면서 일시적으로 빈 화면이 보이거나, 일부 행만 보이는 상태가 될 수 있습니다. LockService.getScriptLock()으로 전체 함수를 감싸고, finally에서 releaseLock()을 호출해 잠금을 항상 해제하도록 만드는 것이 안전합니다. 이 습관은 나중에 “구글시트 입고 예약 현황 자동 갱신”을 시간 기반 트리거로 돌릴 때도 그대로 도움이 됩니다.
맺음말
이 글에서는 구글시트 예약 현황 자동화 방법 중 가장 기초가 되는 “오늘 예약만 자동으로 모아 보여 주는 TODAY 시트”를 만들어 보았습니다.
- TODAY 뷰 시트를 준비하고 안내 문구·헤더·서식을 세팅하는
DASH_getApptDailyViewSheet_() - APPT_MAIN에서 오늘 예약만 필터링해 시작시간 순으로 정렬하고 한 번에 setValues 하는
DASH_updateApptDailyView() - 시트 상단에 “예약 도구 → 오늘 예약 갱신” 메뉴를 추가해 현장에서 한 번 클릭으로 갱신하도록 하는
DASH_addDashboardMenu_()
까지 구현하면, 더 이상 오늘 화면을 만들기 위해 필터와 정렬을 손으로 바꿀 필요가 없습니다. 이 TODAY 뷰는 이후 대시보드(시간대별 입고량, 운송사별 물동량 등)를 올릴 수 있는 안정적인 기반이 됩니다.
바로 해 볼 수 있는 행동을 하나만 제안하겠습니다.
지금 사용 중인 예약 시트가 있다면, 위 코드에서 시트 이름 상수(DASH_APPT_MAIN_SHEET_NAME, DASH_VIEW_SHEET_NAME)만 본인 시트 이름에 맞게 바꾸고 DASH_updateApptDailyView()를 한 번 실행해 보십시오. 오늘 예약만 깔끔하게 정리된 전용 시트가 생기면, 그 위에 어떤 지표와 그래프를 올릴지 아이디어가 바로 떠오를 것입니다.