구글시트 야드 트레일러 현황 만들기 — 대시보드 3편 (수정·완성 버전)
도입: 야드 슬롯, 더 이상 머릿속이 아니라 시트 위에서 관리하기
이 시리즈에서 다루는 구글시트 입고 예약 시스템은 예약·체크인·야드 이동까지 하나의 흐름으로 이어지는 구조입니다. 앞의
- 구글시트 예약 현황 자동화 방법: 오늘 시트 만들기 — 대시보드 1편 에서는 하루치 예약을 TODAY 시트로 모으는 구조를 만들었고,
- 구글시트 예약 상태별 현황 대시보드 만들기 — 대시보드 2편 에서는 상태별 예약 건수를 요약해 관리자가 한눈에 볼 수 있도록 구성했습니다.
이번 글의 목표는 구글시트 야드 트레일러 현황 만들기입니다. “야드의 어느 슬롯에 어떤 트레일러(또는 컨테이너)가 몇 시간째 서 있는지”를 한 화면에서 보는 구글시트 물류 대시보드를 만드는 것이 핵심입니다. 구글시트와 Apps Script로 YARD 시트와 트레일러 이동 기록(TRAILER_MOVES)을 읽어, 슬롯별 현재 점유 상태와 경과 시간을 계산하고 색으로 표시하는 구조를 구현합니다.
운영 현장에서는 한동안 화이트보드와 구두 전달로 야드 슬롯을 관리했습니다. 야드에 들어오는 장비 수가 늘어나고 회전 속도가 빨라지면서 “저 트레일러 언제 들어왔지?”, “어제 밤에 세워 둔 장비가 아직도 안 빠졌나?” 같은 질문에 즉시 답하기가 어려워졌습니다. 그래서 야드 슬롯 점유 현황을 구글시트 대시보드로 만들고, 경과 시간에 따라 색으로 위험 슬롯을 드러내는 방식으로 설계했습니다. 이 글에서는 실제로 운영 중인 구조와 코드를 가능한 범위에서 그대로 정리합니다.
데이터 구조: YARD·TRAILER_MOVES를 어떻게 합쳐 볼지
야드 슬롯 점유 현황을 자동으로 계산하려면 우선 어떤 데이터를 어떤 역할로 볼지부터 정리해야 합니다. 이 시리즈에서는 이미 체크인과 연동해 다음 시트를 만들어 두었습니다.
- 구글시트 야드 슬롯 자동 배정과 이동 기록 — 체크인 5편 에서 만든 TRAILER_MOVES 시트
- 기본 설정에서 사용하는 YARD 시트
YARD·이동 기록의 역할 정리
YARD 시트는 슬롯 마스터입니다.
슬롯 ID, 설명, 활성 여부 등이 들어 있고, 실제 배치 설계에 맞춰 한 번 정해 두면 자주 바꾸지 않습니다. 이 글에서는 기존 구조를 그대로 사용하며, 열 배치는 새로 정의하지 않습니다. 대신 “어떤 슬롯을 화면에 보여줄지”를 결정하는 기준으로 활성 여부를 사용합니다.
TRAILER_MOVES 시트는 트레일러 이동 이력입니다.
체크인·야드 슬롯 이동·출차 등 트레일러 위치가 바뀔 때마다 한 줄씩 쌓이는 로그입니다. 체크인 5편 에서 CHK_appendTrailerMove_() 로 기록을 남기고 있으므로, 이번 글에서는 이 이력을 읽어 슬롯별 최신 상태를 만드는 데 집중합니다.
대시보드 3편에서 할 일은 다음과 같습니다.
- YARD에서 현재 운영 중인 슬롯 목록만 가져온다.
- TRAILER_MOVES에서 슬롯별로 가장 최근 기록 한 줄씩만 남긴다.
- 마지막 동작이
"IN"이면 점유 중, 그 외(예:"OUT")이면 비어 있다고 본다. - 점유 시작 시각과 기준 시각(실행 시각)의 차이로 경과 시간(시간 단위) 을 계산한다.
- TODAY 시트의 지정 영역에
- 슬롯 ID
- 슬롯 이름(또는 구역 설명)
- 현재 컨테이너(또는 트레일러) 번호
- 점유 시작 시각
- 경과 시간(시간)
- 상태 코드
를 행 단위로 표시하고, 경과 시간이 길수록 색을 진하게 보여 준다.
기본 설계는 “2시간 이내는 정상, 4시간까지는 주의, 4시간을 넘기면 심각”으로 두었습니다. 이 임계치는 DASH_YARD_CONFIG 상수로 관리해, 야드 운영 방식에 맞게 언제든 조정할 수 있습니다. 이렇게 해 두면 모니터 하나만 봐도 “어디가 오래 서 있는지”를 빠르게 파악할 수 있습니다.
메인 함수와 설정: 야드 현황 업데이트의 골격 만들기
이번 편의 메인 함수 이름은 DASH_updateYardStatusView() 입니다. 한 줄로 요약하면:
“YARD와 TRAILER_MOVES를 읽어, 슬롯별 현재 상태를 TODAY 시트에 그려 주는 함수”
동시 저장(경합) 방지: 왜 LockService 를 쓰는가
야드 현황은 주기적으로 자동 실행하는 경우가 많습니다(시간 기반 트리거, 수동 실행, 다른 메뉴에서 호출 등). TODAY 시트를 동시에 여러 실행이 수정하면, 색과 값이 서로 덮어쓰이거나 일부만 반영된 상태로 남을 수 있습니다. 예약 대시보드 쪽에서도 비슷하게 신경 썼던 부분입니다.
그래서 이 메인 함수에서는 LockService.getDocumentLock() 을 사용해 문서 단위 잠금을 잡고, 한 번에 하나의 실행만 TODAY 시트를 갱신하도록 했습니다. 잠금 대기가 오래 걸리면(예: 10초) 해당 실행은 오류를 던지고 종료해, 중첩 실행으로 시트가 엉키는 상황을 막습니다.
1단계 — 설정 상수와 메인 함수 틀 만들기
1) 이 코드가 하는 일
- 야드 대시보드에 필요한 시트 이름, 시작 위치, 임계치, 색상 설정을 한 객체로 모은다.
- 문서 잠금을 건 뒤 YARD·TRAILER_MOVES 를 읽고 TODAY 시트를 갱신하는 메인 함수를 만든다.
2) 붙여넣을 위치
- 구글시트 → 확장 프로그램 → Apps Script → 이 시리즈에서 사용 중인 같은 프로젝트에서, 기존 코드 파일의 가장 아래쪽에 붙여넣는다.
- 이미 다른 글에서
onOpen()이 정의되어 있다면 이 편에서는 새로 만들지 않는다. 메뉴 통합은 대시보드 전체를 정리할 때 한 번에 다룬다. - 이 글에서 사용하는 앞 편 함수(
DASH_getApptDailyViewSheet_()등)는 다시 선언하지 않고 그대로 사용한다. 필요한 경우 “대시보드 1편” 을 참고한다.
3) 붙여넣은 뒤 할 일
- 저장 후
DASH_testUpdateYardStatusView()를 한 번 실행해 권한을 허용한다.
// 야드 현황용 설정 상수 모음입니다.
const DASH_YARD_CONFIG = {
TZ: 'America/New_York', // 시간대는 고정
MOVE_SHEET_NAME: 'TRAILER_MOVES', // 이동 기록 시트 이름
YARD_SHEET_NAME: 'YARD', // 야드 슬롯 시트 이름
VIEW_SHEET_NAME: 'TODAY', // 대시보드 표시 시트
VIEW_START_ROW: 30, // 야드 표 시작 행 (필요에 따라 조정)
VIEW_START_COL: 1, // 야드 표 시작 열 (A열)
MAX_SLOT_HOURS_OK: 2, // 2시간 이내: 정상
MAX_SLOT_HOURS_WARN: 4, // 2~4시간: 주의
COLOR_EMPTY: '#ffffff', // 빈 슬롯 색
COLOR_OK: '#e2f0d9', // 정상 점유 색(연녹색)
COLOR_WARN: '#fff2cc', // 경고 색(노랑 계열)
COLOR_ALERT: '#f4cccc' // 심각 색(빨강 계열)
};
/**
* 야드 슬롯별 현재 점유 현황을 TODAY 시트에 그립니다.
* LockService 로 동시 실행을 막아 TODAY 시트가 엉키지 않도록 합니다.
*/
function DASH_updateYardStatusView() {
const lock = LockService.getDocumentLock();
// 동시에 여러 실행이 TODAY 를 건드리지 않도록 최대 10초까지 대기
const gotLock = lock.tryLock(10 * 1000);
if (!gotLock) {
throw new Error('야드 현황 갱신 잠금을 얻지 못했습니다. 잠시 후 다시 실행해 주세요.');
}
try {
const ss = SpreadsheetApp.getActive();
const moveSheet = ss.getSheetByName(DASH_YARD_CONFIG.MOVE_SHEET_NAME);
const yardSheet = ss.getSheetByName(DASH_YARD_CONFIG.YARD_SHEET_NAME);
const viewSheet = ss.getSheetByName(DASH_YARD_CONFIG.VIEW_SHEET_NAME);
if (!moveSheet || !yardSheet || !viewSheet) {
throw new Error('YARD, TRAILER_MOVES, TODAY 시트를 먼저 만들어 주세요.');
}
const now = new Date();
const yardSlots = DASH_loadActiveYardSlots_(yardSheet); // 활성 슬롯 목록
const latestMoves = DASH_loadLatestMovesBySlot_(moveSheet); // 슬롯별 최신 상태
const viewData = DASH_buildYardViewTable_(yardSlots, latestMoves, now);
DASH_writeYardView_(viewSheet, viewData); // TODAY에 값 쓰기
DASH_colorYardView_(viewSheet, viewData); // TODAY에 색 적용
} finally {
// 예외 발생 여부와 관계없이 잠금을 반드시 해제
lock.releaseLock();
}
}
/**
* 테스트용 래퍼입니다. 실행하면 처리 완료 로그를 남기고,
* 출력 데이터의 타입과 구조를 간단히 검증합니다.
*/
function DASH_testUpdateYardStatusView() {
const ss = SpreadsheetApp.getActive();
const viewSheet = ss.getSheetByName(DASH_YARD_CONFIG.VIEW_SHEET_NAME);
if (!viewSheet) {
throw new Error('TODAY 시트가 필요합니다.');
}
DASH_updateYardStatusView();
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const lastRow = viewSheet.getLastRow();
const rows = Math.max(0, lastRow - startRow + 1);
if (rows <= 0) {
Logger.log('야드 현황 데이터가 없습니다.');
return;
}
const range = viewSheet.getRange(startRow, startCol, rows, 6);
const values = range.getValues();
// 기본적인 기대값 검증
if (!Array.isArray(values)) {
throw new Error('TODAY 야드 뷰 데이터가 배열이 아닙니다.');
}
if (values.length > 0) {
const row = values[0];
if (!Array.isArray(row) || row.length !== 6) {
throw new Error('TODAY 야드 뷰 한 행은 길이 6의 배열이어야 합니다.');
}
if (typeof row[0] !== 'string' && typeof row[0] !== 'number') {
throw new Error('첫 번째 열(슬롯ID)은 문자열 또는 숫자여야 합니다.');
}
}
Logger.log('DASH_updateYardStatusView 실행 및 기본 구조 검증 완료');
}여기까지 붙여 넣고 DASH_testUpdateYardStatusView() 를 실행했을 때 오류 없이 완료된다면 골격은 정상입니다. 이제 각 단계별 함수들을 채워 보겠습니다.
활성 야드 슬롯 가져오기: 보여 줄 대상부터 정리하기
다음 단계는 YARD 시트에서 실제로 화면에 보여 줄 슬롯만 골라 오는 작업입니다. 비활성 슬롯까지 다 그리면 정보가 쓸데없이 많아지고, 운영 중단된 영역 때문에 색이 섞여 보이기도 합니다.
2단계 — 활성 야드 슬롯 목록 읽기
현장에서는 임시 슬롯이나 더 이상 쓰지 않는 슬롯이 시트에 남아 있는 경우가 자주 있습니다. 저는 YARD 시트에 활성 여부 열을 두고, 그 값이 허용 목록 안에 들어가는 경우에만 대시보드에 반영하도록 했습니다. 허용 목록을 쓰지 않으면 공백이나 엉뚱한 값이 전부 “활성”으로 해석돼 대시보드가 뒤섞이기 쉽습니다.
1) 이 코드가 하는 일
- YARD 시트에서 활동 중인 슬롯만
{slotId, name}배열로 반환한다. - 활성 여부는 허용 값 목록(
Y,YES,TRUE,1)에 속하는 경우에만 인정한다.
2) 붙여넣을 위치
- 앞 단계 코드 바로 아래에 붙여넣는다.
3) 붙여넣은 뒤 할 일
DASH_testLoadActiveYardSlots_()를 실행해, 실행 로그에 활성 슬롯 목록이 올바르게 출력되는지 확인한다.
/**
* YARD 시트에서 활성 슬롯 목록을 읽어옵니다.
* 반환: [{slotId, name}, ...]
*
* 열 가정: A열=슬롯ID, B열=이름, C열=활성 여부
*/
function DASH_loadActiveYardSlots_(yardSheet) {
const lastRow = yardSheet.getLastRow();
if (lastRow < 2) {
// 헤더만 있는 경우
return [];
}
const range = yardSheet.getRange(2, 1, lastRow - 1, 3); // A:C
const values = range.getValues();
const slots = [];
const ACTIVE_VALUES = ['Y', 'YES', 'TRUE', '1'];
for (let i = 0; i < values.length; i++) {
const row = values[i];
const slotId = row[0]; // A열
const name = row[1]; // B열
const active = row[2]; // C열
if (!slotId) {
// 슬롯ID 없으면 스킵 (지워진 행 등)
continue;
}
const activeStr = String(active).trim().toUpperCase();
if (!ACTIVE_VALUES.includes(activeStr)) {
// 비활성 슬롯은 대시보드에서 제외
continue;
}
slots.push({ slotId, name });
}
return slots;
}
/**
* 활성 슬롯 테스트용.
* 구조와 타입을 검증하고, 슬롯 목록을 로그에 찍습니다.
*/
function DASH_testLoadActiveYardSlots_() {
const ss = SpreadsheetApp.getActive();
const yardSheet = ss.getSheetByName(DASH_YARD_CONFIG.YARD_SHEET_NAME);
if (!yardSheet) {
throw new Error('YARD 시트가 필요합니다.');
}
const slots = DASH_loadActiveYardSlots_(yardSheet);
// 기대값 검증: 배열/길이/필드 존재 여부/타입
if (!Array.isArray(slots)) {
throw new Error('DASH_loadActiveYardSlots_ 결과가 배열이 아닙니다.');
}
if (slots.length > 0) {
const s = slots[0];
if (typeof s !== 'object' || s === null) {
throw new Error('슬롯 항목은 객체여야 합니다.');
}
if (!('slotId' in s) || !('name' in s)) {
throw new Error('슬롯 객체에 slotId 또는 name 필드가 없습니다.');
}
if (typeof s.slotId !== 'string' && typeof s.slotId !== 'number') {
throw new Error('slotId는 문자열 또는 숫자여야 합니다.');
}
if (typeof s.name !== 'string' && s.name !== null && s.name !== undefined) {
throw new Error('name은 문자열이거나 빈 값이어야 합니다.');
}
}
Logger.log('활성 슬롯: ' + JSON.stringify(slots));
}테스트 실행 후, 활성 슬롯만 잘 걸러지고 객체 구조가 기대한 대로인지까지 함께 검증됩니다.
TRAILER_MOVES에서 슬롯별 최신 상태 계산하기
이제 TRAILER_MOVES에 쌓인 이동 이력에서 슬롯별 “현재 상태”를 뽑아내야 합니다. 이 부분이 구글시트로 트레일러 위치를 추적하는 핵심입니다.
3단계 — 이동 이력에서 슬롯별 최신 레코드만 남기기
TRAILER_MOVES 시트에는 체크인, 야드 이동, 출차 등 다양한 동작이 섞여 있습니다. 이 글에서는 동작 코드를 허용 목록으로 관리해, 엉뚱한 값이 들어왔을 때 조용히 잘못 반영되는 일을 막습니다. 필수값이 비었거나 시각이 Date 형식이 아니면 건너뜁니다.
1) 이 코드가 하는 일
- TRAILER_MOVES 시트의 모든 행을 읽고, 슬롯별로 가장 최신 기록 한 줄만
{slotId: {...}}형태로 정리한다. - 동작코드(IN/OUT/MOVE)는 허용 목록에서만 통과시키고, 나머지는 건너뛴다.
2) 붙여넣을 위치
- 2단계 코드 바로 아래에 붙여넣는다.
3) 붙여넣은 뒤 할 일
DASH_testLoadLatestMovesBySlot_()를 실행해 슬롯별 최신 상태가 로그에 잘 찍히는지, 구조가 올바른지 확인한다.
/**
* TRAILER_MOVES 시트에서 슬롯별 최신 이동 이력을 계산합니다.
* 반환: {slotId: {slotId, container, status, movedAt}, ...}
*
* 열 가정: A열=기록시각, B열=슬롯ID, C열=컨테이너번호, D열=동작타입(IN/OUT/MOVE 등)
*/
function DASH_loadLatestMovesBySlot_(moveSheet) {
const lastRow = moveSheet.getLastRow();
if (lastRow < 2) {
// 데이터 없음
return {};
}
const range = moveSheet.getRange(2, 1, lastRow - 1, 4); // A:D
const values = range.getValues();
const latestBySlot = {};
const ALLOWED_ACTIONS = ['IN', 'OUT', 'MOVE'];
for (let i = 0; i < values.length; i++) {
const row = values[i];
const ts = row[0]; // 기록 시각
const slotId = row[1]; // 슬롯 ID
const container = row[2]; // 컨테이너 번호
const actionRaw = row[3]; // IN/OUT/MOVE
if (!slotId || !ts) {
// 필수값 없으면 건너뜀
continue;
}
if (!(ts instanceof Date)) {
// 날짜형이 아니면 무시 (텍스트 등)
continue;
}
const action = String(actionRaw || '').toUpperCase();
if (!ALLOWED_ACTIONS.includes(action)) {
// 허용되지 않은 동작코드는 대시보드에 반영하지 않음
continue;
}
const prev = latestBySlot[slotId];
if (!prev || prev.movedAt < ts) {
// 이 슬롯에서 더 최신 기록이면 교체
latestBySlot[slotId] = {
slotId: slotId,
container: container || '',
status: action,
movedAt: ts
};
}
}
return latestBySlot;
}
/**
* 슬롯별 최신 이동 상태 테스트용.
* 구조와 타입을 검증하고, 결과를 로그로 출력합니다.
*/
function DASH_testLoadLatestMovesBySlot_() {
const ss = SpreadsheetApp.getActive();
const moveSheet = ss.getSheetByName(DASH_YARD_CONFIG.MOVE_SHEET_NAME);
if (!moveSheet) {
throw new Error('TRAILER_MOVES 시트가 필요합니다.');
}
const latest = DASH_loadLatestMovesBySlot_(moveSheet);
// 기대값 검증: 객체 여부/슬롯별 필드·타입 확인
if (typeof latest !== 'object' || latest === null || Array.isArray(latest)) {
throw new Error('DASH_loadLatestMovesBySlot_ 결과는 슬롯ID를 키로 하는 객체여야 합니다.');
}
const keys = Object.keys(latest);
if (keys.length > 0) {
const example = latest[keys[0]];
if (typeof example !== 'object' || example === null) {
throw new Error('슬롯 상태는 객체여야 합니다.');
}
const required = ['slotId', 'container', 'status', 'movedAt'];
required.forEach(function (k) {
if (!(k in example)) {
throw new Error('슬롯 상태 객체에 ' + k + ' 필드가 없습니다.');
}
});
if (!(example.movedAt instanceof Date)) {
throw new Error('movedAt 필드는 Date 타입이어야 합니다.');
}
if (typeof example.status !== 'string') {
throw new Error('status 필드는 문자열이어야 합니다.');
}
}
Logger.log('슬롯별 최신 이동 상태: ' + JSON.stringify(latest));
}이제 슬롯별로 가장 최신 이동만 남기는 로직과 타입 검증까지 끝났습니다.
표 데이터 만들기: 슬롯·이동 정보를 경과 시간으로 묶기
활성 슬롯 목록과 슬롯별 최신 이동 상태를 얻었으니, 이제 TODAY 시트에 바로 쓸 수 있는 2차원 배열을 만들어야 합니다. 이 배열에는 나중에 색을 정할 때 사용할 경과 시간(시간 단위) 값이 포함됩니다.
4단계 — 슬롯별 현황 표 데이터 구성
야드 운영에서 중요한 건 “어디에 무엇이, 몇 시간째 서 있느냐”입니다. 그래서 IN 상태인 슬롯만 경과 시간을 계산하고, OUT 등 다른 상태는 숫자 대신 빈칸으로 둡니다. 시간 계산에서 잘못된 값이 들어가면 NaN 이 퍼져서 시트가 지저분해지기 때문에, Date 타입과 음수 값에 대한 방어를 넣었습니다.
1) 이 코드가 하는 일
- 각 활성 슬롯에 대해
- 슬롯 ID
- 슬롯 이름
- 현재 컨테이너 번호
- 점유 시작 시각(Date)
- 경과 시간(시간, 소수 1자리)
- 상태 코드
를 담은 2차원 배열을 만든다.
2) 붙여넣을 위치
- 3단계 코드 바로 아래에 붙여넣는다.
3) 붙여넣은 뒤 할 일
DASH_testBuildYardViewTable_()를 실행해 로그에 표 데이터가 원하는 형태로 나오는지, 그리고 배열 구조와 타입이 올바른지 확인한다.
/**
* 야드 슬롯 현황 표를 만들기 위한 2차원 배열을 구성합니다.
* 각 행: [슬롯ID, 이름, 컨테이너, 점유시각(Date 또는 ''), 경과시간(시간), 상태]
*/
function DASH_buildYardViewTable_(yardSlots, latestMoves, now) {
const rows = [];
const msPerHour = 1000 * 60 * 60;
for (let i = 0; i < yardSlots.length; i++) {
const slot = yardSlots[i];
const slotId = slot.slotId;
const name = slot.name || '';
const move = latestMoves[slotId];
let container = '';
let startTime = '';
let hours = '';
let status = '';
if (move && move.status === 'IN') {
// 현재 점유 중
container = move.container || '';
startTime = move.movedAt;
if (startTime instanceof Date && !isNaN(startTime.getTime())) {
const diffMs = now.getTime() - startTime.getTime();
const diffHours = diffMs / msPerHour;
const safeHours = diffHours < 0 ? 0 : diffHours; // 미래 시각 방어
const rounded = Math.round(safeHours * 10) / 10; // 소수 1자리 반올림
hours = Number.isFinite(rounded) ? rounded : '';
}
status = 'IN';
} else if (move) {
// 마지막 동작이 OUT, MOVE 등인 경우
status = move.status;
// OUT 상태에서는 경과시간을 계산하지 않고 비워둡니다.
}
rows.push([slotId, name, container, startTime, hours, status]);
}
return rows;
}
/**
* 현황 표 생성 테스트용.
* 결과 배열을 로그로 확인하고, 타입과 구조를 검증합니다.
*/
function DASH_testBuildYardViewTable_() {
const ss = SpreadsheetApp.getActive();
const yardSheet = ss.getSheetByName(DASH_YARD_CONFIG.YARD_SHEET_NAME);
const moveSheet = ss.getSheetByName(DASH_YARD_CONFIG.MOVE_SHEET_NAME);
if (!yardSheet || !moveSheet) {
throw new Error('YARD, TRAILER_MOVES 시트를 확인해 주세요.');
}
const slots = DASH_loadActiveYardSlots_(yardSheet);
const latest = DASH_loadLatestMovesBySlot_(moveSheet);
const now = new Date();
const table = DASH_buildYardViewTable_(slots, latest, now);
// 기대값 검증: 2차원 배열, 각 행 길이와 필드 타입 확인
if (!Array.isArray(table)) {
throw new Error('DASH_buildYardViewTable_ 결과가 배열이 아닙니다.');
}
if (table.length > 0) {
const row = table[0];
if (!Array.isArray(row)) {
throw new Error('야드 뷰 한 행은 배열이어야 합니다.');
}
if (row.length !== 6) {
throw new Error('야드 뷰 한 행은 6개의 필드를 가져야 합니다.');
}
const [slotId, name, container, startTime, hours, status] = row;
if (typeof slotId !== 'string' && typeof slotId !== 'number') {
throw new Error('슬롯ID는 문자열 또는 숫자여야 합니다.');
}
if (typeof name !== 'string' && name !== null && name !== undefined) {
throw new Error('슬롯 이름은 문자열이거나 빈 값이어야 합니다.');
}
if (typeof container !== 'string' && container !== '') {
throw new Error('컨테이너 번호는 문자열이거나 빈 문자열이어야 합니다.');
}
if (!(startTime === '' || startTime instanceof Date)) {
throw new Error('점유시각은 Date 또는 빈 문자열이어야 합니다.');
}
if (!(hours === '' || typeof hours === 'number')) {
throw new Error('경과시간은 숫자 또는 빈 문자열이어야 합니다.');
}
if (typeof status !== 'string') {
throw new Error('상태코드는 문자열이어야 합니다.');
}
}
Logger.log('야드 뷰 테이블: ' + JSON.stringify(table));
}이제 표 데이터 구조와 각 필드 타입까지 자동으로 검증할 수 있습니다. OUT 상태 슬롯의 경과시간이 빈 값인지도 같이 확인해 두면 좋습니다.
TODAY 시트에 쓰고, 경과 시간 기준으로 색 입히기
이제 구글시트 야드 슬롯 현황 대시보드의 마지막 두 작업입니다.
viewData배열을 TODAY 시트의 지정 영역에 쓰기- 경과 시간에 따라 행 전체 색을 바꾸기
성능을 위해 값 쓰기와 색 변경은 각각 한 번씩만 호출합니다.
5단계 — 야드 현황 표 쓰기와 표시 형식 지정
TODAY 시트에는 이미 다른 대시보드(예약 요약, 상태별 현황 등)가 올라가 있을 수 있습니다. 이 야드 뷰는 아래쪽 비어 있는 영역을 잡고 별도의 블록으로 배치하는 방식이 안전합니다. 이 위치는 VIEW_START_ROW 로 제어합니다.
1) 이 코드가 하는 일
- TODAY 시트의 야드 전용 영역을 한 번 지운 뒤,
viewData를 그대로 써 넣는다. - 점유 시각 열에는 Date 값을 그대로 두고, 표시 형식만
"MM-dd HH:mm"으로 맞춘다.
2) 붙여넣을 위치
- 4단계 코드 아래에 붙여넣는다.
3) 붙여넣은 뒤 할 일
DASH_testWriteYardView_()를 실행해 TODAY 시트에서 데이터가 예상 위치에 잘 나타나는지, 그리고 쓰여진 데이터 구조를 검증한다.
/**
* TODAY 시트의 야드 구역에 현황 표를 씁니다.
*/
function DASH_writeYardView_(viewSheet, viewData) {
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const maxRows = Math.max(viewData.length, 1);
const clearRange = viewSheet.getRange(startRow, startCol, maxRows, 6);
// 이전 데이터 및 서식 제거
clearRange.clearContent();
clearRange.clearFormat();
if (viewData.length === 0) {
// 보여 줄 데이터가 없으면 여기까지
return;
}
const range = viewSheet.getRange(startRow, startCol, viewData.length, 6);
range.setValues(viewData);
// 점유 시각 열(4번째 열)에 시각 형식을 지정합니다.
const timeRange = viewSheet.getRange(startRow, startCol + 3, viewData.length, 1);
timeRange.setNumberFormat('MM-dd HH:mm');
}
/**
* 쓰기 동작 테스트용.
* viewData 구조를 검증하고 TODAY에 실제로 써 봅니다.
*/
function DASH_testWriteYardView_() {
const ss = SpreadsheetApp.getActive();
const yardSheet = ss.getSheetByName(DASH_YARD_CONFIG.YARD_SHEET_NAME);
const moveSheet = ss.getSheetByName(DASH_YARD_CONFIG.MOVE_SHEET_NAME);
const viewSheet = ss.getSheetByName(DASH_YARD_CONFIG.VIEW_SHEET_NAME);
if (!yardSheet || !moveSheet || !viewSheet) {
throw new Error('YARD, TRAILER_MOVES, TODAY 시트를 확인해 주세요.');
}
const slots = DASH_loadActiveYardSlots_(yardSheet);
const latest = DASH_loadLatestMovesBySlot_(moveSheet);
const now = new Date();
const table = DASH_buildYardViewTable_(slots, latest, now);
// viewData 기본 검증
if (!Array.isArray(table)) {
throw new Error('viewData(table)가 배열이 아닙니다.');
}
if (table.length > 0) {
const row = table[0];
if (!Array.isArray(row) || row.length !== 6) {
throw new Error('viewData 한 행은 길이 6의 배열이어야 합니다.');
}
}
DASH_writeYardView_(viewSheet, table);
// TODAY에 실제로 쓴 후, 다시 읽어서 길이와 타입 확인
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const range = viewSheet.getRange(startRow, startCol, table.length || 1, 6);
const written = range.getValues();
if (!Array.isArray(written)) {
throw new Error('TODAY에서 읽은 데이터가 배열이 아닙니다.');
}
Logger.log('TODAY 야드 뷰 쓰기 및 기본 검증 완료, 행 수: ' + written.length);
}TODAY 시트에서 VIEW_START_ROW 로 지정한 행부터 6열(A~F)에 슬롯ID·이름·컨테이너·점유시각·경과시간·상태가 채워져 있으면 정상입니다.
6단계 — 경과 시간 기준으로 행 전체 색 입히기
마지막은 시각화입니다. 경과 시간이 길수록 색이 바뀌게 해서, 오래 서 있는 트레일러를 한눈에 찾을 수 있어야 합니다.
1) 이 코드가 하는 일
viewData의 각 행을 돌며 경과시간을 읽어,- 컨테이너 없음 또는 경과시간이 비었을 때: 빈 슬롯 색
MAX_SLOT_HOURS_OK이하: 정상 색MAX_SLOT_HOURS_WARN이하: 경고 색- 그 이상: 심각 색
으로 행 전체의 배경색을 결정한다.
2) 붙여넣을 위치
- 5단계 코드 아래에 붙여넣는다.
3) 붙여넣은 뒤 할 일
DASH_testColorYardView_()를 실행해 TODAY 시트에서 색이 지정한 규칙대로 바뀌는지, viewData 구조 검증까지 함께 수행한다.
/**
* TODAY 시트의 야드 현황 구역에 경과 시간 기준 색을 입힙니다.
*/
function DASH_colorYardView_(viewSheet, viewData) {
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const rows = viewData.length;
if (rows === 0) {
return;
}
const colors = [];
for (let i = 0; i < rows; i++) {
const row = viewData[i];
const container = row[2]; // 컨테이너
const hours = row[4]; // 경과 시간
let color = DASH_YARD_CONFIG.COLOR_EMPTY;
if (container && Number.isFinite(hours)) {
if (hours <= DASH_YARD_CONFIG.MAX_SLOT_HOURS_OK) {
color = DASH_YARD_CONFIG.COLOR_OK;
} else if (hours <= DASH_YARD_CONFIG.MAX_SLOT_HOURS_WARN) {
color = DASH_YARD_CONFIG.COLOR_WARN;
} else {
color = DASH_YARD_CONFIG.COLOR_ALERT;
}
}
const rowColors = [];
for (let c = 0; c < 6; c++) {
rowColors.push(color);
}
colors.push(rowColors);
}
const range = viewSheet.getRange(startRow, startCol, rows, 6);
range.setBackgrounds(colors);
}
/**
* 색상 적용 테스트 함수입니다.
* 인메모리 viewData 구조를 검증하고,
* TODAY에 이미 쓰여 있는 데이터를 다시 읽어서 색만 입혀 봅니다.
*/
function DASH_testColorYardView_() {
const ss = SpreadsheetApp.getActive();
const viewSheet = ss.getSheetByName(DASH_YARD_CONFIG.VIEW_SHEET_NAME);
if (!viewSheet) {
throw new Error('TODAY 시트가 필요합니다.');
}
const startRow = DASH_YARD_CONFIG.VIEW_START_ROW;
const startCol = DASH_YARD_CONFIG.VIEW_START_COL;
const lastRow = viewSheet.getLastRow();
const rows = Math.max(0, lastRow - startRow + 1);
if (rows <= 0) {
Logger.log('TODAY 야드 구역에 데이터가 없습니다.');
return;
}
const range = viewSheet.getRange(startRow, startCol, rows, 6);
const values = range.getValues();
// 기대값 검증: 2차원 배열, 각 행 길이 및 hours 타입
if (!Array.isArray(values)) {
throw new Error('TODAY 야드 뷰 값이 배열이 아닙니다.');
}
if (values.length > 0) {
const row = values[0];
if (!Array.isArray(row) || row.length !== 6) {
throw new Error('TODAY 야드 뷰 한 행은 길이 6의 배열이어야 합니다.');
}
const hours = row[4];
if (!(hours === '' || typeof hours === 'number')) {
throw new Error('경과시간 열은 숫자 또는 빈 문자열이어야 합니다.');
}
}
DASH_colorYardView_(viewSheet, values);
Logger.log('야드 뷰 색상 적용 완료');
}TODAY 시트에서 야드 현황 영역을 보면,
- 컨테이너가 없거나 점유시간이 비어 있는 슬롯: 흰색 (
COLOR_EMPTY) MAX_SLOT_HOURS_OK이하(기본 2시간 이하): 연한 초록색 (COLOR_OK)MAX_SLOT_HOURS_WARN이하(기본 4시간 이하): 노란색 (COLOR_WARN)- 그 이상: 빨간색 계열 (
COLOR_ALERT)
로 구분되면 정상입니다. 색상과 임계치는 DASH_YARD_CONFIG 에서 자유롭게 조정할 수 있습니다.
실무에서 유용한 팁과 오류 방지 포인트
임계치와 색은 운영 회전 속도에 맞춰 조정하기
예시로 2/4시간을 기준으로 잡았지만, 실제 회전 속도와 고객 요구 수준에 따라 기준은 달라집니다. 회전이 느린 야드라면 4/8시간처럼 완충을 두는 편이 나을 수 있고, 초고속 회전이 필요한 영역이라면 1/2시간 단위로 쪼개는 경우도 있습니다.
중요한 건:
- 팀과 “이 색이 나오면 어떤 행동을 할 것인지”를 먼저 합의하고,
- 그 기준을
DASH_YARD_CONFIG와 현장 매뉴얼에 같이 반영해 두는 것
입니다. 색만 바뀌고 행동 기준이 없으면 현장에서 혼란이 생깁니다.
TODAY 시트 레이아웃 충돌 피하기
TODAY 시트에는 이 시리즈에서 만든 다른 대시보드도 함께 올라갑니다.
이들과 겹치지 않게 하려면:
- TODAY 시트에서 이미 사용 중인 마지막 행 번호를 확인한다.
- 그 아래로 5~10행 정도 여유를 두고
VIEW_START_ROW를 잡는다. - 그 시작 행 바로 위에 “야드 현황(자동 갱신 영역, 수동 편집 금지)” 같은 안내를 적어 두어 다른 사용자가 덮어쓰지 않게 한다.
이렇게 해 두면 나중에 대시보드를 확장할 때도 레이아웃 충돌을 줄일 수 있습니다.
테스트 함수로 단계별로 확인하고 붙이기
야드 현황 코드를 처음 붙일 때는 한 번에 전체를 돌리기보다, 이 글에서 제공한 테스트 함수들을 위에서부터 순서대로 실행해 보는 게 좋습니다.
DASH_testLoadActiveYardSlots_— YARD 구조·활성값 및 객체 필드 검증DASH_testLoadLatestMovesBySlot_— TRAILER_MOVES 구조·동작코드·타입 확인DASH_testBuildYardViewTable_— 표 데이터 구조·필드 타입 확인DASH_testWriteYardView_— TODAY 쓰기 위치/서식/길이 확인DASH_testColorYardView_— viewData 구조 검증 + 색상 규칙 적용 확인- 마지막으로
DASH_testUpdateYardStatusView— 전체 플로우와 TODAY 최종 구조 점검
어느 단계에서 막히는지만 바로 알 수 있어서, 시트 구조나 데이터 형식을 고치기 편합니다.
정리
이 글에서는 구글시트 야드 트레일러 현황 만들기를 목표로, 이미 이 시리즈에서 구축한 YARD 시트와 TRAILER_MOVES 이동 이력을 활용해 슬롯별 현재 점유 상태와 경과 시간을 TODAY 시트에 시각적으로 보여 주는 대시보드를 구현했습니다.
핵심 정리:
- YARD 시트에서 활성 슬롯만 골라 대시보드 대상 슬롯을 정리하고,
- TRAILER_MOVES 에서 슬롯별 가장 최근 이동 이력 한 건씩만 추려 현재 상태를 만들었고,
- 점유 시작 시각과 현재 시각의 차이로 경과 시간(시간 단위) 을 계산한 뒤,
- TODAY 시트 지정 영역에 값을 쓰고, 임계치(2/4시간 기준)에 따라 행 전체 색을 바꾸는 구조를 만들었습니다.
- 동시에 여러 실행이 TODAY 시트를 건드리지 않도록
LockService로 문서 잠금을 걸어, 경합으로 인한 깨진 화면을 방지했습니다. - 각 test 함수마다 배열/객체 구조와 필드 타입을 검증하는 코드를 넣어, 시트 구조가 달라졌을 때 즉시 오류를 발견할 수 있게 만들었습니다.
지금 바로 할 수 있는 작업은 한 가지입니다.
- TODAY 시트에서 아래쪽에 비어 있는 영역 하나를 골라
DASH_YARD_CONFIG.VIEW_START_ROW를 그 위치로 맞추고, - 이 글의 코드를 같은 프로젝트에 그대로 붙여넣은 뒤,
DASH_testUpdateYardStatusView()를 실행해 보십시오.
이미 체크인 5편 의 이동 기록을 쓰고 있다면, 각 슬롯에 어떤 컨테이너가 몇 시간째 서 있는지 색으로 구분된 야드 현황판이 즉시 만들어질 것입니다. 이후에는 시간 기반 트리거를 걸어 5분·10분마다 자동 갱신하도록만 추가해 주면, 모니터 한 대로 야드 전체의 체류 시간을 계속 감시할 수 있습니다.