Smart Life US

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

구글시트 예약 상태별 현황 대시보드 만들기 — 대시보드 2편

구글시트 예약 상태별 현황 대시보드 만들기 — 대시보드 2편

구글시트로 예약 상태별 현황 대시보드를 만든다는 건, 결국 “지금 대기·도착·작업중·완료가 각각 몇 건인가”를 한눈에 보는 것입니다. 이 글에서는 구글시트와 Apps Script로 입고 예약 상태별 현황 대시보드를 추가하는 방법을 다룹니다. 이번 편까지 적용하면 TODAY 시트에서 예약 건수를 상태별로 자동 집계하고, 상세 목록까지 같은 화면에 정리할 수 있습니다.

상태별 대시보드 구성 절차

앞선 글인 구글시트 예약 현황 자동화 방법: 오늘 시트 만들기 — 대시보드 1편에서는 TODAY 시트에 특정 날짜의 예약을 복사해 오는 “일일 현황 뷰”까지 만들었습니다. 이번 편은 그 TODAY 데이터를 바탕으로 상태별 개수와 상세 목록을 동시에 보여주는 두 번째 단계입니다.


왜 상태별 현황 대시보드가 필요한가

실제 물류센터에서는 “오늘 예약이 몇 건인가”만으로는 운영 계획을 세우기 어렵습니다. 같은 40건이라도

  • 대기 20건·도착 10건·작업중 5건·완료 5건인 경우와
  • 대부분 완료 상태인 경우

대응 방식은 완전히 달라집니다. 작업자 투입, 도크 전환, 야드 대기 공간 조정 모두 현재 상태 분포를 보고 판단합니다.

입고 예약은 보통 예약 → 도착(체크인) → 도크 작업중 → 완료(체크아웃) 순서로 흐릅니다. 어느 단계에 병목이 생겼는지 보지 못하면, 현장에서는 “차가 많다” 정도의 체감만 있을 뿐 어디를 풀어야 하는지 알기 어렵습니다. 그래서 입고 예약 대시보드에는 단순 건수뿐 아니라 상태별 분포가 반드시 필요합니다. 이 글의 코드는 그 상태별 현황을 TODAY 시트 기반으로 자동 계산해 주는 역할을 합니다.


이번 편에서 만들 상태별 뷰 구조

이번 편에서 만드는 것은 TODAY 시트 안의 “상태별 뷰 레이아웃”입니다. 별도 시트를 추가하지 않고, TODAY에 세 가지 레이어를 더한다고 생각하면 이해가 쉽습니다.

  1. 기준일 표시 영역
  • TODAY 상단 A2 셀에 기준일: YYYY-MM-DD 형식으로 실행 시점의 날짜를 씁니다.
  • 이때 사용하는 함수가 DASH_getViewDateKey_()입니다.
  • TODAY에 어떤 날짜 데이터가 들어 있든, 이 “기준일”은 이 상태 뷰를 마지막으로 새로 고친 날을 뜻하게 됩니다.
  1. 상태별 요약 영역
  • A4 행부터 상태별 요약 블록을 고정 레이아웃으로 둡니다.
  • 예시 상태 이름: 대기 / 도착 / 작업중 / 완료 / 기타
  • 각 상태 옆에 건수를 표시합니다.
  • 구글시트 COUNTIF로 셀마다 수식을 써도 되지만, TODAY 구조가 변할 때마다 수식을 고치는 일이 잦기 때문에 Apps Script에서 한 번에 계산하는 방식을 선택합니다.
  1. 상세 목록 영역
  • A10 행부터 TODAY 시트 원본 헤더와 행들을 다시 그립니다.
  • TODAY의 “원본 데이터 구역”은 그대로 두고, 그 아래 “보기 전용 상세 영역”만 매번 덮어씁니다.
  • 이렇게 해야 DASH_updateApptStatusView()를 여러 번 실행해도, 이미 계산된 요약·상세 영역이 다시 집계에 섞여 들어가는 문제를 막을 수 있습니다.

이 구조를 기준으로 TODAY를 읽어 요약·상세를 동시에 갱신하는 DASH_updateApptStatusView()를 구현합니다. 같은 함수를 여러 번 돌려도 결과가 안정적으로 유지되도록, 재실행에 안전한 구조를 목표로 합니다.


상태별 집계를 위한 전제와 설계

