feat: 회계/결재/생산/출하/대시보드 다수 개선 및 QA 수정
- 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/수정계획, 결재/품질/생산/출하 문서 추가
This commit is contained in:
250
claudedocs/[IMPL-2026-03-08] frontend-weekly-0302-0308.md
Normal file
250
claudedocs/[IMPL-2026-03-08] frontend-weekly-0302-0308.md
Normal file
@@ -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) |
|
||||
@@ -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 배포
|
||||
|
||||
@@ -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)
|
||||
@@ -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`
|
||||
213
claudedocs/dashboard/[FIX-2026-03-09] ceo-dashboard-fix-plan.md
Normal file
213
claudedocs/dashboard/[FIX-2026-03-09] ceo-dashboard-fix-plan.md
Normal file
@@ -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` — `<Badge>당월</Badge>`
|
||||
- `PurchaseStatusSection.tsx:65` — `<span>누적 매입</span>`
|
||||
- `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) | 나머지 모두 수정 불필요 확인 |
|
||||
@@ -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: 데이터 변경 반영 테스트 | ⏸ 이슈 수정 후 |
|
||||
@@ -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`
|
||||
@@ -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 자동 매핑
|
||||
124
claudedocs/quality/[IMPL-2026-03-07] quality-api-migration.md
Normal file
124
claudedocs/quality/[IMPL-2026-03-07] quality-api-migration.md
Normal file
@@ -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 문서에서 수주 연결 정보 동기화
|
||||
@@ -229,7 +229,7 @@ export default function QualityInspectionPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-3 sm:p-4 md:p-5 lg:p-6 bg-slate-100 flex flex-col lg:overflow-hidden">
|
||||
<div className="w-full min-h-[calc(100vh-64px)] lg:h-[calc(100vh-64px)] p-3 sm:p-4 md:p-5 lg:p-6 bg-slate-100 flex flex-col lg:overflow-auto">
|
||||
{/* 헤더 (설정 버튼 포함) */}
|
||||
<Header
|
||||
rightContent={<SettingsButton onClick={() => setSettingsOpen(true)} />}
|
||||
@@ -283,9 +283,9 @@ export default function QualityInspectionPage() {
|
||||
|
||||
{activeDay === 1 ? (
|
||||
// ===== 기준/매뉴얼 심사 심사 =====
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:min-h-0">
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:min-h-[500px]">
|
||||
{/* 좌측: 점검표 항목 */}
|
||||
<div className={`col-span-12 min-h-[250px] sm:min-h-[300px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
<div className={`col-span-12 min-h-[250px] sm:min-h-[300px] lg:min-h-[500px] lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentSection && displaySettings.showDocumentViewer
|
||||
? 'lg:col-span-3'
|
||||
: displaySettings.showDocumentSection || displaySettings.showDocumentViewer
|
||||
@@ -303,7 +303,7 @@ export default function QualityInspectionPage() {
|
||||
|
||||
{/* 중앙: 기준 문서화 */}
|
||||
{displaySettings.showDocumentSection && (
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentViewer ? 'lg:col-span-4' : 'lg:col-span-8'
|
||||
}`}>
|
||||
<Day1DocumentSection
|
||||
@@ -318,7 +318,7 @@ export default function QualityInspectionPage() {
|
||||
|
||||
{/* 우측: 문서 뷰어 */}
|
||||
{displaySettings.showDocumentViewer && (
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto ${
|
||||
<div className={`col-span-12 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto ${
|
||||
displaySettings.showDocumentSection ? 'lg:col-span-5' : 'lg:col-span-8'
|
||||
}`}>
|
||||
<Day1DocumentViewer document={selectedStandardDoc} />
|
||||
@@ -327,8 +327,8 @@ export default function QualityInspectionPage() {
|
||||
</div>
|
||||
) : (
|
||||
// ===== 로트 추적 심사 심사 =====
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:gap-6 lg:min-h-0">
|
||||
<div className="col-span-12 lg:col-span-3 min-h-[250px] sm:min-h-[300px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<div className="flex-1 grid grid-cols-12 gap-3 sm:gap-4 lg:gap-6 lg:min-h-[500px]">
|
||||
<div className="col-span-12 lg:col-span-3 min-h-[250px] sm:min-h-[300px] lg:min-h-[500px] lg:h-full overflow-auto">
|
||||
<ReportList
|
||||
reports={filteredReports}
|
||||
selectedId={selectedReport?.id || null}
|
||||
@@ -336,7 +336,7 @@ export default function QualityInspectionPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-4 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<div className="col-span-12 lg:col-span-4 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto">
|
||||
<RouteList
|
||||
routes={currentRoutes}
|
||||
selectedId={selectedRoute?.id || null}
|
||||
@@ -346,7 +346,7 @@ export default function QualityInspectionPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 lg:col-span-5 min-h-[200px] sm:min-h-[250px] lg:min-h-0 lg:h-full overflow-auto lg:overflow-hidden">
|
||||
<div className="col-span-12 lg:col-span-5 min-h-[200px] sm:min-h-[250px] lg:min-h-[500px] lg:h-full overflow-auto">
|
||||
<DocumentList
|
||||
documents={currentDocuments}
|
||||
routeCode={selectedRoute?.code || null}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format } from 'date-fns';
|
||||
@@ -137,12 +138,14 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
if (isNewMode) {
|
||||
const result = await createBadDebt(formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '등록에 실패했습니다.' };
|
||||
} else {
|
||||
const result = await updateBadDebt(recordId!, formData);
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '수정에 실패했습니다.' };
|
||||
@@ -159,6 +162,7 @@ export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProp
|
||||
try {
|
||||
const result = await deleteBadDebt(String(id));
|
||||
if (result.success) {
|
||||
invalidateDashboard('badDebt');
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: result.error || '삭제에 실패했습니다.' };
|
||||
|
||||
@@ -14,6 +14,7 @@ export { BadDebtDetailClientV2 } from './BadDebtDetailClientV2';
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback, useTransition } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AlertTriangle, Pencil, Trash2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -176,6 +177,7 @@ export function BadDebtCollection({ initialData, initialSummary }: BadDebtCollec
|
||||
deleteItem: async (id: string) => {
|
||||
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 };
|
||||
|
||||
@@ -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({
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeleteConfirmDialog
|
||||
open={deleteDialog.single.isOpen}
|
||||
onOpenChange={deleteDialog.single.onOpenChange}
|
||||
onConfirm={deleteDialog.single.confirm}
|
||||
title="어음 삭제"
|
||||
description="이 어음을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다."
|
||||
loading={deleteDialog.isPending}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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="공급자명"
|
||||
/>
|
||||
<FormField
|
||||
label="사업자번호"
|
||||
value={formData.vendorBusinessNumber}
|
||||
onChange={(value) => handleChange('vendorBusinessNumber', value)}
|
||||
placeholder="사업자번호"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">사업자번호</Label>
|
||||
<BusinessNumberInput
|
||||
value={formData.vendorBusinessNumber}
|
||||
onChange={(value) => handleChange('vendorBusinessNumber', value)}
|
||||
placeholder="000-00-00000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -257,10 +257,14 @@ export function transformFrontendToApi(data: ManualEntryFormData): Record<string
|
||||
const isSales = data.division === 'sales';
|
||||
return {
|
||||
direction: isSales ? 'sales' : 'purchases',
|
||||
issue_type: 'normal',
|
||||
issue_date: data.writeDate,
|
||||
...(isSales
|
||||
? { buyer_corp_name: data.vendorName, buyer_corp_num: data.vendorBusinessNumber }
|
||||
: { supplier_corp_name: data.vendorName, supplier_corp_num: data.vendorBusinessNumber }),
|
||||
// 매출: 거래처=공급받는자(buyer), 매입: 거래처=공급자(supplier)
|
||||
// DB 컬럼이 NOT NULL이므로 빈 문자열로 전송
|
||||
supplier_corp_name: isSales ? '' : data.vendorName,
|
||||
supplier_corp_num: isSales ? '' : data.vendorBusinessNumber,
|
||||
buyer_corp_name: isSales ? data.vendorName : '',
|
||||
buyer_corp_num: isSales ? data.vendorBusinessNumber : '',
|
||||
supply_amount: data.supplyAmount,
|
||||
tax_amount: data.taxAmount,
|
||||
total_amount: data.totalAmount,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
import { Plus, Trash2, Upload } from 'lucide-react';
|
||||
@@ -194,6 +195,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
return { success: false, error: result.message || '저장에 실패했습니다.' };
|
||||
}
|
||||
|
||||
invalidateDashboard('client');
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
@@ -214,6 +216,7 @@ export function VendorDetailClient({ mode, vendorId, initialData }: VendorDetail
|
||||
return { success: false, error: result.message || '삭제에 실패했습니다.' };
|
||||
}
|
||||
|
||||
invalidateDashboard('client');
|
||||
router.refresh();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { invalidateDashboard } from '@/lib/dashboard-invalidation';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { format, startOfMonth, endOfMonth } from 'date-fns';
|
||||
import { formatNumber } from '@/lib/utils/amount';
|
||||
@@ -129,6 +130,7 @@ export function VendorManagement({ initialData, initialTotal }: VendorManagement
|
||||
deleteItem: async (id: string) => {
|
||||
const result = await deleteClient(id);
|
||||
if (result.success) {
|
||||
invalidateDashboard('client');
|
||||
toast.success('거래처가 삭제되었습니다.');
|
||||
}
|
||||
return { success: result.success, error: result.error };
|
||||
|
||||
@@ -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}`,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">{t('rememberMe')}</span>
|
||||
</label>
|
||||
<button type="button" className="text-sm text-primary hover:underline">
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-primary hover:underline"
|
||||
onClick={() => toast.info('비밀번호 초기화는 시스템 관리자에게 요청해 주세요.')}
|
||||
>
|
||||
{t('forgotPassword')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -47,12 +47,7 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
<CollapsibleDashboardCard
|
||||
icon={<ShoppingCart className="h-5 w-5 text-white" />}
|
||||
title="매입 현황"
|
||||
subtitle="당월 매입 실적"
|
||||
rightElement={
|
||||
<Badge className="bg-amber-500 text-white border-none hover:opacity-90">
|
||||
당월
|
||||
</Badge>
|
||||
}
|
||||
subtitle="매입 실적"
|
||||
>
|
||||
{/* 통계카드 3개 - 가로 배치 */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
||||
@@ -158,8 +153,8 @@ export function PurchaseStatusSection({ data }: PurchaseStatusSectionProps) {
|
||||
{/* 당월 매입 내역 (별도 카드) */}
|
||||
<CollapsibleDashboardCard
|
||||
icon={<ShoppingCart className="h-5 w-5 text-white" />}
|
||||
title="당월 매입 내역"
|
||||
subtitle="당월 매입 거래 상세"
|
||||
title="최근 매입 내역"
|
||||
subtitle="매입 거래 상세"
|
||||
bodyClassName="p-0"
|
||||
>
|
||||
<div className="p-3 bg-muted/50 border-b border-border space-y-2">
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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명 참조 추가
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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="즐겨찾기"
|
||||
>
|
||||
<Bookmark className="h-4 w-4 fill-white" />
|
||||
<Pin className="h-4 w-4 fill-white" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
|
||||
@@ -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 ? '즐겨찾기 해제' : '즐겨찾기 추가'}
|
||||
>
|
||||
<Bookmark className={`h-3.5 w-3.5 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
<Pin className={`h-3.5 w-3.5 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -224,7 +224,7 @@ function MenuItemComponent({
|
||||
}`}
|
||||
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
|
||||
>
|
||||
<Bookmark className={`h-3 w-3 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
<Pin className={`h-3 w-3 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -291,7 +291,7 @@ function MenuItemComponent({
|
||||
}`}
|
||||
title={isFav ? '즐겨찾기 해제' : '즐겨찾기 추가'}
|
||||
>
|
||||
<Bookmark className={`h-2.5 w-2.5 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
<Pin className={`h-2.5 w-2.5 ${isFav ? 'fill-yellow-500' : ''}`} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 : '저장 중 오류가 발생했습니다.';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: '작업대기',
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -93,31 +93,11 @@ function generateDebtCollectionCheckPoints(api: BadDebtApiResponse): CheckPoint[
|
||||
return checkPoints;
|
||||
}
|
||||
|
||||
// TODO: 백엔드 per-card sub_label/count 제공 시 더미값 제거
|
||||
// 채권추심 카드별 더미 서브라벨 (회사명 + 건수)
|
||||
const DEBT_COLLECTION_FALLBACK_SUB_LABELS: Record<string, { company: string; count: number }> = {
|
||||
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, string | null>): 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),
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<safety_stock (날짜무관) | /material/stock-status (날짜 필터 적용) |
|
||||
// | 세금 신고 | 가장 가까운 tax 일정 D-day | /accounting/tax-invoices |
|
||||
// | 신규 업체 | 최근 7일 이내 등록된 거래처만 | /accounting/vendors (전체 목록) |
|
||||
// | 연차 현황 | 오늘 기준 approved 휴가만 | /hr/vacation-management (전체 기간) |
|
||||
// | 발주 현황 | status=draft(임시저장)만 | /construction/order (전체 상태) |
|
||||
// | 결재 요청 | 현재 로그인 사용자의 pending 결재만 | /approval/inbox (필터 조건 다름) |
|
||||
//
|
||||
|
||||
/**
|
||||
* 현황판 subLabel 생성 헬퍼
|
||||
* API sub_label이 있으면 사용, 없으면 더미값 + 건수로 조합
|
||||
* 현황판 subLabel: 백엔드 sub_label 필드 직접 사용
|
||||
*/
|
||||
function buildStatusSubLabel(item: { id: string; count: number | string; sub_label?: string }): string | undefined {
|
||||
// API에서 sub_label 제공 시 우선 사용
|
||||
if (item.sub_label) return item.sub_label;
|
||||
|
||||
// 건수가 0이거나 문자열이면 subLabel 불필요
|
||||
const count = typeof item.count === 'number' ? item.count : parseInt(String(item.count), 10);
|
||||
if (isNaN(count) || count <= 0) return undefined;
|
||||
|
||||
const fallback = STATUS_BOARD_FALLBACK_SUB_LABELS[item.id];
|
||||
if (!fallback) return undefined;
|
||||
|
||||
// "대한철강 외" + 나머지 건수
|
||||
const remaining = count - 1;
|
||||
if (remaining > 0) {
|
||||
return `${fallback} ${remaining}건`;
|
||||
}
|
||||
// 1건이면 "외" 제거하고 이름만
|
||||
return fallback.replace(/ 외$/, '');
|
||||
function buildStatusSubLabel(item: { sub_label?: string }): string | undefined {
|
||||
return item.sub_label || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface BadDebtApiResponse {
|
||||
recovered_amount: number; // 회수완료
|
||||
bad_debt_amount: number; // 대손처리
|
||||
client_count?: number; // 거래처 수
|
||||
sub_labels?: Record<string, string | null>; // 카드별 거래처 sub_label (dc1~dc4)
|
||||
}
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -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<DomainKey, DashboardSectionKey[]> = {
|
||||
deposit: ['dailyReport', 'receivable'],
|
||||
@@ -43,6 +53,16 @@ const DOMAIN_SECTION_MAP: Record<DomainKey, DashboardSectionKey[]> = {
|
||||
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';
|
||||
|
||||
Reference in New Issue
Block a user