← Home

Sales Dashboard(HTML) 구축 작업 로그 (2026-08-17)

Original Content

문서 정보

작업일: 2026-08-17 · 작업 볼트: 05_SalesReport · 작업 도구: Claude Code (Sonnet 5) · 최종 산출물: TAB_Dashboard.html (배포용), Web Dashboard/ 폴더(버전 히스토리 10개) 이 문서는 나중에 GuWiki Ingest·log.md 작성용 원본 자료로 쓰기 위한 세션 캡처입니다. 아직 정제된 Wiki 문서가 아닙니다.

목적 / 배경

New Sales Dashboard v7(Power BI, .pbip)와 정확하게 동일한 기능을 하는, 브라우저에서 바로 열리는 인터랙티브 HTML 대시보드를 만드는 작업. 기존에 사용자가 가지고 있던 목업 파일(Turbo_Air_Sales_Dashboard.html, Tailwind + Chart.js + FontAwesome 스타일)의 디자인을 기준으로 삼아, 실제 라이브 PBI 리포트의 시각적 요소(visual.json)와 DAX 측정값을 리버스 엔지니어링해 1:1로 재현했다. 데이터는 Google Sheets 원본에서 스냅샷으로 가져와 파일에 내장(embed)하는 방식(완전 자체완결형 단일 HTML 파일).

이번 세션(2026-08-17)은 이미 v3까지 만들어진 상태에서 시작해 v4 → v9까지 총 6라운드의 사용자 피드백을 반영했고, 마지막에 파일 정리 및 배포 준비까지 진행했다.

핵심 아키텍처 (전 라운드 공통)

  • 데이터 규칙: 라이브 .pbip 모델이 실제로 쓰는 블랙리스트 규칙(Status가 빈값/Cancelled/Hold가 아니면 전부 포함)을 그대로 사용. 이는 GuWiki 영업판매실적 조회 절차와 quartz-site(turboairbrain.uk 챗봇 백엔드, _google-sheets.js)가 쓰는 화이트리스트 규칙(Inv No 발행 + Status∈{Credit,Invoiced,Confirmed})과 의도적으로 다르다 — 이 대시보드의 목표가 “라이브 PBI 리포트와 수치 일치”이기 때문. HWD 딜러의 Rebate(5%, 2026년) = $12,972가 매 라운드 회귀 테스트의 정답값(사용자가 실제 PBI 화면에서 캡처한 값)으로 사용됨.
  • 파일 구조: Tailwind CSS(CDN) + Chart.js(CDN) + chartjs-plugin-datalabels(CDN) + FontAwesome(CDN). SALES_ROWS(배열의 배열, compact) + SALES_META + DAMAGE_DATA를 인라인 <script>로 내장. 빌드는 Python으로 “HTML 셸 + 앱 JS + 데이터 JS”를 병합하는 방식(테스트하기 쉽게 분리해서 작성 → 병합 → 로컬 JS 파일 삭제, 최종 산출물은 단일 HTML 파일 하나만 남김).
  • 검증 방법: 매 라운드마다 (1) node --check로 문법 검사, (2) Node.js + 커스텀 DOM/Chart.js 스텁(dom_stub.js)으로 렌더 함수를 직접 호출해 회귀 테스트, (3) HWD Rebate(5%, 2026)=$12,972 패리티 재확인.
  • 사용자의 고정 지침: 기존 파일을 절대 덮어쓰지 않고, 매 라운드마다 새 번호(_v4, _v5, …)로 새 파일을 만든다.

라운드별 작업 (v4 → v9)

v4 — 한국어 전환 + 페이지별 독립 필터