상태별 집계를 구현하려면 TODAY 시트에 “현재 상태” 열이 하나 있어야 합니다. 체크인·상태 관리 쪽 구현은 구글시트 체크인 예약 상태 자동 변경: Apps Script로 구현하기 — 체크인 4편에서 이미 만들었다고 가정합니다. 그 결과 TODAY에는 예약별로 아래와 비슷한 상태 문자열이 들어 있습니다.

  • PENDING 또는 SCHEDULED → 대기
  • ARRIVED 또는 CHECKED_IN → 도착
  • IN_PROGRESS, LOADING, UNLOADING → 작업중
  • COMPLETED, DONE → 완료

실제 프로젝트에서는 상태 코드가 더 많을 수 있지만, 집계 로직에서는 다음 세 가지가 중요합니다.

  1. 코드 상단에 STATUS_MAP을 두고 “원시 상태값 → 내부 집계용 키”를 명시한다.
  2. 목록에 없는 상태(오타·새 값·임시값)는 반드시 OTHER로 묶는다. 조용히 버리면 “총 행 수 ≠ 상태 합계”가 되어 신뢰할 수 없습니다.
  3. 상태가 비어 있는 행도 OTHER에 포함한다. 빈값은 누락을 눈치채기 어렵습니다.

또 하나의 전제는 TODAY에서 상태 열이 몇 번째 열인지입니다. 이 글 코드에서는 예시로 7번째 열(컬럼 G) 을 상태 열로 사용하고, STATUS_COLUMN_INDEX: 7로 정의합니다.

실제 환경에서는 이 값을 반드시 자신의 시트 구조에 맞게 바꿔야 합니다. 그렇지 않으면 상태별 건수가 전부 0이거나 엉뚱한 값이 나오는 전형적인 오류가 발생합니다.


상태별 집계용 설정 상수 정의

이제 실제 Apps Script 코드를 추가합니다. 이 코드는 대시보드 1편인 구글시트 예약 현황 자동화 방법: 오늘 시트 만들기 — 대시보드 1편에서 만든 코드와 같은 프로젝트 안에 넣습니다. 시리즈 전체 코드가 한 프로젝트에서 충돌 없이 돌아가도록, 이번 편 상수와 함수에는 DASH_ 접두어를 붙였습니다.

붙여넣을 위치

  • 구글시트 → 확장 프로그램 → Apps Script
  • 대시보드 관련 코드 파일 상단 근처
Apps Script (JavaScript)
// 상태별 뷰용 설정입니다.                       // → 상태 요약·상세 위치 정의
const DASH_STATUS_CONFIG = {                     // → 대시보드2 전용 설정
  TODAY_SHEET_NAME: 'TODAY',                    // → 일일 현황 시트 이름
  HEADER_ROW: 1,                                 // → TODAY 헤더 행 번호
  STATUS_COLUMN_INDEX: 7,                        // → TODAY에서 상태가 있는 열 번호 예시(G열이면 7)
  VIEW_INFO_CELL: 'A2',                          // → 기준 날짜 표시 셀
  SUMMARY_START_ROW: 4,                          // → 상태 요약 시작 행
  SUMMARY_START_COL: 1,                          // → 상태 요약 시작 열 (A열)
  DETAIL_START_ROW: 10,                          // → 상세 목록 시작 행
  STATUS_LABELS: [                               // → 화면에 보여줄 상태 이름
    { key: 'WAITING', label: '대기' },           // → 내부 키·한글 라벨
    { key: 'ARRIVED', label: '도착' },           // → 도착 상태
    { key: 'IN_PROGRESS', label: '작업중' },     // → 작업 진행중
    { key: 'DONE', label: '완료' },              // → 작업 완료
    { key: 'OTHER', label: '기타' }              // → 그 외 상태
  ],
  STATUS_MAP: {                                  // → 원시 상태값 → 내부 키 매핑
    'PENDING': 'WAITING',                        // → 예약만 된 상태
    'SCHEDULED': 'WAITING',                      // → 같은 의미로 묶기
    'ARRIVED': 'ARRIVED',                        // → 도착
    'CHECKED_IN': 'ARRIVED',                     // → 체크인 완료
    'IN_PROGRESS': 'IN_PROGRESS',                // → 작업중
    'LOADING': 'IN_PROGRESS',                    // → 로딩중
    'UNLOADING': 'IN_PROGRESS',                  // → 언로딩중
    'COMPLETED': 'DONE',                         // → 완료
    'DONE': 'DONE'                               // → 완료
  }
};

