diff --git a/dev/dev_plans/qms-api-integration-plan.md b/dev/dev_plans/qms-api-integration-plan.md index c760f0d..4332d4d 100644 --- a/dev/dev_plans/qms-api-integration-plan.md +++ b/dev/dev_plans/qms-api-integration-plan.md @@ -1,6 +1,7 @@ # 품질인정심사(QMS) API 연동 계획 > **작성일**: 2026-03-09 +> **최종 업데이트**: 2026-03-10 (아키텍처 결정 + 3-페르소나 심층 분석 반영) > **상태**: 계획 수립 > **URL**: `/quality/qms` > **스토리보드**: 슬라이드 19~20 @@ -8,89 +9,966 @@ --- +## 0. 아키텍처 결정 사항 + +> 2026-03-10 비즈니스 로직 분석 결과 확정된 사항. 모든 개발은 아래 결정을 따를 것. + +| # | 항목 | 결정 | 근거 | +|---|------|------|------| +| 1 | **서류 모달 데이터** | **2단계 로딩** | 목록은 간략히 → 모달 열 때 `GET /qms/lot-audit/documents/{type}/{id}`로 상세 조회 | +| 2 | **InspectionReport.item** | **BOM 최상위(FG) 제품명** 자동 추출 | `Order → OrderNode → OrderItem → Item(FG)` 경로로 대표 품목명 조회. options.product_type 아님 | +| 3 | **InspectionReport.code** | **품질관리서 번호** (KD-QD-...) | 채번관리 시스템과 연동. 목업의 KD-SS 형식은 수주 코드이므로 불일치 → 프론트 수정 필요 | +| 4 | **IQC 추적 경로** | **WorkOrderMaterialInput → StockLot 기반** 실제 투입 LOT 추적 | `WorkOrder → WorkOrderMaterialInput → StockLot → lot_no → Inspection(IQC)`. StockLot.work_order_id는 **생산입고** 관계이므로 직접 사용 불가 | +| 5 | **토글 동기화** | **비관적 업데이트** | 서버 PATCH 성공 후 UI 반영. QMS 심사는 공식 기록이므로 정합성 우선 | +| 6 | **subType 결정** | **WorkOrder.options**에서 추출 | `WorkOrder.options.sub_type` 또는 공정 정보 기반. Process.name 문자열 매칭 아님 | +| 7 | **PR 없는 completed 문서** | **목록에 표시** | 분기 필터 "전체"에서 노출, 특정 분기 필터 시 제외 | + +### 개소(Location) 생성 원리 (필수 이해) + +> `QualityDocumentService::attachOrders()`의 실제 로직: + +``` +Order → OrderNode(parent_id=null, root node = 개소) + → 하위 첫 번째 OrderItem (대표 item) + → QualityDocumentLocation.order_item_id에 저장 +``` + +- **개소 = root OrderNode** (parent_id가 null인 노드) +- 각 개소의 "대표 item" = 해당 node 하위 첫 번째 OrderItem +- `UnitInspection.name` ← OrderNode 기반 코드 조합 +- `UnitInspection.location` ← `OrderItem.floor_code + " " + OrderItem.symbol_code` + +--- + ## 1. 현황 분석 ### 1.1 프론트엔드 현황 -| 항목 | 상태 | 비고 | -|------|------|------| -| `page.tsx` | ✅ 구현됨 | 14KB, 전체 페이지 레이아웃 | -| `types.ts` | ✅ 구현됨 | 95줄, 타입 정의 완료 | -| `mockData.ts` | ✅ 구현됨 | 543줄, 완전한 목업 데이터 | -| `components/` | ✅ 구현됨 | 12개 컴포넌트 + documents/ 7개 | -| `actions.ts` | ❌ 없음 | API 연동 0% | +**경로**: `react/src/app/[locale]/(protected)/quality/qms/` -프론트엔드는 UI가 완성되어 있으나 **100% 목업 데이터**로 동작 중. +``` +qms/ +├── types.ts # 95줄, 타입 정의 +├── mockData.ts # 543줄, 목업 데이터 +├── page.tsx # 14KB, 전체 페이지 +├── actions.ts # ❌ 없음 (API 연동 0%) +└── components/ + ├── Header.tsx # 825B - 페이지 헤더 + 설정 버튼 + ├── Filters.tsx # 1.9KB - 연도/분기/검색 필터 + ├── DayTabs.tsx # 5.8KB - 1일차/2일차 탭 + 진행률 + ├── AuditProgressBar.tsx # 3.4KB - 진행률 시각화 + ├── AuditSettingsPanel.tsx # 6.7KB - 표시 설정 모달 + ├── ReportList.tsx # 2.7KB - 품질관리서 목록 (2일차) + ├── RouteList.tsx # 6.1KB - 수주/개소 목록 + 토글 (2일차) + ├── DocumentList.tsx # 5.3KB - 서류 카테고리 목록 (2일차) + ├── InspectionModal.tsx # 19KB - 서류 뷰어 모달 + ├── Day1ChecklistPanel.tsx # 8.3KB - 점검표 + 토글 (1일차) + ├── Day1DocumentSection.tsx # 4.9KB - 기준 문서 패널 (1일차) + ├── Day1DocumentViewer.tsx # 7.3KB - 문서 미리보기 (1일차) + └── documents/ # 검사 문서 템플릿 + ├── index.ts # 배럴 export + ├── ImportInspectionDocument.tsx # 38KB - 수입검사 성적서 + ├── ProductInspectionDocument.tsx # 9.5KB - 제품검사 성적서 + ├── ScreenInspectionDocument.tsx # 14KB - 스크린 중간검사 + ├── BendingInspectionDocument.tsx # 16KB - 절곡 중간검사 + ├── SlatInspectionDocument.tsx # 13KB - 슬랫 중간검사 + ├── JointbarInspectionDocument.tsx # 17KB - 조인트바 중간검사 + └── QualityDocumentUploader.tsx # 9.6KB - QMS PDF 업로더 +``` -### 1.2 백엔드 현황 +### 1.2 프론트엔드 타입 정의 (`types.ts` 전체) + +```typescript +// ===== Day 2: 로트 추적 심사 ===== + +export interface InspectionReport { + id: string; + code: string; // "KD-QD-202603-0001" (품질관리서 번호, 채번관리 연동) + siteName: string; // "강남 아파트 단지" + item: string; // "실리카 스크린" (BOM 최상위 FG 제품명 자동 추출) + routeCount: number; + totalRoutes: number; + quarter: string; // "2025년 3분기" + year: number; + quarterNum: number; // 1, 2, 3, 4 +} + +export interface RouteItem { + id: string; + code: string; // "KD-SS-240924-19" + date: string; // "2024-09-24" + site: string; // "강남 아파트 A동" + locationCount: number; + subItems: UnitInspection[]; +} + +export interface UnitInspection { + id: string; + name: string; // "KD-SS-240924-19-01" + location: string; // "101동 501호" + isCompleted: boolean; +} + +export interface Document { + id: string; + type: 'import' | 'order' | 'log' | 'report' | 'confirmation' | 'shipping' | 'product' | 'quality'; + title: string; // "수입검사 성적서" + date?: string; + count: number; + items?: DocumentItem[]; +} + +export interface DocumentItem { + id: string; + title: string; + date: string; + code?: string; + subType?: 'screen' | 'bending' | 'slat' | 'jointbar'; +} + +// ===== Day 1: 기준/매뉴얼 심사 ===== + +export interface ChecklistSubItem { + id: string; + name: string; + isCompleted: boolean; +} + +export interface ChecklistCategory { + id: string; + title: string; // "원재료 품질관리 기준" + subItems: ChecklistSubItem[]; +} + +export interface StandardDocument { + id: string; + title: string; + version: string; + date: string; + fileName?: string; + fileUrl?: string; +} + +export interface Day1CheckItem { + id: string; + categoryId: string; + subItemId: string; + title: string; + description: string; + buttonLabel: string; // "기준/매뉴얼 확인" + standardDocuments: StandardDocument[]; +} + +export interface Day1Progress { completed: number; total: number; } +export interface Day2Progress { completed: number; total: number; } +``` + +### 1.3 프론트엔드 상태 관리 (`page.tsx` 핵심) + +```typescript +// ===== State ===== +// 탭/설정 +const [activeDay, setActiveDay] = useState<1 | 2>(1); +const [settingsOpen, setSettingsOpen] = useState(false); +const [displaySettings, setDisplaySettings] = useState(DEFAULT_SETTINGS); +// AuditDisplaySettings: { showProgressBar, showDocumentViewer, showDocumentSection, showCompletedItems, expandAllCategories } + +// Day 1 +const [day1Categories, setDay1Categories] = useState(MOCK_DAY1_CATEGORIES); +const [selectedSubItemId, setSelectedSubItemId] = useState(null); +const [selectedCategoryId, setSelectedCategoryId] = useState(null); +const [selectedStandardDocId, setSelectedStandardDocId] = useState(null); + +// Day 2 +const [selectedYear, setSelectedYear] = useState(2025); +const [selectedQuarter, setSelectedQuarter] = useState<'Q1'|'Q2'|'Q3'|'Q4'|'전체'>('전체'); +const [searchTerm, setSearchTerm] = useState(''); +const [selectedReport, setSelectedReport] = useState(null); +const [selectedRoute, setSelectedRoute] = useState(null); +const [routesData, setRoutesData] = useState>(MOCK_ROUTES_INITIAL); + +// 모달 +const [modalOpen, setModalOpen] = useState(false); +const [selectedDoc, setSelectedDoc] = useState(null); +const [selectedDocItem, setSelectedDocItem] = useState(null); + +// ===== 계산값 (useMemo) ===== +// day1Progress: 카테고리별 subItems.isCompleted 합산 +// day2Progress: routesData 전체 subItems.isCompleted 합산 +// filteredReports: year + quarter + searchTerm 필터링 (MOCK_REPORTS 기준) +// currentRoutes: selectedReport → routesData[selectedReport.id] +// currentDocuments: selectedRoute → MOCK_DOCUMENTS[selectedRoute.id] +// selectedCheckItem: selectedSubItemId → MOCK_DAY1_CHECK_ITEMS 매칭 +// selectedStandardDoc: selectedStandardDocId → MOCK_DAY1_STANDARD_DOCUMENTS 매칭 + +// ===== 핸들러 ===== +// Day 1: handleSubItemSelect, handleSubItemToggle, handleConfirmComplete +// Day 2: handleReportSelect, handleRouteSelect, handleViewDocument +// handleYearChange, handleQuarterChange, handleSearchChange, handleToggleItem +``` + +**렌더링 구조**: +``` +
+ + (진행률 표시) + (연도/분기/검색) + +activeDay === 1: + grid-cols-12: | | + +activeDay === 2: + grid-cols-12: | | + + (모달) + (서류 뷰어 모달) +``` + +### 1.4 mockData 데이터 계층 + +``` +MOCK_REPORTS (3건) — InspectionReport[] + └─ MOCK_ROUTES_INITIAL[reportId] — RouteItem[] + └─ subItems — UnitInspection[] (개소별 isCompleted 토글) + └─ MOCK_DOCUMENTS[routeId] — Document[] (8종 서류) + └─ items — DocumentItem[] + +MOCK_DAY1_CATEGORIES (6개 카테고리) — ChecklistCategory[] + └─ subItems — ChecklistSubItem[] (2~3개씩) + └─ MOCK_DAY1_CHECK_ITEMS — Day1CheckItem[] (16개 항목) + └─ MOCK_DAY1_STANDARD_DOCUMENTS[subItemId] — StandardDocument[] +``` + +### 1.5 백엔드 현황 | 영역 | 기존 API | 신규 필요 | |------|----------|-----------| | **1일차 (기준/매뉴얼 심사)** | ❌ 없음 | 모델, 마이그레이션, 서비스, 컨트롤러 전체 | -| **2일차 (로트 추적 심사)** | ⚠️ 부분 존재 | 기존 API 조합 + 서류 연결 API 신규 | +| **2일차 (로트 추적 심사)** | ⚠️ 부분 존재 | 기존 API 래핑 + 서류 조합 API 신규 | -**기존 활용 가능 API:** -- `GET /quality/documents` — 품질관리서 목록 (2일차 1단계) -- `GET /quality/documents/{id}` — 품질관리서 상세 + 수주/개소 (2일차 2단계) -- `GET /quality/performance-reports` — 실적신고 (분기 필터 활용) -- `GET /inspections` — 수입검사/중간검사 성적서 -- 출하/출고/납품 관련 기존 API +**기존 활용 가능 API** (라우트: `api/routes/api/v1/quality.php`): + +| 엔드포인트 | 컨트롤러 | 서비스 메서드 | 용도 | +|-----------|---------|-------------|------| +| `GET /quality/documents` | QualityDocumentController::index | QualityDocumentService::index() | 품질관리서 목록 (2일차 1단계) | +| `GET /quality/documents/{id}` | QualityDocumentController::show | QualityDocumentService::show() | 상세 + 수주/개소 (2일차 2단계) | +| `GET /quality/performance-reports` | PerformanceReportController::index | PerformanceReportService::index() | 실적신고 (분기 필터) | +| `GET /inspections` | InspectionController::index | InspectionService::index() | 수입검사/중간검사 성적서 | + +### 1.6 기존 모델/서비스 위치 + +| 구분 | 파일 경로 | 핵심 | +|------|----------|------| +| **모델** | | | +| QualityDocument | `api/app/Models/Qualitys/QualityDocument.php` | 상태: received→in_progress→completed | +| QualityDocumentOrder | `api/app/Models/Qualitys/QualityDocumentOrder.php` | FK: quality_document_id, order_id | +| QualityDocumentLocation | `api/app/Models/Qualitys/QualityDocumentLocation.php` | 상태: pending→in_progress→completed | +| PerformanceReport | `api/app/Models/Qualitys/PerformanceReport.php` | 상태: unconfirmed→confirmed→reported | +| Inspection | `api/app/Models/Qualitys/Inspection.php` | 타입: IQC/PQC/FQC, 상태: waiting→in_progress→completed | +| **서비스** | | | +| QualityDocumentService | `api/app/Services/QualityDocumentService.php` | 52KB, 18개 메서드 | +| InspectionService | `api/app/Services/InspectionService.php` | index, stats, show, store, update, complete | +| PerformanceReportService | `api/app/Services/PerformanceReportService.php` | index, stats, confirm, missing | +| **컨트롤러** | | | +| QualityDocumentController | `api/app/Http/Controllers/Api/V1/QualityDocumentController.php` | 14개 메서드 | +| InspectionController | `api/app/Http/Controllers/Api/V1/InspectionController.php` | 8개 메서드 | +| PerformanceReportController | `api/app/Http/Controllers/Api/V1/PerformanceReportController.php` | 6개 메서드 | + +### 1.7 기존 DB 테이블 구조 (QMS 관련) + +#### `quality_documents` + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | bigint PK | | +| tenant_id | bigint FK | multi-tenancy | +| quality_doc_number | varchar(30) | 채번: KD-QD-YYYYMM-0001 | +| site_name | varchar(255) | 현장명 | +| status | varchar(20) | received/in_progress/completed | +| client_id | bigint FK | 수주처 | +| inspector_id | bigint FK | 검사자 | +| received_date | date | 접수일 | +| **options** | **json** | {construction_site, material_distributor, contractor, supervisor} | +| created_by, updated_by, deleted_by | bigint | 감사 | +| timestamps, softDeletes | | | + +**인덱스**: `(tenant_id, quality_doc_number)` UNIQUE, `(tenant_id, status)`, `(tenant_id, client_id)`, `(tenant_id, received_date)` + +#### `quality_document_orders` + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | bigint PK | | +| quality_document_id | bigint FK | CASCADE DELETE | +| order_id | bigint FK | | +| timestamps | | | + +**인덱스**: `(quality_document_id, order_id)` UNIQUE + +#### `quality_document_locations` + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | bigint PK | | +| quality_document_id | bigint FK | CASCADE DELETE | +| quality_document_order_id | bigint FK | CASCADE DELETE | +| order_item_id | bigint FK | 대표 품목 | +| post_width, post_height | int | 시공후 규격 | +| change_reason | varchar(255) | 규격 변경사유 | +| **inspection_data** | **json** | 검사 데이터 (15개 항목 + 사진) | +| document_id | bigint FK | 검사성적서 | +| inspection_status | varchar(20) | pending/in_progress/completed | +| timestamps | | | + +**인덱스**: `(quality_document_id, inspection_status)` + +> **주의**: `quality_document_locations`에는 현재 `options` 컬럼이 **없음**. QMS 로트 심사 확인 데이터 저장을 위해 `options` JSON 컬럼 추가 필요. + +#### `performance_reports` + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | bigint PK | | +| tenant_id | bigint FK | | +| quality_document_id | bigint FK | 1:1 관계 | +| year | smallint | 연도 | +| quarter | tinyint | 분기 (1-4) | +| confirmation_status | varchar(20) | unconfirmed/confirmed/reported | +| confirmed_date | date | 확정일 | +| confirmed_by | bigint FK | 확정자 | +| memo | text | 특이사항 | +| created_by, updated_by, deleted_by | bigint | | +| timestamps, softDeletes | | | + +**인덱스**: `(tenant_id, quality_document_id)` UNIQUE, `(tenant_id, year, quarter)`, `(tenant_id, confirmation_status)` + +#### `inspections` + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| id | bigint PK | | +| tenant_id | bigint FK | | +| inspection_no | varchar(30) | 채번: IQC-YYYYMMDD-0001 | +| inspection_type | enum | IQC/PQC/FQC | +| status | enum | waiting/in_progress/completed | +| result | enum | pass/fail (nullable) | +| request_date | date | 요청일 | +| inspection_date | date | 검사일 (nullable) | +| item_id | bigint FK | 품목 | +| lot_no | varchar(50) | LOT 번호 | +| work_order_id | bigint FK | 작업지시 (PQC/FQC용) | +| inspector_id | bigint FK | 검사자 | +| **meta** | **json** | {process_name, quantity, unit} | +| **items** | **json** | 검사항목 배열 [{id, name, type, spec, result, judgment}] | +| **attachments** | **json** | 첨부파일 | +| **extra** | **json** | {remarks, opinion} | +| created_by, updated_by | bigint | | +| timestamps, softDeletes | | | + +**인덱스**: `(tenant_id, inspection_no)` UNIQUE, `(tenant_id, inspection_type, status)` 복합 + +### 1.8 모델 관계 맵 + +``` +QualityDocument (1) +├── hasMany → QualityDocumentOrder (M) +│ ├── belongsTo → Order +│ │ ├── hasMany → OrderNode (root: parent_id=null = 개소) +│ │ │ └── hasMany → OrderItem (대표 item = 첫 번째) +│ │ ├── hasMany → WorkOrder (via sales_order_id) +│ │ │ ├── hasMany → Inspection (PQC/FQC via work_order_id) +│ │ │ ├── hasMany → WorkOrderMaterialInput (자재 투입) +│ │ │ │ └── belongsTo → StockLot (투입 LOT) +│ │ │ │ └── lot_no → Inspection (IQC 연결 키) +│ │ │ └── hasMany → StockLot (⚠️ 생산입고 관계 — IQC 추적에 사용 금지) +│ │ └── hasMany → Shipment (via order_id) +│ └── hasMany → QualityDocumentLocation (M) +│ ├── belongsTo → OrderItem (대표 item, floor_code/symbol_code 접근) +│ └── belongsTo → Document (검사성적서) +├── hasMany → QualityDocumentLocation (M) +├── belongsTo → Client +├── belongsTo → User (inspector) +└── hasOne → PerformanceReport + └── belongsTo → User (confirmer) + +Inspection (독립) +├── belongsTo → WorkOrder +├── belongsTo → Item +└── belongsTo → User (inspector) +``` + +> **IQC 추적 경로 (WorkOrderMaterialInput → StockLot 기반)**: +> `Order → WorkOrder → WorkOrderMaterialInput → StockLot(실제 투입 LOT) → lot_no → Inspection(IQC)` +> ⚠️ `StockLot.work_order_id`는 **생산입고** 관계(이 LOT이 이 작업지시에서 생산됨)이므로 직접 사용 불가 --- -## 2. 작업 범위 +## 2. 구현 패턴 가이드 + +> 새 코드 작성 시 반드시 아래 패턴을 따를 것. + +### 2.1 Controller 패턴 + +```php +// api/app/Http/Controllers/Api/V1/QmsLotAuditController.php +use App\Helpers\ApiResponse; + +class QmsLotAuditController extends Controller +{ + public function __construct(private QmsLotAuditService $service) {} + + public function index(Request $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->index($request->all()); + }, __('message.fetched')); + } + + public function confirm(QmsLotAuditConfirmRequest $request, int $id) + { + return ApiResponse::handle(function () use ($request, $id) { + return $this->service->confirm($id, $request->validated()); + }, __('message.updated')); + } +} +``` + +### 2.2 Service 패턴 + +```php +// api/app/Services/QmsLotAuditService.php +use App\Services\Service; + +class QmsLotAuditService extends Service +{ + public function index(array $params): array + { + $tenantId = $this->tenantId(); + // BelongsToTenant 스코프 자동 적용됨 — where tenant_id 하드코딩 금지 + // N+1 방지: getFgProductName()에서 사용하는 관계까지 eager load + $query = QualityDocument::with([ + 'documentOrders.order.nodes' => fn($q) => $q->whereNull('parent_id'), + 'documentOrders.order.nodes.items.item', + 'locations', + 'performanceReport', + ]) + ->where('status', QualityDocument::STATUS_COMPLETED); + + // 필터링 + if (!empty($params['year'])) { ... } + if (!empty($params['quarter'])) { ... } + if (!empty($params['q'])) { ... } + + $items = $query->paginate($params['limit'] ?? 20); + return $this->transformPaginated($items); + } +} +``` + +### 2.3 FormRequest 패턴 + +```php +// api/app/Http/Requests/Qms/QmsLotAuditConfirmRequest.php +class QmsLotAuditConfirmRequest extends FormRequest +{ + public function authorize(): bool { return true; } + + public function rules(): array + { + return [ + 'confirmed' => 'required|boolean', + ]; + } + + public function messages(): array + { + return [ + 'confirmed.required' => __('validation.required', ['attribute' => '확인 상태']), + ]; + } +} +``` + +### 2.4 Model 패턴 (신규 모델 생성 시) + +```php +// api/app/Models/Qualitys/AuditChecklist.php +class AuditChecklist extends Model +{ + use Auditable, BelongsToTenant, SoftDeletes; + + const STATUS_DRAFT = 'draft'; + const STATUS_IN_PROGRESS = 'in_progress'; + const STATUS_COMPLETED = 'completed'; + + protected $fillable = ['tenant_id', 'year', 'quarter', 'type', 'status', 'options', 'created_by', 'updated_by', 'deleted_by']; + protected $casts = ['options' => 'array']; + + public function categories() { return $this->hasMany(AuditChecklistCategory::class, 'checklist_id'); } + public function isDraft(): bool { return $this->status === self::STATUS_DRAFT; } +} +``` + +### 2.5 API 응답 형식 + +```json +// 성공 (목록) +{ + "success": true, + "message": "조회되었습니다", + "data": { + "items": [...], + "pagination": { + "current_page": 1, + "last_page": 5, + "per_page": 20, + "total": 100 + } + } +} + +// 성공 (단건) +{ + "success": true, + "message": "조회되었습니다", + "data": { ... } +} + +// 에러 +{ + "success": false, + "message": "확정 실패: 필수정보 미완료", + "data": null +} +``` + +### 2.6 라우트 등록 + +```php +// api/routes/api/v1/quality.php 에 추가 +Route::prefix('qms')->group(function () { + // 2일차: 로트 추적 심사 + Route::prefix('lot-audit')->group(function () { + Route::get('reports', [QmsLotAuditController::class, 'index']); + Route::get('reports/{id}', [QmsLotAuditController::class, 'show']); + Route::get('routes/{id}/documents', [QmsLotAuditController::class, 'routeDocuments']); + Route::get('documents/{type}/{id}', [QmsLotAuditController::class, 'documentDetail']); // 2단계 로딩 + Route::patch('units/{id}/confirm', [QmsLotAuditController::class, 'confirm']); + }); + + // 1일차: 기준/매뉴얼 심사 + Route::resource('checklists', AuditChecklistController::class)->except(['edit', 'create']); + Route::patch('checklists/{id}/complete', [AuditChecklistController::class, 'complete']); + Route::patch('checklist-items/{id}/toggle', [AuditChecklistController::class, 'toggleItem']); + Route::get('checklist-items/{id}/documents', [AuditChecklistController::class, 'itemDocuments']); + Route::post('checklist-items/{id}/documents', [AuditChecklistController::class, 'attachDocument']); + Route::delete('checklist-items/{id}/documents/{docId}', [AuditChecklistController::class, 'detachDocument']); +}); +``` + +--- + +## 3. 작업 범위 ### Phase 1: 2일차 (로트 추적 심사) API 연동 > **우선순위 높음** — 기존 API 활용 가능하여 빠르게 연동 가능 -#### 2.1 Frontend — `actions.ts` 생성 +#### 3.1 Backend — 신규 파일 + +| 파일 | 역할 | +|------|------| +| `api/app/Services/QmsLotAuditService.php` | 로트 심사 전용 서비스 (index, show, routeDocuments, documentDetail, confirm, getFgProductName) | +| `api/app/Http/Controllers/Api/V1/QmsLotAuditController.php` | 컨트롤러 (6개 메서드) | +| `api/app/Http/Requests/Qms/QmsLotAuditConfirmRequest.php` | 확인 완료 검증 | +| `api/app/Http/Requests/Qms/QmsLotAuditIndexRequest.php` | 목록 조회 파라미터 검증 (year: integer, quarter: 1-4, q: string) | +| `api/app/Http/Requests/Qms/QmsLotAuditDocumentDetailRequest.php` | 서류 상세 조회 type enum 검증 | +| `api/database/migrations/XXXX_add_options_to_quality_document_locations.php` | options 컬럼 추가 + Model casts 수정 | + +#### 3.2 Backend — API 엔드포인트 ``` -react/src/app/[locale]/(protected)/quality/qms/actions.ts +GET /api/v1/qms/lot-audit/reports + params: year, quarter, q(검색어) + response: InspectionReport[] 형태로 변환 + 내부: QualityDocument(completed) + PerformanceReport(year/quarter) 조합 + 참고: PR 없는 completed 문서도 포함 (분기="전체"에서 노출, 특정 분기 필터 시 제외) + +GET /api/v1/qms/lot-audit/reports/{id} + response: RouteItem[] 형태 (수주코드 + 개소 + 완료상태) + 내부: QualityDocumentOrder → Order + QualityDocumentLocation 조합 + 참고: 개소 = root OrderNode 기반, 대표 OrderItem에서 floor_code/symbol_code 접근 + +GET /api/v1/qms/lot-audit/routes/{id}/documents + response: Document[] 형태 (8종 서류 간략 목록만 — count + items 기본 정보) + 내부: 아래 8종 서류 조합 로직 + +GET /api/v1/qms/lot-audit/documents/{type}/{id} ← 🆕 2단계 로딩용 + params: type = import|order|log|report|confirmation|shipping|product|quality + response: 타입별 상세 데이터 (모달 렌더링용) + 내부: 타입에 따라 Inspection/WorkOrder/Shipment/QualityDocument 상세 조회 + +PATCH /api/v1/qms/lot-audit/units/{id}/confirm + body: { confirmed: boolean } + response: 업데이트된 UnitInspection + 내부: quality_document_locations.options.lot_audit_confirmed 업데이트 + 참고: 비관적 업데이트 — 서버 성공 후 프론트 UI 반영 + ⚠️ 원자성: DB::transaction + lockForUpdate() 또는 DB::raw("JSON_SET(options, '$.lot_audit_confirmed', ...)") 사용하여 동시 수정 시 JSON 병합 충돌 방지 ``` -| 액션 | 호출 API | 설명 | -|------|----------|------| -| `getQualityReports()` | `GET /quality/documents` | 품질관리서 목록 (분기 필터) | -| `getReportRoutes(reportId)` | `GET /quality/documents/{id}` | 수주코드 + 개소 목록 | -| `getRouteDocuments(routeId)` | 복합 조회 (아래 참조) | 개소별 관련 서류 8종 | -| `confirmUnitInspection(unitId)` | `PATCH /qms/lot-audit/confirm` | 개소 확인 완료 처리 | +#### 3.3 8종 서류 조합 조회 로직 (핵심 복잡도) -#### 2.2 관련 서류 조회 로직 +`QmsLotAuditService::getRouteDocuments(int $qualityDocumentOrderId)` 메서드 구현: -2일차 3단계 "관련 서류"는 개소(Location)에 연결된 8종 서류를 조합 조회: +| # | 서류 타입 | Document.type | 데이터 소스 | 조회 경로 | +|---|-----------|---------------|-------------|-----------| +| 1 | 수입검사 성적서 | `import` | `inspections` (type=IQC) | Order → WorkOrder → WorkOrderMaterialInput → StockLot(투입 LOT) → lot_no → Inspection(IQC) | +| 2 | 수주서 | `order` | `orders` | QualityDocumentOrder.order_id → orders | +| 3 | 작업일지 | `log` | `work_orders` | 수주 → work_orders → 작업일지 데이터, subType은 WorkOrder.options에서 추출 | +| 4 | 중간검사 성적서 | `report` | `inspections` (type=PQC) | work_orders → inspections(PQC, work_order_id), subType은 WorkOrder.options에서 추출 | +| 5 | 납품확인서 | `confirmation` | `shipments` | 수주 → 출하 → 납품확인서 | +| 6 | 출고증 | `shipping` | `shipments` | 수주 → 출하 → 출고증 | +| 7 | 제품검사 성적서 | `product` | `quality_document_locations` | 개소 → document_id (검사성적서) | +| 8 | 품질관리서 | `quality` | `quality_documents` | 해당 품질관리서 원본 | -| 서류 타입 | 데이터 소스 | 조회 방식 | -|-----------|-------------|-----------| -| 수입검사 성적서 | `inspections` (type=IQC) | 수주의 BOM 원자재 LOT 추적 | -| 수주서 | `orders` | 수주코드로 직접 조회 | -| 작업일지 | `work_orders` + 작업일지 | 수주 → 작업지시 → 작업일지 | -| 중간검사 성적서 | `inspections` (type=PQC) | 작업지시별 중간검사 | -| 납품확인서 | `shipments` | 출하 → 납품확인서 | -| 출고증 | `shipments` | 출하 → 출고증 | -| 제품검사 성적서 | `quality_document_locations` | 개소별 검사 문서 (EAV) | -| 품질관리서 | `quality_documents` | 품질관리서 원본 | +**서류 조합 의사 코드**: -#### 2.3 Backend — 신규 API (최소) +```php +public function getRouteDocuments(int $orderId): array +{ + $docOrder = QualityDocumentOrder::with(['order.workOrders', 'locations', 'qualityDocument'])->findOrFail($orderId); + $order = $docOrder->order; + $qualityDoc = $docOrder->qualityDocument; -``` -GET /api/v1/qms/lot-audit/reports — 분기별 품질관리서 목록 (전용 뷰) -GET /api/v1/qms/lot-audit/reports/{id} — 수주코드 + 개소 + 완료 상태 -GET /api/v1/qms/lot-audit/routes/{id}/documents — 개소별 8종 서류 조합 조회 -PATCH /api/v1/qms/lot-audit/units/{id}/confirm — 확인 완료 처리 + $documents = []; + $workOrders = $order->workOrders; + + // 1. 수입검사 성적서: WorkOrderMaterialInput → StockLot 기반 실제 투입 LOT → IQC 추적 + // ⚠️ StockLot.work_order_id는 "생산입고" 관계이므로 직접 사용 불가 + // 올바른 경로: WorkOrder → WorkOrderMaterialInput → StockLot → lot_no → Inspection(IQC) + $investedLotIds = WorkOrderMaterialInput::whereIn('work_order_id', $workOrders->pluck('id')) + ->pluck('stock_lot_id') + ->unique(); + $investedLotNos = StockLot::whereIn('id', $investedLotIds) + ->whereNotNull('lot_no') // null lot_no 방어 + ->pluck('lot_no') + ->unique(); + $iqcInspections = Inspection::where('inspection_type', 'IQC') + ->whereIn('lot_no', $investedLotNos) + ->where('status', 'completed') + ->get(); + $documents[] = $this->formatDocument('import', '수입검사 성적서', $iqcInspections); + + // 2. 수주서 + $documents[] = $this->formatDocument('order', '수주서', collect([$order])); + + // 3. 작업일지 (subType: WorkOrder.options에서 추출) + $documents[] = $this->formatDocumentWithSubType('log', '작업일지', $workOrders); + + // 4. 중간검사 성적서 (PQC, subType: WorkOrder.options에서 추출) + $pqcInspections = Inspection::where('inspection_type', 'PQC') + ->whereIn('work_order_id', $workOrders->pluck('id')) + ->get(); + $documents[] = $this->formatDocumentWithSubType('report', '중간검사 성적서', $pqcInspections); + + // 5. 납품확인서 + // Shipment 조회: Order → hasMany → Shipment (FK: order_id) + $shipments = $order->shipments()->get(); + $documents[] = $this->formatDocument('confirmation', '납품확인서', $shipments); + + // 6. 출고증 + $documents[] = $this->formatDocument('shipping', '출고증', $shipments); + + // 7. 제품검사 성적서 + $locations = $docOrder->locations->filter(fn($loc) => $loc->document_id); + $documents[] = $this->formatDocument('product', '제품검사 성적서', $locations); + + // 8. 품질관리서 + $documents[] = $this->formatDocument('quality', '품질관리서', collect([$qualityDoc])); + + return $documents; +} ``` -> 기존 `quality/documents` API를 래핑하여 QMS 전용 응답 형태로 가공하는 방식 권장. -> 8종 서류 조합 로직이 복잡하므로 **전용 서비스 메서드** 필요. - -#### 2.4 DB 변경 +#### 3.4 DB 변경 | 변경 | 테이블 | 설명 | |------|--------|------| -| 컬럼 추가 | `quality_document_locations` | `options` JSON에 `lot_audit_confirmed`, `lot_audit_confirmed_at` 추가 | +| 컬럼 추가 | `quality_document_locations` | `options` JSON 컬럼 추가 | -> 별도 테이블 없이 기존 개소(Location) 테이블의 `options` 활용 (컬럼 추가 정책 준수) +`options` JSON 활용: +```json +{ + "lot_audit_confirmed": true, + "lot_audit_confirmed_at": "2026-03-10T09:00:00", + "lot_audit_confirmed_by": 15 +} +``` + +#### 3.5 Frontend — `actions.ts` 생성 + +> ⚠️ **프로젝트 표준 준수 필수**: `executeServerAction` + `buildApiUrl` + `ActionResult` 패턴. +> 레퍼런스: `react/src/app/[locale]/(protected)/quality/inspection-management/actions.ts` + +```typescript +// react/src/app/[locale]/(protected)/quality/qms/actions.ts +'use server'; + +import { executeServerAction } from '@/lib/api/execute-server-action'; +import { buildApiUrl } from '@/lib/api/query-params'; +import type { ActionResult } from '@/lib/api/types'; +import type { InspectionReport, RouteItem, Document, UnitInspection } from './types'; + +// ===== API 원본 타입 (snake_case) ===== +// ⚠️ 'use server' 파일에서 타입 re-export 금지 (Turbopack 제한) +// API 타입은 이 파일 내부에서만 사용 + +interface QualityReportApi { + id: number; + code: string; // quality_doc_number + site_name: string; + item: string; // FG 제품명 (서버에서 변환 완료) + route_count: number; + total_routes: number; + quarter: string; + year: number; + quarter_num: number; +} + +interface RouteItemApi { + id: number; + code: string; // order_no + date: string; // received_at + site: string; + location_count: number; + sub_items: { + id: number; + name: string; + location: string; + is_completed: boolean; + }[]; +} + +interface DocumentApi { + id: number; + type: string; + title: string; + date?: string; + count: number; + items?: { + id: number; + title: string; + date: string; + code?: string; + sub_type?: string; + }[]; +} + +// ===== 8종 서류 상세 Discriminated Union ===== +type DocumentDetailApi = + | { type: 'import'; data: ImportInspectionDetailApi } + | { type: 'order'; data: OrderDetailApi } + | { type: 'log'; data: WorkOrderLogDetailApi } + | { type: 'report'; data: PqcInspectionDetailApi } + | { type: 'confirmation'; data: ShipmentDetailApi } + | { type: 'shipping'; data: ShipmentDetailApi } + | { type: 'product'; data: ProductInspectionDetailApi } + | { type: 'quality'; data: QualityDocDetailApi }; +// TODO: 각 DetailApi 인터페이스는 백엔드 구현 시 확정 후 정의 + +// ===== Transform 함수 (snake_case → camelCase) ===== + +function transformReportApi(api: QualityReportApi): InspectionReport { + return { + id: String(api.id), + code: api.code, + siteName: api.site_name, + item: api.item, + routeCount: api.route_count, + totalRoutes: api.total_routes, + quarter: api.quarter, + year: api.year, + quarterNum: api.quarter_num, + }; +} + +function transformRouteApi(api: RouteItemApi): RouteItem { + return { + id: String(api.id), + code: api.code, + date: api.date, + site: api.site, + locationCount: api.location_count, + subItems: api.sub_items.map(s => ({ + id: String(s.id), + name: s.name, + location: s.location, + isCompleted: s.is_completed, + })), + }; +} + +function transformDocumentApi(api: DocumentApi): Document { + return { + id: String(api.id), + type: api.type as Document['type'], + title: api.title, + date: api.date, + count: api.count, + items: api.items?.map(i => ({ + id: String(i.id), + title: i.title, + date: i.date, + code: i.code, + subType: i.sub_type as 'screen' | 'bending' | 'slat' | 'jointbar' | undefined, + })), + }; +} + +// ===== USE_MOCK 패턴 (기존 프로젝트 패턴 준수) ===== +const USE_MOCK_FALLBACK = false; // 프로젝트 표준: process.env가 아닌 상수 플래그 + +// ===== 2일차: 로트 추적 심사 ===== + +export async function getQualityReports(params: { + year: number; + quarter?: number; + q?: string; +}): Promise> { + if (USE_MOCK_FALLBACK) { + const { MOCK_REPORTS } = await import('./mockData'); + return { success: true, data: MOCK_REPORTS.filter(r => r.year === params.year) }; + } + return executeServerAction({ + url: buildApiUrl('/api/v1/qms/lot-audit/reports', { + year: params.year, + quarter: params.quarter, + q: params.q, + }), + transform: (data) => data.items.map(transformReportApi), + errorMessage: '품질관리서 목록 조회에 실패했습니다.', + }); +} + +export async function getReportRoutes(reportId: string): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/qms/lot-audit/reports/${reportId}`), + transform: (data) => data.map(transformRouteApi), + errorMessage: '수주/개소 목록 조회에 실패했습니다.', + }); +} + +export async function getRouteDocuments(routeId: string): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/qms/lot-audit/routes/${routeId}/documents`), + transform: (data) => data.map(transformDocumentApi), + errorMessage: '서류 목록 조회에 실패했습니다.', + }); +} + +export async function confirmUnitInspection( + unitId: string, + confirmed: boolean +): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/qms/lot-audit/units/${unitId}/confirm`), + method: 'PATCH', + body: { confirmed }, + transform: (data) => ({ + id: String(data.id), + name: data.name, + location: data.location, + isCompleted: data.is_completed, + }), + errorMessage: '확인 상태 변경에 실패했습니다.', + }); +} + +// 2단계 로딩: 서류 모달에서 상세 데이터 조회 +export async function getDocumentDetail( + type: Document['type'], + id: string +): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/qms/lot-audit/documents/${type}/${id}`), + // transform은 type별로 다르므로 호출 측에서 처리 + errorMessage: '서류 상세 조회에 실패했습니다.', + }); +} +``` + +#### 3.6 API 응답 매핑 (Backend → Frontend) + +**`GET /qms/lot-audit/reports` 응답 변환**: + +```php +// QmsLotAuditService::transformReportToFrontend() +private function transformReportToFrontend(QualityDocument $doc): array +{ + $performanceReport = $doc->performanceReport; + $confirmedCount = $doc->locations->filter(function ($loc) { + return data_get($loc->options, 'lot_audit_confirmed', false); + })->count(); + + return [ + 'id' => (string) $doc->id, + 'code' => $doc->quality_doc_number, // → 품질관리서 번호 (KD-QD-YYYYMM-NNNN) + 'siteName' => $doc->site_name, + 'item' => $this->getFgProductName($doc), // → BOM 최상위(FG) 제품명 자동 추출 + 'routeCount' => $confirmedCount, // → 확인 완료된 개소 수 + 'totalRoutes' => $doc->locations->count(), + 'quarter' => $performanceReport + ? $performanceReport->year . '년 ' . $performanceReport->quarter . '분기' + : '', // → PR 없으면 빈 문자열 + 'year' => $performanceReport?->year ?? now()->year, + 'quarterNum' => $performanceReport?->quarter ?? 0, // → 0이면 분기 미지정 (PR 없음) + ]; +} + +/** + * BOM 최상위(FG) 제품명 추출 + * Order → root OrderNode → 대표 OrderItem → Item(FG).name + */ +private function getFgProductName(QualityDocument $doc): string +{ + $firstDocOrder = $doc->documentOrders->first(); + if (!$firstDocOrder) return ''; + + $order = $firstDocOrder->order; + // ⚠️ Order 모델의 실제 관계명은 nodes() (orderNodes 아님) + // eager load 시 whereNull('parent_id') 조건 포함하므로 추가 쿼리 없음 + $rootNode = $order->nodes->first(); // eager loaded with parent_id=null filter + if (!$rootNode) return ''; + + // eager loaded: nodes.items.item + $representativeItem = $rootNode->items->first(); + + return $representativeItem?->item?->name ?? ''; +} +``` + +**`GET /qms/lot-audit/reports/{id}` 응답 변환**: + +```php +// QmsLotAuditService::transformRouteToFrontend() +private function transformRouteToFrontend(QualityDocumentOrder $docOrder): array +{ + $qualityDoc = $docOrder->qualityDocument; // eager loaded — $doc 변수 아님에 주의 + + return [ + 'id' => (string) $docOrder->id, + 'code' => $docOrder->order->order_no, // ⚠️ 실제 필드명: order_no (order_code 아님) + 'date' => $docOrder->order->received_at, // ⚠️ 실제 필드명: received_at (order_date 아님) + 'site' => $docOrder->order->site_name ?? '', // → RouteItem.site + 'locationCount' => $docOrder->locations->count(), // → RouteItem.locationCount + 'subItems' => $docOrder->locations->map(fn($loc, $idx) => [ + 'id' => (string) $loc->id, + 'name' => $qualityDoc->quality_doc_number . '-' . str_pad($idx + 1, 2, '0', STR_PAD_LEFT), // KD-QD-202603-0001-01 + 'location' => trim(($loc->orderItem?->floor_code ?? '') . ' ' . ($loc->orderItem?->symbol_code ?? '')), // "1F A-1" + 'isCompleted' => (bool) data_get($loc->options, 'lot_audit_confirmed', false), + ])->values()->all(), + ]; +} +``` --- @@ -98,43 +976,82 @@ PATCH /api/v1/qms/lot-audit/units/{id}/confirm — 확인 완료 처리 > **작업량 많음** — 완전 신규 백엔드 구축 필요 -#### 2.1 DB 설계 (신규 테이블) +#### 4.1 DB 설계 (신규 테이블 4개) -``` -audit_checklists (심사 점검표 마스터) -├── id, tenant_id -├── year, quarter -├── type: 'standard_manual' (1일차) -├── status: draft/in_progress/completed -├── options: JSON -├── created_by, timestamps, soft_delete +```sql +-- 1) audit_checklists (심사 점검표 마스터) +CREATE TABLE audit_checklists ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + year SMALLINT UNSIGNED NOT NULL, + quarter TINYINT UNSIGNED NOT NULL, -- 1~4 + type VARCHAR(30) NOT NULL DEFAULT 'standard_manual', -- 1일차 + status VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft/in_progress/completed + options JSON NULL, + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + UNIQUE INDEX (tenant_id, year, quarter, type), + INDEX (tenant_id, status), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) +); -audit_checklist_categories (점검표 카테고리) -├── id, tenant_id -├── checklist_id (FK → audit_checklists) -├── title: '원재료 품질관리 기준' -├── sort_order -├── options: JSON +-- 2) audit_checklist_categories (점검표 카테고리) +CREATE TABLE audit_checklist_categories ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + checklist_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(200) NOT NULL, -- "원재료 품질관리 기준" + sort_order INT UNSIGNED NOT NULL DEFAULT 0, + options JSON NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX (checklist_id, sort_order), + FOREIGN KEY (checklist_id) REFERENCES audit_checklists(id) ON DELETE CASCADE +); -audit_checklist_items (점검표 세부 항목) -├── id, tenant_id -├── category_id (FK → audit_checklist_categories) -├── name: '수입검사 기준 확인' -├── description -├── is_completed: boolean -├── completed_at, completed_by -├── sort_order -├── options: JSON +-- 3) audit_checklist_items (점검표 세부 항목) +CREATE TABLE audit_checklist_items ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + category_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(200) NOT NULL, -- "수입검사 기준 확인" + description TEXT NULL, + is_completed BOOLEAN NOT NULL DEFAULT FALSE, + completed_at TIMESTAMP NULL, + completed_by BIGINT UNSIGNED NULL, + sort_order INT UNSIGNED NOT NULL DEFAULT 0, + options JSON NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX (category_id, sort_order), + INDEX (category_id, is_completed), + FOREIGN KEY (category_id) REFERENCES audit_checklist_categories(id) ON DELETE CASCADE, + FOREIGN KEY (completed_by) REFERENCES users(id) ON DELETE SET NULL +); -audit_standard_documents (기준 문서) -├── id, tenant_id -├── checklist_item_id (FK → audit_checklist_items) -├── title, version, date -├── document_id (FK → documents, EAV) -├── options: JSON +-- 4) audit_standard_documents (기준 문서 연결) +CREATE TABLE audit_standard_documents ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + checklist_item_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(200) NOT NULL, -- "수입검사기준서" + version VARCHAR(20) NULL, -- "REV12" + date DATE NULL, + document_id BIGINT UNSIGNED NULL, -- FK → documents (EAV 파일) + options JSON NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX (checklist_item_id), + FOREIGN KEY (checklist_item_id) REFERENCES audit_checklist_items(id) ON DELETE CASCADE, + FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE SET NULL +); ``` -#### 2.2 Backend 구현 +#### 4.2 Backend 신규 파일 | 파일 | 역할 | |------|------| @@ -144,157 +1061,389 @@ audit_standard_documents (기준 문서) | `api/app/Models/Qualitys/AuditStandardDocument.php` | 기준 문서 모델 | | `api/app/Services/AuditChecklistService.php` | 서비스 | | `api/app/Http/Controllers/Api/V1/AuditChecklistController.php` | 컨트롤러 | -| `api/database/migrations/XXXX_create_audit_checklists_table.php` | 마이그레이션 (4테이블) | +| `api/app/Http/Requests/Qms/AuditChecklistStoreRequest.php` | 생성 검증 | +| `api/app/Http/Requests/Qms/AuditChecklistUpdateRequest.php` | 수정 검증 | +| `api/database/migrations/XXXX_create_audit_checklists_tables.php` | 마이그레이션 (4테이블) | +| `api/database/seeders/AuditChecklistSeeder.php` | 초기 점검표 데이터 | -#### 2.3 API 엔드포인트 +#### 4.3 API 엔드포인트 ``` -GET /api/v1/qms/checklists — 점검표 목록 (연도/분기 필터) -POST /api/v1/qms/checklists — 점검표 생성 -GET /api/v1/qms/checklists/{id} — 점검표 상세 (카테고리+항목+문서) -PUT /api/v1/qms/checklists/{id} — 점검표 수정 -PATCH /api/v1/qms/checklists/{id}/complete — 점검표 완료 처리 +GET /api/v1/qms/checklists — 점검표 목록 (year, quarter 필터) +POST /api/v1/qms/checklists — 점검표 생성 (카테고리+항목 일괄) +GET /api/v1/qms/checklists/{id} — 점검표 상세 (카테고리→항목→문서 중첩) +PUT /api/v1/qms/checklists/{id} — 점검표 수정 +PATCH /api/v1/qms/checklists/{id}/complete — 점검표 완료 처리 -PATCH /api/v1/qms/checklist-items/{id}/toggle — 항목 완료/미완료 토글 -GET /api/v1/qms/checklist-items/{id}/documents — 항목별 기준 문서 조회 -POST /api/v1/qms/checklist-items/{id}/documents — 기준 문서 연결 +PATCH /api/v1/qms/checklist-items/{id}/toggle — 항목 완료/미완료 토글 +GET /api/v1/qms/checklist-items/{id}/documents — 항목별 기준 문서 조회 +POST /api/v1/qms/checklist-items/{id}/documents — 기준 문서 연결 DELETE /api/v1/qms/checklist-items/{id}/documents/{docId} — 기준 문서 연결 해제 ``` -#### 2.4 Frontend — actions.ts 확장 +**`GET /qms/checklists/{id}` 예상 응답**: -| 액션 | 설명 | -|------|------| -| `getChecklists(year, quarter)` | 점검표 목록 | -| `getChecklistDetail(id)` | 점검표 상세 (카테고리+항목+문서) | -| `toggleChecklistItem(itemId)` | 항목 완료/미완료 토글 | -| `getCheckItemDocuments(itemId)` | 기준 문서 조회 | -| `confirmCheckItem(itemId)` | 기준/매뉴얼 확인 완료 | +```json +{ + "success": true, + "message": "조회되었습니다", + "data": { + "id": 1, + "year": 2025, + "quarter": 3, + "type": "standard_manual", + "status": "in_progress", + "progress": { "completed": 8, "total": 12 }, + "categories": [ + { + "id": "cat-1", + "title": "원재료 품질관리 기준", + "sortOrder": 1, + "subItems": [ + { + "id": "cat-1-1", + "name": "수입검사 기준 확인", + "isCompleted": true, + "completedAt": "2026-03-09T10:00:00", + "standardDocuments": [ + { + "id": "std-1", + "title": "수입검사기준서", + "version": "REV12", + "date": "2024-10-20", + "fileName": "수입검사기준서_REV12.pdf", + "fileUrl": "/api/v1/documents/123/download" + } + ] + }, + { + "id": "cat-1-2", + "name": "불합격품 처리 기준 확인", + "isCompleted": false, + "standardDocuments": [] + } + ] + } + ] + } +} +``` + +#### 4.4 Frontend — actions.ts 확장 + +> 동일하게 `executeServerAction` + `buildApiUrl` + `ActionResult` 패턴 준수 + +```typescript +// actions.ts에 추가 — 1일차: 기준/매뉴얼 심사 + +export async function getChecklists(params: { + year: number; + quarter?: number; +}): Promise> { + return executeServerAction({ + url: buildApiUrl('/api/v1/qms/checklists', { year: params.year, quarter: params.quarter }), + transform: (data) => data.items.map((c: any) => ({ + id: String(c.id), year: c.year, quarter: c.quarter, status: c.status, + progress: c.progress, + })), + errorMessage: '점검표 목록 조회에 실패했습니다.', + }); +} + +export async function getChecklistDetail(id: string): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/qms/checklists/${id}`), + transform: (data) => transformChecklistResponse(data), + errorMessage: '점검표 상세 조회에 실패했습니다.', + }); +} + +// ⚠️ 1일차 토글도 비관적 업데이트 적용 (공식 기록이므로 2일차와 동일) +export async function toggleChecklistItem(itemId: string): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/qms/checklist-items/${itemId}/toggle`), + method: 'PATCH', + transform: (data) => ({ + id: String(data.id), + name: data.name, + isCompleted: data.is_completed, + }), + errorMessage: '항목 상태 변경에 실패했습니다.', + }); +} + +export async function getCheckItemDocuments(itemId: string): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/qms/checklist-items/${itemId}/documents`), + transform: (data) => data.map((d: any) => ({ + id: String(d.id), title: d.title, version: d.version, date: d.date, + fileName: d.file_name, fileUrl: d.file_url, + })), + errorMessage: '기준 문서 조회에 실패했습니다.', + }); +} + +export async function attachStandardDocument(itemId: string, data: { + title: string; + version?: string; + date?: string; + documentId?: number; +}): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/qms/checklist-items/${itemId}/documents`), + method: 'POST', + body: { title: data.title, version: data.version, date: data.date, document_id: data.documentId }, + transform: (d) => ({ id: String(d.id), title: d.title, version: d.version, date: d.date, fileName: d.file_name, fileUrl: d.file_url }), + errorMessage: '기준 문서 연결에 실패했습니다.', + }); +} + +export async function detachStandardDocument(itemId: string, docId: string): Promise> { + return executeServerAction({ + url: buildApiUrl(`/api/v1/qms/checklist-items/${itemId}/documents/${docId}`), + method: 'DELETE', + errorMessage: '기준 문서 연결 해제에 실패했습니다.', + }); +} +``` --- ### Phase 3: 프론트엔드 목업 → API 전환 -#### 3.1 page.tsx 수정 +#### 5.1 page.tsx 수정 포인트 -- `mockData.ts` import 제거 -- `actions.ts` import로 교체 -- `useEffect`에서 API 호출 -- 로딩/에러 상태 추가 +> ⚠️ **상태 관리 리팩토링 필수**: 현재 15+ useState → 커스텀 훅 분리 권장 -#### 3.2 컴포넌트 수정 +**커스텀 훅 설계** (page.tsx 상태 폭발 방지): +```typescript +// hooks/useDay2LotAudit.ts — 2일차 전용 상태 + 핸들러 캡슐화 +function useDay2LotAudit() { + const [reports, setReports] = useState([]); + const [selectedReport, setSelectedReport] = useState(null); + const [routes, setRoutes] = useState([]); + const [selectedRoute, setSelectedRoute] = useState(null); + const [documents, setDocuments] = useState([]); -| 컴포넌트 | 변경 내용 | -|----------|-----------| -| `ReportList.tsx` | API 데이터 바인딩 | -| `RouteList.tsx` | API 데이터 바인딩 | -| `DocumentList.tsx` | 8종 서류 실제 조회 | -| `InspectionModal.tsx` | 실제 검사 문서 렌더링 | -| `Day1ChecklistPanel.tsx` | API 데이터 바인딩 | -| `Day1DocumentSection.tsx` | 기준 문서 API 조회 | -| `Day1DocumentViewer.tsx` | 실제 파일 미리보기 | -| `AuditProgressBar.tsx` | 실시간 진행률 계산 | -| `Filters.tsx` | 연도/분기 필터 API 연동 | + // ⚠️ 로딩 상태 세분화 — 하나의 loading으로 통합하면 안 됨 + const [loadingReports, setLoadingReports] = useState(false); + const [loadingRoutes, setLoadingRoutes] = useState(false); + const [loadingDocuments, setLoadingDocuments] = useState(false); + const [pendingConfirmIds, setPendingConfirmIds] = useState>(new Set()); // 토글 중복 클릭 방지 -#### 3.3 mockData.ts 처리 + // handlers... + return { reports, selectedReport, routes, selectedRoute, documents, + loadingReports, loadingRoutes, loadingDocuments, pendingConfirmIds, + /* handlers */ }; +} + +// hooks/useDay1Audit.ts — 1일차 전용 상태 + 핸들러 캡슐화 +function useDay1Audit() { + const [categories, setCategories] = useState([]); + const [loadingChecklist, setLoadingChecklist] = useState(false); + const [pendingToggleIds, setPendingToggleIds] = useState>(new Set()); + // ... +} +``` + +**page.tsx 변경**: +```typescript +// 제거 +import { MOCK_REPORTS, MOCK_ROUTES_INITIAL, MOCK_DOCUMENTS, ... } from './mockData'; + +// 추가 +import { getQualityReports, getReportRoutes, getRouteDocuments, ... } from './actions'; + +// 초기 상태 변경 — 커스텀 훅 사용 +const day2 = useDay2LotAudit(); +const day1 = useDay1Audit(); + +// useEffect — ActionResult 패턴에 맞춰 result.success 체크 +useEffect(() => { + async function fetchReports() { + day2.setLoadingReports(true); + const result = await getQualityReports({ + year: selectedYear, + quarter: selectedQuarter === '전체' ? undefined : parseInt(selectedQuarter.replace('Q','')), + q: debouncedSearchTerm, // ⚠️ 검색어 디바운스 필수 (300ms 권장) + }); + if (result.success) { + day2.setReports(result.data); + } else { + toast.error(result.error); // 프로젝트 표준: sonner toast 사용 + } + day2.setLoadingReports(false); + } + fetchReports(); +}, [selectedYear, selectedQuarter, debouncedSearchTerm]); + +// ⚠️ 1일차 데이터는 탭 전환 시 lazy loading (페이지 마운트 시 로드하지 않음) +useEffect(() => { + if (activeDay === 1 && day1.categories.length === 0) { + day1.fetchChecklist(selectedYear, selectedQuarter); + } +}, [activeDay]); + +// filteredReports useMemo → reports 직접 사용 (서버사이드 필터링) +``` + +#### 5.2 컴포넌트별 수정 사항 + +| 컴포넌트 | 현재 (목업) | 변경 후 (API) | 핵심 변경 | +|----------|------------|-------------|----------| +| `ReportList.tsx` | props로 MOCK_REPORTS | props로 API 데이터 | 변경 없음 (props 동일) | +| `RouteList.tsx` | MOCK_ROUTES_INITIAL | API에서 동적 로드 | `onSelect` 시 `getReportRoutes()` 호출 | +| `DocumentList.tsx` | MOCK_DOCUMENTS | API에서 동적 로드 | route 선택 시 `getRouteDocuments()` 호출 | +| `InspectionModal.tsx` | 내장 MOCK 데이터 | **2단계 로딩** | 모달 열릴 때 `getDocumentDetail(type, id)` 호출 → 상세 데이터 렌더링 | +| `Day1ChecklistPanel.tsx` | MOCK_DAY1_CATEGORIES | API에서 로드 | `getChecklistDetail()` 결과 바인딩 | +| `Day1DocumentSection.tsx` | MOCK_DAY1_STANDARD_DOCUMENTS | API에서 로드 | `getCheckItemDocuments()` 호출 | +| `Day1DocumentViewer.tsx` | 정적 미리보기 | 실제 파일 URL | `fileUrl` → 다운로드 API 연결 | +| `AuditProgressBar.tsx` | 계산값 | 서버 progress | 변경 최소 (props 동일) | +| `Filters.tsx` | 로컬 필터링 | 서버 필터링 | year/quarter 변경 → API 재호출 | + +#### 5.3 mockData.ts 처리 - Phase 3 완료 후 `mockData.ts` 삭제 -- 또는 `USE_MOCK` 플래그 패턴 적용 (개발 편의) +- 또는 `USE_MOCK_FALLBACK` 상수 패턴 적용 (기존 프로젝트 표준): + +```typescript +// actions.ts 상단 — 이미 3.5절에 포함됨 +const USE_MOCK_FALLBACK = false; // 개발 시 true로 변경, 커밋 시 false 유지 +// ⚠️ process.env.NEXT_PUBLIC_USE_MOCK 대신 상수 사용 (기존 InspectionManagement 패턴 준수) +``` --- -## 3. 데이터 매핑 +## 4. 데이터 매핑 (전체) -### 3.1 InspectionReport ↔ QualityDocument +### 4.1 InspectionReport ↔ QualityDocument + PerformanceReport -| 프론트 (InspectionReport) | 백엔드 (QualityDocument) | -|---------------------------|-------------------------| -| `id` | `quality_documents.id` | -| `code` | `quality_documents.code` (채번) | -| `siteName` | `quality_documents.site_name` | -| `item` | `quality_documents.options.product_type` 또는 인정특성 | -| `routeCount` | `quality_document_orders` COUNT | -| `totalRoutes` | `quality_document_locations` COUNT | -| `quarter` | `performance_reports.year` + `quarter` | -| `year` | `performance_reports.year` | -| `quarterNum` | `performance_reports.quarter` | +| 프론트 (InspectionReport) | 백엔드 소스 | 변환 | +|---------------------------|------------|------| +| `id` | `quality_documents.id` | string 캐스팅 | +| `code` | `quality_documents.quality_doc_number` | 직접 매핑 (KD-QD-YYYYMM-NNNN) | +| `siteName` | `quality_documents.site_name` | 직접 매핑 | +| `item` | `Order → root OrderNode → OrderItem → Item.name` | BOM 최상위(FG) 제품명 자동 추출 | +| `routeCount` | `quality_document_locations` WHERE options→lot_audit_confirmed=true | COUNT | +| `totalRoutes` | `quality_document_locations` | COUNT | +| `quarter` | `performance_reports.year` + `quarter` | 포맷: "2025년 3분기" | +| `year` | `performance_reports.year` | 직접 매핑 | +| `quarterNum` | `performance_reports.quarter` | 직접 매핑 | -### 3.2 RouteItem ↔ QualityDocumentOrder +### 4.2 RouteItem ↔ QualityDocumentOrder + Order -| 프론트 (RouteItem) | 백엔드 (QualityDocumentOrder) | -|--------------------|-------------------------------| -| `id` | `quality_document_orders.id` | -| `code` | `orders.order_code` | -| `date` | `orders.order_date` | -| `site` | `orders.site_name` | -| `locationCount` | `quality_document_locations` COUNT | -| `subItems` | `quality_document_locations` 변환 | +| 프론트 (RouteItem) | 백엔드 소스 | 변환 | +|--------------------|------------|------| +| `id` | `quality_document_orders.id` | string 캐스팅 | +| `code` | `orders.order_no` | 관계 접근 (⚠️ order_code 아님) | +| `date` | `orders.received_at` | 관계 접근 (⚠️ order_date 아님) | +| `site` | `orders.site_name` | 관계 접근 | +| `locationCount` | `quality_document_locations` | COUNT | +| `subItems` | `quality_document_locations` → UnitInspection[] | 아래 참조 | -### 3.3 ChecklistCategory ↔ AuditChecklistCategory +### 4.3 UnitInspection ↔ QualityDocumentLocation -| 프론트 (ChecklistCategory) | 백엔드 (AuditChecklistCategory) | -|---------------------------|--------------------------------| -| `id` | `audit_checklist_categories.id` | -| `title` | `audit_checklist_categories.title` | -| `subItems` | `audit_checklist_items` 관계 | +| 프론트 (UnitInspection) | 백엔드 소스 | 변환 | +|------------------------|------------|------| +| `id` | `quality_document_locations.id` | string 캐스팅 | +| `name` | `quality_doc_number + "-" + 순번` | 조합: "KD-QD-202603-0001-01" | +| `location` | `OrderItem.floor_code + " " + OrderItem.symbol_code` | 대표 item에서 (root OrderNode 기반) | +| `isCompleted` | `quality_document_locations.options.lot_audit_confirmed` | JSON boolean | + +### 4.4 ChecklistCategory ↔ AuditChecklistCategory + +| 프론트 (ChecklistCategory) | 백엔드 소스 | 변환 | +|---------------------------|------------|------| +| `id` | `audit_checklist_categories.id` | string 캐스팅 | +| `title` | `audit_checklist_categories.title` | 직접 매핑 | +| `subItems` | `audit_checklist_items` 관계 | ChecklistSubItem[] 변환 | + +### 4.5 ChecklistSubItem ↔ AuditChecklistItem + +| 프론트 (ChecklistSubItem) | 백엔드 소스 | 변환 | +|--------------------------|------------|------| +| `id` | `audit_checklist_items.id` | string 캐스팅 | +| `name` | `audit_checklist_items.name` | 직접 매핑 | +| `isCompleted` | `audit_checklist_items.is_completed` | boolean | + +### 4.6 StandardDocument ↔ AuditStandardDocument + +| 프론트 (StandardDocument) | 백엔드 소스 | 변환 | +|--------------------------|------------|------| +| `id` | `audit_standard_documents.id` | string 캐스팅 | +| `title` | `audit_standard_documents.title` | 직접 매핑 | +| `version` | `audit_standard_documents.version` | 직접 매핑 | +| `date` | `audit_standard_documents.date` | 직접 매핑 | +| `fileName` | `documents.original_name` | 관계 접근 | +| `fileUrl` | `/api/v1/documents/{document_id}/download` | URL 생성 | --- -## 4. 일정 산정 +## 5. 일정 산정 | Phase | 작업 내용 | 예상 소요 | |-------|----------|-----------| | **Phase 1** | 2일차 API 연동 (기존 API 활용) | | -| ├ 1-1 | Backend: 전용 서비스 + 컨트롤러 + 라우트 | 1일 | -| ├ 1-2 | Backend: 8종 서류 조합 조회 로직 | 1일 | -| ├ 1-3 | Frontend: actions.ts 생성 + 목업 교체 | 1일 | -| └ 1-4 | 테스트 및 디버깅 | 0.5일 | +| ├ 1-1 | Backend: QmsLotAuditService + Controller + Route + FormRequest | 1일 | +| ├ 1-2 | Backend: 8종 서류 조합 조회 로직 (getRouteDocuments) | 1일 | +| ├ 1-3 | Backend: DB 마이그레이션 (options 컬럼 추가) | 0.5일 | +| ├ 1-4 | Frontend: actions.ts 2일차 함수 + page.tsx useEffect | 1일 | +| └ 1-5 | 테스트 및 디버깅 | 0.5일 | | **Phase 2** | 1일차 백엔드 구축 (완전 신규) | | -| ├ 2-1 | DB 설계 + 마이그레이션 (4테이블) | 0.5일 | +| ├ 2-1 | DB 마이그레이션 (4테이블) | 0.5일 | | ├ 2-2 | 모델 4개 + 관계 설정 | 0.5일 | -| ├ 2-3 | 서비스 + 컨트롤러 + 라우트 | 1일 | -| └ 2-4 | 초기 데이터 시딩 (점검표 마스터) | 0.5일 | +| ├ 2-3 | AuditChecklistService + Controller + FormRequest | 1일 | +| └ 2-4 | 초기 데이터 시딩 (6개 카테고리 × 12개 항목) | 0.5일 | | **Phase 3** | 프론트엔드 전환 | | -| ├ 3-1 | 2일차 컴포넌트 API 바인딩 | 1일 | -| ├ 3-2 | 1일차 컴포넌트 API 바인딩 | 1일 | -| └ 3-3 | 통합 테스트 + mockData 정리 | 0.5일 | +| ├ 3-1 | 상태 관리 리팩토링 (커스텀 훅 분리: useDay1Audit, useDay2LotAudit) | 1일 | +| ├ 3-2 | 2일차 컴포넌트 API 바인딩 (ReportList, RouteList, DocumentList, InspectionModal 2단계 로딩) | 1.5일 | +| ├ 3-3 | 1일차 컴포넌트 API 바인딩 (Day1ChecklistPanel, Day1DocumentSection, Day1DocumentViewer) | 1일 | +| └ 3-4 | 통합 테스트 + mockData 정리 + 로딩/에러 상태 세분화 + 디바운스 | 1일 | -**총 예상: ~8일** +**총 예상: ~11.5일** (Phase 3 증가: 2.5일 → 4.5일) --- -## 5. 의존성 및 리스크 +## 6. 의존성 및 리스크 -### 5.1 의존성 +### 6.1 의존성 -| 항목 | 의존 대상 | 상태 | -|------|-----------|------| -| 품질관리서 데이터 | `quality_documents` 실 데이터 | ✅ 운영 중 | -| 실적신고 데이터 | `performance_reports` 실 데이터 | ✅ 운영 중 | -| 수입검사 성적서 | `inspections` (IQC) | ✅ 운영 중 | -| 중간검사 성적서 | `inspections` (PQC) | ⚠️ 구현 중 | -| 작업일지 | `work_orders` 연결 | ✅ 운영 중 | -| 출하/납품 | `shipments` | ✅ 운영 중 | -| 기준 문서 파일 | EAV Document 시스템 | ✅ 운영 중 | +| 항목 | 의존 대상 | 상태 | 비고 | +|------|-----------|------|------| +| 품질관리서 데이터 | `quality_documents` 실 데이터 | ✅ 운영 중 | QualityDocumentService 활용 | +| 실적신고 데이터 | `performance_reports` 실 데이터 | ✅ 운영 중 | 분기 필터용 | +| 수입검사 성적서 | `inspections` (IQC) | ✅ 운영 중 | InspectionService 활용 | +| 중간검사 성적서 | `inspections` (PQC) | ⚠️ 구현 중 | work_order_id 연결 필요 | +| 작업일지 | `work_orders` 연결 | ✅ 운영 중 | 수주→작업지시 경로 | +| 출하/납품 | `shipments` | ✅ 운영 중 | 납품확인서/출고증 | +| 기준 문서 파일 | EAV Document 시스템 | ✅ 운영 중 | 파일 업로드/다운로드 | -### 5.2 리스크 +### 6.2 리스크 | 리스크 | 영향 | 완화 방안 | |--------|------|-----------| -| 8종 서류 추적 로직 복잡 | Phase 1 지연 | 서류별 독립 조회 후 프론트에서 조합 | -| 1일차 점검표 초기 데이터 부재 | Phase 2 테스트 어려움 | 시더로 기본 점검표 생성 | -| 중간검사 미완성 | 2일차 일부 서류 누락 | 빈 상태로 표시, 추후 연동 | +| 8종 서류 추적 로직 복잡 | Phase 1 지연 | 서류별 독립 조회 → 서비스 메서드 분리, 빈 결과 허용 | +| 1일차 점검표 초기 데이터 부재 | Phase 2 테스트 어려움 | AuditChecklistSeeder로 기본 6카테고리×12항목 생성 | +| 중간검사(PQC) 미완성 | 2일차 중간검사 서류 누락 | 빈 배열로 표시, count: 0, 추후 연동 | +| quality_document_locations.options 없음 | 로트 확인 상태 저장 불가 | 마이그레이션으로 options 컬럼 추가 필수 | +| StockLot 데이터 충분성 | IQC 추적 누락 가능 | WorkOrderMaterialInput 미등록 시 빈 배열 반환, 데이터 적재 후 자연 해소 | +| confirm 토글 동시 수정 | JSON 병합 충돌 가능 | DB::raw JSON_SET 또는 lockForUpdate() 적용 | +| WorkOrder ↔ StockLot 관계 미정의 | eager loading 불가 | WorkOrder 모델에 `materialInputs()` hasMany는 있으나 `stockLots()` 없음. materialInputs 경유 쿼리 사용 | +| Order.nodes() 관계명 | 계획서 orderNodes()와 불일치 | 실제 관계명 `nodes()` 사용 또는 alias 추가 | --- -## 6. 권장 진행 순서 +## 7. 권장 진행 순서 ``` -Phase 1 (2일차 API 연동) — 3.5일 +Phase 1 (2일차 API 연동) — 4일 ↓ Phase 2 (1일차 백엔드 구축) — 2.5일 ↓ -Phase 3 (프론트엔드 전환) — 2.5일 +Phase 3 (프론트엔드 전환) — 4.5일 ``` **Phase 1을 먼저 하는 이유:** @@ -304,13 +1453,59 @@ Phase 3 (프론트엔드 전환) — 2.5일 --- +## 8. 체크리스트 (작업 완료 검증) + +### Phase 1 완료 기준 +- [ ] `QmsLotAuditService` 구현 (index, show, routeDocuments, documentDetail, confirm) +- [ ] `QmsLotAuditController` + FormRequest 3개 (Confirm, Index, DocumentDetail) +- [ ] 라우트 등록 (`api/routes/api/v1/quality.php`) +- [ ] `quality_document_locations.options` 마이그레이션 + Model $fillable + $casts 수정 +- [ ] 8종 서류 조합 조회 테스트 (각 타입별 빈 결과 포함) +- [ ] IQC 추적: **WorkOrderMaterialInput → StockLot** 기반 LOT 추적 구현 + 테스트 +- [ ] IQC 추적: `whereNotNull('lot_no')` null 방어 적용 +- [ ] FG 제품명 추출 로직 (`getFgProductName`) + eager loading 확인 (N+1 없음) +- [ ] 서류 상세 2단계 로딩 API (`documents/{type}/{id}`) 구현 +- [ ] subType 결정: WorkOrder.options 기반 매핑 구현 +- [ ] confirm 토글: 원자적 JSON 업데이트 (lockForUpdate 또는 JSON_SET) +- [ ] Frontend `actions.ts` — `executeServerAction` + `buildApiUrl` + `ActionResult` 패턴 +- [ ] Frontend `actions.ts` — snake→camelCase transform 함수 구현 +- [ ] Swagger 문서 작성 + +### Phase 2 완료 기준 +- [ ] 4개 테이블 마이그레이션 실행 +- [ ] 4개 모델 (관계, 캐스팅, 상수, 헬퍼) +- [ ] `AuditChecklistService` 구현 +- [ ] `AuditChecklistController` + FormRequest 2개 +- [ ] 라우트 등록 +- [ ] 시더 실행 (6카테고리 × 12항목) +- [ ] Swagger 문서 작성 + +### Phase 3 완료 기준 +- [ ] 상태 관리 리팩토링: `useDay1Audit()`, `useDay2LotAudit()` 커스텀 훅 분리 +- [ ] 로딩 상태 세분화 (reports/routes/documents/modal 각각 독립) +- [ ] 검색 디바운스 (300ms) 적용 +- [ ] `page.tsx`에서 mockData import 제거 +- [ ] 모든 컴포넌트 API 데이터 바인딩 (ActionResult.success 체크 포함) +- [ ] 에러 처리: sonner toast 사용 (setError 문자열 아님) +- [ ] 토글 비관적 업데이트: `pendingConfirmIds` 중복 클릭 방지 + 서버 성공 후 UI 반영 +- [ ] 1일차 토글도 비관적 업데이트 적용 +- [ ] InspectionModal 2단계 로딩 구현 (모달 열 때 getDocumentDetail 호출) +- [ ] 1일차 데이터 lazy loading (탭 전환 시 로드) +- [ ] mockData.ts 삭제 또는 `USE_MOCK_FALLBACK` 상수 패턴 적용 +- [ ] 전체 플로우 수동 테스트 (1일차 + 2일차) + +--- + ## 관련 문서 - [품질인정심사 기능 문서](../../features/quality-management/quality-certification-audit.md) - [제품검사 관리](../../features/quality-management/inspection-management.md) - [생산실적신고](../../features/quality-management/performance-reports.md) - [통합 개선 마스터 플랜](./integrated-master-plan.md) +- [API 개발 규칙](../standards/api-rules.md) +- [DB 스키마 (생산/품질)](../../system/database/production.md) +- [JSON options 컬럼 정책](../standards/options-column-policy.md) --- -**최종 업데이트**: 2026-03-09 +**최종 업데이트**: 2026-03-10 (3-페르소나 심층 분석 반영) \ No newline at end of file