구글시트 예약 KPI 집계 방법: 정시율·체류시간 자동 계산
도입: 구글시트 예약 KPI 집계 방법이 필요한 이유
구글시트로 입고 예약을 관리하다 보면 어느 순간부터 질문이 생깁니다.
“운송사별 정시 도착률은 어느 정도인지, 평균 체류시간은 얼마나 되는지, 매일 자동으로 보고 싶다.”
이 글은 이런 요구를 가진 운영자를 위한 구글시트 예약 KPI 집계 방법 안내입니다. Apps Script 코드 붙여넣기만으로 정시율과 체류시간을 자동 계산하도록 만드는 것을 목표로 합니다.
이 글은 입고 예약·체크인 시리즈 위에 올라가는 KPI 1편입니다. 앞 글들에서 이미 예약 시트(APPT_MAIN)를 만들고, 예약 생성과 체크인, 상태 변경, 오늘 현황 대시보드까지 연결해 두었습니다. 이제 그 위에 “정시율·체류시간”이라는 KPI 레이어를 얹습니다. 이 편에서는 일자별 KPI를 계산하는 핵심 함수와 시트 구조를 완성하고, 이후 편에서 운송사·고객사별 리포트와 대시보드로 확장할 예정입니다.
예약과 체크인까지 자동화해도 “얼마나 잘 운용되고 있는지” 숫자로 안 보이면 의사결정을 하기가 어렵다는 점을 운영에서 계속 느꼈습니다. 같은 데이터를 가지고도 구글시트 정시율 계산 자동화와 체류시간 집계를 Apps Script로 붙여 두면, 매일 반복되는 수작업 리포트 없이도 현황을 빠르게 확인할 수 있습니다.
KPI 설계: 무엇을 어떻게 계산할지 먼저 정하기
코드를 짜기 전에 먼저 “무엇을 KPI로 삼을지”를 명확히 정해야 합니다. 여기서는 두 가지 지표에 집중합니다.
1. 정시율(On-time Arrival Rate)
정시율의 분모는 “도착이 실제로 기록된 예약 건수”입니다. 예약만 있고 차량이 오지 않은 건은 분모에 넣지 않습니다. 분자는 그중에서 예약 시각과 실제 도착 시각의 차이가 설정한 기준(예: ±15분) 이내인 건수입니다.
정리하면:
- 분모: 도착 기록 있는 예약 건수
- 분자: 그중 정시 도착 건수
- 정시 기준 예시:
|도착시각 - 예약시각| ≤ 15분 - 계산식:
정시율(%) = (정시 도착 건수 ÷ 도착 기록 있는 건수) × 100
2. 평균 체류시간(Average Dwell Time)
체류시간은 차량이 사이트에 체크인한 시각부터 작업 완료·체크아웃까지 걸린 시간입니다. 도착 시각과 완료 시각이 모두 기록된 건만 계산 대상으로 삼고, 아직 처리 중인 예약은 뺍니다. 단위는 분으로 계산하고, 나중에 시트에서 시간·분 포맷으로 보정하면 됩니다.
3. 계산에서 제외할 조건 명확히 하기
정시율과 체류시간은 “들어갈 건은 넣고, 빠질 건은 확실히 빼는 것”이 더 중요합니다. 예를 들어 이런 행은 KPI에서 제외합니다.
- 도착 기록이 없는 예약
- 도착은 했지만 완료 시간이 비어 있어 아직 작업 중인 예약
- 날짜·시간 셀에 텍스트가 섞인 행(예: “지연”, “보류” 등)
이런 행을 0으로 취급해 평균에 포함하면 KPI가 실제보다 좋아 보일 수 있습니다. 이 글에서 구현하는 computeKpiForDate_() 함수는 날짜·시간을 Date와 숫자로 변환해 보고, 유한 숫자만 집계에 포함합니다. Number.isFinite() 검증에서 걸리는 값은 skipped로 따로 세고, Apps Script 로그에 날짜·건수 요약을 남깁니다. 이렇게 하면 분모가 기대보다 작을 때 “어디에서 빠졌는지”를 추적하기가 훨씬 쉽습니다.
KPI 시트 구조: 하루 한 줄로 쌓이는 KPI_DAILY 설계
예약 원본과 KPI는 가능하면 분리해서 관리하는 편이 안전합니다. 이 시리즈에서는 예약·체크인 데이터가 이미 APPT_MAIN에 쌓이고 있다고 가정하고, KPI 전용 시트 KPI_DAILY를 새로 둡니다.
추천 구조는 다음과 같습니다.
- A 열: 기준일(yyyy-MM-dd 문자열)
- B 열: 해당 날짜 전체 예약 건수
- C 열: 도착 기록 있는 예약 건수
- D 열: 정시 도착 건수
- E 열: 정시율(%)
- F 열: 평균 체류시간(분)
- G 열: 계산에서 제외된 예약 건수(skipped)
- H 열: KPI 계산 실행 시각
방식은 “하루에 한 줄씩 쌓는 구조”입니다. 같은 날짜를 여러 번 재계산해도 같은 날짜 행을 찾아 덮어쓰도록 만들기 때문에, KPI 산식·기준을 바꿨을 때도 부담 없이 재계산할 수 있습니다. 예약 원본(APPT_MAIN)은 손대지 않고 KPI만 별도로 보존되므로, 운영 데이터와 리포트 데이터를 분리하는 효과도 있습니다.
이 글의 saveDailyKpi_() 함수는 반드시 KPI_DAILY 시트를 기준일로 조회해, 있으면 갱신·없으면 추가하는 방식으로 동작합니다. 이렇게 해 두면 이후에 주간·월간 리포트를 만들거나, 입고예약 대시보드 1편 같은 뷰와 엮을 때도 구조를 바꿀 필요가 없습니다.
Apps Script 흐름: 정시율·체류시간 계산 절차
이번 글에서 구현하는 Apps Script 전체 흐름은 다음과 같습니다. 핵심은 하나의 함수 computeKpiForDate_(dateObj)이고, 나머지는 이 함수를 돕는 보조 함수와 테스트용 함수입니다.
- 입력 검증
- 인자로 받은 값이 유효한
Date객체인지 확인합니다. - 유효하지 않으면 예외를 던져, 잘못된 호출에서 조용히 틀린 KPI가 쌓이지 않게 합니다.
- 대상 날짜 문자열 만들기
- 시간대는 항상
'America/New_York'로 고정합니다. Utilities.formatDate()로yyyy-MM-dd문자열을 만들고, 이 문자열로 APPT_MAIN 행을 필터링합니다.
- 예약 데이터 읽기
APPT_MAIN에서 2행부터 마지막 행까지 한 번에 읽어 옵니다.- 시트 구조에 맞게 예약일·예약시각·체크인·체크아웃 열 번호를 상수로 지정합니다.
- 행 단위 계산
- 예약일이 대상 날짜와 같은 행만 골라
total을 증가시킵니다. - 예약 시각과 도착 시각이 모두 있는 행에 대해서만 정시 여부를 계산합니다.
- 도착·완료 시각이 모두 있는 행에 대해서만 체류시간(분)을 계산합니다.
- 날짜·시간 값이 잘못되어 유효하지 않거나, 분 단위로 바꿨을 때 숫자가 아니면
skipped로 보냅니다.
- 집계값 계산
- 도착 기록이 있는 건수(
arrived)가 0이면 정시율은 0으로 두고, NaN이 나오지 않게 합니다. - 체류시간 계측 건수(
dwellCount)가 0이면 평균 체류시간도 0으로 둡니다.
- 로그와 저장
- 요약 결과를
Logger.log()로 남겨 디버깅에 활용합니다. - 결과 객체를
saveDailyKpi_()로 넘겨KPI_DAILY에 upsert(기존 행 갱신 또는 새 행 추가)합니다. - 함수는 최종 결과 객체를 반환해 테스트 함수에서 다시 확인할 수 있게 합니다.
이 구조를 기준으로 필요한 코드를 단계별로 보겠습니다.
1단계 — KPI 설정 상수와 시트 구조 정의
먼저 이 KPI 기능에서만 사용하는 설정과 열 번호 상수를 정의합니다. 앞 시리즈에 있는 다른 상수들과 이름이 겹치지 않도록 모두 KPI_ 접두어를 붙였습니다.
1) 이 코드가 하는 일
- KPI 계산에 필요한 시트 이름, 열 번호, 시간대, 정시 기준(분)을 상수로 정의합니다.
2) 붙여넣을 위치
- 구글시트 → 확장 프로그램 → Apps Script → Code.gs 맨 위쪽(다른 시리즈 상수 위나 아래, 이미 있는 상수 이름과 겹치지 않는 곳)에 붙여넣습니다.
3) 붙여넣은 뒤 할 일
- 저장(⌘S 또는 Ctrl+S)을 한 번 눌러 둡니다. 이 단계에서는 별도 실행 함수가 없습니다.
const KPI_CONFIG = { // → KPI용 설정 모음
TZ: 'America/New_York', // → 고정 시간대
APPT_SHEET_NAME: 'APPT_MAIN', // → 예약 원본 시트 이름
KPI_SHEET_NAME: 'KPI_DAILY', // → 일일 KPI 시트 이름
ONTIME_MINUTES_THRESHOLD: 15 // → 정시 판정 기준(분)
};
// 여기만 본인 시트 구조에 맞게 바꾸세요
const KPI_COL_APPT_DATE = 1; // → APPT_MAIN: 예약일(열 A)
const KPI_COL_APPT_TIME = 2; // → APPT_MAIN: 예약 시작시간(열 B)
// 도착·완료 시각은 체크인/체크아웃 구현에 맞게 사용
// 아직 APPT_MAIN 에 체크인/체크아웃 열을 만들지 않았다면 그대로 0으로 두고,
// 나중에 열을 추가했을 때 실제 열 번호(1부터)를 넣어 주세요.
const KPI_COL_CHECKIN_AT = 0; // → 도착시각 열 번호(없으면 0)
const KPI_COL_CHECKOUT_AT = 0; // → 완료시각 열 번호(없으면 0)
const KPI_COL_KPI_DATE = 1; // → KPI_DAILY: 기준일(열 A)
const KPI_COL_KPI_TOTAL = 2; // → KPI_DAILY: 전체 예약 건수(열 B)
const KPI_COL_KPI_ARRIVED = 3; // → KPI_DAILY: 도착 기록 있는 건수(열 C)
const KPI_COL_KPI_ONTIME = 4; // → KPI_DAILY: 정시 도착 건수(열 D)
const KPI_COL_KPI_ONTIME_RATE = 5; // → KPI_DAILY: 정시율 % (열 E)
const KPI_COL_KPI_AVG_DWELL = 6; // → KPI_DAILY: 평균 체류시간 분(열 F)
const KPI_COL_KPI_SKIPPED = 7; // → KPI_DAILY: 계산 제외 건수(열 G)
const KPI_COL_KPI_CALC_AT = 8; // → KPI_DAILY: 계산 시각(열 H)KPI_COL_CHECKIN_AT과 KPI_COL_CHECKOUT_AT을 0으로 두면, 이 글의 코드는 도착·체류시간 계산 부분을 안전하게 건너뜁니다. 이후 시리즈에서 체크인/체크아웃 시트 구조를 실제로 확정한 뒤, 그 열 번호를 여기에 채워 넣으면 같은 KPI 코드를 그대로 사용할 수 있습니다.
2단계 — 일자별 KPI 계산 핵심 함수 구현
다음은 이 글의 핵심인 computeKpiForDate_()입니다. 지정한 날짜의 예약 데이터를 읽어 정시율과 평균 체류시간을 계산하고, 결과를 KPI_DAILY에 저장합니다. 입력 검증·숫자 검증·분모 0 처리·로그까지 포함해 한 번에 돌아가도록 구성했습니다.
1) 이 코드가 하는 일
- 지정한 날짜(Date 객체)의 예약 데이터에서 정시 도착률과 평균 체류시간을 계산하고 KPI_DAILY 시트에 반영합니다.
2) 붙여넣을 위치
- 방금 정의한 상수들 아래, 다른 함수들과 같은 수준에 붙여넣습니다.
3) 붙여넣은 뒤 할 일
- 4단계의
KPI_testComputeToday_()를 통해 테스트 실행만 해 주면 됩니다.
function computeKpiForDate_(dateObj) { // → 특정 일자의 KPI 계산
if (!(dateObj instanceof Date) || isNaN(dateObj.getTime())) { // → 입력이 유효한 날짜인지 확인
throw new Error('computeKpiForDate_: 유효한 Date 객체가 아닙니다.');
}
const ss = SpreadsheetApp.getActiveSpreadsheet(); // → 현재 스프레드시트 가져오기
const apptSheet = ss.getSheetByName(KPI_CONFIG.APPT_SHEET_NAME); // → 예약 시트 찾기
if (!apptSheet) { // → 시트 없으면 오류
throw new Error('예약 시트(APPT_MAIN)를 찾을 수 없습니다.');
}
const targetYmd = Utilities.formatDate( // → 기준일을 yyyy-MM-dd 문자열로
dateObj,
KPI_CONFIG.TZ,
'yyyy-MM-dd'
);
const lastRow = apptSheet.getLastRow(); // → 예약 시트 마지막 행
if (lastRow < 2) { // → 헤더만 있으면
const emptyResult = { // → KPI 계산할 데이터 없음
date: targetYmd,
total: 0,
arrived: 0,
ontime: 0,
ontimeRate: 0,
avgDwellMinutes: 0,
skipped: 0
};
saveDailyKpi_(emptyResult); // → 빈 결과도 시트에 반영
Logger.log(JSON.stringify(emptyResult)); // → 로그 남기기
return emptyResult; // → 결과 반환
}
const lastCol = apptSheet.getLastColumn(); // → 마지막 열 번호
const range = apptSheet.getRange(2, 1, lastRow - 1, lastCol); // → 데이터 범위
const values = range.getValues(); // → 2차원 배열로 읽기
let total = 0; // → 대상 날짜 예약 건수
let arrived = 0; // → 도착 기록 있는 건수
let ontime = 0; // → 정시 도착 건수
let dwellSum = 0; // → 체류시간 합(분)
let dwellCount = 0; // → 체류시간 계산된 건수
let skipped = 0; // → 계산에서 제외된 건수
const tz = KPI_CONFIG.TZ; // → 시간대 상수 사용
const threshold = KPI_CONFIG.ONTIME_MINUTES_THRESHOLD; // → 정시 기준(분)
for (let i = 0; i < values.length; i++) { // → 각 예약 행 반복
const row = values[i]; // → 현재 행 데이터
const apptDateCell = row[KPI_COL_APPT_DATE - 1]; // → 예약일 값
if (!apptDateCell) { // → 예약일이 비어 있으면
skipped++; // → 제외 건수 증가
continue; // → 다음 행
}
let apptYmd; // → 예약일 문자열
try {
const apptDateObj = new Date(apptDateCell); // → 날짜로 변환
if (isNaN(apptDateObj.getTime())) { // → Invalid Date면
skipped++; // → 제외 건수 증가
continue; // → 다음 행
}
apptYmd = Utilities.formatDate( // → 예약일을 문자열로
apptDateObj,
tz,
'yyyy-MM-dd'
);
} catch (e) { // → 변환 실패 시
skipped++; // → 제외 건수 증가
continue; // → 다음 행
}
if (apptYmd !== targetYmd) { // → 대상 날짜가 아니면
continue; // → 건너뛰기
}
total++; // → 대상 날짜 예약 건수 증가
const apptTimeCell = row[KPI_COL_APPT_TIME - 1]; // → 예약 시각 셀
const checkinCell = KPI_COL_CHECKIN_AT > 0 // → 도착시각 열 설정
? row[KPI_COL_CHECKIN_AT - 1] // → 설정된 열이면 읽기
: null; // → 아니면 null
const checkoutCell = KPI_COL_CHECKOUT_AT > 0 // → 완료시각 열 설정
? row[KPI_COL_CHECKOUT_AT - 1] // → 설정된 열이면 읽기
: null; // → 아니면 null
// 예약·도착 시각이 모두 있는 경우에만 정시/지각 판정
if (apptTimeCell && checkinCell) { // → 두 값 다 있으면
let apptDateTime; // → 예약 DateTime
try {
apptDateTime = new Date(apptDateCell); // → 예약일 시각 00:00 기준
if (apptDateTime instanceof Date && !isNaN(apptDateTime.getTime())) {
// 예약 시각이 Date 로 들어온 경우와 문자열로 들어온 경우만 단순 처리
let apptHours = 0;
let apptMinutes = 0;
if (apptTimeCell instanceof Date) {
apptHours = apptTimeCell.getHours();
apptMinutes = apptTimeCell.getMinutes();
} else if (typeof apptTimeCell === 'string') {
const parts = apptTimeCell.split(':');
if (parts.length >= 2) {
apptHours = Number(parts[0]);
apptMinutes = Number(parts[1]);
}
}
if (!Number.isFinite(apptHours) || !Number.isFinite(apptMinutes)) {
throw new Error('잘못된 예약 시간 값입니다.');
}
apptDateTime.setHours(apptHours, apptMinutes, 0, 0);
} else {
throw new Error('잘못된 예약 날짜 값입니다.');
}
} catch (e) { // → 변환 오류
skipped++; // → 제외 건수 증가
continue; // → 다음 행
}
const checkinDateTime = new Date(checkinCell); // → 도착 DateTime
const ciMs = checkinDateTime.getTime(); // → 도착 ms
if (isFinite(apptDateTime.getTime()) && // → 예약 시각 유효
isFinite(ciMs)) { // → 도착 시각 유효
arrived++; // → 도착 건수 증가
const diffMs = ciMs - apptDateTime.getTime(); // → 차이(ms)
const diffMinutes = diffMs / 1000 / 60; // → 차이(분)
if (Number.isFinite(diffMinutes)) { // → 숫자인 경우만
if (Math.abs(diffMinutes) <= threshold) { // → 기준 이내면
ontime++; // → 정시 건수 증가
}
} else { // → 숫자가 아니면
skipped++; // → 제외 건수 증가
}
} else { // → 유효하지 않은 시각
skipped++; // → 제외 건수 증가
}
}
// 도착·완료 시각이 모두 있는 경우에만 체류시간 계산
if (checkinCell && checkoutCell) { // → 두 값 다 있으면
const checkinDateTime = new Date(checkinCell); // → 도착 DateTime
const checkoutDateTime = new Date(checkoutCell); // → 완료 DateTime
const ciMs = checkinDateTime.getTime(); // → 도착 ms
const coMs = checkoutDateTime.getTime(); // → 완료 ms
if (isFinite(ciMs) && isFinite(coMs) && coMs >= ciMs) { // → 정상 범위인지
const dwellMinutes = (coMs - ciMs) / 1000 / 60; // → 체류시간(분)
if (Number.isFinite(dwellMinutes)) { // → 숫자인 경우
dwellSum += dwellMinutes; // → 합계에 더하기
dwellCount++; // → 건수 증가
} else { // → 숫자 아님
skipped++; // → 제외 건수 증가
}
} else { // → 잘못된 시각
skipped++; // → 제외 건수 증가
}
}
}
const ontimeRate = arrived > 0 // → 정시율 계산
? (ontime / arrived) * 100 // → % 계산
: 0; // → 분모 0이면 0
const avgDwellMinutes = dwellCount > 0 // → 평균 체류시간
? dwellSum / dwellCount // → 평균 값
: 0; // → 분모 0이면 0
const result = { // → 결과 객체
date: targetYmd,
total,
arrived,
ontime,
ontimeRate,
avgDwellMinutes,
skipped
};
Logger.log(JSON.stringify(result)); // → 요약 로그 출력
saveDailyKpi_(result); // → KPI_DAILY 시트에 저장
return result; // → 결과 반환
}이 함수는 오늘뿐 아니라 과거 어느 날짜에도 재사용할 수 있습니다. 향후에는 예약이 많았던 특정 날짜만 골라 돌려보면서 패턴을 확인하는 용도로도 쓸 수 있습니다.
3단계 — KPI_DAILY 시트에 결과 저장 함수 구현
다음은 계산된 KPI를 KPI_DAILY 시트에 반영하는 saveDailyKpi_()입니다. 시트가 없으면 생성하고, 같은 날짜가 이미 있으면 그 행을 갱신합니다. 이렇게 해 두면 정시 기준(예: 15분 → 30분)을 바꾼 뒤 과거 날짜를 다시 돌리는 것도 편해집니다.
1) 이 코드가 하는 일
- KPI_DAILY 시트에서 기준일이 같은 행을 찾아 있으면 덮어쓰고, 없으면 새 행을 추가합니다.
2) 붙여넣을 위치
computeKpiForDate_()바로 아래에 붙여넣습니다.
3) 붙여넣은 뒤 할 일
- 4단계의
KPI_testComputeToday_()실행 시 자동으로 호출됩니다.
function saveDailyKpi_(result) { // → 일일 KPI 저장
const ss = SpreadsheetApp.getActiveSpreadsheet(); // → 현재 스프레드시트
let sheet = ss.getSheetByName(KPI_CONFIG.KPI_SHEET_NAME); // → KPI_DAILY 시트 찾기
if (!sheet) { // → 없으면
sheet = ss.insertSheet(KPI_CONFIG.KPI_SHEET_NAME); // → 새 시트 만들기
sheet.getRange(1, KPI_COL_KPI_DATE, 1, KPI_COL_KPI_CALC_AT) // → 헤더 범위
.setValues([[
'DATE', // → 기준일
'TOTAL_APPTS', // → 전체 예약 건수
'ARRIVED_APPTS', // → 도착 기록 건수
'ONTIME_APPTS', // → 정시 도착 건수
'ONTIME_RATE_PCT', // → 정시율(%)
'AVG_DWELL_MIN', // → 평균 체류시간(분)
'SKIPPED_APPTS', // → 제외 건수
'CALCULATED_AT' // → 계산 시각
]]);
}
const lastRow = sheet.getLastRow(); // → 마지막 행
let targetRow = 0; // → 쓸 행 번호
if (lastRow >= 2) { // → 데이터가 있으면
const range = sheet.getRange(2, KPI_COL_KPI_DATE, lastRow - 1, 1); // → 날짜 열 범위
const values = range.getValues(); // → 2차원 배열
for (let i = 0; i < values.length; i++) { // → 각 행 검사
const ymd = values[i][0]; // → 날짜 값
if (ymd === result.date) { // → 같은 날짜 찾으면
targetRow = i + 2; // → 실제 행 번호
break; // → 반복 종료
}
}
}
if (!targetRow) { // → 기존 행 없으면
targetRow = lastRow + 1; // → 새 행 번호
}
const now = new Date(); // → 현재 시각
const rowValues = [ // → 기록할 값 배열
result.date, // → 기준일
result.total, // → 전체 예약 수
result.arrived, // → 도착 기록 수
result.ontime, // → 정시 도착 수
result.ontimeRate, // → 정시율(%)
result.avgDwellMinutes, // → 평균 체류시간
result.skipped, // → 제외 건수
now // → 계산 시각
];
sheet.getRange(targetRow, KPI_COL_KPI_DATE, 1, rowValues.length)
.setValues([rowValues]); // → 행에 쓰기
// 날짜 문자열과 숫자 값은 시트에서 포맷으로 보정하는 걸 권장합니다.
// 여기서는 계산 시각 열만 기본적인 날짜-시간 포맷을 설정합니다.
sheet.getRange(2, KPI_COL_KPI_CALC_AT, sheet.getLastRow() - 1, 1)
.setNumberFormat('yyyy-MM-dd HH:mm'); // → 시각 포맷
}DATE, TOTAL_APPTS 같은 헤더 이름은 이후 피벗 테이블이나 차트 만들 때 그대로 쓰기 좋게 영문으로 두었습니다. 필요하면 나중에 시트 화면에서만 한글로 보조 설명을 붙이는 식으로 보완해도 됩니다.
4단계 — 테스트 함수로 오늘 날짜 KPI 계산해 보기
마지막으로, 전체 흐름이 정상인지 확인하는 간단한 테스트 함수를 하나 둡니다. 오늘 날짜를 기준으로 KPI를 계산하고 결과를 실행 로그에 찍어 보면서 동작을 확인합니다.
1) 이 코드가 하는 일
- 오늘 날짜를 기준으로
computeKpiForDate_()를 호출하고, 결과를 실행 로그와 KPI_DAILY 시트에서 확인할 수 있게 합니다.
2) 붙여넣을 위치
saveDailyKpi_()바로 아래에 붙여넣습니다.
3) 붙여넣은 뒤 할 일
- Apps Script 편집기 상단 함수 목록에서
KPI_testComputeToday_를 선택 후 실행 → 처음 한 번 권한을 허용합니다.
function KPI_testComputeToday_() { // → 오늘 KPI 테스트 함수
const today = new Date(); // → 오늘 날짜
const result = computeKpiForDate_(today); // → KPI 계산 호출
Logger.log('KPI_testComputeToday_ 결과: ' + // → 결과 로그 출력
JSON.stringify(result));
}실행 후 보기 → 실행 로그를 열었을 때 KPI_testComputeToday_ 결과: {...} 형태의 JSON이 찍히고, KPI_DAILY 시트에 오늘 날짜 행이 생겨 있다면 전체 흐름이 정상적으로 동작하고 있는 것입니다.
실무 팁: 데이터 품질·시간 열 설정·에러 대응
구글시트로 물류 창고 KPI를 실제 운영에 올리면서 특히 중요했던 부분을 간단히 정리하면 다음과 같습니다.
1. 시간 열 설정은 “0 → 실제 열 번호” 순서로 점진적으로
이번 글에서는 KPI_COL_CHECKIN_AT·KPI_COL_CHECKOUT_AT를 기본값 0으로 두었습니다. 이렇게 하면 체크인이 아직 시트에 도입되지 않은 상태에서도 정시율·체류시간 코드를 먼저 올려 놓고, 이후 시트 구조가 잡혔을 때 열 번호만 채우면 바로 KPI에 반영할 수 있습니다.
- 체크인·체크아웃 열을 APPT_MAIN에 추가한 뒤
예: 체크인 시각이 열 N, 체크아웃 시각이 열 O라면
KPI_COL_CHECKIN_AT = 14;
KPI_COL_CHECKOUT_AT = 15;
처럼 바꾸고 다시 테스트 함수를 실행해 보십시오.
2. 입력 규칙으로 시간 값 포맷을 고정해 두기
정시율과 체류시간은 시간 값이 안정적으로 들어올수록 정확해집니다.
- 체크인/체크아웃 시각은 가능하면 웹앱(예: 입고예약 체크인 4편에서 만든
saveCheckInData())으로만 기록하고, 시트에서 직접 수정하는 것을 최소화합니다. - 시트에서 시간을 직접 변경해야 한다면 해당 열 전체에 “시간” 형식과 데이터 유효성(예:
HH:MM패턴)을 걸어 두는 것이 좋습니다.
텍스트 “9시”, “늦게 도착” 같은 값이 섞이기 시작하면 그 행은 자동으로 skipped로 빠지고, 나중에 KPI가 왜 안 맞는지 추적하는 데 시간을 쓰게 됩니다.
3. 작은 샘플 날짜로 정합성 먼저 확인하기
실운영 데이터 전체를 한꺼번에 검증하기보다는, 예약이 3~5건 정도인 하루를 골라 다음 패턴을 구성해 보는 걸 권합니다.
- 정시 도착 2건(예약 시간 ±10분 이내)
- 지연 도착 1건(예: +40분)
- 예약만 있고 도착 기록이 없는 1건
- 도착은 있으나 완료 기록이 없는 1건
그 날짜를 기준으로 computeKpiForDate_(새 Date('YYYY-MM-DD'))를 직접 실행해 보거나, 날짜를 오늘로 맞춰 KPI_testComputeToday_()를 돌려 보면:
- 정시율이
2 / 3 × 100수준으로 나오는지 - 평균 체류시간이 “도착·완료 둘 다 기록된 건만” 기준으로 계산되는지
를 빠르게 확인할 수 있습니다. 숫자가 예상과 다르면, 해당 날짜 행만 로그를 더 찍어보면서 원인을 좁혀 가면 됩니다.
4. 자주 나오는 오류와 빠른 해결법
예약 시트(APPT_MAIN)를 찾을 수 없습니다.
→ 실제 예약 시트 이름과 KPI_CONFIG.APPT_SHEET_NAME를 다시 비교해 보십시오. 대소문자나 공백까지 정확히 맞아야 합니다.
유효한 Date 객체가 아닙니다.
→ computeKpiForDate_()를 직접 호출할 때 문자열을 그대로 넘긴 경우가 많습니다.
computeKpiForDate_(new Date('2026-08-23')) 처럼 new Date()를 꼭 감싸 주세요.
맺음말: 오늘 하루 분부터 KPI를 쌓아 보길 권합니다
이 글에서는 구글시트 예약 KPI 집계 방법의 첫 단계로, 정시율·평균 체류시간을 일자별로 자동 집계하는 Apps Script를 완성했습니다. 핵심은:
computeKpiForDate_()로 일자별 KPI를 계산하고KPI_DAILY시트에 하루 한 줄씩 쌓은 뒤- 숫자가 아닌 값은
skipped로 따로 분리해 데이터 품질을 드러내는 설계입니다.
지금 바로 할 수 있는 한 가지를 꼽는다면,
예약이 적은 날짜 하나를 골라 이 코드를 붙여 넣고 KPI_testComputeToday_()를 실행해 보시길 권합니다.
그날의 정시 도착률과 평균 체류시간이 숫자로 보이는 순간,
- 앞으로 어떤 KPI를 더 나누어 보고 싶은지
- 운송사·고객사별로 어떻게 잘라볼지
- 어느 구간에서 지연과 체류가 집중되는지
방향이 훨씬 선명해집니다.
다음 편에서는 이 일일 KPI 데이터를 바탕으로 운송사·고객사별 정시 도착률과 체류시간 리포트, 간단한 대시보드 구성 방법을 이어서 다룰 예정입니다.