실무 팁

  • 이 단계에서 TODAY 시트에서 상태가 들어 있는 정확한 열 위치를 먼저 확인한 뒤 STATUS_COLUMN_INDEX를 바꾸는 것이 좋습니다.
  • 상태 열에 데이터 유효성 검사(드롭다운) 를 걸어 두면 오타로 인한 OTHER 과다 발생을 줄일 수 있습니다.

뷰 기준 날짜를 기록하는 함수

이제 TODAY 상단에 “이 상태 뷰가 언제 기준인지”를 나타내는 문자열을 씁니다. 여기서는 실행 시점의 오늘 날짜를 기준일로 사용합니다. TODAY에 담긴 날짜와 다를 수 있게 의도적으로 분리한 것입니다. 운영 관점에서는 어느 날짜 데이터인지도 중요하지만, “이 화면을 마지막으로 새로고침한 시각” 을 같이 보는 게 더 도움이 되었습니다.

붙여넣을 위치

  • 같은 Apps Script 파일에서 DASH_STATUS_CONFIG 바로 아래
Apps Script (JavaScript)
// 상태 뷰 기준 날짜 키를 계산해 씁니다.          // → 기준일 텍스트 기록
function DASH_getViewDateKey_() {                // → 내부 도우미 함수
  const ss = SpreadsheetApp.getActive();        // → 현재 스프레드시트
  const sheet = ss.getSheetByName(              // → TODAY 시트 찾기
    DASH_STATUS_CONFIG.TODAY_SHEET_NAME
  );
  if (!sheet) {                                 // → 시트 없으면
    throw new Error('TODAY 시트를 찾을 수 없습니다.'); // → 오류 발생
  }

  const today = new Date();                     // → 지금 시각
  const tz = 'America/New_York';                // → 고정 시간대
  const y = Utilities.formatDate(               // → 연도 추출
    today, tz, 'yyyy'
  );
  const m = Utilities.formatDate(               // → 월 추출
    today, tz, 'MM'
  );
  const d = Utilities.formatDate(               // → 일 추출
    today, tz, 'dd'
  );
  const key = y + '-' + m + '-' + d;            // → YYYY-MM-DD 문자열

  sheet.getRange(DASH_STATUS_CONFIG.VIEW_INFO_CELL) // → 기준 셀 선택
    .setValue('기준일: ' + key);                // → 텍스트로 기록
  return key;                                   // → 키를 반환
}

실무 팁

  • 이 시리즈에서는 시간대를 항상 'America/New_York'로 고정해 사용합니다. 프로젝트 설정에 따라 달라지는 Session.getScriptTimeZone()은 쓰지 않습니다.
  • 기준일을 날짜 형식이 아닌 문자열로 적는 이유는, 지역 설정이 바뀌었을 때도 포맷이 깨지지 않도록 하기 위함입니다.

상태별 집계 메인 함수 구현

이제 핵심 함수인 DASH_updateApptStatusView()를 만듭니다. TODAY의 원본 데이터 구역을 읽어 상태별로 개수를 세고, 상단 요약(A4~)과 하단 상세(A10~)를 매번 다시 그립니다.

TODAY 한 시트 안에서 요약·상세 영역을 덮어쓰는 구조이므로, 동시에 두 번 이상 실행되면 한쪽 결과가 일부 덮어써질 수 있습니다. 이를 피하기 위해 LockService로 잠금을 사용합니다.

붙여넣을 위치

  • DASH_getViewDateKey_() 바로 아래