요청사항: 전체 UI 한국어화, Overview 페이지 문구 정리, Today’s Snapshot 위치 이동, Backlog/Damaged 등 NSW·VIC 통합 박스, 라인차트 3버튼(수주/출고/매출)으로 축소, 월간 선택 시 월 지정 컨트롤 추가, 툴팁 AVG 소수점 2자리 + 라벨 단축, 페이지별 필터 완전 독립(전역 공유 금지), 연도 필터 최신순 정렬, TAB 아이콘 교체, 서브타이틀 변경, 기본 필터값(월간·2024-2026·전체·QLD 제외) 지정. 작업내용: tabFilters 객체로 탭별 독립 필터 상태 아키텍처 전면 개편, Damaged Sale/Clearance Sale을 주(state)별로 재계산, DASH_CHART_DEFS를 3개로 축소. 결과/이슈: turboairbrain.uk의 TAB 아이콘을 가져오려 했으나 401 Unauthorized로 실패 → 이후 모든 라운드에서 “TAB” 텍스트 배지로 대체(미해결 상태로 계속 이월).

v5 — 레퍼런스 목업 디자인 이식

요청사항: 스크린샷(레퍼런스 목업)과 동일한 카드/폰트 디자인, 오늘의 현황 카드를 좌측 사이드바 상단에 NSW/VIC 2행 배치 + 아이콘, 리베이트·운임·기타 박스와 백로그류 박스 위치 교체 + 크기·폰트 축소, 필터 폰트를 레퍼런스와 통일, 라인차트 색상 대비 강화, YrCompare 진입 시 차트 기본 미표시 버그 수정(1차 시도), 헤더에 Live DB Connected/필터초기화/스냅샷 배지 복원, 딜러·리베이트35·리베이트41 기본 거래처 지정. 작업내용: Turbo_Air_Sales_Dashboard.html(원본 목업)의 정확한 클래스/폰트(Inter)를 그대로 이식, 딜러 기본값=HWD-Hospitality World Direct, 리베이트35 기본값=Industry Kitchens, 리베이트41 기본값=Alpha Catering Equipment로 지정(실제 데이터에 존재하는 정확한 회사명 문자열을 사전 조회해서 사용). 결과: 디자인 통일 완료. YrCompare 차트 미표시 문제는 이 라운드에서 완전히 해결되지 않음 (이후 v6, v7, v8에서도 반복 보고됨 — 최종적으로 v9에서 근본 원인 해결).

v6 — 레이아웃 고정 + 아이콘 통일 + 완료월 기준 비교

요청사항: 상단 기간 필터바를 스크롤해도 고정, 아이콘 없는 박스에 전부 아이콘 추가 + 동일 KPI는 동일 디자인, AVG 툴팁/범례 삭제, AVG 선을 계열과 동일 색+반투명, YrCompare 차트 미표시 재수정 요청, 연도별 요약을 “오늘 기준 완료된 월”까지만 비교(예: 8월이면 17월만, 미완료 연도 때문에 전년비가 왜곡되는 문제 해결), “필터” 텍스트 제거, 랭킹 막대차트 값 레이블 추가. 작업내용: 헤더+필터바를 하나의 sticky 블록으로 병합(JS로 실제 높이를 측정해 사이드바 offset 동기화), completedMonthCutoff() 함수로 모든 연도를 동일하게 “1월(최신월-1)월”로 잘라서 비교하도록 수정. 결과: 완료월 기준 비교 로직은 정확히 작동 확인(2026 vs 2025 YoY가 왜곡된 큰 음수 대신 -0.2%로 정상화됨). YrCompare 차트 미표시 문제는 여전히 미해결(원인 오진단 — 당시엔 Chart.js/캔버스 타이밍 문제로 추정하고 rAF 지연 렌더링으로 “수정”했다고 판단했으나 실제로는 다른 원인이었음).

v7 — 상세 UX 다듬기

요청사항: AVG 라벨이 오른쪽 끝에서 잘려 보이는 문제 수정 + 투명도 강화, 툴팁 폰트를 더 얇게 + 색상 스와치를 포인트 스타일이 아닌 계열 실제 색상 박스로 수정, 라인차트에 세로 크로스헤어(보조선) 추가, 리베이트 35%/41% 페이지에 구간표(Scheme table) 삽입, 랭킹 페이지 필터를 차트 바로 위로 이동. 작업내용: AVG 배지를 왼쪽 첫 지점으로 이동, tooltip.callbacks.labelColor로 스와치 강제 고정, chart.tooltip._active 기반 커스텀 crosshair 플러그인 작성, SCHEME 상수 기반 구간표 렌더 함수 추가. 결과: 모두 정상 반영 확인.

