From 68331be0ef43792c90ea9023af2bfc9e4caa199f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Mon, 9 Mar 2026 21:06:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=9A=8C=EA=B3=84/=EA=B2=B0=EC=9E=AC/?= =?UTF-8?q?=EC=83=9D=EC=82=B0/=EC=B6=9C=ED=95=98/=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=8B=A4=EC=88=98=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20QA=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BadDebtCollection, BillManagement, CardTransaction, TaxInvoice 회계 개선 - VendorManagement/VendorDetailClient 소폭 추가 - DocumentCreate/DraftBox 결재 기능 개선 - WorkOrder Create/Detail/Edit, ShipmentEdit 생산/출하 개선 - CEO 대시보드: PurchaseStatusSection, receivable/status-issue transformer 정비 - dashboard types/invalidation 확장 - LoginPage, Sidebar, HeaderFavoritesBar 레이아웃 수정 - QMS 페이지, StockStatusDetail, OrderRegistration 소폭 수정 - AttendanceManagement, VacationManagement HR 수정 - ConstructionDetailClient 건설 상세 개선 - claudedocs: 주간 구현내역, 대시보드 QA/수정계획, 결재/품질/생산/출하 문서 추가 --- ...L-2026-03-08] frontend-weekly-0302-0308.md | 250 +++++++++++++++++ claudedocs/_index.md | 11 +- ...26-03-07] approval-box-linked-documents.md | 59 ++++ .../[IMPL-2026-03-06] lazy-snapshot-system.md | 103 +++++++ ...[FIX-2026-03-09] ceo-dashboard-fix-plan.md | 213 +++++++++++++++ ...26-03-09] ceo-dashboard-ui-verification.md | 252 ++++++++++++++++++ ...3-04] shipment-dispatch-api-integration.md | 70 +++++ ...3-05] production-orders-api-integration.md | 105 ++++++++ ...[IMPL-2026-03-07] quality-api-migration.md | 124 +++++++++ .../[locale]/(protected)/quality/qms/page.tsx | 18 +- .../BadDebtCollection/BadDebtDetail.tsx | 4 + .../accounting/BadDebtCollection/index.tsx | 2 + .../BillManagement/BillManagementClient.tsx | 49 ++-- .../CardTransactionInquiry/index.tsx | 2 +- .../TaxInvoiceManagement/ManualEntryModal.tsx | 15 +- .../accounting/TaxInvoiceManagement/types.ts | 10 +- .../VendorManagement/VendorDetailClient.tsx | 3 + .../accounting/VendorManagement/index.tsx | 2 + .../approval/DocumentCreate/index.tsx | 21 +- src/components/approval/DraftBox/index.tsx | 5 + src/components/auth/LoginPage.tsx | 7 +- .../sections/PurchaseStatusSection.tsx | 11 +- src/components/business/CEODashboard/types.ts | 12 +- .../management/ConstructionDetailClient.tsx | 3 + .../dev/generators/accountingData.ts | 12 +- .../hr/AttendanceManagement/index.tsx | 3 + .../hr/VacationManagement/index.tsx | 5 + src/components/layout/HeaderFavoritesBar.tsx | 4 +- src/components/layout/Sidebar.tsx | 8 +- .../StockStatus/StockStatusDetail.tsx | 2 + src/components/orders/OrderRegistration.tsx | 2 + .../ShipmentManagement/ShipmentEdit.tsx | 2 + .../production/WorkOrders/WorkOrderCreate.tsx | 2 + .../production/WorkOrders/WorkOrderDetail.tsx | 2 + .../production/WorkOrders/WorkOrderEdit.tsx | 2 + .../api/dashboard/transformers/receivable.ts | 34 +-- .../dashboard/transformers/status-issue.ts | 50 ++-- src/lib/api/dashboard/types.ts | 1 + src/lib/dashboard-invalidation.ts | 22 +- 39 files changed, 1363 insertions(+), 139 deletions(-) create mode 100644 claudedocs/[IMPL-2026-03-08] frontend-weekly-0302-0308.md create mode 100644 claudedocs/approval/[IMPL-2026-03-07] approval-box-linked-documents.md create mode 100644 claudedocs/architecture/[IMPL-2026-03-06] lazy-snapshot-system.md create mode 100644 claudedocs/dashboard/[FIX-2026-03-09] ceo-dashboard-fix-plan.md create mode 100644 claudedocs/dashboard/[QA-2026-03-09] ceo-dashboard-ui-verification.md create mode 100644 claudedocs/outbound/[IMPL-2026-03-04] shipment-dispatch-api-integration.md create mode 100644 claudedocs/production/[IMPL-2026-03-05] production-orders-api-integration.md create mode 100644 claudedocs/quality/[IMPL-2026-03-07] quality-api-migration.md 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';