Apps Script (JavaScript)
// TODAY 시트를 읽어 상태별 요약·상세 뷰를 만듭니다. // → 대시보드2 메인 함수
function DASH_updateApptStatusView() {          // → 메뉴·수동 실행용
  const lock = LockService.getScriptLock();     // → 잠금 객체
  lock.waitLock(30000);                         // → 최대 30초 대기
  try {
    const ss = SpreadsheetApp.getActive();      // → 현재 스프레드시트
    const sheet = ss.getSheetByName(            // → TODAY 시트 찾기
      DASH_STATUS_CONFIG.TODAY_SHEET_NAME
    );
    if (!sheet) {                               // → 없으면
      throw new Error('TODAY 시트를 찾을 수 없습니다.'); // → 오류
    }

    const lastRow = sheet.getLastRow();         // → 마지막 행 번호
    const headerRow = DASH_STATUS_CONFIG.HEADER_ROW; // → 헤더 행
    if (lastRow <= headerRow) {                 // → 데이터 없음
      clearStatusViewArea_(sheet);              // → 기존 뷰 지우기
      DASH_getViewDateKey_();                   // → 기준일만 기록
      return 0;                                 // → 처리 행 수 0
    }

    const lastCol = sheet.getLastColumn();      // → 마지막 열 번호
    const dataRange = sheet.getRange(           // → 데이터 범위
      headerRow + 1, 1,                         // → 헤더 다음 행부터
      lastRow - headerRow,                      // → 데이터 행 수
      lastCol                                   // → 전체 열 수
    );
    const values = dataRange.getValues();       // → 2차원 배열로 읽기

    const statusCol = DASH_STATUS_CONFIG.STATUS_COLUMN_INDEX; // → 상태 열
    const counts = initStatusCounts_();         // → 상태별 카운터 초기화
    const detailRows = [];                      // → 상세 목록 배열

    for (let i = 0; i < values.length; i++) {   // → 각 행 순회
      const row = values[i];                    // → 현재 행

      if (row.join('').trim() === '') {         // → 완전히 빈 행이면
        continue;                               // → 건너뛰기
      }

      const rawStatus = String(row[statusCol - 1] || '') // → 상태값 읽기
        .trim()
        .toUpperCase();

      const key = mapStatusKey_(rawStatus);     // → 내부 키로 변환
      if (!counts.hasOwnProperty(key)) {        // → 사전에 없으면
        counts.OTHER++;                         // → 기타로 올리기
      } else {
        counts[key]++;                          // → 해당 상태 증가
      }

      detailRows.push(row);                     // → 상세 목록에 추가
    }

    DASH_getViewDateKey_();                     // → 기준일 기록
    writeStatusSummary_(sheet, counts);         // → 요약 영역 갱신
    writeStatusDetails_(sheet, dataRange, detailRows); // → 상세 영역 갱신

    return detailRows.length;                   // → 처리된 행 수
  } finally {
    lock.releaseLock();                         // → 잠금 해제
  }
}

// 상태별 카운터 객체를 초기화합니다.            // → 0으로 시작
function initStatusCounts_() {                  // → 내부 도우미
  return {
    WAITING: 0,                                 // → 대기
    ARRIVED: 0,                                 // → 도착
    IN_PROGRESS: 0,                             // → 작업중
    DONE: 0,                                    // → 완료
    OTHER: 0                                    // → 기타
  };
}

// 원시 상태값을 내부 상태 키로 매핑합니다.       // → STATUS_MAP 사용
function mapStatusKey_(rawStatus) {             // → 내부 도우미
  if (!rawStatus) {                             // → 빈 문자열이면
    return 'OTHER';                             // → 기타로 처리
  }
  const map = DASH_STATUS_CONFIG.STATUS_MAP;    // → 매핑 객체
  if (map.hasOwnProperty(rawStatus)) {          // → 정의된 값이면
    return map[rawStatus];                      // → 매핑된 키
  }
  return 'OTHER';                               // → 나머지는 기타
}

실무 팁

  • lock.waitLock(30000) 값은 환경에 따라 줄일 수 있지만, 너무 짧게 잡으면 사용자가 빠르게 버튼을 두 번 눌렀을 때 바로 실패할 수 있습니다.
  • row.join('').trim() === '' 조건은 “완전히 빈 줄”을 걸러, 중간에 남겨둔 간격 행이 통계에 들어가지 않도록 하는 장치입니다.

상태 뷰 영역 정리와 요약·상세 쓰기

마지막으로, 상태 뷰를 매번 새로 그리기 위한 정리 함수와 출력 함수를 구현합니다. 여기서 중요한 건 원본 TODAY 데이터 구역과 상태 뷰 구역을 명확하게 분리하는 것입니다.

  • 원본 TODAY 데이터: 1행 헤더 + 아래 데이터 행
  • 상태 요약: A4 행부터
  • 상태 상세: A10 행부터

이렇게 나누면 재실행 시 요약·상세 영역이 다시 집계 대상에 들어가 OTHER로 잡히는 문제를 막을 수 있습니다.