v8 — 대규모 아키텍처 변경 (여기서 오히려 회귀 발생)

요청사항: YrCompare “차트가 안 보이다가 월별/누적 클릭해야 나타나는” 문제 재수정, YrCompare 필터(지역/거래처/모델) 연동 안 되는 문제 수정, 연도별 요약에 매출건수/수주건수 전년비 열 추가, 리베이트35/41 디자인을 v6로 되돌리고 구간표는 사이드바로 축소 이동, 툴팁 폰트 추가 축소, 제목에 이미 범례 설명이 들어간 단일 계열 차트는 범례 숨김, 거래 내역 표를 “최근 200건 제한” 대신 전체 레코드 10건씩 페이지네이션으로 변경. 작업내용: ensureChart()를 매번 destroy+재생성하던 방식에서 기존 인스턴스를 data/options만 교체 후 .update()하는 방식으로 전면 교체, 탭 전환 시 즉시 렌더 + 60ms 후 재렌더(보정 패스) 추가, renderPagination() 헬퍼로 딜러/리베이트35/리베이트41 거래내역 표 페이지네이션 구현. 결과: Node 테스트는 전부 통과 처리했으나, 사용자가 실제 브라우저에서 재확인한 결과 YrCompare 차트 미표시 + 필터 미연동 문제가 v6/v7과 완전히 동일하게 재발. → 다음 라운드로 이월.

v9 — 근본 원인 발견 및 수정 (이번 세션의 핵심 성과)

요청사항: “위 1과 2(YrCompare 차트 초기 미표시 + 필터 연동)가 아직도 제대로 작동하지 않는다”는 재보고. 진단 과정: 기존 Node 테스트가 왜 이 버그를 못 잡았는지부터 재검토. dom_stub.js의 document.getElementById가 요청받은 어떤 id에도 가짜 엘리먼트를 자동 생성하는 방식이었다는 것을 발견 — 즉, 실제 브라우저라면 null을 반환했을 상황에서도 테스트 환경에서는 항상 유효한 엘리먼트가 반환되어 버그가 은폐되고 있었음. 실제 원인: renderCoreKpis('yr') 함수가 yrRebate/yrFreight/yrExtra라는, YrCompare 패널에는 애초에 존재하지 않는 엘리먼트에 .textContent를 대입하려고 시도 → 실제 브라우저에서는 TypeError: Cannot set properties of null 발생 → 이 예외 때문에 함수가 중단되면서 바로 다음 줄의 renderYrCompareCharts() 호출이 전혀 실행되지 못함. 이 하나의 크래시가 “탭 진입 시 차트 미표시”와 “필터 변경 시 차트/표 미갱신” 두 증상을 동시에 설명한다 (월별/누적 드롭다운만은 renderYrCompareCharts()를 직접 호출해 이 크래시를 우회했기 때문에 그때만 정상 동작했던 것). 수정: document.getElementById(id).textContent = ... 직접 대입을, 엘리먼트가 없으면 조용히 넘어가는 setText(id, text) 헬퍼로 교체. YrCompare 패널은 원래 설계대로 4개 KPI 카드(수주/출고/매출/리베이트차감후매출)만 유지. 테스트 하네스 보강: 실제 배포 HTML에서 id="..."를 전부 파싱해 그 목록에 없는 id는 null을 반환하는 dom_stub_strict.js를 새로 작성. 이 엄격한 스텁으로 v8을 재검사하니 실제 브라우저와 동일한 TypeError가 그대로 재현됐고, v9에서는 전체 탭·전체 컨트롤·HWD $12,972 패리티까지 모두 깨끗하게 통과함을 확인. (같은 클래스의 버그가 앞으로도 재발하지 않도록 이 스텁을 표준 회귀 테스트 도구로 채택.)

