diff --git a/claudedocs/[IMPL-2026-03-08] frontend-weekly-0302-0308.md b/claudedocs/[IMPL-2026-03-08] frontend-weekly-0302-0308.md
new file mode 100644
index 00000000..3511af63
--- /dev/null
+++ b/claudedocs/[IMPL-2026-03-08] frontend-weekly-0302-0308.md
@@ -0,0 +1,250 @@
+# 프론트엔드 주간 구현내역 (2026-03-02 ~ 2026-03-08)
+
+> 총 커밋 59개 (feat 30 / fix 17 / refactor 3 / chore 3 / merge 1 / 기타 5)
+
+---
+
+## 1. 품질관리 — Mock→API 전환 + 검사 모달/문서 대폭 개선
+
+**커밋**: 50e4c72c, 563b240f, 899493a7, fe930b58, e7263fee, 4ea03922, 295585d8, c150d807, e75d8f9b (9개)
+**변경 규모**: +2,210 / -566 라인
+
+### 1-1. API 전환
+- `USE_MOCK_FALLBACK = false` 설정, Mock 데이터 제거
+- 엔드포인트: `/api/v1/quality/documents`, `/api/v1/quality/performance-reports`
+- snake_case → camelCase 변환 함수 구현
+- InspectionFormData에 `clientId`, `inspectorId`, `receptionDate` 필드 추가
+
+### 1-2. 검사 모달 개선 (InspectionInputModal)
+- 일괄 합격/초기화 토글 버튼 추가
+- 시공 치수 필드 (너비/높이) 추가
+- 변경사유 입력 필드 추가
+- 사진 첨부 (최대 2장, base64)
+- 이전/다음 개소 네비게이션 + 자동저장
+- 레거시 검사 데이터 통합 (합격/불합격/진행중/미완)
+
+### 1-3. 수주선택 모달 (OrderSelectModal)
+- 발주처(clientName) 컬럼 추가
+- 동일 발주처 + 동일 모델 필터링 제약
+- `SearchableSelectionModal`에 `isItemDisabled` 콜백 추가 (공통 컴포넌트 확장)
+- 비활성 항목 스타일링 + 전체선택 시 비활성 항목 제외
+
+### 1-4. 제품검사 성적서 (FqcDocumentContent)
+- 8컬럼 동적 렌더링: No / 검사항목 / 세부항목 / 검사기준 / 검사방법 / 검사주기 / 측정값 / 판정
+- rowSpan 병합: 카테고리 단일 + method+frequency 복합 병합
+- measurement_type별 처리: checkbox → 양호/불량, numeric → 숫자입력, none → 비활성
+- FQC 모드 우선 + legacy fallback 패턴
+
+### 1-5. 제품검사 요청서 (FqcRequestDocumentContent) — 신규
+- 양식 기반 동적 렌더링 (template_id: 66)
+- 결재라인 + 기본정보(7개) + 입력섹션(4개) + 사전통보 테이블
+- EAV 데이터 구조: section_id, column_id, row_index, field_key, field_value
+- EAV 문서 없을 때 legacy fallback 적용
+
+### 1-6. 수주 연결 동기화
+- order_ids 배열 매핑 (다중 수주 지원)
+- 개소별 inspectionData 서버 저장
+
+### 주요 파일
+- `src/components/quality/InspectionManagement/actions.ts`
+- `src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx`
+- `src/components/quality/InspectionManagement/OrderSelectModal.tsx`
+- `src/components/quality/InspectionManagement/documents/FqcDocumentContent.tsx` (신규)
+- `src/components/quality/InspectionManagement/documents/FqcRequestDocumentContent.tsx` (신규)
+- `src/components/organisms/SearchableSelectionModal/SearchableSelectionModal.tsx`
+
+---
+
+## 2. 문서스냅샷 시스템 (Lazy Snapshot) — 신규 기능
+
+**커밋**: 31f523c8, a1fb0d4f, 8250eaf2, 72a2a3e9, 04f2a8a7 (5개)
+**변경 규모**: +300 라인
+
+### 개요
+문서 저장/조회 시 `rendered_html` 스냅샷을 자동 캡처하여 백엔드에 전송하는 시스템.
+
+### 2-1. 수동 캡처 (저장 시)
+- 검사성적서(InspectionReportModal): `contentWrapperRef.innerHTML` 캡처 → 저장 시 `rendered_html` 파라미터 포함
+- 작업일지(WorkLogModal): 동일 패턴
+- 수입검사(ImportInspectionInputModal): 오프스크린 렌더링 방식
+
+### 2-2. Lazy Snapshot (조회 시 자동 캡처)
+- 조건: `rendered_html === NULL`인 문서 조회 시
+- 동작: 500ms 지연 → innerHTML 캡처 → 백그라운드 PATCH
+- 비차단(non-blocking): UI에 영향 없이 백그라운드 처리
+- `patchDocumentSnapshot()` 서버 액션으로 전송
+
+### 2-3. 오프스크린 렌더링 유틸리티
+- `src/lib/utils/capture-rendered-html.tsx` (신규)
+- 폼 HTML이 아닌 실제 문서 렌더링 결과를 캡처
+- readOnly 모드 자동 캡처 useEffect 제거 (불필요한 PUT 요청 방지)
+
+### 적용 범위
+| 문서 | 수동 캡처 | Lazy Snapshot |
+|------|-----------|---------------|
+| 검사성적서 | ✅ | ✅ |
+| 작업일지 | ✅ | ✅ |
+| 수입검사 | ✅ (오프스크린) | - |
+| 제품검사 요청서 | ✅ | ✅ |
+
+### 주요 파일
+- `src/lib/utils/capture-rendered-html.tsx` (신규)
+- `src/components/production/WorkOrders/documents/InspectionReportModal.tsx`
+- `src/components/production/WorkerScreen/WorkLogModal.tsx`
+- `src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx`
+- `src/components/production/WorkOrders/actions.ts`
+
+---
+
+## 3. 생산지시 — API 연동 + 작업자 화면 + 중간검사
+
+**커밋**: fa7efb7b, 8b6da749, 4331b84a, 0166601b, 8bcabafd, 2a2a356f, b45c35a5, 0b81e9c1 (8개)
+**변경 규모**: +2,000 라인
+
+### 3-1. 생산지시 목록/상세 API 연동
+- Mock → API 전환 (`executePaginatedAction` + `buildApiUrl` 패턴)
+- 서버사이드 페이지네이션 + 동적 탭 카운트 (stats API)
+- WorkOrder 상태 배지 6단계: 미배정→배정→작업중→검사→완료→출하
+- BOM null 상태 처리
+
+### 3-2. 절곡 중간검사 입력 모달 (InspectionInputModal)
+- 7개 제품 항목 통합 폼
+- 제품 ID 자동 매칭: 정규화 → 키워드 → 인덱스 fallback (3단계)
+- cellValues 구조: `{bending_state, length, width, spacing}`
+- PO 단위 데이터 공유 모델: 동일 PO 내 모든 아이템이 inspection_data 공유
+
+### 3-3. 자재투입 모달 (MaterialInputModal)
+- 동일 자재 다중 BOM 그룹 LOT 독립 관리
+- `bomGroupKey = ${item_id}-${category}-${partType}` 그룹핑
+- 카테고리 정렬: 가이드레일(1) → 하단마감재(2) → 셔터박스(3) → 연기차단재(4)
+- FIFO 자동충전 시 그룹 간 물리적 LOT 가용량 추적
+- 번호 배지(①②③) + partType 배지
+
+### 3-4. 공정 단계 검사범위 설정 (InspectionScope) — 신규
+- 전수검사 / 샘플링 / 그룹 3가지 타입
+- 샘플링 시 샘플 수(n) 입력 지원
+- StepForm 컴포넌트에 UI 추가, options JSON으로 API 저장
+
+### 주요 파일
+- `src/components/production/ProductionOrders/actions.ts`, `types.ts`
+- `src/components/production/WorkerScreen/InspectionInputModal.tsx`
+- `src/components/production/WorkerScreen/MaterialInputModal.tsx`
+- `src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx` (신규)
+- `src/components/process-management/StepForm.tsx`
+- `src/types/process.ts`
+
+---
+
+## 4. 출하/배차 — 배차 다중행 + 차량관리 API + 출고관리
+
+**커밋**: 83a23701, 1d380578, 5ff5093d, f653960a, a4f99ae3, 03d129c3 (6개)
+**변경 규모**: +2,400 / -1,100 라인
+
+### 4-1. 배차정보 다중 행 API 연동
+- `vehicle_dispatches` 배열 지원 (기존 단일 배차 → 다중 배차)
+- transform 함수: `transformApiToDetail`, `transformCreateFormToApi`, `transformEditFormToApi` 갱신
+- 레거시 단일 배차 필드 하위호환 유지
+
+### 4-2. 배차차량관리 Mock→API 전환
+- `executePaginatedAction` + `buildApiUrl` 패턴 적용
+- `transformToListItem()` / `transformToDetail()` snake_case → camelCase 변환
+- 쿼리 파라미터: `search`, `status`, `start_date`, `end_date`, `page`, `per_page`
+
+### 4-3. 출고관리 목록 필드 매핑
+- `writer_name`, `writer_id`, `delivery_date` 등 5개 필드 API 매핑 추가
+- `OrderInfoApiData` 타입으로 주문 연결 정보 처리
+
+### 4-4. 배차 상세/수정 레이아웃 개선
+- 기본정보 그리드: 1열 → 2×4열 레이아웃
+
+### 4-5. 출하관리 캘린더
+- 기본 뷰: day → week-time 변경
+
+### 주요 파일
+- `src/components/outbound/ShipmentManagement/actions.ts`
+- `src/components/outbound/VehicleDispatchManagement/actions.ts`
+- `src/components/outbound/ShipmentManagement/ShipmentDetail.tsx`, `ShipmentEdit.tsx`
+
+---
+
+## 5. 전자결재 — 결재함 확장 + 연결문서
+
+**커밋**: 181352d7, 72cf5d86 (2개)
+**변경 규모**: +458 / -127 라인
+
+### 5-1. 결재함 기능 확장
+- 결재함 API 연동:
+ - `GET /api/v1/approvals/inbox` — 결재함 목록
+ - `GET /api/v1/approvals/inbox/summary` — 통계
+ - `POST /api/v1/approvals/{id}/approve` — 승인
+ - `POST /api/v1/approvals/{id}/reject` — 반려
+- 문서 상태: DRAFT → PENDING → APPROVED / REJECTED / CANCELLED
+
+### 5-2. 연결문서 기능 (LinkedDocumentContent) — 신규
+- 검사성적서, 작업일지 등을 결재 문서에 연결하여 렌더링
+- DocumentHeader 컴포넌트 활용, 결재라인/상태배지/메타 정보 표시
+
+### 5-3. 모바일 반응형
+- AuthenticatedLayout: 사이드바/메인 콘텐츠 모바일 대응
+- HeaderFavoritesBar 전면 재설계
+- SearchableSelectionModal HTML 유효성 수정
+
+### 주요 파일
+- `src/components/approval/ApprovalBox/actions.ts`, `index.tsx`, `types.ts`
+- `src/components/approval/DocumentDetail/LinkedDocumentContent.tsx` (신규)
+- `src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx`
+- `src/layouts/AuthenticatedLayout.tsx`
+- `src/components/layout/HeaderFavoritesBar.tsx`
+
+---
+
+## 6. CEO 대시보드 — API 연동 + 섹션 확장 + 리팩토링
+
+**커밋**: 9ad4c8ee, 23fa9c0e, cde93336, 4e179d2e, db84d679, 1bccaffe, bec933b3, 1675f3ed (8개)
+**별도 문서**: `claudedocs/dashboard/[VERIFY-2026-03-06] ceo-dashboard-data-flow-verification.md`
+
+### 주요 변경
+- SummaryNavBar 추가 (상단 요약 데이터 네비게이션)
+- 접대비/복리후생비/매출채권/캘린더 섹션 개선
+- 컴포넌트 분리 및 모달/섹션 리팩토링
+- mockData/modalConfigs 정리
+- API 연동 강화 (회계/결재/HR 섹션)
+- `invalidateDashboard()` 시스템 추가 (5개 도메인 연동)
+
+---
+
+## 7. 회계 — 계정과목 공통화 + 어음 리팩토링
+
+**커밋**: 7d369d14, 1691337f, a4f99ae3(일부) (3개)
+**별도 문서**: `claudedocs/[IMPL-2026-03-06] account-subject-unification-checklist.md`
+
+### 주요 변경
+- AccountSubjectSelect 공통 컴포넌트: 7개 페이지에 일괄 적용
+- 매출/매입/부실채권/일일보고 UI 개선
+- BillManagement 섹션 분리: 11개 섹션 컴포넌트 + 커스텀 훅(`useBillForm`, `useBillConditions`)
+
+---
+
+## 8. 기타
+
+### E2E 테스트
+- `f5bdc5ba`: 11개 FAIL 시나리오 수정 후 전체 PASS
+
+### 인프라
+- `f9eea0c9`, `c18c68b6`: Slack 알림 채널 분리 (product_infra → deploy_react)
+- `888fae11`: next dev에서 --turbo 플래그 제거
+
+---
+
+## 문서 현황
+
+| 도메인 | 문서 상태 |
+|--------|----------|
+| 품질관리 Mock→API | ✅ 본 문서 §1 |
+| 문서스냅샷 (Lazy Snapshot) | ✅ 본 문서 §2 |
+| 생산지시 API 연동 | ✅ 본 문서 §3 |
+| 출하/배차 API 연동 | ✅ 본 문서 §4 |
+| 전자결재 확장 | ✅ 본 문서 §5 |
+| CEO 대시보드 | ✅ 별도 문서 존재 |
+| 계정과목 공통화 | ✅ 별도 문서 존재 |
+| 백엔드 구현내역 | ✅ 일별 문서 존재 (03-02 ~ 03-08) |
diff --git a/claudedocs/_index.md b/claudedocs/_index.md
index 2517fe6d..7dce9e7c 100644
--- a/claudedocs/_index.md
+++ b/claudedocs/_index.md
@@ -1,6 +1,6 @@
# claudedocs 문서 맵
-> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-02-23)
+> 프로젝트 기술 문서 인덱스 (Last Updated: 2026-03-09)
## 빠른 참조
@@ -10,6 +10,13 @@
| **[`[REF] technical-decisions.md`](./architecture/[REF]%20technical-decisions.md)** | 프로젝트 기술 결정 사항 (13개 항목) |
| **[`[GUIDE] common-page-patterns.md`](./guides/[GUIDE]%20common-page-patterns.md)** | 공통 페이지 패턴 가이드 |
+## 주간 구현내역
+
+| 기간 | 문서 |
+|------|------|
+| 2026-03-02 ~ 03-08 | **[`[IMPL-2026-03-08] frontend-weekly-0302-0308.md`](./%5BIMPL-2026-03-08%5D%20frontend-weekly-0302-0308.md)** |
+| (백엔드 일별) | `backend/2026-03-02_구현내역.md` ~ `2026-03-08_구현내역.md` |
+
---
## 폴더 구조
@@ -38,9 +45,11 @@ claudedocs/
├── architecture/ # 아키텍처 & 시스템 & 기술 결정
├── changes/ # 변경이력
├── refactoring/ # 리팩토링 체크리스트
+├── outbound/ # 출하/배차관리
├── vehicle/ # 차량관리
├── material/ # 자재관리
├── approval/ # 결재관리
+├── backend/ # 백엔드 일별 구현내역
├── customer-center/ # 고객센터
├── components/ # 컴포넌트 문서
├── vercel/ # Vercel 배포
diff --git a/claudedocs/approval/[IMPL-2026-03-07] approval-box-linked-documents.md b/claudedocs/approval/[IMPL-2026-03-07] approval-box-linked-documents.md
new file mode 100644
index 00000000..fd183b32
--- /dev/null
+++ b/claudedocs/approval/[IMPL-2026-03-07] approval-box-linked-documents.md
@@ -0,0 +1,59 @@
+# 전자결재 결재함 확장 및 연결문서 기능
+
+> **작업일**: 2026-03-01 ~ 03-07
+> **상태**: ✅ 완료
+> **커밋**: 181352d7, 72cf5d86
+
+---
+
+## 개요
+
+결재함(ApprovalBox) API 연동, 연결문서(LinkedDocumentContent) 렌더링,
+모바일 반응형 레이아웃 개선.
+
+---
+
+## 1. 결재함 API 연동
+
+- [x] 결재함 목록: `GET /api/v1/approvals/inbox`
+- [x] 결재함 통계: `GET /api/v1/approvals/inbox/summary`
+- [x] 승인 처리: `POST /api/v1/approvals/{id}/approve`
+- [x] 반려 처리: `POST /api/v1/approvals/{id}/reject`
+- [x] 문서 상태 매핑: DRAFT → PENDING → APPROVED / REJECTED / CANCELLED
+- [x] 결재함 상태 헬퍼 함수 추가
+
+### 주요 파일
+- `src/components/approval/ApprovalBox/actions.ts` (+123/-7)
+- `src/components/approval/ApprovalBox/index.tsx` (+47/-1)
+- `src/components/approval/ApprovalBox/types.ts` (+9/-1)
+
+---
+
+## 2. 연결문서 기능 (LinkedDocumentContent) — 신규
+
+검사성적서, 작업일지 등 문서관리 시스템의 문서를 결재 문서에 연결하여 렌더링.
+
+- [x] `LinkedDocumentContent` 컴포넌트 신규 생성
+- [x] `DocumentHeader` 컴포넌트 활용 (일관된 스타일)
+- [x] 결재라인 / 상태배지 / 문서 메타정보 표시
+- [x] `DocumentDetailModalV2`에 연결문서 렌더링 통합
+
+### 주요 파일
+- `src/components/approval/DocumentDetail/LinkedDocumentContent.tsx` (신규, +133)
+- `src/components/approval/DocumentDetail/DocumentDetailModalV2.tsx`
+- `src/components/approval/DocumentDetail/types.ts` (+27/-1)
+
+---
+
+## 3. 모바일 반응형 개선
+
+- [x] `AuthenticatedLayout`: 사이드바/메인 콘텐츠 모바일 대응
+- [x] `HeaderFavoritesBar`: 전면 재설계 (+315/-127)
+- [x] `Sidebar`: 반응형 숨김/표시
+- [x] `SearchableSelectionModal`: HTML 유효성 에러 수정
+
+### 주요 파일
+- `src/layouts/AuthenticatedLayout.tsx` (+12/-1)
+- `src/components/layout/HeaderFavoritesBar.tsx` (+315/-127)
+- `src/components/layout/Sidebar.tsx` (+8/-1)
+- `src/components/organisms/SearchableSelectionModal.tsx` (+79/-2)
diff --git a/claudedocs/architecture/[IMPL-2026-03-06] lazy-snapshot-system.md b/claudedocs/architecture/[IMPL-2026-03-06] lazy-snapshot-system.md
new file mode 100644
index 00000000..e792e398
--- /dev/null
+++ b/claudedocs/architecture/[IMPL-2026-03-06] lazy-snapshot-system.md
@@ -0,0 +1,103 @@
+# 문서스냅샷 시스템 (Lazy Snapshot)
+
+> **작업일**: 2026-03-06 ~ 03-07
+> **상태**: ✅ 완료
+> **커밋**: 31f523c8, a1fb0d4f, 8250eaf2, 72a2a3e9, 04f2a8a7
+
+---
+
+## 개요
+
+문서 저장/조회 시 `rendered_html` 스냅샷을 자동 캡처하여 백엔드에 전송하는 시스템.
+MNG 측에서 문서 인쇄 시 스냅샷 기반 렌더링에 활용.
+
+---
+
+## 아키텍처
+
+```
+[문서 저장 시]
+ 컴포넌트 → contentWrapperRef.innerHTML 캡처
+ → API 요청에 rendered_html 파라미터 포함 → 백엔드 저장
+
+[문서 조회 시 — Lazy Snapshot]
+ rendered_html === NULL 감지
+ → 500ms 대기 (렌더링 완료 대기)
+ → innerHTML 캡처
+ → 백그라운드 PATCH 전송 (비차단)
+```
+
+---
+
+## 1. 수동 캡처 (저장 시)
+
+문서 저장 시 DOM에서 `innerHTML`을 읽어 `rendered_html` 파라미터로 함께 전송.
+
+- [x] 검사성적서 (InspectionReportModal) — `contentWrapperRef.innerHTML`
+- [x] 작업일지 (WorkLogModal) — `contentWrapperRef.innerHTML`
+- [x] 수입검사 (ImportInspectionInputModal) — 오프스크린 렌더링 방식
+
+### 주요 파일
+- `src/components/production/WorkOrders/documents/InspectionReportModal.tsx`
+- `src/components/production/WorkerScreen/WorkLogModal.tsx`
+- `src/components/material/ReceivingManagement/ImportInspectionInputModal.tsx`
+
+---
+
+## 2. Lazy Snapshot (조회 시 자동 캡처)
+
+`rendered_html`이 NULL인 기존 문서를 조회할 때 자동으로 스냅샷을 캡처하여 백그라운드 저장.
+
+### 동작 흐름
+1. 문서 조회 API 응답에서 `snapshot_document_id` 확인
+2. `rendered_html === NULL` → Lazy Snapshot 트리거
+3. 500ms 지연 (콘텐츠 렌더링 완료 대기)
+4. `contentWrapperRef.innerHTML` 캡처
+5. `patchDocumentSnapshot()` 서버 액션으로 백그라운드 PATCH
+
+### 특성
+- **비차단(non-blocking)**: UI에 영향 없이 백그라운드 처리
+- **1회성**: 스냅샷 저장 후 재조회 시 캡처하지 않음
+- **readOnly 자동 캡처 제거**: 불필요한 PUT 요청 방지
+
+### 적용 대상
+| 문서 | 수동 캡처 | Lazy Snapshot |
+|------|-----------|---------------|
+| 검사성적서 | ✅ | ✅ |
+| 작업일지 | ✅ | ✅ |
+| 수입검사 | ✅ (오프스크린) | — |
+| 제품검사 요청서 | ✅ | ✅ |
+
+---
+
+## 3. 오프스크린 렌더링 유틸리티
+
+폼 HTML이 아닌 실제 문서 렌더링 결과를 캡처하기 위한 유틸리티.
+
+```typescript
+// src/lib/utils/capture-rendered-html.tsx
+// 오프스크린 DOM에 문서 컴포넌트를 렌더링하여 innerHTML 추출
+```
+
+- [x] 수입검사 모달에서 활용 (폼 캡처 → 문서 캡처 전환)
+- [x] DocumentViewer 스냅샷 렌더링 지원
+
+### 주요 파일
+- `src/lib/utils/capture-rendered-html.tsx` (신규)
+- `src/components/document-system/viewer/DocumentViewer.tsx`
+
+---
+
+## 4. 서버 액션
+
+```typescript
+// patchDocumentSnapshot — 백그라운드 PATCH
+export async function patchDocumentSnapshot(
+ documentId: string,
+ rendered_html: string
+): Promise<{ success: boolean }>;
+```
+
+### 주요 파일
+- `src/components/production/WorkOrders/actions.ts` — `patchDocumentSnapshot`
+- `src/components/quality/InspectionManagement/fqcActions.ts` — `patchDocumentSnapshot`
diff --git a/claudedocs/dashboard/[FIX-2026-03-09] ceo-dashboard-fix-plan.md b/claudedocs/dashboard/[FIX-2026-03-09] ceo-dashboard-fix-plan.md
new file mode 100644
index 00000000..22964407
--- /dev/null
+++ b/claudedocs/dashboard/[FIX-2026-03-09] ceo-dashboard-fix-plan.md
@@ -0,0 +1,213 @@
+# CEO 대시보드 수정계획서 (최종)
+
+**작성일**: 2026-03-09
+**기반 문서**: `[QA-2026-03-09] ceo-dashboard-ui-verification.md`
+**검증 수준**: 화면(Chrome DevTools) + 프론트엔드 코드 + 백엔드 코드 + 실제 데이터 전부 확인 완료
+
+---
+
+## 최종 이슈 요약
+
+| 분류 | 건수 | 내용 |
+|------|------|------|
+| 🟡 백엔드 개선 | 1건 | 현황판/채권추심 sub_label 필드 추가 |
+| 🟢 프론트엔드 개선 | 2건 | 더미값 제거, 매입 라벨 명확화 |
+| ✅ 수정 불필요 (오진 정정) | 8건 | 아래 상세 표 참조 |
+
+---
+
+## 1. 수정 필요 항목
+
+### B3. 현황판/채권추심 거래처 sub_label 필드 추가 🟡
+
+**현상**: 프론트엔드에서 하드코딩된 더미 거래처명 사용 중
+
+| 위치 | 더미값 | TODO 주석 |
+|------|--------|----------|
+| `transformers/status-issue.ts:20-29` | "주식회사 부산화학 외" 등 | ✅ 있음 (line 18) |
+| `transformers/receivable.ts:96-103` | "(주)부산화학 외" 등 | ✅ 있음 (line 96) |
+
+**백엔드 수정 내용**:
+
+1. **`StatusBoardService.php`** — 각 항목 응답에 `sub_label` 필드 추가
+ - `getBadDebtStatus()` (line 68-83): 최다 금액 거래처 실제 이름 조회
+ - `getNewClientStatus()`: 최근 등록 업체명 조회
+ - 기타 항목도 해당 시 sub_label 제공
+
+2. **`BadDebtService.php`** — `summary()` 응답에 per-card 거래처 정보 추가
+ - `top_client_name`: 누적 악성채권 최다 금액 거래처명
+ - 카드별 `sub_label`: 해당 카테고리 최다 금액 거래처명 + 건수
+
+**프론트엔드 후속 작업**: B3 완료 후 → F1 (더미값 제거)
+
+---
+
+### F1. 더미 거래처명 제거 (B3 완료 후) 🟢
+
+**대상 파일**:
+- `src/lib/api/dashboard/transformers/status-issue.ts`
+ - Line 20-29: `STATUS_BOARD_FALLBACK_SUB_LABELS` 상수 제거
+ - Line 35-53: `buildStatusSubLabel()` → API `sub_label` 직접 사용
+
+- `src/lib/api/dashboard/transformers/receivable.ts`
+ - Line 98-103: `DEBT_COLLECTION_FALLBACK_SUB_LABELS` 상수 제거
+ - Line 109-121: `buildDebtSubLabel()` → API 제공 값 사용
+
+---
+
+### F3. 매입 섹션 "당월" 컨텍스트 명확화 🟢
+
+**현상**:
+- 섹션 subtitle: "당월 매입 실적" + Badge: "당월"
+- 카드 라벨: "누적 매입" (line 65 — 이것 자체는 정확)
+- **실제 데이터**: `cumulative_purchase` = 연간 누적 (2026-01-01 ~ 오늘, Feb 포함)
+- 일별 매입 내역: 2026-02-27 거래 표시 → "당월 매입 내역" 제목 하에 전월 데이터 포함
+
+**코드 확인**:
+- `PurchaseStatusSection.tsx:50` — `subtitle="당월 매입 실적"`
+- `PurchaseStatusSection.tsx:53` — `당월`
+- `PurchaseStatusSection.tsx:65` — `누적 매입`
+- `DashboardCeoService.php:175-180` — `whereYear('purchase_date', $year)` = 연간 누적
+
+**수정 방향**:
+- subtitle: "당월 매입 실적" → "매입 실적" 또는 "연간 매입 현황"
+- Badge: "당월" → 제거 또는 "YTD"로 변경
+- 하단 카드 title: "당월 매입 내역" → "최근 매입 내역"
+
+---
+
+## 2. 수정 불필요 항목 (최종 정리)
+
+### 1차 QA → 2차 검증 → 최종 검토로 순차 정정된 이슈들
+
+| # | 이전 보고 | 최종 검증 결과 | 검증 근거 |
+|---|----------|-------------|----------|
+| ~~C1~~ | 5개 섹션 본문 미렌더링 | **LazySection 정상** — 스크롤 시 로드 | `LazySection.tsx` IntersectionObserver + DOM 확인 |
+| ~~C2~~ | 매출 금액 10배 차이 | **NavBar=누적, 본문=당월 구분 표시** | `SalesStatusSection.tsx:63` "누적 매출" 라벨 확인 |
+| ~~C3~~ | 발행어음 데이터 불일치 | **다른 테이블 (설계 의도)** | `expected_expenses` 테이블(예측) ≠ `bills` 테이블(실제) |
+| ~~C4~~ | 채권추심 건수 불일치 | **다른 산출 기준 (설계 의도)** | StatusBoard=레코드 7건(status=collecting) vs BadDebt=거래처 5곳(distinct client_id) |
+| ~~I2~~ | 카드 월별 합계 0원 버그 | **정상 — 해당 월 데이터 없음** | 카드 거래 20건 모두 2025-01~2026-01-28, 2/3월 거래 0건 |
+| ~~I3~~ | 미수금 산출 기준 차이 | **다른 산출 방식 (설계 의도)** | 화면 확인 |
+| ~~M1~~ | 가지급금 카드 금액 차이 | **다른 기준 (설계 의도)** | 가지급금 전환 기준 vs 카드 사용 총액 |
+| ~~발주~~ | 현황판 "발주" 미표시 | **의도적 숨김** | `STATUS_BOARD_HIDDEN_ITEMS.has('purchases')` (2026-03-03 비활성화) |
+
+### 상세 정정 사항
+
+#### ~~B1: 카드 월별 합계 0원~~ — SQL 버그 아님 ✅
+
+**이전 판단**: `DB::raw('DATE(COALESCE(used_at, withdrawal_date))')` SQL 버그
+
+**최종 판단**: **데이터가 없어서 0이 정상**
+
+```
+카드 거래 20건 날짜 분포:
+- 가장 오래된 거래: 2025-01-12 (used_at=NULL, withdrawal_date만)
+- 가장 최근 거래: 2026-01-28 (used_at='2026-01-28')
+- 2026-02 거래: 0건
+- 2026-03 거래: 0건
+→ current_month_total=0, previous_month_total=0 모두 정확
+```
+
+**QA 오진 원인**: 카드사용내역 리스트 페이지에서 "15건 표시"를 보고 당월/전월에도 데이터가 있을 것으로 추정했으나, 리스트는 전체 기간 데이터를 표시.
+
+**참고**: `summary()` 메서드의 SQL 패턴(`DB::raw COALESCE`)이 `index()` 메서드(`whereDate + orWhere`)와 다르지만, 현재 데이터에서는 정상 작동 확인. 향후 코드 일관성을 위해 패턴 통일은 권장하나, 긴급하지 않음.
+
+#### ~~B2: 현황판 vs 채권추심 건수~~ — 설계 의도 ✅
+
+**이전 판단**: 건수 통일 필요
+
+**최종 판단**: **의도적으로 다른 관점 제공**
+
+| API | 쿼리 | 의미 |
+|-----|------|------|
+| `StatusBoardService.php:72` | `where('status', 'collecting')->count()` | "지금 추심중인 건" = 7건 |
+| `BadDebtService.php:107-111` | `distinct('client_id')->count('client_id')` | "악성채권이 있는 거래처 수" = 5곳 |
+
+현황판은 "현재 추심 진행 중인 건수"를, 채권추심 본문은 "악성채권 보유 거래처 수"를 보여주는 것으로, 각각 다른 관점의 지표임. 사용자가 혼동할 수 있으나, 정보 제공 목적이 다름.
+
+#### ~~B5: 매입 "당월" 기간~~ — 백엔드 정상, 프론트 라벨 이슈 ✅
+
+`DashboardCeoService.php:175-180`의 `cumulative_purchase`는 `whereYear(2026)` = 연간 누적으로 정확히 산출. "당월" 라벨은 프론트엔드 이슈 → F3으로 처리.
+
+---
+
+## 3. 수정 우선순위
+
+| 순위 | 이슈 | 영역 | 난이도 | 비고 |
+|------|------|------|--------|------|
+| 1 | B3: sub_label 필드 추가 | 백엔드 | 중 | 거래처 조회 쿼리 추가 |
+| 2 | F1: 더미 거래처명 제거 | 프론트 | 하 | B3 완료 후 상수/함수 제거 |
+| 3 | F3: 매입 섹션 라벨 명확화 | 프론트 | 하 | 텍스트만 변경 |
+
+---
+
+## 4. 수정 후 재검수 계획
+
+| 단계 | 항목 | 검증 방법 |
+|------|------|----------|
+| 1 | B3+F1 수정 후: 거래처명 | 현황판/채권추심에 실제 거래처명 표시 확인 |
+| 2 | F3 수정 후: 매입 라벨 | "당월" 대신 적절한 기간 표현 확인 |
+| 3 | 전체: 상세 모달 | 각 섹션 모달 열기/닫기/날짜필터 동작 확인 |
+
+---
+
+## 부록: 관련 파일 위치
+
+### 백엔드 (sam-api)
+| 파일 | 이슈 | 상태 |
+|------|------|------|
+| `app/Services/StatusBoardService.php:68-83` | B3 (sub_label 추가) | 수정 필요 |
+| `app/Services/BadDebtService.php:107-111` | B3 (per-card 거래처) | 수정 필요 |
+| `app/Services/CardTransactionService.php:109-153` | ~~B1~~ | ✅ 정상 (데이터 없음이 원인) |
+| `app/Services/ExpectedExpenseService.php:241-301` | ~~B4~~ | ✅ 정상 (다른 테이블, 설계 의도) |
+| `app/Services/DashboardCeoService.php:166-234` | ~~B5~~ | ✅ 정상 (프론트 라벨만 수정) |
+
+### 프론트엔드 (sam-react-prod)
+| 파일 | 이슈 | 상태 |
+|------|------|------|
+| `src/lib/api/dashboard/transformers/status-issue.ts:18-53` | F1 (더미 sub_label) | 수정 필요 (B3 후) |
+| `src/lib/api/dashboard/transformers/receivable.ts:96-121` | F1 (더미 sub_label) | 수정 필요 (B3 후) |
+| `src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx:50-53,161` | F3 (라벨) | 수정 필요 |
+| `src/components/business/CEODashboard/LazySection.tsx` | ~~C1~~ | ✅ 정상 |
+| `src/components/business/CEODashboard/sections/SalesStatusSection.tsx:63` | ~~F2~~ | ✅ 정상 ("누적 매출" 명확) |
+
+---
+
+## 5. 하단 섹션 추가 검증 결과 (3차)
+
+### 생산/출고/시공/근태 4개 섹션 소스 페이지 대조 + 코드 검증
+
+| 섹션 | 대시보드 | 소스 페이지 | API 엔드포인트 | 결론 |
+|------|---------|-----------|-------------|------|
+| 생산 현황 | 0공정 | 작업지시 39건 (모두 2월/작업대기) | `dashboard/production/summary` | ✅ 정확 (오늘 예정 없음) |
+| 출고 현황 | 0건/0원 | 당일출고 0건, 전체 8건 (12~1월) | (생산과 동일 API) | ✅ 정확 (당월 건 없음) |
+| 시공 현황 | 0건 | 시공진행 7/완료 4 (**Mock 데이터**) | `dashboard/construction/summary` | ✅ 비교불가 (소스=Mock) |
+| 근태 현황 | 0명 전부 | 미출근 55명/출근 0명 | `dashboard/attendance/summary` | ✅ 설계 차이 (기록 vs 명부) |
+
+### 참고 사항 (향후 개선 검토)
+
+1. **시공 현황 — NULL end_date 처리** (`DashboardCeoService.php:555-567`)
+ - 현재 쿼리: `contract_end_date >= $monthEnd` 조건에서 NULL 제외됨
+ - 실제 계약 데이터 투입 시, 진행 중(`end_date IS NULL`) 계약이 대시보드에 미표시될 수 있음
+ - 권장: `orWhere(fn($q) => $q->where('start_date', '<=', $monthEnd)->whereNull('end_date'))` 조건 추가 검토
+
+2. **시공관리 페이지 — API 미연동** (`construction/management/actions.ts`)
+ - `TODO: 실제 API 연동 시 구현` 주석 → 현재 전체가 Mock 데이터
+ - 대시보드 시공 섹션은 실제 `contracts` 테이블 조회 → 소스 페이지와의 정합성 검증은 API 연동 완료 후 재실시
+
+3. **근태 대시보드 — "미출근" 미표시**
+ - 대시보드는 `attendances` 테이블 레코드 기반 → 출근 기록 없으면 모두 0
+ - 근태관리 페이지는 사원 명부 기반 → "미출근 55명" 표시
+ - CEO 관점에서 "미출근" 정보가 필요한지는 비즈니스 결정 사항
+
+---
+
+## 검증 이력
+
+| 단계 | 내용 | 결과 |
+|------|------|------|
+| 1차 QA | 화면 검수 (18개 섹션) | Critical 4건 + Important 3건 보고 |
+| 2차 검증 | LazySection + API 응답 확인 | Critical 4건 → 전부 정정 (버그 아님) |
+| 3차 검토 | 백엔드 코드 + 실제 DB 데이터 확인 | Important 3건 중 1건(카드 0원) 추가 정정 |
+| 4차 추가 검증 | 하단 4개 섹션 소스 대조 + 코드 검증 | 4건 모두 정상 (참고 3건 기록) |
+| **최종 결론** | **실제 수정 필요: 3건** (백엔드 1 + 프론트 2) | 나머지 모두 수정 불필요 확인 |
diff --git a/claudedocs/dashboard/[QA-2026-03-09] ceo-dashboard-ui-verification.md b/claudedocs/dashboard/[QA-2026-03-09] ceo-dashboard-ui-verification.md
new file mode 100644
index 00000000..5f0c4aef
--- /dev/null
+++ b/claudedocs/dashboard/[QA-2026-03-09] ceo-dashboard-ui-verification.md
@@ -0,0 +1,252 @@
+# CEO 대시보드 UI 검수 결과 (2차 검증 포함)
+
+**작성일**: 2026-03-09
+**목적**: 대시보드 전체 18개 섹션의 API 데이터 정합성 및 연동 검증
+**방법**: 화면 검수 (Chrome DevTools MCP로 실제 화면 조작 + DOM 검증)
+
+---
+
+## 검수 범위 요약
+
+| 구분 | 수량 | 비고 |
+|------|------|------|
+| 대시보드 카드 섹션 | 18개 | SummaryNavBar 기준 |
+| 본문 렌더링 | **18개 전부** | LazySection으로 스크롤 시 로드 (2차 검증) |
+| 상세 모달 | 10개 | 날짜필터 포함 |
+| Mock 섹션 (제외) | 2개 | 일별 매출/매입 내역 |
+
+---
+
+## Phase 1: 카드 수치 표출 확인 ✅ 완료
+
+대시보드 로드 후 각 카드에 표시된 수치를 기록.
+
+| # | 섹션 | SummaryNavBar 값 | 본문 카드 | 확인 |
+|---|------|-----------------|----------|------|
+| 1 | 오늘의 이슈 | 3건 | ✅ 렌더링 | - [x] |
+| 2 | 자금현황 | 0원 | ✅ 렌더링 (미수금 9억4,697만 / 미지급금 1억5,944만) | - [x] |
+| 3 | 현황판 | 7항목 | ✅ 렌더링 (수주0/채권추심7/안전재고833/세금신고-/신규업체45/연차0/결재1) | - [x] |
+| 4 | 당월 예상 지출 | 1억 | ✅ 렌더링 (매입0/카드0/발행어음1억) | - [x] |
+| 5 | 가지급금 현황 | 1,150만 | ✅ 렌더링 (카드1,150만/경조사0/상품권0/접대비0) | - [x] |
+| 6 | 접대비 현황 | 0원 | ✅ 렌더링 (리스크 항목 4개 모두 0) | - [x] |
+| 7 | 복리후생비 현황 | 40만 | ✅ 렌더링 (리스크 항목: 사적사용20만1건/특정인편중20만1건) | - [x] |
+| 8 | 미수금 현황 | 9.4억 | ✅ **LazySection으로 렌더링** (스크롤 시 로드) | - [x] |
+| 9 | 채권추심 현황 | 1.2억 | ✅ **LazySection으로 렌더링** (스크롤 시 로드) | - [x] |
+| 10 | 부가세 현황 | 0원 | ✅ **LazySection으로 렌더링** (스크롤 시 로드) | - [x] |
+| 11 | 캘린더 | 26일정 | ✅ **LazySection으로 렌더링** (스크롤 시 로드) | - [x] |
+| 12 | 매출 현황 | 1.1억 | ✅ **LazySection으로 렌더링** (스크롤 시 로드) | - [x] |
+| 13 | 매입 현황 | 165만 | ✅ 렌더링 (당월 누적매입165만 / 미결제165만 / 차트+테이블) | - [x] |
+| 14 | 생산 현황 | 0공정 | ✅ 렌더링 (작업지시 없음) | - [x] |
+| 15 | 출고 현황 | 0건 | ✅ 렌더링 (7일이내0 / 30일이내0) | - [x] |
+| 16 | 미출고 내역 | 6건 | ✅ 렌더링 (6건 상세목록 표시) | - [x] |
+| 17 | 시공 현황 | 0건 | ✅ 렌더링 (시공진행0/시공완료0) | - [x] |
+| 18 | 근태 현황 | 0명 | ✅ 렌더링 (출근0/휴가0/지각0/결근0) | - [x] |
+
+### 2차 검증: LazySection 확인 (1차 QA 오류 정정)
+
+1차 QA에서 "본문 미렌더링"으로 보고된 5개 섹션(미수금/채권추심/부가세/캘린더/매출)은 실제로는 **LazySection**(IntersectionObserver 기반 lazy loading)으로 정상 작동합니다. 스크롤하여 뷰포트에 진입하면 콘텐츠가 로드됩니다.
+
+**확인 방법**:
+- DOM 검사: `[data-section-key]` 18개 전부 존재 확인
+- 스크롤 후 콘텐츠 확인: 5개 섹션 모두 데이터 정상 렌더링
+- LazySection.tsx 분석: IntersectionObserver + rootMargin='300px' 패턴
+
+**스크롤 후 확인된 본문 데이터**:
+| 섹션 | 본문 주요 수치 | NavBar 값 | 일치 |
+|------|--------------|----------|------|
+| 미수금 | 누적 미수금 9억 4,164만 / 미수금 거래처 79건 / 연체 1건 / 악성채권 11건 | 9.4억 | ✅ |
+| 채권추심 | 누적 악성채권 1억 1,869만 / (주)부산화학 외 4건 | 1.2억 | ✅ |
+| 부가세 | 매출세액 0원 / 매입세액 0원 / 예상 납부세액 0원 / 미발행 1건 | 0원 | ✅ |
+| 캘린더 | 2026년 3월 전체 일정 표시 | 26일정 | ✅ |
+| 매출 | 당월누적 매출 1억 673만 / 달성률 6% / 전년대비 -93.6% / 당월 매출 1,045만 | 1.1억 | ✅ |
+
+---
+
+## Phase 2: 상세 모달 + 날짜필터 검증
+
+### 2-4. 복리후생비 상세 모달 ✅ (검증 완료)
+| 테스트 | 방법 | 확인 |
+|--------|------|------|
+| 모달 열기 | 카드 클릭 → 요약/차트/테이블 확인 | - [x] 완료 |
+| 당월 날짜필터 | 당월 → 데이터 있음 (1건 200,000) | - [x] 완료 |
+| 전월 날짜필터 | 전월 → 데이터 없음 (0건) | - [x] 완료 |
+
+### 나머지 모달 (Phase 2)
+> 당월 예상 지출, 가지급금, 접대비 등 나머지 모달은 하단 수정계획에 따라 이슈 수정 후 재검수 예정.
+
+---
+
+## Phase 3: 소스 페이지 ↔ 대시보드 데이터 연동 검증 ✅ 완료
+
+### 3-1. 복리후생비 (세금계산서 분개) ✅ 검증 완료
+| 테스트 | 소스 페이지 | 결과 | 확인 |
+|--------|-----------|------|------|
+| 분개 추가 | 세금계산서관리 | 대시보드 금액 변동 확인 (51만→31만) | - [x] ✅ |
+| 계정 변경 | 세금계산서관리 | 대시보드 금액 변동 확인 (51만→40만) | - [x] ✅ |
+| 날짜필터 | 대시보드 모달 | 전월 변경 → 0건 표시 | - [x] ✅ |
+
+### 3-2. 미수금 현황 ⚠️ 산출 기준 다름
+| 테스트 | 대시보드 | 소스 페이지 | 결과 |
+|--------|---------|-----------|------|
+| 미수금 잔액 | 9억 4,697만 | 미수금현황 합계 미수금 = **음수** (-311,979,400) | ⚠️ 산출 기준 불일치 |
+
+> 대시보드의 미수금은 자금현황 카드 내 "미수금 잔액"으로 표시. 미수금현황 페이지의 합계 행은 월별 차이금액의 합산으로 음수 표시. 두 페이지의 산출 기준이 완전히 다름.
+
+### 3-3. 매출 현황 ✅ 정정 (2차 검증)
+| 테스트 | 대시보드 | 소스 페이지 | 결과 |
+|--------|---------|-----------|------|
+| 매출 금액 (NavBar) | 1.1억 | cumulative_sales = 106,726,323 (1.07억) | ✅ NavBar는 누적매출 표시 (반올림 1.1억) |
+| 매출 금액 (본문) | 당월누적 1억 673만 / 당월 1,045만 | 매출관리 당월 매출 = 10,450,000원 | ✅ 본문에서 구분 표시 |
+
+> **1차 QA 오류 정정**: NavBar "1.1억"은 `cumulative_sales`(누적매출)이며, 본문에서는 "당월누적 매출 1억 673만"과 "당월 매출 1,045만"을 구분 표시. 10배 차이가 아닌 다른 지표 표시.
+
+### 3-4. 매입 현황 ✅ 일치
+| 테스트 | 대시보드 | 소스 페이지 | 결과 |
+|--------|---------|-----------|------|
+| 매입 금액 | 165만 | 매입관리 합계 = **1,650,000원** | ✅ 일치 |
+
+> 단, "당월" 라벨이지만 데이터는 2026-02-27 것임 (3월 매입 없음). 라벨 정확성 재검토 필요.
+
+### 3-5. 당월 예상 지출 (발행어음) ⚠️ 소스 확인 필요
+| 테스트 | 대시보드 | 소스 페이지 | 결과 |
+|--------|---------|-----------|------|
+| 발행어음 | 1억 | 어음관리 당월 = 수취어음 2건 40,000원 **(발행어음 0건)** | ⚠️ 다른 데이터 소스 |
+
+> 대시보드의 발행어음 1억은 `expected-expenses` API에서 `by_transaction_type.bill.total = 100,000,000`으로 제공. 어음관리 페이지(`bills` 테이블)와 다른 데이터 소스(`expected_expenses` 테이블) 사용. **최종 확인: 설계 의도** — expected_expenses는 수동 입력된 지출 예측 데이터이며, bills는 실제 발행어음 문서. 두 시스템은 독립적.
+
+### 3-6. 가지급금 현황 ⚠️ 기준 다름
+| 테스트 | 대시보드 | 소스 페이지 | 결과 |
+|--------|---------|-----------|------|
+| 카드 | 1,150만 | 카드사용내역 당월 합계 ≈ 467만 | ⚠️ 기준 다름 (가지급금 전환 기준) |
+| 상품권 | 0원 | 상품권관리 보유 0건/0원 | ✅ 일치 |
+
+> ~~카드사용내역 요약(전월/당월/건수)이 모두 0원/0건으로 표시 — API 버그~~
+> **최종 확인: 버그 아님** — 카드 거래 20건의 날짜 범위가 2025-01~2026-01-28이며, 2026년 2월/3월 거래는 0건. 따라서 전월/당월 합계 0원은 정확한 값.
+
+### 3-7. 미출고 내역 ✅ 대시보드 내 확인
+| 테스트 | 대시보드 | 결과 |
+|--------|---------|------|
+| 미출고 | 6건 | 대시보드 카드 내 6건 상세목록 표시 (LOT번호, 현장명, 납기일 포함) | ✅ |
+
+### 3-8. 채권추심 현황 ⚠️ 건수 불일치 + 더미 거래처명
+| 테스트 | 대시보드 | 소스 페이지 | 결과 |
+|--------|---------|-----------|------|
+| 금액 | 본문 1억 1,869만 / NavBar 1.2억 | 악성채권 5건 합계 ≈ 1.19억 | ✅ 일치 |
+| 건수 (현황판) | 7건 | 악성채권관리 = **5건** | ⚠️ status-board API 별도 산출 |
+| 건수 (채권추심 본문) | 5건 (client_count) | 악성채권관리 = 5건 | ✅ 일치 |
+| 거래처명 | "(주)부산화학 외 4건" | 실제 거래처 미확인 | ⚠️ **하드코딩 더미값** |
+
+> **2차 검증 발견**: 채권추심 본문/현황판의 거래처명("부산화학", "삼성테크" 등)은 `DEBT_COLLECTION_FALLBACK_SUB_LABELS`와 `STATUS_BOARD_FALLBACK_SUB_LABELS`에 하드코딩된 **더미값**. 코드에 `// TODO: 백엔드 per-card sub_label/count 제공 시 더미값 제거` 주석 있음.
+
+### 3-9. 현황판 "발주" 미표시 ✅ 의도적 숨김 (2차 검증)
+
+> `STATUS_BOARD_HIDDEN_ITEMS`에 `purchases`가 포함되어 의도적으로 숨김 처리. 사용자 설정에서도 `purchase: false`. 백엔드 path 오류 + 데이터 정합성 이슈 해결 전까지 비활성화 (코드 주석: `[2026-03-03] 비활성화`).
+
+---
+
+## 발견된 이슈 요약 (최종 검토 반영)
+
+### 🔴 Critical → 없음 (1차 이슈 모두 정정)
+
+1차 QA의 Critical 이슈 4건은 2차 검증에서 모두 재분류됨:
+- ~~C1 (5개 섹션 미렌더링)~~: LazySection 정상 → **이슈 아님**
+- ~~C2 (매출 10배 차이)~~: NavBar=누적, 본문=당월 구분 → **이슈 아님**
+- ~~C3 (발행어음 불일치)~~: `expected_expenses` 테이블(예측) ≠ `bills` 테이블(실제) → **설계 의도**
+- ~~C4 (채권추심 건수)~~: StatusBoard=레코드 7건 vs BadDebt=거래처 5곳 → **설계 의도**
+
+### 🟡 Important (실제 수정 필요: 3건)
+
+| # | 이슈 | 상세 | 조치 |
+|---|------|------|------|
+| I1 | **채권추심/현황판 더미 거래처명** | "(주)부산화학" 등 하드코딩 — 실제 거래처가 아님 | 백엔드 sub_label 필드 추가 → 프론트 더미값 제거 |
+| ~~I2~~ | ~~현황판 vs 채권추심 건수 불일치~~ | 현황판=`status=collecting` 레코드 7건, 채권추심=`distinct(client_id)` 거래처 5곳 | **설계 의도** (다른 관점 지표) |
+| ~~I3~~ | ~~카드사용내역 월별 합계 0원~~ | 카드 거래 20건 전부 2025-01~2026-01-28, 2/3월 거래 0건 | **버그 아님** (데이터 없음이 원인) |
+| ~~I4~~ | ~~발행어음 데이터 소스 불명확~~ | `expected_expenses`(예측)와 `bills`(실제)는 별도 테이블 | **설계 의도** (독립 데이터) |
+| I5 | **매입 "당월" 라벨 부정확** | subtitle "당월 매입 실적" + Badge "당월"이나 실제 데이터는 연간 누적(`whereYear`) | 프론트엔드 라벨 수정 |
+
+### 🟢 Minor → 수정 불필요 (최종 확인)
+
+| # | 이슈 | 최종 판단 |
+|---|------|----------|
+| ~~M1~~ | 미수금 산출 기준 차이 | **설계 의도** — 다른 산출 방식 |
+| ~~M2~~ | 가지급금 카드 금액 대조 불가 | **설계 의도** — 가지급금 전환 기준 vs 카드 사용 총액 |
+
+### 최종 수정 필요 항목: 3건만
+
+| 순위 | 이슈 | 영역 | 내용 |
+|------|------|------|------|
+| 1 | I1(B3) | 백엔드 | StatusBoardService/BadDebtService에 sub_label 필드 추가 |
+| 2 | I1(F1) | 프론트 | 더미 거래처명 상수/함수 제거 → API sub_label 사용 |
+| 3 | I5(F3) | 프론트 | 매입 섹션 "당월" → "연간"/"YTD" 라벨 수정 |
+
+---
+
+## Phase 4: 하단 섹션 추가 검증 (생산/출고/시공/근태) ✅ 완료
+
+### 4-1. 생산 현황 (0공정) ✅ 정확
+
+| 항목 | 대시보드 | 소스 페이지 (작업지시 관리) | 결과 |
+|------|---------|--------------------------|------|
+| 공정 수 | 0공정 | 전체 39건 (작업대기 39, 작업중 0, 완료 0) | ✅ |
+| 본문 | "오늘 등록된 작업 지시가 없습니다" | 39건 모두 2월 날짜, 상태 "미배정" | ✅ |
+
+> **검증**: 대시보드 API(`dashboard/production/summary`)는 `scheduled_date = today` 기준 조회. 39건의 작업지시는 모두 2026년 2월 날짜이므로 오늘(3월 9일) 예정 작업 없음 → 0공정 정확.
+>
+> **백엔드 코드**: `DashboardCeoService.php` — `work_orders` 테이블에서 `scheduled_date = today`, `is_active = true` 조건으로 공정별 집계. 출고 데이터도 동일 API에서 `shipment` 필드로 제공.
+
+### 4-2. 출고 현황 (0건/0원) ✅ 정확
+
+| 항목 | 대시보드 | 소스 페이지 (출고관리) | 결과 |
+|------|---------|---------------------|------|
+| 예상 출고 (7일 이내) | 0건/0원 | 당일 출고대기 0건 | ✅ |
+| 예상 출고 (30일 이내) | 0건/0원 | 전체 8건 (모두 2025-12~2026-01) | ✅ |
+
+> **검증**: 출고관리 페이지의 8건은 모두 2025년 12월~2026년 1월 날짜. 대시보드는 당월(3월) 기준 `status IN ('scheduled','ready')` 필터 → 해당 없음 → 0 정확.
+>
+> **미출고 6건**: `dashboard/unshipped/summary` API로 별도 조회. LOT번호(LOT-2024001~008), 현장명, 납기일 모두 소스 데이터와 일치. days_left가 모두 음수(D-64~D-69) → 납기 초과 상태.
+
+### 4-3. 시공 현황 (0건) ✅ 비교 불가 (소스=Mock)
+
+| 항목 | 대시보드 | 소스 페이지 (시공관리) | 결과 |
+|------|---------|---------------------|------|
+| 시공 진행 | 0건 | 시공진행 7건 | ⚠️ 차이 |
+| 시공 완료 | 0건 | 시공완료 4건 | ⚠️ 차이 |
+
+> **원인 분석**: 시공관리 페이지(`construction/management/actions.ts`)는 **Mock 데이터 사용 중** (line 22: `// 목업 데이터`, line 21: `TODO: 실제 API 연동 시 구현`). 화면에 표시되는 "시공진행 7건"은 하드코딩된 가짜 데이터.
+>
+> 대시보드는 실제 `contracts` 테이블 조회 (`DashboardCeoService.php:555-567`) — `contract_start_date`/`contract_end_date`가 당월(3월) 범위에 해당하는 계약 없음 → 0건 정확.
+>
+> **참고**: `contracts` 테이블에서 `end_date IS NULL`인 진행 중 계약 처리 — 현재 쿼리는 `contract_end_date >= $monthEnd` 조건에서 NULL이 제외됨. 실제 계약 데이터 투입 시 이 조건의 적정성 재검토 권장 (NULL end_date = 아직 진행 중).
+
+### 4-4. 근태 현황 (0명) ✅ 설계 차이
+
+| 항목 | 대시보드 | 소스 페이지 (근태관리) | 결과 |
+|------|---------|---------------------|------|
+| 출근 | 0명 | 정시 출근 0명 | ✅ |
+| 지각 | 0명 | 지각 0명 | ✅ |
+| 휴가 | 0명 | 휴가 0명 | ✅ |
+| 결근 | 0명 | - | ✅ |
+| 미출근 | (미표시) | **55명** | ⚠️ 관점 차이 |
+
+> **검증**: 대시보드 API(`dashboard/attendance/summary`)는 `attendances` 테이블에서 `base_date = today` 레코드만 조회 (`DashboardCeoService.php:677-694`). 오늘 출근 기록이 없으므로 모든 카운트 0, employees 배열 비어있음.
+>
+> 근태관리 페이지는 **전체 사원 명부 기반** — 등록된 55명의 사원에 대해 출근 기록 유무를 확인하고, 기록 없으면 "미출근"으로 표시.
+>
+> **설계 차이**: 대시보드="출근 기록 기반"(기록 있는 것만 카운트), 관리 페이지="사원 명부 기반"(전체 사원 대비 상태 표시). 대시보드에서 "미출근" 정보를 보여줄지는 비즈니스 결정 사항.
+>
+> **참고**: 55명 전원 "E2E_TEST_사원"(테스트 데이터), 부서/직책 모두 미지정. 실 운영 시에는 출근 기록이 생성되므로 정상 동작 예상.
+
+---
+
+## 검수 완료 항목
+
+| 항목 | 상태 |
+|------|------|
+| Phase 1: 전체 18개 카드 수치 기록 | ✅ 완료 |
+| Phase 1: LazySection 5개 섹션 재확인 | ✅ 완료 (2차) |
+| Phase 2: 복리후생비 모달/날짜필터 | ✅ 완료 |
+| Phase 3: 소스 페이지 대조 (9개 항목) | ✅ 완료 |
+| Phase 3: 복리후생비 데이터 변경 반영 | ✅ 완료 |
+| Phase 3: 코드 분석 (transformer/fallback) | ✅ 완료 (2차) |
+| Phase 4: 하단 섹션 추가 검증 (생산/출고/시공/근태) | ✅ 완료 (3차) |
+| Phase 5: 데이터 변경 반영 테스트 | ⏸ 이슈 수정 후 |
diff --git a/claudedocs/outbound/[IMPL-2026-03-04] shipment-dispatch-api-integration.md b/claudedocs/outbound/[IMPL-2026-03-04] shipment-dispatch-api-integration.md
new file mode 100644
index 00000000..b5fe3b30
--- /dev/null
+++ b/claudedocs/outbound/[IMPL-2026-03-04] shipment-dispatch-api-integration.md
@@ -0,0 +1,70 @@
+# 출하/배차 API 연동 — 배차 다중행 + 차량관리 + 출고관리
+
+> **작업일**: 2026-03-03 ~ 03-07
+> **상태**: ✅ 완료
+> **커밋**: 83a23701, 1d380578, 5ff5093d, f653960a, a4f99ae3, 03d129c3
+
+---
+
+## 개요
+
+출하/배차 관련 3개 모듈의 API 연동 및 레이아웃 개선.
+
+---
+
+## 1. 배차정보 다중 행 API 연동
+
+기존 단일 배차 → `vehicle_dispatches` 배열 지원.
+
+- [x] `ShipmentApiData`에 `vehicle_dispatches` 배열 필드 추가
+- [x] `transformApiToDetail()` — vehicle_dispatches 배열 매핑
+- [x] `transformCreateFormToApi()` — 폼 vehicleDispatches → API vehicle_dispatches 변환
+- [x] `transformEditFormToApi()` — 수정 시 동일 변환
+- [x] `transformApiToListItem()` — 첫 번째 배차의 arrival_datetime 목록에 표시
+- [x] 레거시 단일 배차 필드 하위호환 유지
+
+### 주요 파일
+- `src/components/outbound/ShipmentManagement/actions.ts`
+
+---
+
+## 2. 배차차량관리 Mock→API 전환
+
+- [x] `executePaginatedAction` + `buildApiUrl` 패턴 적용
+- [x] `transformToListItem()` — snake_case → camelCase 목록 변환
+- [x] `transformToDetail()` — snake_case → camelCase 상세 변환
+- [x] 쿼리 파라미터: `search`, `status`, `start_date`, `end_date`, `page`, `per_page`
+- [x] options/shipment 관계 데이터 중첩 API 응답에서 추출
+
+### 주요 파일
+- `src/components/outbound/VehicleDispatchManagement/actions.ts` (+207/-207)
+
+---
+
+## 3. 출고관리 목록 필드 매핑
+
+- [x] 5개 필드 API 매핑 추가: `writer_name`, `writer_id`, `delivery_date` 등
+- [x] `OrderInfoApiData` 타입으로 주문 연결 정보 처리
+- [x] `transformApiToListItem()` 수신자/수신주소/수신처/작성자/출고일 반영
+
+### 주요 파일
+- `src/components/outbound/ShipmentManagement/actions.ts`
+
+---
+
+## 4. 배차 상세/수정 레이아웃
+
+- [x] 기본정보 그리드: 1열 → 2×4열 레이아웃 개선
+
+### 주요 파일
+- `src/components/outbound/ShipmentManagement/ShipmentDetail.tsx`
+- `src/components/outbound/ShipmentManagement/ShipmentEdit.tsx`
+
+---
+
+## 5. 출하관리 캘린더
+
+- [x] 기본 뷰: day → week-time 변경
+
+### 주요 파일
+- `src/components/outbound/ShipmentManagement/ShipmentList.tsx`
diff --git a/claudedocs/production/[IMPL-2026-03-05] production-orders-api-integration.md b/claudedocs/production/[IMPL-2026-03-05] production-orders-api-integration.md
new file mode 100644
index 00000000..23e39269
--- /dev/null
+++ b/claudedocs/production/[IMPL-2026-03-05] production-orders-api-integration.md
@@ -0,0 +1,105 @@
+# 생산지시 API 연동 + 작업자 화면 + 중간검사
+
+> **작업일**: 2026-03-01 ~ 03-07
+> **상태**: ✅ 완료
+> **커밋**: fa7efb7b, 8b6da749, 4331b84a, 0166601b, 8bcabafd, 2a2a356f, b45c35a5, 0b81e9c1
+
+---
+
+## 개요
+
+생산지시(ProductionOrders) 목록/상세 페이지를 Mock→API 전환하고,
+작업자 화면의 중간검사 입력 모달과 자재투입 모달을 대폭 개선한 작업.
+
+---
+
+## 1. 생산지시 목록/상세 API 연동
+
+- [x] Mock → API 전환 (`executePaginatedAction` + `buildApiUrl` 패턴)
+- [x] 서버사이드 페이지네이션 + 동적 탭 카운트 (stats API)
+- [x] WorkOrder 상태 배지 6단계: 미배정 → 배정 → 작업중 → 검사 → 완료 → 출하
+- [x] BOM null 상태 처리
+- [x] PO 번호 = 생산지시 번호 매핑 (별도 PO 번호 필드 불필요)
+- [x] `clientSideFiltering: false` (서버사이드 처리)
+
+### 주요 파일
+- `src/components/production/ProductionOrders/actions.ts` — 서버 액션 (getProductionOrders, getProductionOrderStats, getProductionOrderDetail)
+- `src/components/production/ProductionOrders/types.ts` — API/프론트엔드 타입 정의
+- `src/app/[locale]/(protected)/production-orders/page.tsx` — 목록 뷰
+- `src/app/[locale]/(protected)/production-orders/[id]/page.tsx` — 상세 뷰
+
+---
+
+## 2. 절곡 중간검사 입력 모달 (InspectionInputModal)
+
+- [x] 7개 제품 항목 통합 폼
+- [x] 제품 ID 자동 매칭 (3단계): 정규화 → 키워드 → 인덱스 fallback
+- [x] cellValues 구조: `{bending_state, length, width, spacing}`
+- [x] PO 단위 데이터 공유 모델: 동일 PO 내 모든 아이템이 inspection_data 공유
+- [x] 데이터 로딩: bending 공정 아이템 중 inspection_data 보유 시 전체 적용
+- [x] 데이터 저장: 중간검사 완료 시 모든 workItem에 동기화
+
+### 제품 ID 매칭 전략 (bending/utils.ts)
+```
+1순위: 정규화 후 정확 매치 (대소문자/공백/특수문자 제거)
+2순위: 키워드 포함 검색
+3순위: 인덱스 기반 fallback
+```
+
+### 주요 파일
+- `src/components/production/WorkerScreen/InspectionInputModal.tsx` (+396)
+- `src/components/production/WorkOrders/documents/TemplateInspectionContent.tsx` (신규, +118)
+- `src/components/production/WorkOrders/documents/bending/utils.ts` (신규, +60)
+
+---
+
+## 3. 자재투입 모달 (MaterialInputModal)
+
+- [x] 동일 자재 다중 BOM 그룹 LOT 독립 관리
+- [x] `bomGroupKey = ${item_id}-${category}-${partType}` 그룹핑
+- [x] 카테고리 정렬 순서:
+ 1. 가이드레일
+ 2. 하단마감재
+ 3. 셔터박스
+ 4. 연기차단재
+- [x] FIFO 자동충전 시 그룹 간 물리적 LOT 가용량 추적
+- [x] 번호 배지 (①②③) + partType 배지
+- [x] `allGroupsFulfilled` 조건으로 입력 버튼 활성화 제어
+- [x] 그룹별 독립 전송: `bom_group_key` + `replace` 모드
+
+### 주요 파일
+- `src/components/production/WorkerScreen/MaterialInputModal.tsx` (+356)
+- `src/components/production/WorkerScreen/actions.ts`
+
+---
+
+## 4. 공정 단계 검사범위 설정 (InspectionScope) — 신규
+
+- [x] 전수검사 / 샘플링 / 그룹 3가지 타입
+- [x] 샘플링 시 샘플 수(n) 입력 지원
+- [x] StepForm 컴포넌트에 UI 추가
+- [x] options JSON으로 API 저장
+
+### 타입 정의
+```typescript
+type InspectionScopeType = 'FULL' | 'SAMPLING' | 'GROUP';
+
+interface InspectionScope {
+ type: InspectionScopeType;
+ sampleSize?: number; // SAMPLING 타입일 때만
+}
+```
+
+### 주요 파일
+- `src/components/process-management/StepForm.tsx`
+- `src/components/process-management/actions.ts`
+- `src/types/process.ts`
+
+---
+
+## 5. 기타 개선
+
+- [x] 작업자 화면 제품명: productCode만 표시 (간소화)
+- [x] 작업자 화면 하드코딩 도면 이미지 영역 제거
+- [x] BOM 공정 분류 접이식 카드 UI
+- [x] TemplateInspectionContent: products 배열 → cellValues 자동 매핑
diff --git a/claudedocs/quality/[IMPL-2026-03-07] quality-api-migration.md b/claudedocs/quality/[IMPL-2026-03-07] quality-api-migration.md
new file mode 100644
index 00000000..9b32088d
--- /dev/null
+++ b/claudedocs/quality/[IMPL-2026-03-07] quality-api-migration.md
@@ -0,0 +1,124 @@
+# 품질관리 Mock→API 전환 및 검사 모달/문서 개선
+
+> **작업일**: 2026-03-05 ~ 03-07
+> **상태**: ✅ 완료
+> **커밋**: 50e4c72c, 563b240f, 899493a7, fe930b58, e7263fee, 4ea03922, 295585d8, c150d807, e75d8f9b
+
+---
+
+## 개요
+
+품질관리(InspectionManagement) 전체 모듈을 Mock 데이터에서 실제 API로 전환하고,
+검사 모달/문서 렌더링/수주선택 기능을 대폭 개선한 작업.
+
+---
+
+## 1. API 전환
+
+- [x] `USE_MOCK_FALLBACK = false` 설정, Mock 데이터 제거
+- [x] 엔드포인트 연동
+ - `GET /api/v1/quality/documents` — 검사 목록
+ - `GET /api/v1/quality/documents/{id}` — 검사 상세
+ - `POST /api/v1/quality/documents` — 검사 등록
+ - `PUT /api/v1/quality/documents/{id}` — 검사 수정
+ - `GET /api/v1/quality/performance-reports` — 실적신고 목록
+- [x] snake_case → camelCase 변환 함수 구현
+- [x] InspectionFormData 필드 추가: `clientId`, `inspectorId`, `receptionDate`
+- [x] 실적신고 API 응답 snake_case → camelCase 변환
+
+### 주요 파일
+- `src/components/quality/InspectionManagement/actions.ts`
+- `src/components/quality/PerformanceReportManagement/actions.ts`
+
+---
+
+## 2. 검사 모달 개선 (ProductInspectionInputModal)
+
+- [x] 기본값 null(미선택) 상태로 변경
+- [x] 일괄 합격/초기화 토글 버튼
+- [x] 시공 치수 필드 (너비/높이) — ConstructionInfo 인터페이스
+- [x] 변경사유 입력 필드
+- [x] 사진 첨부 (최대 2장, base64 인코딩)
+- [x] 이전/다음 개소 네비게이션 + 자동저장
+- [x] 레거시 검사 데이터 통합 (합격/불합격/진행중/미완)
+- [x] 사진 없는 항목 → "진행중" 상태 표시
+- [x] Eye 아이콘 → "보기" 텍스트 배지 변경
+- [x] 배지 사이즈 통일
+
+### 주요 파일
+- `src/components/quality/InspectionManagement/ProductInspectionInputModal.tsx` (+428/-210)
+
+---
+
+## 3. 수주선택 모달 (OrderSelectModal)
+
+- [x] 발주처(clientName) 컬럼 추가
+- [x] 모델명 컬럼 추가
+- [x] 동일 발주처 + 동일 모델 필터링 제약
+- [x] 모달 너비 확장: `sm:max-w-2xl` → `sm:max-w-3xl`
+- [x] 수주 선택 시 개소 자동 펼침
+- [x] 필터 안내 텍스트 추가
+
+### SearchableSelectionModal 공통 컴포넌트 확장
+- [x] `isItemDisabled` 콜백 prop 추가
+- [x] 비활성 항목 스타일링 (opacity 감소, cursor 변경)
+- [x] 전체선택 시 비활성 항목 제외
+- [x] 이미 선택된 항목은 비활성이라도 해제 가능
+
+### 주요 파일
+- `src/components/quality/InspectionManagement/OrderSelectModal.tsx`
+- `src/components/organisms/SearchableSelectionModal/SearchableSelectionModal.tsx`
+- `src/components/organisms/SearchableSelectionModal/types.ts`
+
+---
+
+## 4. 제품검사 성적서 (FqcDocumentContent) — 신규
+
+8컬럼 동적 렌더링 테이블 구현.
+
+| 컬럼 | 설명 |
+|------|------|
+| No | 순번 |
+| 검사항목 | 카테고리 기반 rowSpan 병합 |
+| 세부항목 | 개별 항목명 |
+| 검사기준 | 스펙/기준값 |
+| 검사방법 | method + frequency 복합 rowSpan 병합 |
+| 검사주기 | (검사방법과 함께 병합) |
+| 측정값 | measurement_type에 따라: checkbox→양호/불량, numeric→숫자입력, none→비활성 |
+| 판정 | 적합/부적합/null |
+
+- [x] `buildFieldRowSpan` — 단일 필드 병합 (카테고리)
+- [x] `buildCompositeRowSpan` — 복합 필드 병합 (method+frequency)
+- [x] FQC 모드 우선 + legacy fallback 패턴
+- [x] `useImperativeHandle`로 `getInspectionData()` 외부 접근
+- [x] Lazy Snapshot 준비 (`contentWrapperRef`)
+
+### 주요 파일
+- `src/components/quality/InspectionManagement/documents/FqcDocumentContent.tsx` (신규, +483)
+
+---
+
+## 5. 제품검사 요청서 (FqcRequestDocumentContent) — 신규
+
+양식 기반(template_id: 66) 동적 렌더링 구현.
+
+- [x] 결재라인 섹션
+- [x] 기본정보 섹션 (7개 필드, 2컬럼 배치)
+- [x] 입력 섹션 4개: 현장, 자재유통사, 시공자, 감리
+- [x] 사전통보 테이블 (group_name 기반 3단계 헤더)
+- [x] 오픈사이즈 발주 / 시공 치수 그룹 병합
+- [x] EAV 데이터 구조: `section_id`, `column_id`, `row_index`, `field_key`, `field_value`
+- [x] EAV 문서 없을 때 legacy fallback 적용
+
+### 주요 파일
+- `src/components/quality/InspectionManagement/documents/FqcRequestDocumentContent.tsx` (신규, +461)
+- `src/components/quality/InspectionManagement/documents/InspectionRequestModal.tsx`
+- `src/components/quality/InspectionManagement/fqcActions.ts`
+
+---
+
+## 6. 수주 연결 동기화
+
+- [x] `order_ids` 배열 매핑 (다중 수주 지원)
+- [x] 개소별 `inspectionData` 서버 저장
+- [x] FQC 문서에서 수주 연결 정보 동기화
diff --git a/src/app/[locale]/(protected)/quality/qms/page.tsx b/src/app/[locale]/(protected)/quality/qms/page.tsx
index 7cafb88f..1658882f 100644
--- a/src/app/[locale]/(protected)/quality/qms/page.tsx
+++ b/src/app/[locale]/(protected)/quality/qms/page.tsx
@@ -229,7 +229,7 @@ export default function QualityInspectionPage() {
}, []);
return (
-
+
{/* 헤더 (설정 버튼 포함) */}
setSettingsOpen(true)} />}
@@ -283,9 +283,9 @@ export default function QualityInspectionPage() {
{activeDay === 1 ? (
// ===== 기준/매뉴얼 심사 심사 =====
-
+
{/* 좌측: 점검표 항목 */}
-
@@ -327,8 +327,8 @@ export default function QualityInspectionPage() {
) : (
// ===== 로트 추적 심사 심사 =====
-
-
+
+
-
+
-
+
{
const result = await deleteBadDebt(id);
if (result.success) {
+ invalidateDashboard('badDebt');
setData((prev) => prev.filter((item) => item.id !== id));
}
return { success: result.success, error: result.error };
diff --git a/src/components/accounting/BillManagement/BillManagementClient.tsx b/src/components/accounting/BillManagement/BillManagementClient.tsx
index 6988c959..434b4a38 100644
--- a/src/components/accounting/BillManagement/BillManagementClient.tsx
+++ b/src/components/accounting/BillManagement/BillManagementClient.tsx
@@ -22,8 +22,6 @@ import {
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
-import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
-import { useDeleteDialog } from '@/hooks/useDeleteDialog';
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { TableRow, TableCell } from '@/components/ui/table';
import {
@@ -90,25 +88,6 @@ export function BillManagementClient({
const [currentPage, setCurrentPage] = useState(initialPagination.currentPage);
const itemsPerPage = initialPagination.perPage;
- // 삭제 다이얼로그
- const deleteDialog = useDeleteDialog({
- onDelete: async (id) => {
- const result = await deleteBill(id);
- if (result.success) {
- invalidateDashboard('bill');
- // 서버에서 재조회 (로컬 필터링 대신 - 페이지네이션 정합성 보장)
- await loadData(currentPage);
- setSelectedItems(prev => {
- const newSet = new Set(prev);
- newSet.delete(id);
- return newSet;
- });
- }
- return result;
- },
- entityName: '어음',
- });
-
// 날짜 범위 상태
const { startDate, endDate, setStartDate, setEndDate } = useDateRange('currentYear');
@@ -337,6 +316,25 @@ export function BillManagementClient({
totalCount: pagination.total,
};
},
+ deleteItem: async (id: string) => {
+ const result = await deleteBill(id);
+ if (result.success) {
+ invalidateDashboard('bill');
+ await loadData(currentPage);
+ setSelectedItems(prev => {
+ const newSet = new Set(prev);
+ newSet.delete(id);
+ return newSet;
+ });
+ }
+ return { success: result.success, error: result.error };
+ },
+ },
+
+ // 삭제 확인 메시지
+ deleteConfirmMessage: {
+ title: '어음 삭제',
+ description: '이 어음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.',
},
// 테이블 컬럼
@@ -448,6 +446,7 @@ export function BillManagementClient({
isLoading,
router,
loadData,
+ currentPage,
handleSave,
renderTableRow,
renderMobileCard,
@@ -474,14 +473,6 @@ export function BillManagementClient({
}}
/>
-
>
);
}
diff --git a/src/components/accounting/CardTransactionInquiry/index.tsx b/src/components/accounting/CardTransactionInquiry/index.tsx
index 0045157b..e95c7182 100644
--- a/src/components/accounting/CardTransactionInquiry/index.tsx
+++ b/src/components/accounting/CardTransactionInquiry/index.tsx
@@ -90,7 +90,7 @@ const tableColumns = [
{ key: 'deductionType', label: '공제', className: 'min-w-[95px]', sortable: false },
{ key: 'businessNumber', label: '사업자번호', className: 'min-w-[110px]' },
{ key: 'merchantName', label: '가맹점명', className: 'min-w-[100px]' },
- { key: 'vendorName', label: '증빙/판매자상호', className: 'min-w-[130px]', sortable: false },
+ { key: 'vendorName', label: '증빙/판매자상호', className: 'min-w-[160px]', sortable: false },
{ key: 'description', label: '내역', className: 'min-w-[120px]', sortable: false },
{ key: 'totalAmount', label: '합계금액', className: 'min-w-[100px] text-right' },
{ key: 'supplyAmount', label: '공급가액', className: 'min-w-[110px] text-right', sortable: false },
diff --git a/src/components/accounting/TaxInvoiceManagement/ManualEntryModal.tsx b/src/components/accounting/TaxInvoiceManagement/ManualEntryModal.tsx
index e173bfa3..a2e7e02d 100644
--- a/src/components/accounting/TaxInvoiceManagement/ManualEntryModal.tsx
+++ b/src/components/accounting/TaxInvoiceManagement/ManualEntryModal.tsx
@@ -18,6 +18,7 @@ import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { DatePicker } from '@/components/ui/date-picker';
import { FormField } from '@/components/molecules/FormField';
+import { BusinessNumberInput } from '@/components/ui/business-number-input';
import {
Dialog,
DialogContent,
@@ -199,12 +200,14 @@ export function ManualEntryModal({
onChange={(value) => handleChange('vendorName', value)}
placeholder="공급자명"
/>
- handleChange('vendorBusinessNumber', value)}
- placeholder="사업자번호"
- />
+
+
+ handleChange('vendorBusinessNumber', value)}
+ placeholder="000-00-00000"
+ />
+
diff --git a/src/components/accounting/TaxInvoiceManagement/types.ts b/src/components/accounting/TaxInvoiceManagement/types.ts
index db90f433..0db94ca8 100644
--- a/src/components/accounting/TaxInvoiceManagement/types.ts
+++ b/src/components/accounting/TaxInvoiceManagement/types.ts
@@ -257,10 +257,14 @@ export function transformFrontendToApi(data: ManualEntryFormData): Record
{
const result = await deleteClient(id);
if (result.success) {
+ invalidateDashboard('client');
toast.success('거래처가 삭제되었습니다.');
}
return { success: result.success, error: result.error };
diff --git a/src/components/approval/DocumentCreate/index.tsx b/src/components/approval/DocumentCreate/index.tsx
index f7335658..5cad414f 100644
--- a/src/components/approval/DocumentCreate/index.tsx
+++ b/src/components/approval/DocumentCreate/index.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useCallback, useEffect, useTransition, useRef, useMemo } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { useRouter, useSearchParams } from 'next/navigation';
import { usePermission } from '@/hooks/usePermission';
import { format } from 'date-fns';
@@ -50,7 +51,7 @@ import { getClients } from '@/components/accounting/VendorManagement/actions';
// 초기 데이터 - SSR에서는 빈 문자열, 클라이언트에서 날짜 설정
const getInitialBasicInfo = (): BasicInfo => ({
- drafter: '홍길동',
+ drafter: '', // 클라이언트에서 currentUser로 설정
draftDate: '', // 클라이언트에서 설정
documentNo: '',
documentType: 'proposal',
@@ -118,14 +119,22 @@ export function DocumentCreate() {
const today = format(new Date(), 'yyyy-MM-dd');
const now = format(new Date(), 'yyyy-MM-dd HH:mm');
- setBasicInfo(prev => ({ ...prev, draftDate: prev.draftDate || now }));
+ // localStorage 'user' 키에서 사용자 이름 가져오기 (로그인 시 저장됨)
+ const userDataStr = typeof window !== 'undefined' ? localStorage.getItem('user') : null;
+ const userName = userDataStr ? JSON.parse(userDataStr).name : currentUser?.name || '';
+
+ setBasicInfo(prev => ({
+ ...prev,
+ drafter: prev.drafter || userName,
+ draftDate: prev.draftDate || now,
+ }));
setProposalData(prev => ({ ...prev, vendorPaymentDate: prev.vendorPaymentDate || today }));
setExpenseReportData(prev => ({
...prev,
requestDate: prev.requestDate || today,
paymentDate: prev.paymentDate || today,
}));
- }, []);
+ }, [currentUser?.name]);
// 미리보기 모달 상태
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
@@ -172,6 +181,7 @@ export function DocumentCreate() {
setBasicInfo(prev => ({
...prev,
...mockData.basicInfo,
+ drafter: currentUserName || prev.drafter,
draftDate: prev.draftDate || mockData.basicInfo.draftDate,
documentType: (mockData.basicInfo.documentType || prev.documentType) as BasicInfo['documentType'],
}));
@@ -343,6 +353,7 @@ export function DocumentCreate() {
try {
const result = await deleteApproval(parseInt(documentId));
if (result.success) {
+ invalidateDashboard('approval');
toast.success('문서가 삭제되었습니다.');
router.back();
} else {
@@ -375,6 +386,7 @@ export function DocumentCreate() {
if (isEditMode && documentId) {
const result = await updateAndSubmitApproval(parseInt(documentId), formData);
if (result.success) {
+ invalidateDashboard('approval');
toast.success('수정 및 상신 완료', {
description: `문서번호: ${result.data?.documentNo}`,
});
@@ -386,6 +398,7 @@ export function DocumentCreate() {
// 새 문서: 생성 후 상신
const result = await createAndSubmitApproval(formData);
if (result.success) {
+ invalidateDashboard('approval');
toast.success('상신 완료', {
description: `문서번호: ${result.data?.documentNo}`,
});
@@ -411,6 +424,7 @@ export function DocumentCreate() {
if (isEditMode && documentId) {
const result = await updateApproval(parseInt(documentId), formData);
if (result.success) {
+ invalidateDashboard('approval');
toast.success('저장 완료', {
description: `문서번호: ${result.data?.documentNo}`,
});
@@ -421,6 +435,7 @@ export function DocumentCreate() {
// 새 문서: 임시저장
const result = await createApproval(formData);
if (result.success) {
+ invalidateDashboard('approval');
toast.success('임시저장 완료', {
description: `문서번호: ${result.data?.documentNo}`,
});
diff --git a/src/components/approval/DraftBox/index.tsx b/src/components/approval/DraftBox/index.tsx
index 1fe37dad..7ff81c26 100644
--- a/src/components/approval/DraftBox/index.tsx
+++ b/src/components/approval/DraftBox/index.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useMemo, useCallback, useEffect, useTransition, useRef } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { useRouter } from 'next/navigation';
import { useDateRange } from '@/hooks';
import {
@@ -175,6 +176,7 @@ export function DraftBox() {
try {
const result = await submitDrafts(ids);
if (result.success) {
+ invalidateDashboard('approval');
toast.success(`${ids.length}건의 문서를 상신했습니다.`);
loadData();
loadSummary();
@@ -200,6 +202,7 @@ export function DraftBox() {
try {
const result = await deleteDrafts(ids);
if (result.success) {
+ invalidateDashboard('approval');
toast.success(`${ids.length}건의 문서를 삭제했습니다.`);
loadData();
loadSummary();
@@ -222,6 +225,7 @@ export function DraftBox() {
try {
const result = await deleteDraft(id);
if (result.success) {
+ invalidateDashboard('approval');
toast.success('문서를 삭제했습니다.');
loadData();
loadSummary();
@@ -298,6 +302,7 @@ export function DraftBox() {
try {
const result = await submitDraft(selectedDocument.id);
if (result.success) {
+ invalidateDashboard('approval');
toast.success('문서를 상신했습니다.');
setIsModalOpen(false);
setSelectedDocument(null);
diff --git a/src/components/auth/LoginPage.tsx b/src/components/auth/LoginPage.tsx
index 24882ab9..aab81288 100644
--- a/src/components/auth/LoginPage.tsx
+++ b/src/components/auth/LoginPage.tsx
@@ -4,6 +4,7 @@ import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import Image from "next/image";
+import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -268,7 +269,11 @@ export function LoginPage() {
/>
{t('rememberMe')}
-
diff --git a/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx b/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx
index 713bcfa3..aa276257 100644
--- a/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx
+++ b/src/components/business/CEODashboard/sections/PurchaseStatusSection.tsx
@@ -47,12 +47,7 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
}
title="매입 현황"
- subtitle="당월 매입 실적"
- rightElement={
-
- 당월
-
- }
+ subtitle="매입 실적"
>
{/* 통계카드 3개 - 가로 배치 */}
@@ -158,8 +153,8 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
{/* 당월 매입 내역 (별도 카드) */}
}
- title="당월 매입 내역"
- subtitle="당월 매입 거래 상세"
+ title="최근 매입 내역"
+ subtitle="매입 거래 상세"
bodyClassName="p-0"
>
diff --git a/src/components/business/CEODashboard/types.ts b/src/components/business/CEODashboard/types.ts
index 6405def2..80434976 100644
--- a/src/components/business/CEODashboard/types.ts
+++ b/src/components/business/CEODashboard/types.ts
@@ -725,13 +725,13 @@ export const DEFAULT_DASHBOARD_SETTINGS: DashboardSettings = {
orders: true,
debtCollection: true,
safetyStock: true,
- taxReport: false,
- newVendor: false,
+ taxReport: true,
+ newVendor: true,
annualLeave: true,
vehicle: false,
equipment: false,
purchase: false,
- approvalRequest: false,
+ approvalRequest: true,
fundStatus: true,
},
},
@@ -774,13 +774,13 @@ export const DEFAULT_DASHBOARD_SETTINGS: DashboardSettings = {
orders: true,
debtCollection: true,
safetyStock: true,
- taxReport: false,
- newVendor: false,
+ taxReport: true,
+ newVendor: true,
annualLeave: true,
vehicle: false,
equipment: false,
purchase: false,
- approvalRequest: false,
+ approvalRequest: true,
fundStatus: true,
},
},
diff --git a/src/components/business/construction/management/ConstructionDetailClient.tsx b/src/components/business/construction/management/ConstructionDetailClient.tsx
index 986e6321..02df109a 100644
--- a/src/components/business/construction/management/ConstructionDetailClient.tsx
+++ b/src/components/business/construction/management/ConstructionDetailClient.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect, useCallback, useMemo } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/navigation';
import { getTodayString, formatDate } from '@/lib/utils/date';
@@ -243,6 +244,7 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai
try {
const result = await updateConstructionManagementDetail(id, formData);
if (result.success) {
+ invalidateDashboard('construction');
toast.success('저장되었습니다.');
return { success: true };
} else {
@@ -265,6 +267,7 @@ export default function ConstructionDetailClient({ id, mode }: ConstructionDetai
try {
const result = await completeConstruction(id);
if (result.success) {
+ invalidateDashboard('construction');
toast.success('시공이 완료되었습니다.');
router.push('/ko/construction/project/construction-management');
} else {
diff --git a/src/components/dev/generators/accountingData.ts b/src/components/dev/generators/accountingData.ts
index 563e4396..45f1c7c5 100644
--- a/src/components/dev/generators/accountingData.ts
+++ b/src/components/dev/generators/accountingData.ts
@@ -248,12 +248,14 @@ export function generatePurchaseApprovalData(options: GeneratePurchaseApprovalDa
const { vendors = SAMPLE_VENDORS, documentType = 'proposal' } = options;
const vendor = randomPick(vendors);
- // 현재 사용자를 결재선에 추가 (기본값: 홍길동)
+ // 현재 사용자를 결재선에 추가 (기본값: 로그인 사용자 정보)
+ const userDataStr = typeof window !== 'undefined' ? localStorage.getItem('user') : null;
+ const userData = userDataStr ? JSON.parse(userDataStr) : null;
const currentUser: ApprovalPerson = options.currentUser || {
- id: 'user-1',
- department: '개발팀',
- position: '사원',
- name: '홍길동',
+ id: userData?.id || 'user-1',
+ department: userData?.department || '',
+ position: userData?.position || '',
+ name: userData?.name || '',
};
// 경리/회계/재무 직원 중 랜덤으로 1명 참조 추가
diff --git a/src/components/hr/AttendanceManagement/index.tsx b/src/components/hr/AttendanceManagement/index.tsx
index 30fe53e7..7e01e403 100644
--- a/src/components/hr/AttendanceManagement/index.tsx
+++ b/src/components/hr/AttendanceManagement/index.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { useRouter } from 'next/navigation';
import {
Clock,
@@ -310,6 +311,7 @@ export function AttendanceManagement() {
if (attendanceDialogMode === 'create') {
const result = await createAttendance(data);
if (result.success && result.data) {
+ invalidateDashboard('attendance');
setAttendanceRecords(prev => [result.data!, ...prev]);
} else {
console.error('Create failed:', result.error);
@@ -317,6 +319,7 @@ export function AttendanceManagement() {
} else if (selectedAttendance) {
const result = await updateAttendance(selectedAttendance.id, data);
if (result.success && result.data) {
+ invalidateDashboard('attendance');
setAttendanceRecords(prev =>
prev.map(r => r.id === selectedAttendance.id ? result.data! : r)
);
diff --git a/src/components/hr/VacationManagement/index.tsx b/src/components/hr/VacationManagement/index.tsx
index f67e9eb1..5276d9e1 100644
--- a/src/components/hr/VacationManagement/index.tsx
+++ b/src/components/hr/VacationManagement/index.tsx
@@ -1,6 +1,7 @@
'use client';
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { format } from 'date-fns';
import { useDateRange } from '@/hooks';
import {
@@ -312,6 +313,7 @@ export function VacationManagement() {
const ids = Array.from(selectedItems).map((id) => parseInt(id, 10));
const result = await approveLeavesMany(ids);
if (result.success) {
+ invalidateDashboard('leave');
await fetchLeaveRequests();
await fetchUsageData(); // 휴가 사용현황도 갱신
} else {
@@ -340,6 +342,7 @@ export function VacationManagement() {
const ids = Array.from(selectedItems).map((id) => parseInt(id, 10));
const result = await rejectLeavesMany(ids, '관리자에 의해 반려됨');
if (result.success) {
+ invalidateDashboard('leave');
await fetchLeaveRequests();
} else {
console.error('[VacationManagement] 반려 실패:', result.error);
@@ -750,6 +753,7 @@ export function VacationManagement() {
reason: data.reason,
});
if (result.success) {
+ invalidateDashboard('leave');
await fetchGrantData();
await fetchUsageData();
} else {
@@ -780,6 +784,7 @@ export function VacationManagement() {
days: data.vacationDays,
});
if (result.success) {
+ invalidateDashboard('leave');
await fetchLeaveRequests();
await fetchUsageData();
} else {
diff --git a/src/components/layout/HeaderFavoritesBar.tsx b/src/components/layout/HeaderFavoritesBar.tsx
index d4421391..9600e0aa 100644
--- a/src/components/layout/HeaderFavoritesBar.tsx
+++ b/src/components/layout/HeaderFavoritesBar.tsx
@@ -2,7 +2,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/navigation';
-import { Bookmark, MoreHorizontal } from 'lucide-react';
+import { Pin, MoreHorizontal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Tooltip,
@@ -51,7 +51,7 @@ function StarDropdown({
className={`p-0 rounded-xl bg-blue-600 hover:bg-blue-700 text-white flex items-center justify-center ${className ?? 'w-10 h-10'}`}
title="즐겨찾기"
>
-
+
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx
index fe7212f7..c065ba54 100644
--- a/src/components/layout/Sidebar.tsx
+++ b/src/components/layout/Sidebar.tsx
@@ -1,4 +1,4 @@
-import { Bookmark, ChevronRight, ChevronsDownUp, ChevronsUpDown, Circle } from 'lucide-react';
+import { Pin, ChevronRight, ChevronsDownUp, ChevronsUpDown, Circle } from 'lucide-react';
import type { MenuItem } from '@/stores/menuStore';
import { useEffect, useRef, useCallback } from 'react';
import { useFavoritesStore, MAX_FAVORITES } from '@/stores/favoritesStore';
@@ -159,7 +159,7 @@ function MenuItemComponent({
}`}
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
>
-
+
)}
@@ -224,7 +224,7 @@ function MenuItemComponent({
}`}
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
>
-
+
)}
@@ -291,7 +291,7 @@ function MenuItemComponent({
}`}
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
>
-
+
)}
diff --git a/src/components/material/StockStatus/StockStatusDetail.tsx b/src/components/material/StockStatus/StockStatusDetail.tsx
index 5781d638..c0b71a76 100644
--- a/src/components/material/StockStatus/StockStatusDetail.tsx
+++ b/src/components/material/StockStatus/StockStatusDetail.tsx
@@ -9,6 +9,7 @@
*/
import { useState, useCallback, useEffect } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { useRouter, useSearchParams } from 'next/navigation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
@@ -133,6 +134,7 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
const result = await updateStock(id, formData);
if (result.success) {
+ invalidateDashboard('stock');
toast.success('재고 정보가 저장되었습니다.');
// 상세 데이터 업데이트
setDetail((prev) =>
diff --git a/src/components/orders/OrderRegistration.tsx b/src/components/orders/OrderRegistration.tsx
index 01bc4f04..f1425c01 100644
--- a/src/components/orders/OrderRegistration.tsx
+++ b/src/components/orders/OrderRegistration.tsx
@@ -12,6 +12,7 @@
*/
import { useState, useEffect, useCallback, useMemo } from "react";
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { useDaumPostcode } from "@/hooks/useDaumPostcode";
import { useClientList } from "@/hooks/useClientList";
import { Input } from "@/components/ui/input";
@@ -504,6 +505,7 @@ export function OrderRegistration({
setIsSaving(true);
try {
await onSave(form);
+ invalidateDashboard('order');
return { success: true };
} catch (e) {
const errorMsg = e instanceof Error ? e.message : '저장 중 오류가 발생했습니다.';
diff --git a/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx b/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx
index dadba78b..a6ce622a 100644
--- a/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx
+++ b/src/components/outbound/ShipmentManagement/ShipmentEdit.tsx
@@ -6,6 +6,7 @@
*/
import { useState, useCallback, useEffect } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { useRouter } from 'next/navigation';
import { Plus, Trash2, ChevronDown, Search } from 'lucide-react';
import { Input } from '@/components/ui/input';
@@ -305,6 +306,7 @@ export function ShipmentEdit({ id }: ShipmentEditProps) {
if (!result.success) {
return { success: false, error: result.error || '출고 수정에 실패했습니다.' };
}
+ invalidateDashboard('shipment');
return { success: true };
} catch (err) {
if (isNextRedirectError(err)) throw err;
diff --git a/src/components/production/WorkOrders/WorkOrderCreate.tsx b/src/components/production/WorkOrders/WorkOrderCreate.tsx
index 512ac0ab..13904ba4 100644
--- a/src/components/production/WorkOrders/WorkOrderCreate.tsx
+++ b/src/components/production/WorkOrders/WorkOrderCreate.tsx
@@ -7,6 +7,7 @@
*/
import { useState, useEffect, useCallback } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { useRouter } from 'next/navigation';
import { FileText, X, Edit, Loader2, Plus, Search, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -291,6 +292,7 @@ export function WorkOrderCreate() {
if (!result.success) {
return { success: false, error: result.error || '작업지시 등록에 실패했습니다.' };
}
+ invalidateDashboard('production');
return { success: true };
} catch (error) {
if (isNextRedirectError(error)) throw error;
diff --git a/src/components/production/WorkOrders/WorkOrderDetail.tsx b/src/components/production/WorkOrders/WorkOrderDetail.tsx
index d27d2457..f8e386b3 100644
--- a/src/components/production/WorkOrders/WorkOrderDetail.tsx
+++ b/src/components/production/WorkOrders/WorkOrderDetail.tsx
@@ -7,6 +7,7 @@
*/
import React, { useState, useEffect, useCallback, useMemo } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { useRouter } from 'next/navigation';
import { FileText, Play, CheckCircle2, Loader2, Undo2, ClipboardCheck } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -272,6 +273,7 @@ export function WorkOrderDetail({ orderId }: WorkOrderDetailProps) {
try {
const result = await updateWorkOrderStatus(orderId, newStatus);
if (result.success && result.data) {
+ invalidateDashboard('production');
setOrder(result.data);
const statusLabels = {
waiting: '작업대기',
diff --git a/src/components/production/WorkOrders/WorkOrderEdit.tsx b/src/components/production/WorkOrders/WorkOrderEdit.tsx
index 1eaabe58..cf9d3b80 100644
--- a/src/components/production/WorkOrders/WorkOrderEdit.tsx
+++ b/src/components/production/WorkOrders/WorkOrderEdit.tsx
@@ -8,6 +8,7 @@
*/
import { useState, useEffect, useCallback } from 'react';
+import { invalidateDashboard } from '@/lib/dashboard-invalidation';
import { useRouter } from 'next/navigation';
import { SquarePen, Trash2 } from 'lucide-react';
import { Input } from '@/components/ui/input';
@@ -239,6 +240,7 @@ export function WorkOrderEdit({ orderId }: WorkOrderEditProps) {
});
if (result.success) {
+ invalidateDashboard('production');
toast.success('작업지시가 수정되었습니다.');
router.push(`/production/work-orders/${orderId}?mode=view`);
return { success: true };
diff --git a/src/lib/api/dashboard/transformers/receivable.ts b/src/lib/api/dashboard/transformers/receivable.ts
index ec77c1d7..63d48625 100644
--- a/src/lib/api/dashboard/transformers/receivable.ts
+++ b/src/lib/api/dashboard/transformers/receivable.ts
@@ -93,31 +93,11 @@ function generateDebtCollectionCheckPoints(api: BadDebtApiResponse): CheckPoint[
return checkPoints;
}
-// TODO: 백엔드 per-card sub_label/count 제공 시 더미값 제거
-// 채권추심 카드별 더미 서브라벨 (회사명 + 건수)
-const DEBT_COLLECTION_FALLBACK_SUB_LABELS: Record
= {
- dc1: { company: '(주)부산화학 외', count: 5 },
- dc2: { company: '(주)삼성테크 외', count: 3 },
- dc3: { company: '(주)대한전자 외', count: 2 },
- dc4: { company: '(주)한국정밀 외', count: 3 },
-};
-
/**
- * 채권추심 subLabel 생성 헬퍼
- * dc1(누적)은 API client_count 사용, 나머지는 더미값
+ * 채권추심 subLabel: 백엔드 sub_labels 필드 직접 사용
*/
-function buildDebtSubLabel(cardId: string, clientCount?: number): string | undefined {
- const fallback = DEBT_COLLECTION_FALLBACK_SUB_LABELS[cardId];
- if (!fallback) return undefined;
-
- const count = cardId === 'dc1' && clientCount !== undefined ? clientCount : fallback.count;
- if (count <= 0) return undefined;
-
- const remaining = count - 1;
- if (remaining > 0) {
- return `${fallback.company} ${remaining}건`;
- }
- return fallback.company.replace(/ 외$/, '');
+function buildDebtSubLabel(cardId: string, subLabels?: Record): string | undefined {
+ return subLabels?.[cardId] || undefined;
}
/**
@@ -130,25 +110,25 @@ export function transformDebtCollectionResponse(api: BadDebtApiResponse): DebtCo
id: 'dc1',
label: '누적 악성채권',
amount: api.total_amount,
- subLabel: buildDebtSubLabel('dc1', api.client_count),
+ subLabel: buildDebtSubLabel('dc1', api.sub_labels),
},
{
id: 'dc2',
label: '추심중',
amount: api.collecting_amount,
- subLabel: buildDebtSubLabel('dc2'),
+ subLabel: buildDebtSubLabel('dc2', api.sub_labels),
},
{
id: 'dc3',
label: '법적조치',
amount: api.legal_action_amount,
- subLabel: buildDebtSubLabel('dc3'),
+ subLabel: buildDebtSubLabel('dc3', api.sub_labels),
},
{
id: 'dc4',
label: '회수완료',
amount: api.recovered_amount,
- subLabel: buildDebtSubLabel('dc4'),
+ subLabel: buildDebtSubLabel('dc4', api.sub_labels),
},
],
checkPoints: generateDebtCollectionCheckPoints(api),
diff --git a/src/lib/api/dashboard/transformers/status-issue.ts b/src/lib/api/dashboard/transformers/status-issue.ts
index d1192839..b61b434d 100644
--- a/src/lib/api/dashboard/transformers/status-issue.ts
+++ b/src/lib/api/dashboard/transformers/status-issue.ts
@@ -14,42 +14,26 @@ import { normalizePath } from './common';
// ============================================
// 현황판 (StatusBoard)
// ============================================
-
-// TODO: 백엔드 sub_label 필드 제공 시 더미값 제거
-// API id 기준: orders, bad_debts, safety_stock, tax_deadline, new_clients, leaves, purchases, approvals
-const STATUS_BOARD_FALLBACK_SUB_LABELS: Record = {
- orders: '(주)삼성전자 외',
- bad_debts: '주식회사 부산화학 외',
- safety_stock: '',
- tax_deadline: '',
- new_clients: '대한철강 외',
- leaves: '',
- // purchases: '(유)한국정밀 외', // [2026-03-03] 비활성화 — 백엔드 path 오류 + 데이터 정합성 이슈 (N4 참조)
- approvals: '구매 결재 외',
-};
+//
+// [대시보드 vs 원본 페이지 쿼리 조건 차이 — 건수 불일치는 버그 아님]
+//
+// | 항목 | 대시보드 조건 | 원본 페이지 |
+// |----------------|---------------------------------------------------|------------------------------------------|
+// | 수주 현황 | 오늘 날짜 + status=confirmed만 | /sales/order-management-sales (전체 기간) |
+// | 채권 추심 | status=collecting + is_active=true만 | /accounting/bad-debt-collection (전체) |
+// | 안전 재고 | safety_stock>0 && stock_qty 0) {
- return `${fallback} ${remaining}건`;
- }
- // 1건이면 "외" 제거하고 이름만
- return fallback.replace(/ 외$/, '');
+function buildStatusSubLabel(item: { sub_label?: string }): string | undefined {
+ return item.sub_label || undefined;
}
/**
diff --git a/src/lib/api/dashboard/types.ts b/src/lib/api/dashboard/types.ts
index 8b9c5cb8..4b5791b2 100644
--- a/src/lib/api/dashboard/types.ts
+++ b/src/lib/api/dashboard/types.ts
@@ -107,6 +107,7 @@ export interface BadDebtApiResponse {
recovered_amount: number; // 회수완료
bad_debt_amount: number; // 대손처리
client_count?: number; // 거래처 수
+ sub_labels?: Record; // 카드별 거래처 sub_label (dc1~dc4)
}
// ============================================
diff --git a/src/lib/dashboard-invalidation.ts b/src/lib/dashboard-invalidation.ts
index 6c17e649..6ce78414 100644
--- a/src/lib/dashboard-invalidation.ts
+++ b/src/lib/dashboard-invalidation.ts
@@ -31,7 +31,17 @@ type DomainKey =
| 'expectedExpense'
| 'bill'
| 'giftCertificate'
- | 'journalEntry';
+ | 'journalEntry'
+ | 'order'
+ | 'stock'
+ | 'schedule'
+ | 'client'
+ | 'leave'
+ | 'approval'
+ | 'attendance'
+ | 'production'
+ | 'shipment'
+ | 'construction';
const DOMAIN_SECTION_MAP: Record = {
deposit: ['dailyReport', 'receivable'],
@@ -43,6 +53,16 @@ const DOMAIN_SECTION_MAP: Record = {
bill: ['dailyReport', 'receivable'],
giftCertificate: ['entertainment', 'cardManagement'],
journalEntry: ['entertainment', 'welfare', 'monthlyExpense'],
+ order: ['statusBoard', 'salesStatus'],
+ stock: ['statusBoard'],
+ schedule: ['statusBoard'],
+ client: ['statusBoard'],
+ leave: ['statusBoard', 'dailyAttendance'],
+ approval: ['statusBoard'],
+ attendance: ['statusBoard', 'dailyAttendance'],
+ production: ['statusBoard', 'dailyProduction'],
+ shipment: ['statusBoard', 'unshipped'],
+ construction: ['statusBoard', 'construction'],
};
const STORAGE_KEY = 'dashboard:stale-sections';