붙여넣을 위치

  • mapStatusKey_() 아래
Apps Script (JavaScript)
// 상태 뷰 요약·상세 영역을 초기화합니다.        // → 이전 결과 삭제
function clearStatusViewArea_(sheet) {          // → 내부 도우미
  const startRow = DASH_STATUS_CONFIG.SUMMARY_START_ROW; // → 요약 시작 행
  const lastRow = sheet.getMaxRows();          // → 시트 최대 행
  const lastCol = sheet.getMaxColumns();       // → 시트 최대 열

  if (lastRow >= startRow) {                   // → 지울 행이 있으면
    const range = sheet.getRange(              // → 요약+상세 범위
      startRow,                                // → 시작 행
      1,                                       // → A열부터
      lastRow - startRow + 1,                  // → 행 수
      lastCol                                  // → 모든 열
    );
    range.clearContent();                      // → 값 지우기
  }
}

// 상태별 카운트를 요약 영역에 씁니다.            // → 위쪽 표 만들기
function writeStatusSummary_(sheet, counts) {   // → 내부 도우미
  const startRow = DASH_STATUS_CONFIG.SUMMARY_START_ROW; // → 시작 행
  const startCol = DASH_STATUS_CONFIG.SUMMARY_START_COL; // → 시작 열
  const labels = DASH_STATUS_CONFIG.STATUS_LABELS; // → 라벨 목록

  const output = [];                            // → 시트에 쓸 2차원 배열
  output.push(['상태', '건수']);                // → 헤더 행

  labels.forEach(function(item) {               // → 각 상태 순회
    const key = item.key;                       // → 내부 키
    const label = item.label;                   // → 한글 라벨
    const count = counts[key] || 0;             // → 개수
    output.push([label, count]);                // → 한 줄 추가
  });

  const range = sheet.getRange(                 // → 출력 범위
    startRow,                                   // → 시작 행
    startCol,                                   // → 시작 열
    output.length,                              // → 행 수
    output[0].length                            // → 열 수
  );
  range.setValues(output);                      // → 값 쓰기
}

// TODAY 데이터에서 읽은 상세 행을 뷰 영역에 씁니다. // → 아래쪽 목록
function writeStatusDetails_(sheet, sourceRange, detailRows) { // → 내부 도우미
  const detailStartRow = DASH_STATUS_CONFIG.DETAIL_START_ROW; // → 상세 시작 행
  const headerRow = DASH_STATUS_CONFIG.HEADER_ROW; // → TODAY 헤더 행
  const lastCol = sourceRange.getNumColumns(); // → 열 개수

  // 먼저 기존 상세 영역만 지웁니다.             // → 다른 영역 보호
  const maxRows = sheet.getMaxRows();           // → 시트 전체 행 수
  if (maxRows >= detailStartRow) {              // → 상세 영역 존재
    const clearRange = sheet.getRange(          // → 상세 전체 범위
      detailStartRow,                           // → 시작 행
      1,                                        // → A열
      maxRows - detailStartRow + 1,             // → 행 수
      lastCol                                   // → 열 수
    );
    clearRange.clearContent();                  // → 내용 지우기
  }

  // 헤더를 TODAY 헤더에서 복사합니다.           // → 동일 헤더 유지
  const headerRange = sheet.getRange(           // → TODAY 헤더 범위
    headerRow,                                  // → 헤더 행
    1,                                          // → A열
    1,                                          // → 1행
    lastCol                                     // → 전체 열
  );
  const headerValues = headerRange.getValues(); // → 헤더 값 읽기

  const totalRows = detailRows.length + 1;      // → 헤더+데이터 행
  if (totalRows <= 1) {                         // → 데이터 없으면
    const destHeader = sheet.getRange(          // → 헤더 위치
      detailStartRow,                           // → 시작 행
      1,                                        // → A열
      1,                                        // → 1행
      lastCol                                   // → 열 수
    );
    destHeader.setValues(headerValues);         // → 헤더만 쓰기
    return;                                     // → 종료
  }

  const destRange = sheet.getRange(             // → 전체 출력 범위
    detailStartRow,                             // → 헤더 위치
    1,                                          // → A열
    totalRows,                                  // → 행 수
    lastCol                                     // → 열 수
  );

  const output = [];                            // → 출력용 배열
  output.push(headerValues[0]);                 // → 첫 행에 헤더
  for (let i = 0; i < detailRows.length; i++) { // → 상세 행 복사
    output.push(detailRows[i]);                 // → 한 행씩 추가
  }

  destRange.setValues(output);                  // → 한 번에 쓰기
}