최종 파일 정리 및 배포 준비

  • 원본 목업(Turbo_Air_Sales_Dashboard.html)부터 _v9까지 10개 버전 파일 전체를 05_SalesReport/Web Dashboard/ 폴더로 이동(버전 히스토리 보관용). 기존 05_SalesReport/Dashboard/는 Power BI .pbip 원본 프로젝트 폴더라 혼동 방지를 위해 별도 폴더로 분리.
  • v9 내용을 복사해 05_SalesReport/TAB_Dashboard.html(볼트 루트, 버전 번호 없는 배포용 파일명)로 저장 — turboairbrain.uk 업로드 예정. 최초 요청 시 Turbo_Air_Sales_Dashboard.html로 저장했다가, 기존 원본 목업 파일명과 혼동될 수 있다는 사용자 지적에 따라 TAB_Dashboard.html로 재변경.
  • 미해결 질문 (다음 세션 논의 필요): TAB_Dashboard.html은 완전 정적 스냅샷(기준일 2026-08-18, 16,906행)이며 fetch/API 호출이 전혀 없어 실시간 갱신이 안 됨. 실시간화하려면 (1) 수동 재빌드·재배포를 반복하거나, (2) quartz-site의 기존 Google Sheets 백엔드(_google-sheets.js)를 연동하되 그 백엔드가 쓰는 화이트리스트 규칙과 이 대시보드의 블랙리스트 규칙 차이를 먼저 조율해야 함 — 아직 어느 방향으로 갈지 결정되지 않음.

교훈 / 향후 재발 방지 (Ingest 시 특히 주목할 부분)

  1. “테스트 통과”와 “실제로 작동함”은 다르다. Node 기반 회귀 테스트가 두 라운드 연속(v6→v7→v8) 같은 버그를 “고쳤다”고 오판했던 이유는 테스트 더블(DOM 스텁)이 실제 환경보다 관대했기 때문. 브라우저 전용 실패 모드(예: getElementById가 null을 반환하는 경우)를 흉내 내지 못하는 테스트는 거짓 안전감을 준다.
  2. 공용 렌더 함수(renderCoreKpis(prefix))를 여러 페이지에서 재사용할 때, 페이지마다 존재하는 DOM 엘리먼트 집합이 다르면(Dash=7개 vs YrCompare=4개) 방어적 코딩(엘리먼트 존재 확인 후 대입)이 필수.
  3. 사용자가 같은 버그를 “여전히 안 된다”고 반복 보고할 때는, 기존 가설을 미세조정하기보다 테스트 방법론 자체를 의심하는 것이 더 빠른 길이었다.
  4. TAB 아이콘(turboairbrain.uk 401 Unauthorized), 실시간 데이터 연동 여부 — 두 건 모두 사용자 확인/결정이 필요한 미해결 항목으로 남아있음.

Ingest Notes

  • 후속 진행 확인 (2026-08-18 ingest 시점): 이 로그의 “미해결 질문” §(실시간 데이터 연동 방향)은 2026-08-18 별도 세션에서 해결됨 — quartz-site의 _google-sheets.js를 직접 재사용하지 않고, 전용 로컬 Node 스크립트(pull-sales-data.mjs)를 새로 만들어 서비스 계정을 별도 발급해 인증하는 방식을 택했다. 상세: Google Sheets 연동 실시간 대시보드 구축 가이드 (TAB Dashboard 사례).
  • 화이트리스트/블랙리스트 규칙 차이는 의도적으로 유지됨: TAB_Dashboard.html의 매출/수주 집계는 여전히 이 로그가 기록한 블랙리스트 규칙(Status 빈값/Cancelled/Hold 제외)을 그대로 쓴다 — 2026-08-18 실시간 연동 작업은 데이터 소스(정적 임베드 → sales-data.js 외부 파일)만 바꿨을 뿐, 이 로그에 기록된 집계 규칙 자체는 건드리지 않았다. /ask 챗봇(_google-sheets.js)의 화이트리스트 규칙과는 여전히 별개다 — 두 시스템이 다른 목적(하나는 라이브 PBI 패리티, 하나는 자연어 질의응답)을 가지므로 의도적 분기로 유지.
  • TAB 아이콘 401 Unauthorized 이슈: 2026-08-18 세션에서도 별도로 다루지 않음 — 여전히 미해결.