실무 팁

  • 상세 영역을 A10 이후로 통째로 지우기 때문에, 이 구역에는 다른 메모나 계산식을 두지 않는 편이 안전합니다.
  • 필요하다면 “상태별 상세”를 별도 시트(TODAY_VIEW 등)로 분리하는 방식으로 응용해도 됩니다. 이때는 sheet.getSheetByName('TODAY_VIEW')처럼 시트를 바꿔 주면 됩니다.

onOpen 중복 선언 없이 메뉴 통합하기

이 시리즈의 여러 글(기본 편·등록 편·체크인 편·대시보드 구글시트 예약 현황 자동화 방법: 오늘 시트 만들기 — 대시보드 1편 등)에서 각자 onOpen을 선언하고 있습니다. Apps Script에서는 프로젝트 전체에서 onOpen 함수는 마지막 선언 하나만 동작하기 때문에, 여러 파일에 onOpen을 따로 두면 나중에 추가한 것만 실행되고 앞의 메뉴는 전부 사라집니다.

검사에서도 “시리즈 중복 선언 'onOpen'” 경고가 떴기 때문에, 이 글에서는 onOpen을 공용 엔트리 포인트 하나로 통합하고, 각 글에서 만든 메뉴 추가 함수만 나눠서 부르는 구조로 정리합니다.

앞 편들에서는 이미 이런 패턴을 쓰고 있습니다.

  • 입고예약 기본 1편: addApptBaseMenu_(menu)
  • 입고예약 기본 2편: APPT_addSeedMenu_(menu)
  • 입고예약 기본 3편: APPT_addSettingsMenu_(menu)
  • 입고예약 기본 4편: APPT_addWebAppMenu_(menu)
  • 입고예약 대시보드 1편: DASH_addDashboardMenu_(menu)

이 함수들은 이미 각각의 글에서 정의되어 있으므로, 이 편에서 다시 선언하면 안 됩니다.

이 편에서는 “이 함수들을 하나의 onOpen에서 어떻게 함께 부를지”만 정리합니다.

1) 공용 onOpen 패턴

아래처럼 프로젝트 전체에서 딱 하나의 onOpen만 두고, 그 안에서 각 모듈의 add○○Menu_를 모두 호출합니다.

Apps Script (JavaScript)
// 스프레드시트가 열릴 때 한 번 실행되는 공용 onOpen입니다.
// 주의: 프로젝트 전체에서 onOpen은 이 함수 하나만 남기고,
//       나머지 파일들에 있던 onOpen 선언은 삭제하거나 주석 처리합니다.
function onOpen(e) {
  const ui = SpreadsheetApp.getUi();
  const menu = ui.createMenu('예약 도구'); // 공용 상위 메뉴 이름

  // 1) 기본·시드 데이터 메뉴 (입고예약 기본 1·2편)
  if (typeof addApptBaseMenu_ === 'function') {
    addApptBaseMenu_(menu);        // /posts/20260811-q218-906ac0
  }
  if (typeof APPT_addSeedMenu_ === 'function') {
    APPT_addSeedMenu_(menu);       // /posts/20260812-q219-2e1150
  }

  // 2) 설정·웹앱 메뉴 (입고예약 기본 3·4편)
  if (typeof APPT_addSettingsMenu_ === 'function') {
    APPT_addSettingsMenu_(menu);   // /posts/20260812-q220-4bcc4c
  }
  if (typeof APPT_addWebAppMenu_ === 'function') {
    APPT_addWebAppMenu_(menu);     // /posts/20260813-q221-120eba
  }

  // 3) 대시보드 메뉴 (대시보드 1편)
  if (typeof DASH_addDashboardMenu_ === 'function') {
    DASH_addDashboardMenu_(menu);  // /posts/20260821-q233-f92dcd
  }

  // 4) 이 글(대시보드 2편)의 상태별 현황 메뉴를 추가합니다.
  //    다른 add○○Menu_ 함수와 같은 최상위 메뉴 아래에 붙입니다.
  menu.addSeparator()
      .addItem('상태별 현황 갱신', 'DASH_updateApptStatusView');

  menu.addToUi();                  // 실제 UI에 메뉴 추가
}

이 구조의 장점은 다음과 같습니다.

  • onOpen하나만 있어 Apps Script 규칙을 어기지 않습니다.
  • 각 글에서 만든 메뉴 확장 함수(addApptBaseMenu_, APPT_addSeedMenu_, APPT_addSettingsMenu_, APPT_addWebAppMenu_, DASH_addDashboardMenu_)를 그대로 재사용합니다.
  • 이 글에서 새로 추가한 DASH_updateApptStatusView도 “예약 시스템” 상위 메뉴 아래에 함께 묶여 보여집니다.

2) 기존 onOpen을 통합하는 순서

  1. 프로젝트에서 모든 파일을 열어 function onOpen을 검색합니다.
  2. 여러 개가 있다면, 내용은 위 공용 패턴 안으로 옮기고 실제 선언은 이 글의 onOpen 하나만 남깁니다.
  3. 나머지 파일에서 onOpen 선언은 주석 처리하거나 삭제합니다.
  4. 스크립트를 저장하고 시트를 새로 열어, 상단 메뉴에 “예약 시스템”이 보이고 그 안에 기존 메뉴 + “상태별 현황 갱신”이 함께 뜨는지 확인합니다.

실무에서 테스트할 때 체크할 것들

현장에 적용할 때는 아래 세 가지만 확인해도 “상태별 대시보드가 믿을 만한지” 감이 옵니다.

  1. 상태 오타·빈값 처리
  • TODAY 시트에서 일부 행의 상태를 ABC 같이 등록되지 않은 값이나 빈칸으로 넣어 봅니다.
  • DASH_updateApptStatusView() 실행 후 OTHER 건수가 이 행들까지 포함하는지 확인합니다.
  • TODAY 데이터 행 수와 (WAITING+ARRIVED+IN_PROGRESS+DONE+OTHER) 합계가 정확히 일치해야 합니다.
  1. 재실행 시 결과 일관성
  • TODAY에 테스트 데이터 5~10행 정도 두고
  • DASH_updateApptStatusView() 실행
  • 다시 한 번 연속 실행
  • 두 번 사이에 TODAY 원본 데이터는 건드리지 않은 상태에서 요약·상세 영역이 그대로 유지되는지 봅니다.
  • 그다음 특정 행의 상태를 PENDING → ARRIVED로 바꾸고 다시 실행해, 대기·도착 건수가 1씩 줄고 늘어나는지 확인합니다.
  1. STATUS_COLUMN_INDEX 위치 검증
  • TODAY의 상태 열이 실제로 몇 번째인지 다시 확인하고, STATUS_COLUMN_INDEX가 그 값과 일치하는지 확인합니다.
  • 의심되면 복사본 스프레드시트에서 값을 바꿔 가며 테스트하는 편이 안전합니다.

맺음말

이번 글에서는 구글시트 물류 대시보드 2편으로, TODAY 시트를 기반으로 예약 상태별 현황 대시보드를 구현했습니다.

  • DASH_STATUS_CONFIG로 상태 열·요약/상세 영역 위치·상태 매핑을 한곳에 모아두고,
  • DASH_updateApptStatusView() 하나로 기준일 표시, 상태별 요약, 상세 목록을 한 번에 갱신할 수 있게 만들었고,
  • onOpen을 공용 엔트리 포인트 하나로 통합해, 시리즈 전체 메뉴를 충돌 없이 한 메뉴 트리 안에 유지하는 방법까지 정리했습니다.

바로 해 볼 수 있는 최소 작업은 다음 네 단계입니다.

  1. TODAY 시트에서 “현재 상태”가 들어 있는 열 번호를 확인한다.
  2. DASH_STATUS_CONFIG.STATUS_COLUMN_INDEX를 그 값으로 맞춘다.
  3. 이 글의 코드를 Apps Script에 붙여넣고 저장한다.
  4. 스프레드시트를 새로 열어 “예약 시스템 ▸ 상태별 현황 갱신” 메뉴를 눌러 본다.

예약 데이터 몇 줄만 있어도 “대기·도착·작업중·완료·기타”가 몇 건인지 한눈에 보이는 느낌이 전과 확연히 다를 것입니다.