diff --git a/plans/AI_리포트_키워드_색상체계_가이드_v1.4.md b/plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md similarity index 100% rename from plans/AI_리포트_키워드_색상체계_가이드_v1.4.md rename to plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md diff --git a/plans/SEEDERS_LIST.md b/plans/archive/SEEDERS_LIST.md similarity index 100% rename from plans/SEEDERS_LIST.md rename to plans/archive/SEEDERS_LIST.md diff --git a/plans/api-analysis-report.md b/plans/archive/api-analysis-report.md similarity index 100% rename from plans/api-analysis-report.md rename to plans/archive/api-analysis-report.md diff --git a/plans/bending-lot-pipeline-dev-plan.md b/plans/archive/bending-lot-pipeline-dev-plan.md similarity index 100% rename from plans/bending-lot-pipeline-dev-plan.md rename to plans/archive/bending-lot-pipeline-dev-plan.md diff --git a/plans/archive/bending-worklog-reimplementation-plan.md b/plans/archive/bending-worklog-reimplementation-plan.md new file mode 100644 index 0000000..1da3252 --- /dev/null +++ b/plans/archive/bending-worklog-reimplementation-plan.md @@ -0,0 +1,860 @@ +# 절곡 작업일지 완전 재구현 계획 + +> **작성일**: 2026-02-19 +> **목적**: PHP viewBendingWork_slat.php와 동일한 구조로 React BendingWorkLogContent.tsx 완전 재구현 +> **기준 문서**: `5130/output/viewBendingWork_slat.php` (~1400줄) +> **상태**: ✅ 구현 완료 (커밋: 59b9b1b) + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | Phase 1~5 전체 구현 완료 + 슬랫 입고 LOT NO 개소별 표시 버그 수정 | +| **다음 작업** | 실 데이터 테스트 (bending_info가 채워진 작업지시로 화면 확인) | +| **진행률** | 15/15 (100%) | +| **마지막 업데이트** | 2026-02-19 | +| **Git 커밋** | `59b9b1b` feat(WEB): 절곡 작업일지 완전 재구현 + 슬랫 입고 LOT NO 개소별 표시 수정 | + +--- + +## 1. 개요 + +### 1.1 배경 + +현재 React `BendingWorkLogContent.tsx`는 **빈 껍데기 상태**로, 단순 테이블에 `item.productName`, `item.specification`, `item.quantity`만 평면 나열함. PHP 원본(`viewBendingWork_slat.php`)의 4개 카테고리 구조를 전혀 지원하지 않음. + +**현재 React 컴포넌트 상태:** +- 헤더 + 결재란 (ConstructionApprovalTable 사용) ✅ +- 신청업체 / 신청내용 테이블 ✅ +- 제품 정보 테이블 (빈 칸) ❌ 데이터 바인딩 없음 +- 작업내역 (유형명/세부품명/재질/LOT/길이/수량) ❌ 단순 flat 리스트 +- 생산량 합계 [kg] SUS/EGI ❌ 빈 칸 +- **4개 카테고리 섹션 완전 부재** ❌ + +**PHP 원본 구조 (구현 목표):** +- 가이드레일: 벽면형/측면형 분류, 이미지 + 세부품명별 길이/수량/LOT NO/무게 계산 +- 하단마감재: 3000/4000mm 길이별 수량, 별도마감재 +- 셔터박스: 동적 이미지 + 구성요소(전면부/린텔부/점검구/후면코너부/상부덮개/측면부) +- 연기차단재: W50 레일용, W80 케이스용 +- 생산량 합계: SUS(7.93g/cm3) / EGI(7.85g/cm3) 무게 자동 계산 + +### 1.2 데이터 흐름 (전체 파이프라인) + +``` +[수주 시스템] +order_nodes.options.bending_info (JSON) + │ + ▼ WorkOrderService.php (Line 276) + │ $nodeOptions['bending_info'] ?? null + │ + ▼ +work_order_items.options (JSON) + │ { floor, code, width, height, bending_info, slat_info, cutting_info, wip_info } + │ + ▼ API GET /work-orders/{id} → items[].options.bending_info + │ + ▼ Frontend getWorkOrderById() → WorkOrder.items + │ + ▼ WorkLogModal.tsx (Line 207-213) + │ + │ ※ materialLots 미전달 (bending은 slat과 다르게 LOT를 별도로 안 받음) + │ + ▼ BendingWorkLogContent.tsx (재작성 대상) +``` + +**핵심**: `bending_info`는 `work_order_items.options` JSON 안에 저장되며, 현재 프론트엔드 `WorkOrderItem` 타입에는 `bendingInfo` 필드가 **없음** (slatInfo처럼 추가 필요). + +### 1.3 현재 bending_info 구조 (SAM에 정의된 것) + +```typescript +// react/src/components/production/WorkerScreen/types.ts (Lines 91-107) +export interface BendingInfo { + drawingUrl?: string; + common: BendingCommonInfo; + detailParts: BendingDetailPart[]; +} + +export interface BendingCommonInfo { + kind: string; // "혼합형 120X70" + type: string; // "혼합형" | "벽면형" | "측면형" + lengthQuantities: { length: number; quantity: number }[]; +} + +export interface BendingDetailPart { + partName: string; // "엘바", "하장바" + material: string; // "EGI 1.6T" + barcyInfo: string; // "16 I 75" +} +``` + +### 1.4 현재 WorkOrderItem 타입 (types.ts Lines 106-120) + +```typescript +// react/src/components/production/WorkOrders/types.ts +export interface WorkOrderItem { + id: string; + no: number; + status: ItemStatus; + productName: string; + floorCode: string; + specification: string; + width?: number; + height?: number; + quantity: number; + unit: string; + orderNodeId: number | null; + orderNodeName: string; + slatInfo?: { length: number; slatCount: number; jointBar: number; glassQty: number }; + // ❌ bendingInfo 없음 → 추가 필요 +} +``` + +**transform 함수** (types.ts Lines 457-474): `slatInfo`는 `item.options.slat_info`에서 파싱하지만, `bending_info`는 아직 매핑하지 않음. + +### 1.5 PHP col → SAM 매핑 (완전 테이블) + +PHP에서 데이터는 `estimateSlatList` JSON의 각 아이템에 `col{N}` 키로 저장됨. + +| PHP 컬럼 | 의미 | SAM bending_info 필드 | 상태 | +|---------|------|----------------------|------| +| `col4` | 제품코드 (KQTS01, KTE01 등) | `productCode` | ⚠️ item_code로 별도 존재, bending_info에도 추가 | +| `col6` | 가이드레일 유형 | `common.type` | ✅ 존재 | +| `col7` | 마감유형 (SUS마감/EGI마감) | `finishMaterial` | ❌ 추가 필요 | +| `col24` | 유효 길이 (mm) | `common.lengthQuantities` | ✅ 존재 | +| `col32` | 연기차단재 W50 수량 - 2438mm | `smokeBarrier.w50[].quantity` | ❌ 추가 필요 | +| `col33` | 연기차단재 W50 수량 - 3000mm | 상동 | ❌ | +| `col34` | 연기차단재 W50 수량 - 3500mm | 상동 | ❌ | +| `col35` | 연기차단재 W50 수량 - 4000mm | 상동 | ❌ | +| `col36` | 연기차단재 W50 수량 - 4300mm | 상동 | ❌ | +| `col37` | 셔터박스 크기 (500*380 등) | `shutterBox[].size` | ❌ 추가 필요 | +| `col37_custom` | 셔터박스 커스텀 크기 | `shutterBox[].size` (custom일 때) | ❌ | +| `col37_railwidth` | 셔터박스 레일 폭 | `shutterBox[].railWidth` | ❌ | +| `col37_frontbottom` | 셔터박스 전면 하단 치수 | `shutterBox[].frontBottom` | ❌ | +| `col37_boxdirection` | 셔터박스 방향 (양면/밑면/후면) | `shutterBox[].direction` | ❌ | +| `col39` | 셔터박스 수량 - 1219mm | `shutterBox[].lengthData` | ❌ | +| `col40` | 셔터박스 수량 - 2438mm | 상동 | ❌ | +| `col41` | 셔터박스 수량 - 3000mm | 상동 | ❌ | +| `col42` | 셔터박스 수량 - 3500mm | 상동 | ❌ | +| `col43` | 셔터박스 수량 - 4000mm | 상동 | ❌ | +| `col44` | 셔터박스 수량 - 4150mm | 상동 | ❌ | +| `col45` | 상부덮개 수량 | `shutterBox[].coverQty` | ❌ | +| `col47` | 마구리 수량 | `shutterBox[].finCoverQty` | ❌ | +| `col48` | 연기차단재 W80 수량 | `smokeBarrier.w80Qty` | ❌ | +| `col50` | 하단마감재 3000mm 수량 | `bottomBar.length3000Qty` | ❌ | +| `col51` | 하단마감재 4000mm 수량 | `bottomBar.length4000Qty` | ❌ | + +### 1.6 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────┤ +│ - options JSON 확장 (컬럼 추가 금지 - 멀티테넌시 원칙) │ +│ - PHP 원본과 동일한 계산 로직 (calWeight, 길이 버킷팅) │ +│ - 이미지는 정적 파일로 서빙 (셔터박스만 SVG/Canvas 대체) │ +│ - 카테고리별 독립 컴포넌트 (가이드레일/하단마감/셔터박스/연기차단재)│ +│ - 현재 WorkOrderItem에 bendingInfo 필드 추가 (slatInfo 패턴) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.7 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | React 컴포넌트 추가/수정, 타입 정의 추가, 이미지 복사 | 불필요 | +| ⚠️ 컨펌 필요 | bending_info JSON 스키마 변경, API 응답 구조 변경, 계산 로직 변경 | **필수** | +| 🔴 금지 | work_order_items 테이블 컬럼 추가, 기존 API 삭제 | 별도 협의 | + +--- + +## 2. 대상 범위 + +### 2.1 Phase 1: 데이터 스키마 확장 (백엔드) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1.1 | bending_info JSON 스키마 확장 설계 | ✅ | BendingInfoExtended 타입 정의 완료 | +| 1.2 | WorkOrderService.php - options 매핑 확인/수정 | ✅ | Line 277에서 bending_info 정상 전달 확인 | +| 1.3 | API 응답에 확장된 bending_info 포함 확인 | ✅ | transform 함수에 bendingInfo 매핑 추가 완료 | + +### 2.2 Phase 2: 이미지 서빙 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | 5130/img/ → api/public/images/bending/ 복사 | ✅ | guiderail(12) + bottombar(6) + part(1) + box source(3) = 22개 | +| 2.2 | 이미지 URL 빌더 유틸 (프론트) | ✅ | bending/utils.ts getBendingImageUrl() | + +### 2.3 Phase 3: 프론트엔드 타입 & 유틸리티 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 3.1 | BendingWorkLog 타입 정의 확장 | ✅ | bending/types.ts + WorkOrderItem.bendingInfo 추가 | +| 3.2 | 무게 계산 유틸리티 (`calcWeight`) | ✅ | bending/utils.ts (calcWeight, getMaterialMapping 등 11개 함수) | +| 3.3 | WorkOrderItem transform에 bendingInfo 매핑 추가 | ✅ | item.options.bending_info → bendingInfo | + +### 2.4 Phase 4: 프론트엔드 컴포넌트 구현 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 4.1 | GuideRailSection 컴포넌트 | ✅ | 벽면형/측면형 분류, 이미지+파트테이블 | +| 4.2 | BottomBarSection 컴포넌트 | ✅ | 하단마감재 + 별도마감재 | +| 4.3 | ShutterBoxSection 컴포넌트 | ✅ | 방향별(양면/밑면/후면) 구성요소, source 이미지 | +| 4.4 | SmokeBarrierSection 컴포넌트 | ✅ | W50 레일용 + W80 케이스용 | +| 4.5 | ProductionSummarySection 컴포넌트 | ✅ | SUS/EGI/합계 표시 | +| 4.6 | BendingWorkLogContent 통합 | ✅ | 헤더 + 신청업체/내용 + 제품정보 + 4섹션 + 합계 + 비고 | + +### 2.5 Phase 5: 검증 & 정리 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 5.1 | PHP 원본과 출력 비교 검증 | ✅ | TypeScript 타입 체크 통과, 실 데이터 테스트 대기 | + +--- + +## 3. 작업 절차 + +### 3.1 단계별 절차 + +``` +Phase 1: 데이터 스키마 확장 (백엔드) +├── 1.1 bending_info 확장 스키마 설계 +│ ├── guideRail: { wall, side } (길이 버킷팅 + 수량 + baseSize) +│ ├── bottomBar: { material, extraFinish, length3000Qty, length4000Qty } +│ ├── shutterBox: [{ size, direction, railWidth, frontBottom, coverQty, finCoverQty, lengthData }] +│ └── smokeBarrier: { w50: [...], w80Qty } +├── 1.2 WorkOrderService.php 매핑 확인 (Line 276) +└── 1.3 API 응답 검증 (curl로 직접 확인) + +Phase 2: 이미지 서빙 +├── 2.1 정적 이미지 복사 (guiderail 12jpg + bottombar 6jpg + part 1jpg = 19개) +└── 2.2 이미지 URL 헬퍼 유틸 + +Phase 3: 프론트엔드 타입 & 유틸 +├── 3.1 타입 정의 (bending/types.ts 신규 + WorkOrderItem.bendingInfo 추가) +├── 3.2 calcWeight + getMaterialMapping 유틸 (bending/utils.ts) +└── 3.3 transform 함수에 bendingInfo 매핑 추가 (slatInfo 패턴 동일) + +Phase 4: 컴포넌트 구현 +├── 4.1 GuideRailSection (가장 복잡 - 벽면/측면 분리, 파트 구성, 무게 계산) +├── 4.2 BottomBarSection (3000/4000 수량, 별도마감) +├── 4.3 ShutterBoxSection (방향별 구성요소, SVG 다이어그램) +├── 4.4 SmokeBarrierSection (W50 길이별 + W80 고정) +├── 4.5 ProductionSummarySection (SUS/EGI 누적 합계) +└── 4.6 BendingWorkLogContent 통합 (헤더+신청+4섹션+합계 조립) + +Phase 5: 검증 +└── 5.1 PHP 원본과 비교 (num=24822) +``` + +--- + +## 4. 상세 작업 내용 (PHP 로직 완전 인라인) + +### 4.1 Phase 1: bending_info 확장 스키마 + +#### 1.1 확장된 bending_info JSON 구조 + +```typescript +interface BendingInfoExtended { + // === 기존 필드 (유지) === + drawingUrl?: string; + common: BendingCommonInfo; // { kind, type, lengthQuantities } + detailParts: BendingDetailPart[]; // [{ partName, material, barcyInfo }] + + // === 신규 필드 === + productCode: string; // "KTE01", "KQTS01", "KSE01", "KSS01", "KWE01" + finishMaterial: string; // "EGI마감", "SUS마감" + + guideRail: { + wall: { + lengthData: { length: number; quantity: number }[]; + baseSize: string; // "135*80" 또는 "135*130" + } | null; + side: { + lengthData: { length: number; quantity: number }[]; + baseSize: string; // "135*130" + } | null; + }; + + bottomBar: { + material: string; // "EGI 1.55T" 또는 "SUS 1.5T" + extraFinish: string; // "SUS 1.2T" 또는 "없음" + length3000Qty: number; + length4000Qty: number; + }; + + shutterBox: { + size: string; // "500*380" 등 + direction: string; // "양면" | "밑면" | "후면" + railWidth: number; + frontBottom: number; + coverQty: number; // 상부덮개 수량 + finCoverQty: number; // 마구리 수량 + lengthData: { length: number; quantity: number }[]; + }[]; // 배열 (여러 사이즈 가능) + + smokeBarrier: { + w50: { length: number; quantity: number }[]; // 레일용 W50 + w80Qty: number; // 케이스용 W80 수량 + }; +} +``` + +#### 1.2 calWeight 함수 (PHP 원본 Lines 27-55 → TypeScript 구현) + +```typescript +// PHP 원본: +// $volume_cm3 = ($thickness * $calWidth * $calHeight) / 1000; +// $weight_kg = ($volume_cm3 * $density) / 1000; +// SUS → $SUS_total += $weight_kg, EGI → $EGI_total += $weight_kg + +function calcWeight( + material: string, // "SUS 1.2T", "EGI 1.55T", "EGI 0.8T" 등 + width: number, // mm + height: number // mm (= 길이) +): { weight: number; type: 'SUS' | 'EGI' } { + const thickness = parseFloat(material.match(/\d+(\.\d+)?/)?.[0] || '0'); + const isSUS = material.includes('SUS'); + const density = isSUS ? 7.93 : 7.85; // g/cm3 + const volume_cm3 = (thickness * width * height) / 1000; + const weight_kg = (volume_cm3 * density) / 1000; + return { + weight: Math.round(weight_kg * 100) / 100, + type: isSUS ? 'SUS' : 'EGI', + }; +} +``` + +#### 1.3 제품코드별 재질 매핑 (PHP Lines 330-366) + +```typescript +function getMaterialMapping(productCode: string, finishMaterial: string) { + // Group 1: KQTS01 + if (productCode === 'KQTS01') { + return { + guideRailFinish: 'SUS 1.2T', // ①②마감재 + bodyMaterial: 'EGI 1.55T', // ③본체, ④C형, ⑤D형 + guideRailExtraFinish: '', // 별도마감 없음 + bottomBarFinish: 'SUS 1.5T', // 하단마감재 + bottomBarExtraFinish: '없음', // 별도마감 없음 + }; + } + // Group 2: KTE01 + if (productCode === 'KTE01') { + const isSUS = finishMaterial === 'SUS마감'; + return { + guideRailFinish: 'EGI 1.55T', + bodyMaterial: 'EGI 1.55T', + guideRailExtraFinish: isSUS ? 'SUS 1.2T' : '', + bottomBarFinish: 'EGI 1.55T', + bottomBarExtraFinish: isSUS ? 'SUS 1.2T' : '없음', + }; + } + // 기타 제품코드 (KSE01, KSS01, KWE01 등) - KTE01 + EGI마감과 동일 패턴 + return { + guideRailFinish: 'EGI 1.55T', + bodyMaterial: 'EGI 1.55T', + guideRailExtraFinish: '', + bottomBarFinish: 'EGI 1.55T', + bottomBarExtraFinish: '없음', + }; +} +``` + +#### 1.4 가이드레일 길이 버킷팅 알고리즘 (PHP Lines 384-413) + +```typescript +// 고정 버킷: [2438, 3000, 3500, 4000, 4300] +// 각 아이템의 col24(유효길이)를 "첫 번째로 수용 가능한 버킷"에 넣음 (first-fit) + +const LENGTH_BUCKETS = [2438, 3000, 3500, 4000, 4300]; + +function bucketGuideRails(items: Array<{ validLength: number; railType: string }>) { + const buckets = LENGTH_BUCKETS.map(len => ({ + length: len, wallSum: 0, sideSum: 0, + wallBaseSize: null as string | null, sideBaseSize: null as string | null, + })); + + for (const item of items) { + for (const bucket of buckets) { + if (item.validLength <= bucket.length) { + if (item.railType === '혼합형(130*75)(130*125)') { + bucket.wallSum += 1; + bucket.sideSum += 1; + bucket.wallBaseSize = '135*80'; + bucket.sideBaseSize = '135*130'; + } else if (item.railType === '벽면형(130*75)') { + bucket.wallSum += 2; + bucket.wallBaseSize = '135*130'; + } else if (item.railType === '측면형(130*125)') { + bucket.sideSum += 2; + bucket.sideBaseSize = '135*130'; + } + break; // first-fit: 한 버킷에 넣으면 다음 아이템으로 + } + } + } + return buckets.filter(b => b.wallSum > 0 || b.sideSum > 0); +} +``` + +#### 1.5 가이드레일 세부품명 + LOT 접두사 + 무게 계산 폭 + +**벽면형 [130*75] 파트 구성:** + +| 세부품명 | LOT 접두사 | 재질 | 무게 계산 폭 (mm) | +|---------|-----------|------|-----------------| +| ①②마감재 | XX | `guideRailFinish` | 412 | +| ③본체 | RT | `bodyMaterial` | 412 | +| ④C형 | RC | `bodyMaterial` | 412 | +| ⑤D형 | RD | `bodyMaterial` | 412 | +| ⑥별도마감 (SUS마감 시만) | RS | `guideRailExtraFinish` | 412 | +| 하부BASE | XX | EGI 1.55T (고정) | 135 (높이=80) | + +무게: `calcWeight(재질, 412, 길이)` / 하부BASE: `calcWeight('EGI 1.55T', 135, 80)` +baseSize는 `135*80` (혼합형) 또는 `135*130` (벽면형 단독) + +**측면형 [130*125] 파트 구성:** + +| 세부품명 | LOT 접두사 | 재질 | 무게 계산 폭 (mm) | +|---------|-----------|------|-----------------| +| ①②마감재 | SS | `guideRailFinish` | 462 | +| ③본체 | ST | `bodyMaterial` | 462 | +| ④C형 | SC | `bodyMaterial` | 462 | +| ⑤D형 | SD | `bodyMaterial` | 462 | +| 하부BASE | XX | EGI 1.55T (고정) | 135 (높이=130) | + +무게: `calcWeight(재질, 462, 길이)` / 하부BASE: `calcWeight('EGI 1.55T', 135, 130)` + +#### 1.6 하단마감재 세부품명 + +| 세부품명 | LOT 접두사 | 재질 | 무게 계산 폭 (mm) | 길이 옵션 | +|---------|-----------|------|-----------------|---------| +| ①하단마감재 | TE(EGI)/TS(SUS) | `bottomBarFinish` | 184 | 3000, 4000 | +| ④별도마감재 | TE/TS | `bottomBarExtraFinish` | 238 | 3000, 4000 | + +별도마감재는 `bottomBarExtraFinish !== '없음'`일 때만 표시. + +#### 1.7 셔터박스 구성요소 (방향별 - PHP Lines 819-1190) + +**셔터박스 재질**: 항상 `EGI 1.55T` (= `$BoxFinish`) + +**표준 사이즈 (500*380) 구성:** + +| 구성요소 | LOT 접두사 | 치수 공식 | +|---------|-----------|----------| +| ①전면부 | CF | `boxHeight + 122` | +| ②린텔부 | CL | `boxWidth - 330` | +| ③⑤점검구 | CP | `boxWidth - 200` | +| ④후면코너부 | CB | `170` (고정) | + +**비표준 사이즈 - 양면 구성:** + +| 구성요소 | LOT 접두사 | 치수 공식 | +|---------|-----------|----------| +| ①전면부 | XX | `boxHeight + 122` | +| ②린텔부 | CL | `boxWidth - 330` | +| ③점검구 | XX | `boxWidth - 200` | +| ④후면코너부 | CB | `170` (고정) | +| ⑤점검구 | XX | `boxHeight - 100` | +| ⑥상부덮개 | XX | `1219 * (boxWidth - 111)` | +| ⑦측면부(마구리) | XX | `(boxWidthFin+5) * (boxHeightFin+5)` 표시 | + +**비표준 사이즈 - 밑면 구성:** + +| 구성요소 | LOT 접두사 | 치수 공식 | +|---------|-----------|----------| +| ①전면부 | XX | `boxHeight + 122` | +| ②린텔부 | CL | `boxWidth - 330` | +| ③점검구 | XX | `boxWidth - 200` | +| ④후면부 | CB | `boxHeight + 85*2` | +| ⑤상부덮개 | XX | `1219 * (boxWidth - 111)` | +| ⑥측면부(마구리) | XX | `(boxWidthFin+5) * (boxHeightFin+5)` 표시 | + +**비표준 사이즈 - 후면 구성:** + +| 구성요소 | LOT 접두사 | 치수 공식 | +|---------|-----------|----------| +| ①전면부 | XX | `boxHeight + 122` | +| ②린텔부 | CL | `boxWidth + 85*2` | +| ③점검구 | XX | `boxHeight - 200` | +| ④후면코너부 | CB | `boxHeight + 85*2` | +| ⑤상부덮개 | XX | `1219 * (boxWidth - 111)` | +| ⑥측면부(마구리) | XX | `(boxWidthFin+5) * (boxHeightFin+5)` 표시 | + +**공통 사항:** +- 상부덮개 무게: `calcWeight('EGI 1.55T', boxWidth - 111, 1219)` × coverQty +- 마구리 무게: `calcWeight('EGI 1.55T', boxWidthFin, boxHeightFin)` × finCoverQty +- 셔터박스 길이 버킷: [1219, 2438, 3000, 3500, 4000, 4150] + +#### 1.8 연기차단재 (PHP Lines 1195-1321) + +| 파트 | 재질 | 무게 계산 폭 (mm) | 길이 버킷 | +|-----|------|-----------------|---------| +| 레일용 [W50] | EGI 0.8T | 26 | 2438, 3000, 3500, 4000, 4300 | +| 케이스용 [W80] | EGI 0.8T | 26 | 3000 (고정) | + +LOT 접두사: 모두 `GI` +LOT 코드 생성: `GI-{getSLengthCode(length, category)}` + +#### 1.9 getSLengthCode 함수 (PHP Lines 56-100) + +```typescript +function getSLengthCode(length: number, category: string): string | null { + if (category === '연기차단재50') { + return length === 3000 ? '53' : length === 4000 ? '54' : null; + } + if (category === '연기차단재80') { + return length === 3000 ? '83' : length === 4000 ? '84' : null; + } + // category === '기타' (일반) + const map: Record = { + 1219: '12', 2438: '24', 3000: '30', 3500: '35', + 4000: '40', 4150: '41', 4200: '42', 4300: '43', + }; + return map[length] || null; +} +``` + +--- + +### 4.2 Phase 2: 이미지 서빙 + +#### 복사 대상 (총 19개 JPG 파일) + +**가이드레일 (12개):** +``` +5130/img/guiderail/ → api/public/images/bending/guiderail/ +├── guiderail_KQTS01_wall_130x75.jpg +├── guiderail_KQTS01_side_130x125.jpg +├── guiderail_KTE01_wall_130x75.jpg +├── guiderail_KTE01_side_130x125.jpg +├── guiderail_KSE01_wall_120x70.jpg +├── guiderail_KSE01_side_120x120.jpg +├── guiderail_KSS01_wall_120x70.jpg +├── guiderail_KSS01_side_120x120.jpg +├── guiderail_KSS02_wall_120x70.jpg +├── guiderail_KSS02_side_120x120.jpg +├── guiderail_KWE01_wall_120x70.jpg +└── guiderail_KWE01_side_120x120.jpg +``` + +**하단마감재 (6개):** +``` +5130/img/bottombar/ → api/public/images/bending/bottombar/ +├── bottombar_KQTS01.jpg +├── bottombar_KTE01.jpg +├── bottombar_KSE01.jpg +├── bottombar_KSS01.jpg +├── bottombar_KSS02.jpg +└── bottombar_KWE01.jpg +``` + +**연기차단재 (1개):** +``` +5130/img/part/ → api/public/images/bending/part/ +└── smokeban.jpg +``` + +**셔터박스 이미지**: PHP에서 GD 라이브러리로 동적 생성 → React에서는 SVG/Canvas로 대체 +- 소스 이미지: `5130/img/box/source/box_{both|bottom|rear}.jpg` +- 치수 텍스트를 오버레이하는 구조 → SVG 컴포넌트로 재구현 + +#### 이미지 URL 패턴 + +```typescript +const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'https://api.sam.kr'; + +function getBendingImageUrl(category: string, productCode: string, type?: string): string { + switch (category) { + case 'guiderail': { + // PHP: guiderail_{prodCode}_{wall|side}_{size}.jpg + // KQTS01, KTE01 → 130x75 (wall) / 130x125 (side) + // KSE01, KSS01, KSS02, KWE01 → 120x70 (wall) / 120x120 (side) + const size = ['KQTS01', 'KTE01'].includes(productCode) + ? (type === 'wall' ? '130x75' : '130x125') + : (type === 'wall' ? '120x70' : '120x120'); + return `${API_BASE}/images/bending/guiderail/guiderail_${productCode}_${type}_${size}.jpg`; + } + case 'bottombar': + return `${API_BASE}/images/bending/bottombar/bottombar_${productCode}.jpg`; + case 'smokebarrier': + return `${API_BASE}/images/bending/part/smokeban.jpg`; + default: + return ''; + } +} +``` + +--- + +### 4.3 Phase 3: 프론트엔드 타입 & 유틸리티 + +#### 파일 구조 + +``` +react/src/components/production/WorkOrders/documents/ +├── BendingWorkLogContent.tsx ← 기존 파일 (재작성) +├── bending/ +│ ├── types.ts ← 절곡 작업일지 전용 타입 +│ ├── utils.ts ← calcWeight, getMaterialMapping, getBendingImageUrl, getSLengthCode +│ ├── GuideRailSection.tsx ← 가이드레일 섹션 +│ ├── BottomBarSection.tsx ← 하단마감재 섹션 +│ ├── ShutterBoxSection.tsx ← 셔터박스 섹션 +│ ├── SmokeBarrierSection.tsx ← 연기차단재 섹션 +│ └── ProductionSummarySection.tsx ← 생산량 합계 +``` + +#### WorkOrderItem.bendingInfo 추가 (slatInfo 패턴 참고) + +```typescript +// types.ts에 추가 +export interface WorkOrderItem { + // ... 기존 필드 ... + slatInfo?: { length: number; slatCount: number; jointBar: number; glassQty: number }; + bendingInfo?: BendingInfoExtended; // ← 신규 추가 +} + +// transform 함수에 추가 (slatInfo 패턴 동일) +bendingInfo: item.options?.bending_info + ? (item.options.bending_info as BendingInfoExtended) + : undefined, +``` + +--- + +### 4.4 Phase 4: 컴포넌트 구현 상세 + +#### 4.1 GuideRailSection 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 1.1 벽면형 [130*75] │ +│ ┌─────────────────────┐ ┌──────────────────────────────────────────────────┐│ +│ │ [guiderail 이미지] │ │ 세부품명 │ 재질 │ 길이 │ 수량 │ LOT NO │ 무게 ││ +│ │ │ │──────────┼──────────┼──────┼──────┼────────┼──────││ +│ │ │ │ ①②마감재 │ SUS 1.2T │ 4000 │ 6 │ ____ │ XX.X ││ +│ │ 입고&생산 LOT NO: │ │ ③본체 │ EGI 1.55T│ 4000 │ 6 │ ____ │ XX.X ││ +│ │ ___________ │ │ ④C형 │ EGI 1.55T│ 4000 │ 6 │ ____ │ XX.X ││ +│ └─────────────────────┘ │ ⑤D형 │ EGI 1.55T│ 4000 │ 6 │ ____ │ XX.X ││ +│ │ ⑥별도마감 │ SUS 1.2T │ 4000 │ 6 │ ____ │ XX.X ││ +│ │ 하부BASE │ EGI 1.55T│135*80│ N │ ____ │ XX.X ││ +│ └──────────────────────────────────────────────────┘│ +├──────────────────────────────────────────────────────────────────────────────┤ +│ 1.2 측면형 [130*125] (동일 구조, 폭=462mm, baseSize=135*130) │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +각 길이 버킷(2438/3000/3500/4000/4300)별로 수량이 있는 행만 표시. +각 파트의 무게는 `calcWeight(재질, 폭, 길이)` × 수량으로 계산. + +#### 4.2 BottomBarSection 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 2. 하단마감재 │ +│ ┌─────────────────────┐ ┌──────────────────────────────────────────────────┐│ +│ │ [bottombar 이미지] │ │ 세부품명 │ 재질 │ 길이 │ 수량 │ LOT │ 무게 ││ +│ │ │ │─────────────┼──────────┼──────┼──────┼──────┼──────││ +│ │ │ │ ①하단마감재 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ +│ └─────────────────────┘ │ ①하단마감재 │ EGI 1.55T│ 4000 │ N │ ____ │ XX.X ││ +│ │ ④별도마감재 │ SUS 1.2T │ 3000 │ N │ ____ │ XX.X ││ +│ │ ④별도마감재 │ SUS 1.2T │ 4000 │ N │ ____ │ XX.X ││ +│ └──────────────────────────────────────────────────┘│ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +#### 4.3 ShutterBoxSection 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 3. 셔터박스 [500*380] 양면 │ +│ ┌─────────────────────┐ ┌──────────────────────────────────────────────────┐│ +│ │ [SVG 다이어그램] │ │ 구성요소 │ 재질 │ 길이 │ 수량 │ LOT │ 무게 ││ +│ │ (치수 텍스트 포함) │ │────────────┼──────────┼──────┼──────┼──────┼──────││ +│ │ boxHeight+122 │ │ ①전면부 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ +│ │ boxWidth-330 │ │ ②린텔부 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ +│ │ boxWidth-200 │ │ ③점검구 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ +│ └─────────────────────┘ │ ④후면코너부 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ +│ │ ⑤점검구 │ EGI 1.55T│ 3000 │ N │ ____ │ XX.X ││ +│ │ ⑥상부덮개 │ EGI 1.55T│ 1219 │ N │ ____ │ XX.X ││ +│ │ ⑦마구리 │ EGI 1.55T│ - │ N │ ____ │ XX.X ││ +│ └──────────────────────────────────────────────────┘│ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +#### 4.4 SmokeBarrierSection 레이아웃 + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 4. 연기차단재 │ +│ ┌─────────────────────┐ ┌──────────────────────────────────────────────────┐│ +│ │ [smokeban.jpg] │ │ 파트 │ 재질 │ 길이 │ 수량 │ LOT │ 무게 ││ +│ │ │ │───────────────┼─────────┼──────┼──────┼──────┼──────││ +│ └─────────────────────┘ │ 레일용 [W50] │EGI 0.8T │ 3000 │ N │ ____ │ XX.X ││ +│ │ 레일용 [W50] │EGI 0.8T │ 4000 │ N │ ____ │ XX.X ││ +│ │ 케이스용 [W80]│EGI 0.8T │ 3000 │ N │ ____ │ XX.X ││ +│ └──────────────────────────────────────────────────┘│ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +#### 4.5 ProductionSummarySection 레이아웃 + +``` +┌──────────────────────────────────────────────────────┐ +│ 생산량 합계(KG) │ SUS │ EGI │ 합계 │ +│ │ XX.XX kg │ XX.XX kg │ XX.XX kg │ +└──────────────────────────────────────────────────────┘ +``` + +SUS_total과 EGI_total은 4개 섹션의 모든 calcWeight 호출에서 누적. + +--- + +## 5. 모든 하드코딩 상수 (PHP 원본 기준) + +| 상수 | 값 | 용도 | +|------|-----|------| +| SUS 밀도 | 7.93 g/cm3 | calWeight | +| EGI 밀도 | 7.85 g/cm3 | calWeight | +| 벽면형 파트 폭 | 412 mm | 가이드레일 무게 계산 | +| 측면형 파트 폭 | 462 mm | 가이드레일 무게 계산 | +| 벽면형 하부BASE | 135 × 80 mm | 가이드레일 | +| 측면형 하부BASE | 135 × 130 mm | 가이드레일 | +| 하단마감재 폭 | 184 mm | 하단마감재 무게 | +| 별도마감재 폭 | 238 mm | 별도마감재 무게 | +| 연기차단재 폭 (W50/W80) | 26 mm | 연기차단재 무게 | +| 상부덮개 길이 | 1219 mm (고정) | 셔터박스 | +| 상부덮개 폭 | boxWidth - 111 | 셔터박스 | +| 전면부 치수 | boxHeight + 122 | 셔터박스 | +| 린텔부 치수 | boxWidth - 330 | 셔터박스 | +| 점검구 치수 | boxWidth - 200 | 셔터박스 | +| 후면코너부 치수 (표준/양면) | 170 | 셔터박스 | +| 가이드레일 길이 버킷 | [2438, 3000, 3500, 4000, 4300] | 길이 분류 | +| 셔터박스 길이 버킷 | [1219, 2438, 3000, 3500, 4000, 4150] | 길이 분류 | +| 하단마감재 길이 | [3000, 4000] | 길이 분류 | +| 연기차단재 W50 길이 버킷 | [2438, 3000, 3500, 4000, 4300] | 길이 분류 | +| 케이스용 W80 길이 | 3000 (고정) | 연기차단재 | +| 마구리 표시 크기 보정 | +5 mm (양쪽) | 셔터박스 | + +--- + +## 6. 컨펌 대기 목록 + +| # | 항목 | 변경 내용 | 영향 범위 | 상태 | +|---|------|----------|----------|------| +| 1 | bending_info 스키마 확장 | guideRail, bottomBar, shutterBox, smokeBarrier 필드 추가 | api options JSON | ⚠️ 컨펌 필요 | +| 2 | 이미지 파일 복사 | 5130/img/ → api/public/images/bending/ (19개 JPG) | api 서버 | ⚠️ 컨펌 필요 | +| 3 | 셔터박스 이미지 처리 | SVG 컴포넌트로 클라이언트 렌더링 (PHP GD 대체) | react | ⚠️ 컨펌 필요 | + +--- + +## 7. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-02-19 | - | 문서 초안 작성 | - | - | +| 2026-02-19 | - | 자기완결성 보완 (PHP 로직 완전 인라인, 이미지 목록, 상수 테이블, 데이터 흐름) | - | - | + +--- + +## 8. 참고 문서 & 핵심 파일 경로 + +### 수정 대상 파일 + +| 파일 | 역할 | 작업 | +|------|------|------| +| `react/src/components/production/WorkOrders/documents/BendingWorkLogContent.tsx` | 메인 컴포넌트 | **재작성** | +| `react/src/components/production/WorkOrders/types.ts` | WorkOrderItem 타입 | `bendingInfo` 필드 추가 + transform 함수 수정 | +| `react/src/components/production/WorkOrders/documents/bending/` | 신규 디렉토리 | **6개 파일 생성** (types, utils, 4개 섹션 + 합계) | + +### 참조 파일 (읽기 전용) + +| 파일 | 역할 | +|------|------| +| `5130/output/viewBendingWork_slat.php` | PHP 원본 (~1400줄) | +| `react/src/components/production/WorkerScreen/types.ts` | BendingInfo 인터페이스 (Lines 91-107) | +| `react/src/components/production/WorkerScreen/WorkLogModal.tsx` | 작업일지 모달 - BendingWorkLogContent 호출 (Lines 207-213) | +| `api/app/Services/WorkOrderService.php` | options에 bending_info 저장 (Line 276) | +| `react/src/components/production/WorkOrders/documents/SlatWorkLogContent.tsx` | 슬랫 작업일지 참고 (유사 패턴) | +| `react/src/components/production/WorkOrders/documents/index.ts` | export 파일 (BendingWorkLogContent 등록됨) | + +### 이미지 원본 경로 + +| 소스 | 대상 | 파일 수 | +|------|------|---------| +| `5130/img/guiderail/*.jpg` | `api/public/images/bending/guiderail/` | 12개 | +| `5130/img/bottombar/*.jpg` | `api/public/images/bending/bottombar/` | 6개 | +| `5130/img/part/smokeban.jpg` | `api/public/images/bending/part/` | 1개 | + +**참고**: `api/public/images/bending/` 디렉토리는 아직 존재하지 않음 → 생성 필요. + +--- + +## 9. 세션 관리 + +### Serena 메모리 ID +- `bending-worklog-state`: 진행 상태 +- `bending-worklog-snapshot`: 스냅샷 +- `bending-worklog-active-symbols`: 수정 중 파일 + +--- + +## 10. 검증 결과 + +### 10.1 성공 기준 + +| 기준 | 달성 | 비고 | +|------|------|------| +| 4개 카테고리 섹션이 PHP와 동일한 레이아웃으로 렌더링 | ⏳ | | +| SUS/EGI 무게 계산이 PHP calWeight와 동일한 결과 | ⏳ | calcWeight(SUS 1.2T, 412, 4000) 등으로 검증 | +| 생산량 합계(KG)가 SUS/EGI 별도 + 합산으로 표시 | ⏳ | | +| 가이드레일/하단마감재/연기차단재 이미지가 정상 표시 | ⏳ | | +| 셔터박스 SVG 다이어그램에 치수 텍스트 표시 | ⏳ | | +| 제품코드/마감유형에 따라 세부품명 동적 변경 | ⏳ | KQTS01 vs KTE01+SUS vs KTE01+EGI | +| 가이드레일 길이 버킷팅이 PHP first-fit과 동일 | ⏳ | | +| 빌드 에러 없음 | ⏳ | | + +### 10.2 검증 방법 +- PHP 원본: `5130/output/viewBendingWork_slat.php?num=24822` 출력과 비교 +- 무게 계산 단위 테스트: `calcWeight('SUS 1.2T', 412, 4000)` → 예상값과 비교 + - `thickness=1.2, width=412, height=4000, density=7.93` + - `volume_cm3 = (1.2 * 412 * 4000) / 1000 = 1977.6` + - `weight_kg = (1977.6 * 7.93) / 1000 = 15.68` + +--- + +## 11. 자기완결성 점검 결과 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | PHP 동일 구조 재구현 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 10.1 (8개 기준) | +| 3 | 작업 범위가 구체적인가? | ✅ | 5 Phase, 15개 작업 항목 | +| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서 = 의존성, 데이터 흐름 섹션 1.2 | +| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 8 (수정 대상 + 참조 파일 분리) | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | PHP 로직 완전 인라인 (섹션 4) | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | PHP num=24822 비교 + 단위 테스트 예시 | +| 8 | 모호한 표현이 없는가? | ✅ | 모든 상수/공식/조건 구체적으로 명시 | + +### 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------:| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | +| Q2. 데이터가 어디서 어떻게 오는가? | ✅ | 1.2 데이터 흐름 | +| Q3. 어디서부터 시작해야 하는가? | ✅ | 3.1 단계별 절차 | +| Q4. 어떤 파일을 수정/생성해야 하는가? | ✅ | 8 핵심 파일 경로 | +| Q5. PHP 원본의 계산 로직은? | ✅ | 4.1 (calWeight, 버킷팅, 재질매핑 전부 인라인) | +| Q6. 이미지 파일은 어디에 있는가? | ✅ | 4.2 (19개 파일 목록 + URL 패턴) | +| Q7. 모든 하드코딩 상수 값은? | ✅ | 섹션 5 (완전 테이블) | +| Q8. 작업 완료 확인 방법은? | ✅ | 10.1 성공 기준 + 10.2 검증 방법 | +| Q9. 막혔을 때 참고 문서는? | ✅ | 8 참고 문서 | + +**결과**: 9/9 통과 → ✅ 자기완결성 확보 + +--- + +*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/docs-update-plan.md b/plans/archive/docs-update-plan.md similarity index 100% rename from plans/docs-update-plan.md rename to plans/archive/docs-update-plan.md diff --git a/plans/document-management-system-changelog.md b/plans/archive/document-management-system-changelog.md similarity index 100% rename from plans/document-management-system-changelog.md rename to plans/archive/document-management-system-changelog.md diff --git a/plans/archive/document-system-product-inspection.md b/plans/archive/document-system-product-inspection.md new file mode 100644 index 0000000..e43682b --- /dev/null +++ b/plans/archive/document-system-product-inspection.md @@ -0,0 +1,375 @@ +# Phase 5.2: 제품검사(FQC) 폼 구현 계획 + +> **작성일**: 2026-02-10 +> **마스터 문서**: [`document-system-master.md`](./document-system-master.md) +> **상태**: 🔄 진행 중 +> **선행 조건**: Phase 5.0 (공통: 검사기준서↔컬럼 연동) 완료 필요, Phase 5.1과 병렬 진행 가능 +> **최종 분석일**: 2026-02-12 + +--- + +## 1. 개요 + +### 1.1 목적 +mng에서 제품검사(FQC) 양식 템플릿을 관리하고, React 품질관리 화면(`/quality/inspections`)에서 수주건의 **개소별** 제품검사 문서를 생성/입력/결재할 수 있도록 한다. + +### 1.2 제품검사 = 품질검사 +- 동일 개념. "제품검사(FQC: Final Quality Control)"로 통일 +- 수주건(Order) + 개소(OrderItem) 단위로 관리 +- **전수검사**: 수주 50개소 → 제품검사 문서 50건 생성 + +### 1.3 현재 상태 (2026-02-12 분석) + +| 항목 | 상태 | 비고 | +|------|:----:|------| +| React InspectionManagement | ✅ | `components/quality/InspectionManagement/` - 요청관리 CRUD (목록/등록/상세/캘린더) | +| React ProductInspectionDocument | ✅ | `quality/qms/components/documents/` - 하드코딩 11개 항목 | +| React 제품검사 모달 | ✅ | InspectionReportModal, ProductInspectionInputModal | +| React 문서시스템 뷰어 | ✅ | `components/document-system/` - DocumentViewer, TemplateInspectionContent | +| API Inspection 모델 | ✅ | `/api/v1/inspections` - JSON 기반, 단순 status (waiting→completed) | +| API Document 모델 | ✅ | EAV 정규화, 결재 워크플로우 (DRAFT→APPROVED) | +| mng 양식 템플릿 | ❌ | 미존재 (신규 생성 필요) | +| 개소별 문서 자동생성 | ❌ | 미구현 | + +### 1.4 핵심 발견 사항 + +**두 개의 독립적 검사 시스템 존재:** + +| 시스템 | 데이터 모델 | 특징 | +|--------|------------|------| +| InspectionManagement | `inspections` 테이블 (JSON) | 요청관리, 단순 상태, 결재 없음 | +| Document System | `documents` 테이블 (EAV) | 양식 기반, 결재 워크플로우, 이력 관리 | + +**세 가지 검사항목 세트 발견:** + +| 출처 | 항목 | 용도 | +|------|------|------| +| types.ts ProductInspectionData | 겉모양(가공/재봉/조립/연기차단재/하단마감재), 모터, 재질/치수, 시험 | 공장출하검사 | +| 계획문서 (이 문서) | 외관, 작동, 개폐속도, 방연/차연/내화, 안전, 비상개방, 전기배선, 설치, 부속 | **설치 후 최종검사 ← 채택** | +| QMS ProductInspectionDocument | 가공상태, 외관검사, 절단면, 도포상태, 조립, 슬릿, 규격치수, 마감처리, 내벽/마감/배색시트 | 제조품질검사 | + +### 1.5 통합 전략 (확정) + +> **InspectionManagement의 요청관리 흐름(목록/등록/상세/캘린더)은 유지하고, +> 검사 성적서 생성/입력/결재만 documents 시스템으로 전환한다.** + +- `inspections` 테이블: 검사 요청/일정/상태 관리 (meta 정보) → **유지** +- `documents` 테이블: 검사 성적서 (양식 기반 상세 데이터, 결재) → **신규 연동** +- 연결: `documents.linkable_type = 'order_item'`, `document_links`로 Order/Inspection 연결 +- 기존 InspectionReportModal/ProductInspectionInputModal → TemplateInspectionContent 기반 전환 + +### 1.6 성공 기준 +1. mng에서 제품검사 양식 편집/미리보기 정상 동작 +2. 수주 1건 선택 시 개소(OrderItem) 수만큼 Document 자동생성 +3. 각 Document에 해당 개소의 정보(층-부호, 규격, 수량) 자동매핑 +4. 개소별 검사 데이터 입력/저장/조회 가능 +5. 결재 워크플로우 정상 동작 +6. 기존 InspectionManagement 요청관리 기능 정상 유지 + +--- + +## 2. 데이터 흐름 + +``` +Order (수주) +├─ order_no: "KD-TS-260210-01" +├─ client_name: "발주처명" +├─ site_name: "현장명" +├─ quantity: 50 (총 개소 수) +└─ items: OrderItem[] (50건) + ├─ [0] floor_code="1F", symbol_code="A", specification="W7400×H2950" + ├─ [1] floor_code="1F", symbol_code="B", specification="W5200×H3100" + └─ [49] ... + +제품검사 요청 시: + ↓ +Document (50건 자동생성) +├─ Document[0] +│ ├─ template_id → 제품검사 양식 +│ ├─ linkable_type = 'App\Models\OrderItem' +│ ├─ linkable_id = OrderItem[0].id +│ ├─ document_no = "FQC-260210-01" +│ ├─ title = "제품검사 - 1F-A (W7400×H2950)" +│ └─ document_data (EAV) +│ ├─ 기본필드: 납품명, 제품명, 발주처, LOT NO, 로트크기, 검사일자, 검사자 +│ ├─ 검사데이터: 11개 항목별 적합/부적합 +│ └─ Footer: 종합판정(합격/불합격) +├─ Document[1] → OrderItem[1] +└─ Document[49] → OrderItem[49] + ++ document_links 연결: + ├─ link_key="order" → Order.id + └─ link_key="inspection" → Inspection.id (있는 경우) +``` + +### 2.1 linkable 다형성 연결 + +| 필드 | 값 | 설명 | +|------|-----|------| +| `linkable_type` | `App\Models\OrderItem` | OrderItem 모델 | +| `linkable_id` | OrderItem.id | 개소 PK | + +추가로 `document_links` 테이블을 통해: +- Order(수주) 연결: link_key="order" +- Inspection(검사요청) 연결: link_key="inspection" (InspectionManagement에서 연결 시) +- Process(공정) 연결: link_key="process" (해당되는 경우) + +--- + +## 3. 작업 항목 + +| # | 작업 | 상태 | 완료 기준 | 비고 | +|---|------|:----:|----------|------| +| 5.2.1 | mng 제품검사 양식 시더 생성 | ✅ | ProductInspectionTemplateSeeder 작성 (template_id: 65). 결재3+기본필드7+섹션2+항목11+section_fields | 2026-02-12 | +| 5.2.2 | mng 양식 편집/미리보기 검증 | ✅ | 양식 edit → 미리보기 → 저장 정상 동작 확인 | 2026-02-12 | +| 5.2.3 | API 개소별 문서 일괄생성 | ✅ | `POST /api/v1/documents/bulk-create-fqc` + `GET /api/v1/documents/fqc-status`. DocumentService에 bulkCreateFqc/fqcStatus 추가 | 2026-02-12 | +| 5.2.4 | React 제품검사 모달 → 양식 기반 전환 | ✅ | fqcActions.ts + FqcDocumentContent.tsx 신규. InspectionReportModal/ProductInspectionInputModal 듀얼모드(FQC/legacy) | 2026-02-12 | +| 5.2.5 | 개소 목록/진행현황 UI | ✅ | InspectionDetail에 FQC 진행현황 통계 바 + 개소별 상태 뱃지(합격/불합격/진행중/미생성) + 조회 버튼 | 2026-02-12 | + +--- + +## 4. 제품검사 항목 (설치 후 최종검사 11항목 - 확정) + +| # | 카테고리 | 검사항목 | 검사기준 | 검사방식 | 측정유형 | +|---|---------|---------|---------|---------|---------| +| 1 | 외관 | 외관검사 | 사용상 결함이 없을 것 | visual | checkbox | +| 2 | 기능 | 작동상태 | 정상 작동 | visual | checkbox | +| 3 | 기능 | 개폐속도 | 규정 속도 범위 이내 | visual | checkbox | +| 4 | 성능 | 방연성능 | 기준 적합 | visual | checkbox | +| 5 | 성능 | 차연성능 | 기준 적합 | visual | checkbox | +| 6 | 성능 | 내화성능 | 기준 적합 | visual | checkbox | +| 7 | 안전 | 안전장치 | 정상 작동 | visual | checkbox | +| 8 | 안전 | 비상개방 | 정상 작동 | visual | checkbox | +| 9 | 설치 | 전기배선 | 규정 적합 | visual | checkbox | +| 10 | 설치 | 설치상태 | 규정 적합 | visual | checkbox | +| 11 | 부속 | 부속품 | 누락 없음 | visual | checkbox | + +**특성:** +- 모든 항목이 visual/checkbox (적합/부적합) +- numeric 측정값 없음 → columns 구조가 중간검사보다 훨씬 단순 +- **columns 자동 파생(방안1)**: checkbox → 판정(select) 컬럼 + +**결재라인**: 작성(품질) → 검토(품질QC) → 승인(경영) +**Footer**: 부적합 내용 + 종합판정(합격/불합격) +**자동판정**: 모든 항목 적합 → 합격, 1개라도 부적합 → 불합격 + +### 4.1 양식 시더 구조 (MidInspectionTemplateSeeder 패턴) + +```php +// ProductInspectionTemplateSeeder +[ + 'name' => '제품검사 성적서', + 'category' => '품질/제품검사', + 'title' => '제 품 검 사 성 적 서', + 'company_name' => '케이디산업', + 'footer_remark_label' => '부적합 내용', + 'footer_judgement_label' => '종합판정', + 'footer_judgement_options' => ['합격', '불합격'], + + 'approval_lines' => [ + ['name' => '작성', 'dept' => '품질', 'role' => '담당자', 'sort_order' => 1], + ['name' => '검토', 'dept' => '품질', 'role' => 'QC', 'sort_order' => 2], + ['name' => '승인', 'dept' => '경영', 'role' => '대표', 'sort_order' => 3], + ], + + 'basic_fields' => [ + ['label' => '납품명', 'field_type' => 'text'], + ['label' => '제품명', 'field_type' => 'text'], + ['label' => '발주처', 'field_type' => 'text'], + ['label' => 'LOT NO', 'field_type' => 'text'], + ['label' => '로트크기', 'field_type' => 'text'], + ['label' => '검사일자', 'field_type' => 'date'], + ['label' => '검사자', 'field_type' => 'text'], + ], + + 'sections' => [ + [ + 'title' => '제품검사 기준서', + 'items' => [], // 기준서 섹션 (빈 섹션, 향후 확장) + ], + [ + 'title' => '제품검사 DATA', + 'items' => [ + ['category' => '외관', 'item' => '외관검사', ...], + // ... 11개 항목 (모두 visual/checkbox) + ], + ], + ], + + // columns는 자동 파생 (Phase 5.0 방안1) + // checkbox → [NO, 검사항목, 검사기준, 판정(select)] +] +``` + +--- + +## 5. 개소별 문서 일괄생성 로직 + +### 5.1 API 엔드포인트 (계획) + +``` +POST /api/v1/orders/{orderId}/create-fqc +Request: { template_id: number } +Response: { documents: Document[], created_count: number } +``` + +### 5.2 생성 로직 + +```php +// 1. Order + OrderItems 조회 +$order = Order::with('items')->findOrFail($orderId); + +// 2. 개소별 Document 생성 +foreach ($order->items as $index => $orderItem) { + $document = Document::create([ + 'template_id' => $templateId, + 'document_no' => "FQC-" . date('ymd') . "-" . str_pad($index + 1, 2, '0', STR_PAD_LEFT), + 'title' => "제품검사 - {$orderItem->floor_code}-{$orderItem->symbol_code} ({$orderItem->specification})", + 'status' => DocumentStatus::DRAFT, + 'linkable_type' => OrderItem::class, + 'linkable_id' => $orderItem->id, + ]); + + // 3. 기본필드 자동매핑 + $autoFillData = [ + '납품명' => $order->title, + '제품명' => $orderItem->item_name, + '발주처' => $order->client_name, + 'LOT NO' => $order->order_no, + '로트크기' => "1 EA", + ]; + + // 4. document_data에 기본필드 저장 + foreach ($autoFillData as $key => $value) { + DocumentData::create([ + 'document_id' => $document->id, + 'field_key' => Str::slug($key), + 'field_value' => $value, + ]); + } + + // 5. document_links 연결 + DocumentLink::create([ + 'document_id' => $document->id, + 'link_key' => 'order', + 'linkable_type' => Order::class, + 'linkable_id' => $order->id, + ]); + + // 6. 결재라인 초기화 + // ... (기존 패턴 재사용) +} +``` + +### 5.3 개소 진행현황 조회 + +``` +GET /api/v1/orders/{orderId}/fqc-status +Response: { + total: 50, + inspected: 30, + passed: 28, + failed: 2, + pending: 20, + items: [ + { order_item_id: 1, floor_code: "1F", symbol_code: "A", document_id: 101, status: "APPROVED", result: "합격" }, + { order_item_id: 2, floor_code: "1F", symbol_code: "B", document_id: 102, status: "DRAFT", result: null }, + ... + ] +} +``` + +--- + +## 6. 핵심 파일 경로 + +### mng +| 파일 | 용도 | 상태 | +|------|------|:----:| +| `mng/database/seeders/ProductInspectionTemplateSeeder.php` | 제품검사 양식 시더 | 🔄 작성 중 | +| `mng/database/seeders/MidInspectionTemplateSeeder.php` | 참조 패턴 (중간검사) | ✅ | + +### api +| 파일 | 용도 | 상태 | +|------|------|:----:| +| `api/app/Models/Order.php` | 수주 모델 | ✅ | +| `api/app/Models/OrderItem.php` | 수주 상세(개소) 모델 | ✅ | +| `api/app/Models/Documents/Document.php` | 문서 모델 | ✅ | +| `api/app/Models/Qualitys/Inspection.php` | 기존 검사 모델 (IQC/PQC/FQC) | ✅ | +| `api/app/Http/Controllers/Api/V1/OrderController.php` | 수주 컨트롤러 (createFqc 추가 필요) | ⏳ | +| `api/app/Services/DocumentService.php` | 문서 생성 서비스 | ✅ | + +### react +| 파일 | 용도 | 상태 | +|------|------|:----:| +| `react/src/components/quality/InspectionManagement/` | 품질검사 요청관리 (15+ 파일) | ✅ 유지 | +| `react/src/components/quality/InspectionManagement/InspectionList.tsx` | 검사 목록 | ✅ 유지 | +| `react/src/components/quality/InspectionManagement/InspectionDetail.tsx` | 검사 상세 | 🔄 수정 필요 | +| `react/src/components/quality/InspectionManagement/modals/InspectionReportModal.tsx` | 성적서 모달 | 🔄 전환 필요 | +| `react/src/components/quality/InspectionManagement/modals/ProductInspectionInputModal.tsx` | 입력 모달 | 🔄 전환 필요 | +| `react/src/components/document-system/viewer/DocumentViewer.tsx` | 문서 뷰어 | ✅ | +| `react/src/components/document-system/content/TemplateInspectionContent.tsx` | 양식 기반 렌더링 | ✅ | +| `react/src/app/[locale]/(protected)/quality/qms/components/documents/ProductInspectionDocument.tsx` | 하드코딩 문서 | ❌ 대체 예정 | + +--- + +## 7. 기존 Inspection 모델과의 관계 (통합 전략) + +### 7.1 현재 구조 + +``` +inspections 테이블 (JSON 기반) +├─ inspection_type: IQC/PQC/FQC +├─ status: waiting → in_progress → completed +├─ meta: { ... } (JSON) +├─ items: { ... } (JSON - 검사 결과) +└─ extra: { ... } (JSON) + +documents 테이블 (EAV 정규화) +├─ template_id → document_templates +├─ status: DRAFT → PENDING → APPROVED/REJECTED +├─ linkable_type + linkable_id (다형성) +├─ document_data (EAV - 섹션/컬럼/행 기반) +└─ document_approvals (결재 이력) +``` + +### 7.2 통합 후 구조 + +``` +InspectionManagement (요청관리 레이어) - 유지 +├─ 검사 목록/등록/상세/캘린더 +├─ inspections 테이블 (요청/일정/상태) +└─ API: /api/v1/inspections (CRUD) + +Document System (성적서 레이어) - 신규 연동 +├─ 양식 기반 검사 데이터 입력 +├─ documents 테이블 (EAV + 결재) +├─ linkable → OrderItem (개소별) +└─ document_links → Order, Inspection + +연결 포인트: +├─ InspectionDetail에서 "성적서 작성/조회" 시 → Document System 호출 +├─ InspectionReportModal → TemplateInspectionContent 기반 전환 +└─ ProductInspectionInputModal → 양식 기반 입력으로 전환 +``` + +--- + +## 8. 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2026-02-10 | Phase 5.2 계획 문서 신규 생성 | +| 2026-02-10 | 방안1 반영: 시더에 section_fields 필수, columns 자동 파생. 선행조건 Phase 5.0 추가 | +| 2026-02-12 | 코드베이스 분석 반영: InspectionManagement 발견, 3개 검사항목 세트 정리, 통합 전략 확정 | +| 2026-02-12 | 설치 후 최종검사 11항목 확정, documents 기반 통합 방향 확정 | +| 2026-02-12 | 5.2.1 ProductInspectionTemplateSeeder 작성 완료 (template_id: 65) | +| 2026-02-12 | 5.2.2 mng 양식 편집/미리보기 검증 완료 | +| 2026-02-12 | 5.2.3 API bulk-create-fqc + fqc-status 엔드포인트 구현 완료 | +| 2026-02-12 | 5.2.4 React fqcActions.ts + FqcDocumentContent + 모달 듀얼모드 전환 완료 | +| 2026-02-12 | 5.2.5 InspectionDetail FQC 진행현황 통계 바 + 개소별 상태/조회 UI 완료 | +| 2026-02-12 | **Phase 5.2 전체 완료 (5/5)** | + +--- + +*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/fcm-user-targeted-notification-plan.md b/plans/archive/fcm-user-targeted-notification-plan.md similarity index 100% rename from plans/fcm-user-targeted-notification-plan.md rename to plans/archive/fcm-user-targeted-notification-plan.md diff --git a/plans/archive/formula-engine-real-data-plan.md b/plans/archive/formula-engine-real-data-plan.md new file mode 100644 index 0000000..7114c42 --- /dev/null +++ b/plans/archive/formula-engine-real-data-plan.md @@ -0,0 +1,1077 @@ +# 수식 엔진 실제 데이터 연동 계획 + +> **작성일**: 2026-02-19 +> **목적**: FormulaEvaluatorService의 테스트 데이터(SF-/SM-)를 실제 품목(BD-)으로 재구성 +> **기준 문서**: `docs/features/quotes/README.md`, `docs/rules/item-policy.md` +> **상태**: ✅ 완료 (Phase 1-3,5 완료 / Phase 4 후순위 보류) + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | 문서 최종 업데이트 및 검증 결과 반영 | +| **다음 작업** | 없음 (Phase 4 Generic 데이터는 후순위 보류) | +| **진행률** | 4/5 완료 (Phase 1-3,5 ✅ / Phase 4 ⏭️ 후순위) | +| **마지막 업데이트** | 2026-02-20 17:00 | + +--- + +## 1. 개요 + +### 1.1 배경 + +수식 엔진(FormulaEvaluatorService)에는 두 가지 실행 경로가 있다: +- **Generic 경로**: `quote_formula_*` 4개 테이블 기반 (데이터 드리븐) +- **Kyungdong 경로**: `KyungdongFormulaHandler` 코드 기반 (tenant_id=287 전용) + +**현재 문제:** +1. Generic 경로의 `quote_formula_items` (24건)이 모두 삭제된 SF-/SM- 테스트 품목을 참조 +2. `quote_formula_ranges` (12건)도 모두 SF- 코드 반환 +3. `quote_formula_mappings`는 비어있음 +4. Mapping 수식(id:20,21)이 참조하는 product_id 468, 473도 삭제됨 +5. Kyungdong 핸들러는 BD- 품목을 참조하지만, EST- 코드 일부가 items 테이블에 미등록 +6. 핸들러가 `KyungdongFormulaHandler`로 하드코딩 → 업체 추가 시 확장 불가 구조 + +### 1.2 두 경로 비교 + +| 구분 | Generic 경로 | Kyungdong 경로 | +|------|-------------|---------------| +| **진입 조건** | 전용 핸들러 없는 tenant | 전용 핸들러 있는 tenant | +| **BOM 구성** | quote_formula_items + items.bom 전개 | 코드 기반 동적 조립 | +| **모델 인식** | 없음 (단일 수식 세트) | 모델/마감/타입별 분기 | +| **아이템 참조** | SF-/SM- (삭제됨) | BD- 동적 코드 조합 + EST- 코드 | +| **단가 조회** | prices 테이블 + items.attributes | EstimatePriceService | +| **핸들러 해석** | FormulaHandlerFactory → null → Generic | FormulaHandlerFactory → Tenant{id}/FormulaHandler | +| **상태** | ⏭️ FG.bom 비어있음 (후순위) | ✅ 정비 완료 | + +### 1.3 실행 흐름 (MNG → API) + +#### 현재 (Before) +``` +FormulaEvaluatorService::calculateBomWithDebug() + │ + ├─ if ($tenantId === 287) ← 하드코딩! + │ └─ new KyungdongFormulaHandler() ← 직접 생성! + │ + └─ else → Generic 10단계 +``` + +#### 목표 (After) - Strategy + Factory, Zero Config +``` +[MNG 품목관리 UI] + │ 사용자가 FG 선택 + W0/H0/QTY/MP 입력 + ▼ +ItemManagementApiController::calculateFormula() (mng, 라인 60-86) + │ $item->code, {W0, H0, QTY, MP}, session('selected_tenant_id') + ▼ +FormulaApiService::calculateBom() (mng, 라인 24-82) + │ POST https://nginx/api/v1/quotes/calculate/bom + │ Headers: X-API-KEY, X-TENANT-ID + ▼ +FormulaEvaluatorService::calculateBomWithDebug() (api, 라인 592-596) + │ + ├─ FormulaHandlerFactory::make($tenantId) + │ │ class_exists("Handlers\Tenant{$tenantId}\FormulaHandler") ? + │ │ + │ ├─ 핸들러 존재 → calculateTenantBom($handler, ...) + │ │ └─ Tenant287/FormulaHandler::calculateDynamicItems() + │ │ ├─ calculateSteelItems() → BD- 절곡품 (10종) + │ │ ├─ calculatePartItems() → EST- 부자재 (5종) + │ │ └─ 모터/제어기/주자재/검사비 + │ │ + │ └─ 핸들러 없음 (null) → 10단계 Generic 계산 (라인 613-791) + │ └─ quote_formula_* 테이블 (DB 드리븐) + │ + ▼ +[BOM 결과 JSON 반환] +``` + +#### 핸들러 자동 발견 원리 +``` +FormulaHandlerFactory::make(287) + → class_exists("App\Services\Quote\Handlers\Tenant287\FormulaHandler") + → YES → new Tenant287\FormulaHandler() + → 인터페이스 TenantFormulaHandler 구현 보장 + +FormulaHandlerFactory::make(999) + → class_exists("App\Services\Quote\Handlers\Tenant999\FormulaHandler") + → NO → return null → Generic DB 경로 +``` + +**업체 추가 시**: `Handlers/Tenant{id}/FormulaHandler.php` 파일 1개만 생성. 설정/매핑 불필요. + +### 1.4 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. 업체별 핸들러 구조화 (Tenant{id} 기반 자동 발견, Zero Config) │ +│ 2. 경동(287) 핸들러가 실제 운영 로직 (우선 정비) │ +│ 3. Generic 경로는 핸들러 없는 테넌트용 (DB 드리븐, 후순위) │ +│ 4. 품목 마스터에 실제 품목이 모두 등록되어야 함 │ +│ 5. 수식 데이터는 실제 품목 코드만 참조 │ +│ 6. 기존 테스트 데이터는 삭제하지 않음 (완전 이관 후 별도 삭제) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.5 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | items 테이블에 EST- 품목 등록, 핸들러 디렉토리 구조 변경(이동) | 불필요 | +| ⚠️ 컨펌 필요 | 인터페이스/팩토리 신규 생성, FormulaEvaluatorService 분기 로직 변경, quote_formula_* 데이터 추가 | **필수** | +| 🔴 금지 | 테이블 스키마 변경, 핸들러 핵심 계산 로직 변경 | 별도 협의 | + +--- + +## 2. 현황 분석 + +### 2.1 items 테이블 현황 (tenant_id=287) + +| 코드 접두어 | item_type | 건수 | 설명 | 상태 | +|------------|-----------|------|------|------| +| FG- | FG | 18 | 완제품 (7모델 × 타입/마감 조합) | ✅ 정상 | +| BD- | PT | 58 | 절곡물 (모델별 가이드레일/케이스/마구리 등) | ✅ 정상 | +| PT- (레거시) | PT | ~650 | 레거시 부품 (5자리 숫자 코드) | ✅ 정상 | +| RM- | RM | 28 | 원자재 | ✅ 정상 | +| SM- | SM | 61 | 부자재 (레거시) | ✅ 정상 | +| CS- | CS | 4 | 소모품 | ✅ 정상 | +| SF- | - | 0 | 삭제됨 (테스트 데이터) | ❌ 삭제 완료 | +| EST- | PT | 72 | 부자재 (모터/제어기/샤프트/앵글/파이프/원자재 등) | ✅ 등록 완료 | + +### 2.2 KyungdongFormulaHandler가 참조하는 미등록 품목 + +> **중요**: 핸들러는 `EST-` 접두어를 사용 (이전 문서의 `ST-`는 오류) + +#### EST- 코드 (items 미등록, 핸들러가 동적 생성) + +| 코드 패턴 | 라인 | 메서드 | 용도 | 대안 | +|-----------|------|--------|------|------| +| `EST-SMOKE-케이스용` | 519 | calculateSteelItems | 케이스용 연기차단재 | `BD-케이스용 연기차단재` (id:15587) | +| `EST-SMOKE-레일용` | 557 | calculateSteelItems | 가이드레일용 연기차단재 | `BD-가이드레일용 연기차단재` (id:15572) | +| `EST-SHAFT-{size}인치-{length}` | 795 | calculatePartItems | 감기샤프트 | 신규 등록 | +| `EST-PIPE-1.4-{length}` | 854,868 | calculatePartItems | 앵글파이프 | 신규 등록 | +| `EST-ANGLE-BRACKET-{type}` | 891 | calculatePartItems | 모터받침 앵글 | 신규 등록 | +| `EST-ANGLE-MAIN-{type}-{size}` | 912 | calculatePartItems | 부자재 앵글 | 신규 등록 | +| `EST-INSPECTION` | 1010 | calculateDynamicItems | 검사비 | 신규 등록 | +| `EST-RAW-스크린-{type}` | 1019 | calculateDynamicItems | 스크린 원단 | 신규 등록 | +| `EST-RAW-슬랫-{type}` | 1025 | calculateDynamicItems | 슬랫 원단 | 신규 등록 | +| `EST-MOTOR-{voltage}-{capacity}` | 1044 | calculateDynamicItems | 모터 | 신규 등록 | +| `EST-CTRL-{type}` | 1062 | calculateDynamicItems | 제어기 | 신규 등록 | +| `EST-CTRL-뒷박스` | 1087 | calculateDynamicItems | 뒷박스 제어기 | 신규 등록 | + +#### 레거시 숫자 코드 (items 등록됨) + +| 코드 | 라인 | items.id | items.name | item_type | unit | 용도 | +|------|------|----------|-----------|-----------|------|------| +| `00035` | 564 | 14939 | 철재용하장바(SUS)3000 | PT | EA | 하장바 SUS | +| `00036` | 564 | 14940 | 철재용하장바(SUS1.2T) | SM | M | 하장바 EGI | +| `00021` | 619 | 14928 | 평철12T | PT | M | 무게평철12T | +| `90201` | 631 | 15188 | KD환봉(30파이) | PT | EA | 환봉 30파이 (기본) | +| `90202` | 628 | 15189 | KD환봉 | PT | EA | 환봉 35파이 | +| `90203` | 629 | 15190 | KD환봉 | PT | EA | 환봉 45파이 | +| `90204` | 630 | 15191 | KD환봉 | PT | EA | 환봉 50파이 | +| `00013` | - | 14922 | 점검구3 | PT | EA | 점검구 (핸들러에서 미사용) | + +### 2.3 quote_formula_* 현황 + +#### quote_formulas (21건, tenant_id=1) + +| id | type | variable | name | formula | output_type | +|----|------|----------|------|---------|-------------| +| 1 | input | PC | 제품 카테고리 | (없음) | variable | +| 2 | input | W0 | 오픈사이즈 폭 | (없음) | variable | +| 3 | input | H0 | 오픈사이즈 높이 | (없음) | variable | +| 4 | input | GT | 가이드레일 설치유형 | (없음) | variable | +| 5 | input | MP | 모터 전원 | (없음) | variable | +| 6 | input | CT | 연동제어기 | (없음) | variable | +| 7 | input | QTY | 수량 | (없음) | variable | +| 8 | calculation | W1_SCREEN | 제작폭 W1 (스크린) | W0 + 140 | variable | +| 9 | calculation | W1_STEEL | 제작폭 W1 (철재) | W0 + 110 | variable | +| 10 | calculation | H1 | 제작높이 H1 | H0 + 350 | variable | +| 11 | calculation | W | 제작폭 (W) | IF(PC=="스크린", W0+140, W0+110) | variable | +| 12 | calculation | H | 제작높이 (H) | H0 + 350 | variable | +| 13 | calculation | M | 면적 (M) | W * H / 1000000 | variable | +| 14 | calculation | K_SCREEN | 중량 K (스크린) | M * 2 + W0 / 1000 * 14.17 | variable | +| 15 | calculation | K_STEEL | 중량 K (철재) | M * 25 | variable | +| 16 | calculation | K | 중량 (K) | IF(PC=="스크린", M*2+W0/1000*14.17, M*25) | variable | +| 17 | range | MOTOR | 모터 자동선택 | K | item | +| 18 | range | GUIDE | 가이드레일 자동선택 | H | item | +| 19 | range | CASE | 케이스 자동선택 | W | item | +| 20 | mapping | BOM_SCR_001 | FG-SCR-001 BOM 매핑 | (없음) | item | +| 21 | mapping | BOM_STL_001 | FG-STL-001 BOM 매핑 | (없음) | item | + +- id 20: product_id=468 (삭제됨) +- id 21: product_id=473 (삭제됨) + +#### quote_formula_items (24건) - 전부 삭제된 코드 + +| id | formula_id | item_code | item_name | sort | +|----|-----------|-----------|-----------|------| +| 1 | 20 | SF-SCR-F01 | 스크린 원단 | 1 | +| 2 | 20 | SF-SCR-F02 | 가이드레일 (좌) | 2 | +| 3 | 20 | SF-SCR-F03 | 가이드레일 (우) | 3 | +| 4 | 20 | SF-SCR-F04 | 케이스 | 4 | +| 5 | 20 | SF-SCR-F05 | 하부프레임 | 5 | +| 6 | 20 | SF-SCR-M01 | 모터 (소형) | 6 | +| 7 | 20 | SF-SCR-C01 | 제어반 | 7 | +| 8 | 20 | SF-SCR-S01 | 셋팅박스 | 8 | +| 9 | 20 | SF-SCR-SW01 | 권선드럼 | 9 | +| 10 | 20 | SF-SCR-B01 | 브라켓 세트 | 10 | +| 11 | 20 | SF-SCR-SW01 | 스위치 | 11 | +| 12 | 20 | SM-B002 | 볼트 M8x25 | 12 | +| 13 | 20 | SM-N002 | 너트 M8 | 13 | +| 14 | 20 | SM-W002 | 와셔 M8 | 14 | +| 15 | 21 | SF-STL-P01 | 도어 패널 | 1 | +| 16 | 21 | SF-STL-F01 | 문틀 프레임 | 2 | +| 17 | 21 | SF-STL-G01 | 유리창 | 3 | +| 18 | 21 | SF-STL-H01 | 힌지 | 4 | +| 19 | 21 | SF-STL-L01 | 잠금장치 | 5 | +| 20 | 21 | SF-STL-C01 | 도어클로저 | 6 | +| 21 | 21 | SF-STL-S01 | 실링재 | 7 | +| 22 | 21 | SF-STL-PT01 | 파우더 도장 | 8 | +| 23 | 21 | SM-B002 | 볼트 M8x25 | 9 | +| 24 | 21 | SM-N002 | 너트 M8 | 10 | + +#### quote_formula_ranges (12건) - 전부 삭제된 코드 + +| id | formula_id | condition_variable | min | max | result_value | +|----|-----------|-------------------|-----|-----|--------------| +| 1 | 17 (MOTOR) | K | 0 | 30 | SF-SCR-M01 | +| 2 | 17 | K | 30 | 50 | SF-SCR-M02 | +| 3 | 17 | K | 50 | 80 | SF-SCR-M03 | +| 4 | 17 | K | 80 | 9999 | SF-SCR-M04 | +| 5 | 18 (GUIDE) | H | 0 | 2500 | SF-SCR-F02 | +| 6 | 18 | H | 2500 | 3500 | SF-SCR-F02 | +| 7 | 18 | H | 3500 | 4500 | SF-SCR-F02 | +| 8 | 18 | H | 4500 | 9999 | SF-SCR-F02 | +| 9 | 19 (CASE) | W | 0 | 2000 | SF-SCR-F04 | +| 10 | 19 | W | 2000 | 3000 | SF-SCR-F04 | +| 11 | 19 | W | 3000 | 4000 | SF-SCR-F04 | +| 12 | 19 | W | 4000 | 9999 | SF-SCR-F04 | + +#### quote_formula_mappings (0건) - 비어있음 + +### 2.4 FG 모델 매트릭스 + +| 모델 | 카테고리 | 마감 | 가이드레일 타입 | BD 부품 수 | +|------|---------|------|---------------|-----------| +| KSS01 | 스크린 | SUS | 벽면/측면 | 4 (가이드레일×2, 하단마감재, L-BAR) | +| KSS02 | 스크린 | SUS | 벽면/측면 | 4 | +| KSE01 | 스크린 | SUS+EGI | 벽면/측면 | 8 | +| KWE01 | 스크린 | SUS+EGI | 벽면/측면 | 8 | +| KQTS01 | 철재 | SUS | 벽면/측면 | 3 (가이드레일×2, 하단마감재) | +| KTE01 | 철재 | SUS+EGI | 벽면/측면 | 6 | +| KDSS01 | (FG없음) | SUS | 벽면/측면 | 4 | + +### 2.5 가이드레일 규격 매핑 (모델별) + +``` +KSS01/KSS02/KSE01/KWE01 → 벽면: 120*70, 측면: 120*120 +KTE01/KQTS01 → 벽면: 130*75, 측면: 130*125 +KDSS01 → 벽면: 150*150, 측면: 150*212 +``` + +--- + +## 3. 대상 범위 + +### Phase 1: 누락 품목 등록 (items 테이블) ✅ 완료 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1.1 | EST-SMOKE 코드 → Phase 3.1로 이관 (핸들러 코드 수정) | ⏭️ | Phase 3에서 처리 | +| 1.2 | EST-MOTOR 품목 등록 (150K~2000K, 전압별) | ✅ | 21건 확인 (220V 8종 + 380V 13종) | +| 1.3 | EST-CTRL 품목 등록 (제어기 종류별) | ✅ | 20건 확인 (기본3 + 방범9 + 방화4 + 기타4) | +| 1.4 | EST-SHAFT 품목 등록 (인치×길이별) | ✅ | 16건 확인 (3~12인치) | +| 1.5 | EST-PIPE 품목 등록 | ✅ | 3건 확인 (1.4T×2 + 2T×1) | +| 1.6 | EST-ANGLE 품목 등록 | ✅ | 8건 확인 (BRACKET 4 + MAIN 4) | +| 1.7 | EST-INSPECTION 품목 등록 | ✅ | 1건 확인 | +| 1.8 | EST-RAW 원자재 품목 등록 | ✅ | 6건 확인 (스크린3 + 슬랫3) | + +### Phase 2: 핸들러 구조화 (Strategy + Factory, Zero Config) ✅ 완료 + +> **설계 원칙**: tenant_id 기반 자동 발견. 설정/매핑/options 없이 클래스 존재 여부만으로 라우팅. + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | `TenantFormulaHandler` 인터페이스 생성 | ✅ | `Contracts/TenantFormulaHandler.php` | +| 2.2 | `FormulaHandlerFactory` 생성 (class_exists 자동 발견) | ✅ | `FormulaHandlerFactory.php` (35줄) | +| 2.3 | `KyungdongFormulaHandler` → `Tenant287/FormulaHandler`로 이동 | ✅ | namespace + implements 완료, 원본 삭제 | +| 2.4 | `FormulaEvaluatorService` 분기 로직 변경 | ✅ | KYUNGDONG_TENANT_ID 상수 제거, Factory::make() 사용 | +| 2.5 | `calculateKyungdongBom()` → `calculateTenantBom()` 일반화 | ✅ | 메서드명 + 파라미터(handler) + 문자열 일반화 | + +### Phase 3: 핸들러 아이템 코드 정비 (Tenant287/FormulaHandler) ✅ 완료 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 3.1 | EST-SMOKE 코드 → BD- 코드로 변경 | ✅ | BD-케이스용 연기차단재(id:15587), BD-가이드레일용 연기차단재(id:15572) | +| 3.2 | 레거시 숫자 코드(00035, 00036 등) 유지 | ✅ | items 테이블에 등록됨, 변경 불필요 | +| 3.3 | lookupItem 실패 시 Log::warning() 추가 | ✅ | tenant_id, code 포함 경고 로그 | +| 3.4 | tinker E2E 테스트 통과 | ✅ | 17건, 1,167,934원 (KQTS01-SUS-벽면형) | + +### Phase 4: Generic 수식 데이터 재구성 (quote_formula_* 테이블) ⏭️ 후순위 + +> **분석 결과**: Generic 경로는 `items.bom` JSON 필드 기반이나, FG 품목의 bom 필드가 비어있음. +> `quote_formula_*` 테이블은 독립 수식 평가 기능용으로, 메인 BOM 계산 경로에서 직접 사용하지 않음. +> Tenant 287은 핸들러 경로를 사용하므로 현재 실질적 영향 없음. 다른 테넌트 추가 시 진행. + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 4.1 | 실제 FG 제품용 mapping 수식 신규 생성 | ⏭️ | 다른 테넌트 추가 시 | +| 4.2 | quote_formula_items에 실제 BD- 코드 BOM 세트 추가 | ⏭️ | FG.bom 필드 구성 선행 필요 | +| 4.3 | quote_formula_ranges에 실제 BD- 코드 범위 추가 | ⏭️ | | +| 4.4 | quote_formula_mappings 구성 (FG → BD 모델별 매핑) | ⏭️ | | +| 4.5 | FormulaEvaluatorService 모델 인식 로직 추가 | ⏭️ | | + +### Phase 5: 통합 테스트 및 검증 ✅ 완료 + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 5.1 | 7모델 전수 BOM 계산 테스트 (벽면형) | ✅ | 7모델 전부 PASS (18건씩, 1.1M~1.3M원) | +| 5.1b | 측면형 + 대형 규격 테스트 (3000×3000, QTY=2) | ✅ | 3모델 PASS (18건씩, 2.9M~3.2M원) | +| 5.2 | Factory 엣지 케이스 테스트 | ✅ | tenant 0/-1/999999→null, 287→Handler | +| 5.3 | SF-/SM- 잔여 참조 점검 (코드 기준) | ✅ | api/Services/Quote/ 내 참조 0건 | +| 5.4 | React 견적관리 BOM 테스트 | ⏭️ | Phase 4 후순위와 함께 | + +--- + +## 4. 작업 절차 + +### 4.1 단계별 절차 + +``` +Phase 1: 누락 품목 등록 +├── 1.1 EST-SMOKE → BD- 매핑 (코드만 변경, 품목 신규 등록 불필요) +├── 1.2~1.8 EST- 품목 등록 (items 테이블 INSERT) +│ ├── 코드: EST- 접두어 유지 (핸들러 코드와 일치) +│ ├── item_type: PT, tenant_id: 287 +│ └── options: { lot_managed: false, consumption_method: "none" } +└── 등록 후 lookupItem() 호출로 매핑 확인 + +Phase 2: 핸들러 구조화 (Strategy + Factory, Zero Config) +├── 2.1 TenantFormulaHandler 인터페이스 생성 +│ └── Contracts/TenantFormulaHandler.php (신규) +├── 2.2 FormulaHandlerFactory 생성 +│ └── class_exists("Handlers\Tenant{$tenantId}\FormulaHandler") 자동 발견 +├── 2.3 KyungdongFormulaHandler → Tenant287/FormulaHandler 이동 +│ ├── namespace 변경: Handlers → Handlers\Tenant287 +│ ├── implements TenantFormulaHandler 추가 +│ └── 클래스 docblock에 "경동기업 (tenant_id: 287)" 명시 +├── 2.4 FormulaEvaluatorService 분기 로직 변경 +│ ├── 제거: private const KYUNGDONG_TENANT_ID = 287 +│ ├── 제거: if ($tenantId === self::KYUNGDONG_TENANT_ID) +│ └── 추가: $handler = FormulaHandlerFactory::make($tenantId) +└── 2.5 calculateKyungdongBom() → calculateTenantBom($handler, ...) 일반화 + +Phase 3: 핸들러(Tenant287) 아이템 코드 정비 +├── 3.1 EST-SMOKE 코드 변경 (2곳) +│ ├── 라인 519: 'EST-SMOKE-케이스용' → 'BD-케이스용 연기차단재' +│ └── 라인 557: 'EST-SMOKE-레일용' → 'BD-가이드레일용 연기차단재' +├── 3.2 레거시 코드 검토 (00035, 00036, 00021, 90201~90204) +│ └── 현재 items 테이블에 등록되어 있으므로 동작함. 변경 여부 검토만. +├── 3.3 lookupItem()에 미등록 품목 경고 로깅 추가 +│ └── 라인 42-48: null 반환 시 Log::warning() +└── 3.4 MNG 연동 테스트 (https://mng.sam.kr/item-management) + +Phase 4: Generic 수식 데이터 재구성 (기존 데이터 유지, 실제 데이터 추가) +├── 4.1 실제 FG 제품용 mapping 수식 신규 생성 (quote_formulas INSERT) +├── 4.2~4.4 실제 데이터 INSERT (기존 테스트 데이터와 병행) +│ ├── quote_formula_items: BD-/EST- 코드 기반 BOM 구성 +│ ├── quote_formula_ranges: 실제 규격별 BD- 코드 반환 +│ └── quote_formula_mappings: FG 모델 → BD 부품 매핑 +└── 4.5 FormulaEvaluatorService에 모델 인식 로직 추가 + +Phase 5: 통합 테스트 +├── 5.1 MNG 품목관리 - 7모델 전수 테스트 +├── 5.2 React 견적관리 - BOM 계산 테스트 +├── 5.3 단가 정합성 검증 +└── 5.4 잔여 테스트 데이터 참조 점검 +``` + +### 4.2 EST- 품목 등록 상세 + +#### items INSERT 템플릿 + +```sql +INSERT INTO items (tenant_id, item_type, code, name, unit, is_active, created_at, updated_at) +VALUES (287, 'PT', '{code}', '{name}', '{unit}', 1, NOW(), NOW()); +``` + +#### 등록 대상 품목 목록 + +``` +EST-MOTOR-{voltage}-{capacity}: 모터 (전압-용량) +├── EST-MOTOR-220V-150K 150K 모터 220V +├── EST-MOTOR-220V-300K 300K 모터 220V +├── EST-MOTOR-220V-400K 400K 모터 220V +├── EST-MOTOR-220V-500K 500K 모터 220V +├── EST-MOTOR-220V-600K 600K 모터 220V +├── EST-MOTOR-380V-500K 500K 모터 380V +├── EST-MOTOR-380V-600K 600K 모터 380V +├── EST-MOTOR-380V-800K 800K 모터 380V +├── EST-MOTOR-380V-1000K 1000K 모터 380V +└── item_type: PT, unit: EA + +EST-CTRL-{type}: 제어기 +├── EST-CTRL-뒷박스 뒷박스 제어기 +├── EST-CTRL-일반 일반 제어기 +├── EST-CTRL-동보 동보 제어기 +├── EST-CTRL-자탈 자탈 제어기 +├── EST-CTRL-셋팅 셋팅 박스 +└── item_type: PT, unit: EA + +EST-SHAFT-{inch}인치-{length}: 감기샤프트 +├── EST-SHAFT-3인치-300 3인치 300mm +├── EST-SHAFT-4인치-3000 4인치 3000mm +├── EST-SHAFT-4인치-4500 4인치 4500mm +├── EST-SHAFT-4인치-6000 4인치 6000mm +├── EST-SHAFT-5인치-6000 5인치 6000mm +├── EST-SHAFT-5인치-7000 5인치 7000mm +├── EST-SHAFT-5인치-8200 5인치 8200mm +└── item_type: PT, unit: EA + +EST-PIPE-1.4-{length}: 앵글파이프 +├── EST-PIPE-1.4-3000 1.4T 3000mm +├── EST-PIPE-1.4-4500 1.4T 4500mm (핸들러에 없지만 패턴상 추가) +├── EST-PIPE-1.4-6000 1.4T 6000mm +└── item_type: PT, unit: EA + +EST-ANGLE-BRACKET-{type}: 모터받침 앵글 +├── EST-ANGLE-BRACKET-스크린용 +├── EST-ANGLE-BRACKET-철제300K +├── EST-ANGLE-BRACKET-철제400K +├── EST-ANGLE-BRACKET-철제500K이상 +└── item_type: PT, unit: EA + +EST-ANGLE-MAIN-{type}-{size}: 부자재 앵글 +├── EST-ANGLE-MAIN-앵글3T-2.5 +├── EST-ANGLE-MAIN-앵글3T-10 +├── EST-ANGLE-MAIN-앵글4T-2.5 +└── item_type: PT, unit: EA + +EST-INSPECTION: 검사비 +└── item_type: PT, unit: EA + +EST-RAW-스크린-{type}: 스크린 원단 +├── EST-RAW-스크린-실리카 +└── item_type: PT, unit: ㎡ + +EST-RAW-슬랫-{type}: 슬랫 원단 +├── EST-RAW-슬랫-방화 +└── item_type: PT, unit: ㎡ +``` + +> **참고**: 핸들러가 동적으로 코드를 조합하므로, 실제 사용되는 코드 조합만 등록. +> 등록 후 `lookupItem()` 호출 시 item_id/name이 정상 반환되는지 확인. + +--- + +## 5. 컨펌 대기 목록 + +| # | 항목 | 변경 내용 | 영향 범위 | 상태 | +|---|------|----------|----------|------| +| 1 | 핸들러 구조화 | 인터페이스 + 팩토리 신규, 핸들러 이동 | Services/Quote/ 전체 | ✅ 완료 | +| 2 | FormulaEvaluatorService 분기 변경 | if(287) → Factory::make() | 전체 테넌트 | ✅ 완료 | +| 3 | EST- 품목 코드 체계 | 72건 이미 등록 확인 | items 테이블 | ✅ 완료 (사전 등록됨) | +| 4 | EST-SMOKE → BD- 코드 변경 | 핸들러 라인 519, 557 변경 | Tenant287/FormulaHandler | ✅ 완료 | +| 5 | 레거시 숫자코드 유지 | 00035, 00036 등 유지 결정 | Tenant287/FormulaHandler | ✅ 유지 (items에 등록됨) | +| 6 | Generic 경로에 모델 인식 추가 | 후순위 보류 (Phase 4) | 핸들러 없는 테넌트 | ⏭️ 후순위 | + +--- + +## 6. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-02-19 | - | 문서 초안 작성 | - | - | +| 2026-02-19 | - | 자기완결성 보완 (부록 추가) | - | - | +| 2026-02-20 | Phase 1 | EST- 품목 72건 이미 등록 확인 → Phase 1 완료 | items 테이블 | ✅ | +| 2026-02-20 | Phase 2 | TenantFormulaHandler 인터페이스 + FormulaHandlerFactory 생성 | Contracts/TenantFormulaHandler.php, FormulaHandlerFactory.php | ✅ | +| 2026-02-20 | Phase 2 | KyungdongFormulaHandler → Tenant287/FormulaHandler 이동 | Handlers/Tenant287/FormulaHandler.php (신규), Handlers/KyungdongFormulaHandler.php (삭제) | ✅ | +| 2026-02-20 | Phase 2 | FormulaEvaluatorService 분기 로직 변경 (if(287) → Factory::make()) | FormulaEvaluatorService.php | ✅ | +| 2026-02-20 | Phase 2 | calculateKyungdongBom() → calculateTenantBom() 일반화 | FormulaEvaluatorService.php | ✅ | +| 2026-02-20 | Phase 3 | EST-SMOKE-케이스용 → BD-케이스용 연기차단재 (id:15587) | Tenant287/FormulaHandler.php | ✅ | +| 2026-02-20 | Phase 3 | EST-SMOKE-레일용 → BD-가이드레일용 연기차단재 (id:15572) | Tenant287/FormulaHandler.php | ✅ | +| 2026-02-20 | Phase 3 | lookupItem() 미등록 품목 Log::warning() 추가 | Tenant287/FormulaHandler.php | ✅ | +| 2026-02-20 | Phase 4 | Generic 경로 분석 → items.bom 기반, FG.bom 비어있음 → 후순위 결정 | - | ⏭️ | +| 2026-02-20 | Phase 5 | 벽부형 7모델 + 측면형 3모델 tinker 통합 테스트 PASS | - | ✅ | +| 2026-02-20 | Phase 5 | Factory 엣지케이스 + SF-/SM- 잔존 참조 점검 완료 | - | ✅ | +| 2026-02-20 | - | 문서 최종 업데이트 (검증결과, 변경이력, 상태 반영) | formula-engine-real-data-plan.md | ✅ | + +--- + +## 7. 참고 문서 + +- **견적 시스템**: `docs/features/quotes/README.md` +- **품목 정책**: `docs/rules/item-policy.md` +- **DB 스키마**: `docs/specs/database-schema.md` +- **빠른 시작**: `docs/quickstart/quick-start.md` + +--- + +## 8. 관련 파일 및 코드 위치 + +### 8.1 API (api/) - 핵심 코드 위치 + +| 파일 | 메서드 | 라인 | 역할 | +|------|--------|------|------| +| `Services/Quote/FormulaEvaluatorService.php` | `calculateBomWithDebug()` | 592-596 | 메인 엔트리 | +| 같은 파일 | (경동 분기 if문) | 609-611 | **Phase 2에서 Factory로 교체** | +| 같은 파일 | `calculateKyungdongBom()` | 1574-1881 | **Phase 2에서 calculateTenantBom()으로 일반화** | +| 같은 파일 | `KYUNGDONG_TENANT_ID` | 35 | **Phase 2에서 제거** | +| 같은 파일 | `expandBomWithFormulas()` | 1261-1333 | items.bom 재귀 전개 (Generic, 유지) | +| 같은 파일 | `calculateCategoryPrice()` | 812-862 | 카테고리 그룹 기반 단가 (유지) | +| 같은 파일 | `getItemPrice()` | 1066-1097 | 단가 조회 (유지) | +| **신규** `Contracts/TenantFormulaHandler.php` | - | - | **Phase 2에서 생성** | +| **신규** `FormulaHandlerFactory.php` | `make()` | - | **Phase 2에서 생성** | +| `Handlers/KyungdongFormulaHandler.php` | - | - | **→ `Handlers/Tenant287/FormulaHandler.php`로 이동** | +| `Handlers/Tenant287/FormulaHandler.php` | `calculateDynamicItems()` | 963 | **메인 엔트리** (이동 후) | +| 같은 파일 | `calculateSteelItems()` | 448 | 절곡품 10종 계산 | +| 같은 파일 | `calculatePartItems()` | 778 | 부자재 5종 계산 | +| 같은 파일 | `lookupItem()` | 35-49 | 품목 코드 → id/name 조회 (캐싱) | +| 같은 파일 | `withItemMapping()` | 72-87 | 아이템에 item_code/item_id 매핑 | +| 같은 파일 | `getGuideRailSpecs()` | 666-672 | 모델별 가이드레일 규격 매핑 | +| 같은 파일 | `calculateGuideRails()` | 675-730 | 가이드레일 타입별 계산 | +| `Services/Quote/EstimatePriceService.php` | (전체) | - | 단가 조회 서비스 (유지) | +| `Services/FormulaApiService.php` | `calculateBom()` | - | API 서버 호출 래퍼 (유지) | + +### 8.2 MNG (mng/) + +| 파일 | 메서드 | 라인 | 역할 | +|------|--------|------|------| +| `Controllers/Api/Admin/ItemManagementApiController.php` | `calculateFormula()` | 60-86 | 수식 BOM 계산 API | +| `Services/FormulaApiService.php` | `calculateBom()` | 24-82 | POST /api/v1/quotes/calculate/bom | +| `Services/ItemManagementService.php` | `getBomTree()` | - | BOM 트리 조회 (items.bom) | +| `views/item-management/index.blade.php` | JS `calculateFormula()` | - | 프론트 수식 계산 호출 | + +### 8.3 DB 테이블 스키마 + +#### items 테이블 + +| 컬럼 | 타입 | NULL | 설명 | +|------|------|------|------| +| id | bigint unsigned | NO | PK | +| tenant_id | bigint unsigned | NO | 테넌트 | +| item_type | varchar(15) | NO | FG/PT/SM/RM/CS | +| code | varchar(100) | NO | 품목 코드 | +| name | varchar(255) | NO | 품목명 | +| unit | varchar(20) | YES | 단위 (EA/M/㎡) | +| category_id | bigint unsigned | YES | 카테고리 FK | +| process_type | varchar(20) | YES | 공정 유형 | +| item_category | varchar(50) | YES | 품목 카테고리 | +| bom | json | YES | BOM JSON (FG는 현재 NULL) | +| attributes | json | YES | 동적 속성 | +| options | json | YES | 관리 옵션 | +| is_active | tinyint(1) | NO | 활성 (기본 1) | + +#### quote_formula_items 테이블 + +| 컬럼 | 타입 | NULL | 설명 | +|------|------|------|------| +| id | bigint unsigned | NO | PK | +| formula_id | bigint unsigned | NO | quote_formulas FK | +| item_code | varchar(50) | NO | 품목 코드 | +| item_name | varchar(200) | NO | 품목명 | +| specification | varchar(100) | YES | 규격 | +| unit | varchar(20) | NO | 단위 | +| quantity_formula | varchar(500) | NO | 수량 수식 | +| unit_price_formula | varchar(500) | YES | 단가 수식 | +| sort_order | int unsigned | NO | 정렬 | + +#### quote_formula_ranges 테이블 + +| 컬럼 | 타입 | NULL | 설명 | +|------|------|------|------| +| id | bigint unsigned | NO | PK | +| formula_id | bigint unsigned | NO | quote_formulas FK | +| min_value | decimal(15,4) | NO | 최소값 | +| max_value | decimal(15,4) | NO | 최대값 | +| condition_variable | varchar(50) | NO | 조건 변수 (K/H/W) | +| result_value | varchar(500) | NO | 결과값 (품목 코드) | +| result_type | enum('fixed','formula') | NO | 결과 유형 | +| sort_order | int unsigned | NO | 정렬 | + +#### quote_formula_mappings 테이블 + +| 컬럼 | 타입 | NULL | 설명 | +|------|------|------|------| +| id | bigint unsigned | NO | PK | +| formula_id | bigint unsigned | NO | quote_formulas FK | +| source_variable | varchar(50) | NO | 원본 변수 | +| source_value | varchar(200) | NO | 원본 값 | +| result_value | varchar(500) | NO | 결과값 | +| result_type | enum('fixed','formula') | NO | 결과 유형 | +| sort_order | int unsigned | NO | 정렬 | + +#### quote_formulas 테이블 + +| 컬럼 | 타입 | NULL | 설명 | +|------|------|------|------| +| id | bigint unsigned | NO | PK | +| tenant_id | bigint unsigned | NO | 테넌트 | +| category_id | bigint unsigned | NO | 카테고리 FK | +| product_id | bigint unsigned | YES | 매핑 대상 제품 FK | +| name | varchar(200) | NO | 수식명 | +| variable | varchar(50) | NO | 변수명 | +| type | enum('input','calculation','range','mapping') | NO | 유형 | +| formula | text | YES | 수식 표현식 | +| output_type | enum('variable','item') | NO | 출력 유형 | +| sort_order | int unsigned | NO | 정렬 | +| is_active | tinyint(1) | NO | 활성 | + +--- + +## 9. 검증 결과 + +### 9.1 테스트 케이스 (tinker 수동 실행) + +#### 벽부형 7모델 (W0=2000, H0=2500, QTY=1) + +| 모델 | FG 코드 | BOM 항목수 | 총 금액 | 상태 | +|------|---------|----------|--------|------| +| KQTS01 | FG-KQTS01-벽면형-SUS | 18건 | 1,167,934원 | ✅ | +| KSS01 | FG-KSS01-벽면형-SUS | 18건 | ~1.1M원 | ✅ | +| KSS02 | FG-KSS02-벽면형-SUS | 18건 | ~1.1M원 | ✅ | +| KSE01 | FG-KSE01-벽면형-SUS | 18건 | ~1.2M원 | ✅ | +| KSE01-EGI | FG-KSE01-벽면형-EGI | 18건 | ~1.2M원 | ✅ | +| KWE01 | FG-KWE01-벽면형-SUS | 18건 | ~1.2M원 | ✅ | +| KTE01 | FG-KTE01-벽면형-SUS | 18건 | ~1.3M원 | ✅ | + +#### 측면형 + 대형 규격 (W0=4000, H0=5000, QTY=2) + +| 모델 | FG 코드 | BOM 항목수 | 총 금액 | 상태 | +|------|---------|----------|--------|------| +| KQTS01 | FG-KQTS01-측면형-SUS | 18건 | ~2.9M원 | ✅ | +| KSE01 | FG-KSE01-측면형-SUS | 18건 | ~3.1M원 | ✅ | +| KTE01-EGI | FG-KTE01-측면형-EGI | 18건 | ~3.2M원 | ✅ | + +#### Factory 엣지 케이스 + +| tenant_id | 예상 | 실제 | 상태 | +|-----------|------|------|------| +| 287 | Tenant287\FormulaHandler 인스턴스 | ✅ 정상 반환 | ✅ | +| 0 | null | null | ✅ | +| -1 | null | null | ✅ | +| 999999 | null | null | ✅ | + +#### SF-/SM- 잔존 참조 점검 + +| 검색 범위 | 패턴 | 결과 | 상태 | +|-----------|------|------|------| +| api/app/Services/Quote/ | SF- / SM- 코드 참조 | 0건 | ✅ | + +### 9.2 성공 기준 + +| 기준 | 달성 | 비고 | +|------|------|------| +| FormulaHandlerFactory::make(287)이 Tenant287 핸들러 반환 | ✅ | 자동 발견 정상 동작 | +| FormulaHandlerFactory::make(999)이 null 반환 → Generic 경로 | ✅ | 미등록 테넌트 정상 | +| tinker에서 FG 선택 시 BOM 계산 성공 | ✅ | 벽부 7모델 + 측면 3모델 전수 PASS | +| BOM 결과의 모든 item_code가 items에 존재 | ✅ | BD- 코드 정상 매핑 (lookupItem null 없음) | +| React 견적관리 BOM 벌크 계산 정상 | ⏭️ | Phase 4 후순위와 함께 | +| SF-/SM- 코드 참조 잔존 없음 | ✅ | api/Services/Quote/ 내 0건 확인 | + +--- + +## 부록 A. FG 품목 전체 목록 (18건) + +| id | code | model | guiderail | finishing | major_category | legacy_model_id | +|----|------|-------|-----------|-----------|---------------|-----------------| +| 15515 | FG-KSS01-벽면형-SUS | KSS01 | 벽면형 | SUS마감 | 스크린 | 12 | +| 15516 | FG-KSS01-측면형-SUS | KSS01 | 측면형 | SUS마감 | 스크린 | 13 | +| 15517 | FG-KSE01-벽면형-SUS | KSE01 | 벽면형 | SUS마감 | 스크린 | 14 | +| 15518 | FG-KSE01-벽면형-EGI | KSE01 | 벽면형 | EGI마감 | 스크린 | 15 | +| 15519 | FG-KSE01-측면형-SUS | KSE01 | 측면형 | SUS마감 | 스크린 | 16 | +| 15520 | FG-KSE01-측면형-EGI | KSE01 | 측면형 | EGI마감 | 스크린 | 17 | +| 15521 | FG-KWE01-벽면형-SUS | KWE01 | 벽면형 | SUS마감 | 스크린 | 18 | +| 15522 | FG-KWE01-벽면형-EGI | KWE01 | 벽면형 | EGI마감 | 스크린 | 19 | +| 15523 | FG-KWE01-측면형-SUS | KWE01 | 측면형 | SUS마감 | 스크린 | 20 | +| 15524 | FG-KWE01-측면형-EGI | KWE01 | 측면형 | EGI마감 | 스크린 | 21 | +| 15525 | FG-KQTS01-벽면형-SUS | KQTS01 | 벽면형 | SUS마감 | 철재 | 22 | +| 15526 | FG-KQTS01-측면형-SUS | KQTS01 | 측면형 | SUS마감 | 철재 | 23 | +| 15527 | FG-KTE01-측면형-SUS | KTE01 | 측면형 | SUS마감 | 철재 | 24 | +| 15528 | FG-KTE01-벽면형-SUS | KTE01 | 벽면형 | SUS마감 | 철재 | 25 | +| 15529 | FG-KTE01-측면형-EGI | KTE01 | 측면형 | EGI마감 | 철재 | 26 | +| 15530 | FG-KTE01-벽면형-EGI | KTE01 | 벽면형 | EGI마감 | 철재 | 27 | +| 15531 | FG-KSS02-측면형-SUS | KSS02 | 측면형 | SUS마감 | 스크린 | 28 | +| 15532 | FG-KSS02-벽면형-SUS | KSS02 | 벽면형 | SUS마감 | 스크린 | 29 | + +--- + +## 부록 B. BD- 품목 전체 목록 (58건, 모두 item_type=PT) + +### 가이드레일 (17건) + +| id | code | name | +|----|------|------| +| 15589 | BD-가이드레일-KDSS01-SUS-150*150 | 가이드레일 KDSS01 SUS 150*150 | +| 15590 | BD-가이드레일-KDSS01-SUS-150*212 | 가이드레일 KDSS01 SUS 150*212 | +| 15592 | BD-가이드레일-KQTS01-SUS-130*125 | 가이드레일 KQTS01 SUS 130*125 | +| 15593 | BD-가이드레일-KQTS01-SUS-130*75 | 가이드레일 KQTS01 SUS 130*75 | +| 15596 | BD-가이드레일-KSE01-SUS-120*120 | 가이드레일 KSE01 SUS 120*120 | +| 15597 | BD-가이드레일-KSE01-SUS-120*70 | 가이드레일 KSE01 SUS 120*70 | +| 15598 | BD-가이드레일-KSE01-EGI-120*120 | 가이드레일 KSE01 EGI 120*120 | +| 15599 | BD-가이드레일-KSE01-EGI-120*70 | 가이드레일 KSE01 EGI 120*70 | +| 15603 | BD-가이드레일-KSS01-SUS-120*120 | 가이드레일 KSS01 SUS 120*120 | +| 15604 | BD-가이드레일-KSS01-SUS-120*70 | 가이드레일 KSS01 SUS 120*70 | +| 15607 | BD-가이드레일-KSS02-SUS-120*120 | 가이드레일 KSS02 SUS 120*120 | +| 15608 | BD-가이드레일-KSS02-SUS-120*70 | 가이드레일 KSS02 SUS 120*70 | +| 15610 | BD-가이드레일-KTE01-SUS-130*125 | 가이드레일 KTE01 SUS 130*125 | +| 15611 | BD-가이드레일-KTE01-SUS-130*75 | 가이드레일 KTE01 SUS 130*75 | +| 15612 | BD-가이드레일-KTE01-EGI-130*125 | 가이드레일 KTE01 EGI 130*125 | +| 15613 | BD-가이드레일-KTE01-EGI-130*75 | 가이드레일 KTE01 EGI 130*75 | +| 15617 | BD-가이드레일-KWE01-SUS-120*120 | 가이드레일 KWE01 SUS 120*120 | +| 15618 | BD-가이드레일-KWE01-SUS-120*70 | 가이드레일 KWE01 SUS 120*70 | +| 15619 | BD-가이드레일-KWE01-EGI-120*120 | 가이드레일 KWE01 EGI 120*120 | +| 15620 | BD-가이드레일-KWE01-EGI-120*70 | 가이드레일 KWE01 EGI 120*70 | + +### 하단마감재 (10건) + +| id | code | name | +|----|------|------| +| 15591 | BD-하단마감재-KDSS01-SUS-140*78 | 하단마감재 KDSS01 SUS 140*78 | +| 15594 | BD-하단마감재-KQTS01-SUS-60*30 | 하단마감재 KQTS01 SUS 60*30 | +| 15600 | BD-하단마감재-KSE01-SUS-64*43 | 하단마감재 KSE01 SUS 64*43 | +| 15601 | BD-하단마감재-KSE01-EGI-60*40 | 하단마감재 KSE01 EGI 60*40 | +| 15605 | BD-하단마감재-KSS01-SUS-60*40 | 하단마감재 KSS01 SUS 60*40 | +| 15609 | BD-하단마감재-KSS02-SUS-60*40 | 하단마감재 KSS02 SUS 60*40 | +| 15614 | BD-하단마감재-KTE01-SUS-64*34 | 하단마감재 KTE01 SUS 64*34 | +| 15615 | BD-하단마감재-KTE01-EGI-60*30 | 하단마감재 KTE01 EGI 60*30 | +| 15621 | BD-하단마감재-KWE01-SUS-64*43 | 하단마감재 KWE01 SUS 64*43 | +| 15622 | BD-하단마감재-KWE01-EGI-60*40 | 하단마감재 KWE01 EGI 60*40 | + +### L-BAR (5건) + +| id | code | name | +|----|------|------| +| 15588 | BD-L-BAR-KDSS01-17*100 | L-BAR KDSS01 17*100 | +| 15595 | BD-L-BAR-KSE01-17*60 | L-BAR KSE01 17*60 | +| 15602 | BD-L-BAR-KSS01-17*60 | L-BAR KSS01 17*60 | +| 15606 | BD-L-BAR-KSS02-17*60 | L-BAR KSS02 17*60 | +| 15616 | BD-L-BAR-KWE01-17*60 | L-BAR KWE01 17*60 | + +### 케이스 (11건) + +| id | code | name | +|----|------|------| +| 15577 | BD-케이스-500*350 | 케이스 500*350 | +| 15578 | BD-케이스-500*380 | 케이스 500*380 | +| 15579 | BD-케이스-600*500 | 케이스 600*500 | +| 15580 | BD-케이스-600*550 | 케이스 600*550 | +| 15581 | BD-케이스-650*500 | 케이스 650*500 | +| 15582 | BD-케이스-650*550 | 케이스 650*550 | +| 15583 | BD-케이스-700*550 | 케이스 700*550 | +| 15584 | BD-케이스-700*600 | 케이스 700*600 | +| 15585 | BD-케이스-780*600 | 케이스 780*600 | +| 15586 | BD-케이스-780*650 | 케이스 780*650 | +| 15587 | BD-케이스용 연기차단재 | 케이스용 연기차단재 | + +### 마구리 (10건) + +| id | code | name | +|----|------|------| +| 15565 | BD-마구리-505*355 | 마구리 505*355 | +| 15566 | BD-마구리-505*385 | 마구리 505*385 | +| 15567 | BD-마구리-605*555 | 마구리 605*555 | +| 15568 | BD-마구리-655*555 | 마구리 655*555 | +| 15569 | BD-마구리-705*605 | 마구리 705*605 | +| 15570 | BD-마구리-785*685 | 마구리 785*685 | +| 15573 | BD-마구리-655*505 | 마구리 655*505 | +| 15574 | BD-마구리-705*555 | 마구리 705*555 | +| 15575 | BD-마구리-785*605 | 마구리 785*605 | +| 15576 | BD-마구리-785*655 | 마구리 785*655 | + +### 기타 (5건) + +| id | code | name | +|----|------|------| +| 15571 | BD-보강평철-50 | 보강평철 50 | +| 15572 | BD-가이드레일용 연기차단재 | 가이드레일용 연기차단재 | + +--- + +## 부록 C. 코드 변경 포인트 + +### C.1 EST-SMOKE → BD- 변경 (Phase 3.1) + +**파일**: `api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php` (이동 후) + +``` +라인 519: 'EST-SMOKE-케이스용' → 'BD-케이스용 연기차단재' (id: 15587) +라인 557: 'EST-SMOKE-레일용' → 'BD-가이드레일용 연기차단재' (id: 15572) +``` + +### C.2 레거시 숫자 코드 매핑 (Phase 3.2 검토 대상) + +| 라인 | 현재 코드 | items.id | items.name | 비고 | +|------|----------|----------|-----------|------| +| 564 | 00035 | 14939 | 철재용하장바(SUS)3000 | 하장바 SUS | +| 564 | 00036 | 14940 | 철재용하장바(SUS1.2T) | 하장바 EGI (SM타입) | +| 619 | 00021 | 14928 | 평철12T | 무게평철12T | +| 631 | 90201 | 15188 | KD환봉(30파이) | 환봉 기본 | +| 628 | 90202 | 15189 | KD환봉 | 환봉 35파이 | +| 629 | 90203 | 15190 | KD환봉 | 환봉 45파이 | +| 630 | 90204 | 15191 | KD환봉 | 환봉 50파이 | + +> 모두 items 테이블에 존재하므로 lookupItem() 정상 동작. +> 변경 여부는 코드 가독성 차원에서 검토 (기능적 문제 없음). + +### C.3 lookupItem 로깅 추가 (Phase 3.3) + +**파일**: `api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php` +**위치**: 라인 42-48 `lookupItem()` 메서드 + +```php +// 변경 전 (라인 46) +$cache[$code] = ['id' => $item?->id, 'name' => $item?->name]; + +// 변경 후 +$cache[$code] = ['id' => $item?->id, 'name' => $item?->name]; +if (!$item) { + \Log::warning("[Tenant287\FormulaHandler] 미등록 품목: {$code}"); +} +``` + +--- + +## 부록 D. calculateDynamicItems 입력 파라미터 + +KyungdongFormulaHandler의 메인 엔트리 `calculateDynamicItems()` (라인 963)가 수신하는 파라미터: + +```php +$inputs = [ + // 기본 치수 + 'W0' => float, // 폭 (mm) + 'H0' => float, // 높이 (mm) + 'QTY' => int, // 수량 + + // 제품 정보 + 'product_type' => string, // 'screen' | 'slat' | 'steel' + 'model_name' => string, // 'KSS01' | 'KSE01' | ... + 'finishing_type' => string, // 'SUS마감' | 'EGI마감' (→ 내부에서 '마감' 제거) + + // 가이드레일 + 'guide_type' => string, // '벽면형' | '측면형' | '혼합형' + + // 케이스 + 'case_spec' => string, // '500*380' 등 + + // 모터/제어기 + 'bracket_inch' => string, // '4' | '5' | '6' | '8' + 'motor_power' => string, // 'single' | 'three' + 'controller_type' => string, // '일반' | '동보' | '자탈' 등 + + // 기타 (선택) + 'weight_plate_qty' => int, + 'round_bar_qty' => int, + 'round_bar_phi' => int, // 30 | 35 | 45 | 50 +]; +``` + +**반환값** (아이템 배열): + +```php +[ + [ + 'category' => string, // 'steel' | 'parts' | 'inspection' | 'material' | 'motor' | 'controller' + 'item_name' => string, + 'item_code' => string, // EST-*, BD-*, 또는 레거시 숫자코드 + 'item_id' => int|null, // items.id (lookupItem 결과) + 'specification' => string, + 'unit' => string, // 'EA' | 'm' | '㎡' + 'quantity' => float, + 'unit_price' => float, + 'total_price' => float, + ], + // ... +] +``` + +--- + +## 부록 E. 핸들러 구조화 설계 (Phase 2 상세) + +### E.1 디렉토리 구조 (Before → After) + +``` +Before: +api/app/Services/Quote/ +├── FormulaEvaluatorService.php ← if (287) 하드코딩 +├── EstimatePriceService.php +└── Handlers/ + └── KyungdongFormulaHandler.php ← 독립 클래스, 인터페이스 없음 + +After: +api/app/Services/Quote/ +├── FormulaEvaluatorService.php ← Factory::make($tenantId) 사용 +├── FormulaHandlerFactory.php ← 신규: 자동 발견 팩토리 +├── EstimatePriceService.php +├── Contracts/ +│ └── TenantFormulaHandler.php ← 신규: 인터페이스 +└── Handlers/ + └── Tenant287/ ← 경동기업 (tenant_id: 287) + └── FormulaHandler.php ← KyungdongFormulaHandler 이동 + └── Tenant{N}/ ← 향후 업체 추가 시 + └── FormulaHandler.php +``` + +### E.2 인터페이스 설계 + +```php +// api/app/Services/Quote/Contracts/TenantFormulaHandler.php +namespace App\Services\Quote\Contracts; + +interface TenantFormulaHandler +{ + /** + * 동적 BOM 항목 계산 (메인 엔트리) + */ + public function calculateDynamicItems(array $inputs): array; + + /** + * 모터 용량 계산 + */ + public function calculateMotorCapacity(string $productType, float $weight, string $bracketInch): string; + + /** + * 브라켓 사이즈 계산 + */ + public function calculateBracketSize(float $weight, ?string $bracketInch = null): string; +} +``` + +### E.3 팩토리 설계 + +```php +// api/app/Services/Quote/FormulaHandlerFactory.php +namespace App\Services\Quote; + +use App\Services\Quote\Contracts\TenantFormulaHandler; + +class FormulaHandlerFactory +{ + /** + * tenant_id로 핸들러 자동 발견. + * Handlers/Tenant{id}/FormulaHandler.php가 존재하면 인스턴스 반환. + * 없으면 null → Generic DB 경로. + */ + public static function make(int $tenantId): ?TenantFormulaHandler + { + $class = "App\\Services\\Quote\\Handlers\\Tenant{$tenantId}\\FormulaHandler"; + + if (!class_exists($class)) { + return null; + } + + $handler = new $class(); + + if (!$handler instanceof TenantFormulaHandler) { + throw new \RuntimeException( + "Tenant{$tenantId} FormulaHandler must implement TenantFormulaHandler" + ); + } + + return $handler; + } +} +``` + +### E.4 핸들러 이동 (Tenant287) + +```php +// api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php +namespace App\Services\Quote\Handlers\Tenant287; + +use App\Services\Quote\Contracts\TenantFormulaHandler; +use App\Services\Quote\EstimatePriceService; + +/** + * 경동기업 수식 핸들러 (tenant_id: 287) + * + * 방화셔터/스크린/철재 제품의 BOM 동적 계산. + * KyungdongFormulaHandler에서 이동됨. + */ +class FormulaHandler implements TenantFormulaHandler +{ + private const TENANT_ID = 287; + + // ... 기존 KyungdongFormulaHandler 코드 그대로 유지 +} +``` + +### E.5 FormulaEvaluatorService 변경 포인트 + +```php +// 변경 전 (라인 35) +private const KYUNGDONG_TENANT_ID = 287; + +// 변경 전 (라인 609-611) +if ($tenantId === self::KYUNGDONG_TENANT_ID) { + return $this->calculateKyungdongBom($finishedGoodsCode, $inputVariables, $tenantId); +} + +// ───────────────────────────────────────── + +// 변경 후 (라인 35 제거) +// KYUNGDONG_TENANT_ID 상수 제거 + +// 변경 후 (라인 609-611) +$handler = FormulaHandlerFactory::make($tenantId); +if ($handler) { + return $this->calculateTenantBom($handler, $finishedGoodsCode, $inputVariables, $tenantId); +} +// else → 기존 Generic 10단계 그대로 실행 + +// calculateKyungdongBom() → calculateTenantBom() 리네이밍 +// $handler 파라미터 추가, 내부의 new KyungdongFormulaHandler() 제거 +``` + +### E.6 향후 업체 추가 절차 + +``` +1. Handlers/Tenant{id}/FormulaHandler.php 파일 1개 생성 +2. implements TenantFormulaHandler +3. 끝. (설정 파일, DB 옵션, 매핑 테이블 변경 없음) +``` + +--- + +## 10. 자기완결성 점검 결과 + +### 10.1 체크리스트 검증 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 | +| 3 | 작업 범위가 구체적인가? | ✅ | 4 Phase + 부록 | +| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서 = 의존성 | +| 5 | 참고 파일 경로 + 라인번호가 정확한가? | ✅ | 섹션 8 + 부록 C/E | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | 4.1 + 4.2 (SQL), 부록 E (코드 설계) | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9 | +| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/건수/라인번호 | + +### 10.2 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | +| Q2. 어디서부터 시작해야 하는가? | ✅ | 3 Phase 1, 4.1 단계별 절차 | +| Q3. 어떤 파일의 몇 번째 줄을 수정해야 하는가? | ✅ | 8.1 코드 위치, 부록 C/E | +| Q4. 어떤 품목을 등록해야 하는가? | ✅ | 4.2 등록 상세, 부록 A/B | +| Q5. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | +| Q6. 핸들러가 어떤 파라미터를 받는가? | ✅ | 부록 D | +| Q7. DB INSERT 어떻게 하는가? | ✅ | 4.2 SQL 템플릿 | +| Q8. 기존 데이터 건드려도 되는가? | ✅ | 1.4 원칙 6번 (삭제 금지) | +| Q9. 핸들러 구조는 어떻게 만드는가? | ✅ | 부록 E (인터페이스/팩토리/이동 상세) | +| Q10. 향후 업체 추가 시 절차는? | ✅ | 부록 E.6 (파일 1개 생성, 끝) | + +**결과**: 10/10 통과 → ✅ 자기완결성 확보 + +--- + +*이 문서는 /sc:plan 스킬로 생성되었습니다.* diff --git a/plans/items-table-unification-plan.md b/plans/archive/items-table-unification-plan.md similarity index 100% rename from plans/items-table-unification-plan.md rename to plans/archive/items-table-unification-plan.md diff --git a/plans/kd-items-migration-plan.md b/plans/archive/kd-items-migration-plan.md similarity index 100% rename from plans/kd-items-migration-plan.md rename to plans/archive/kd-items-migration-plan.md diff --git a/plans/archive/material-input-per-item-mapping-plan.md b/plans/archive/material-input-per-item-mapping-plan.md new file mode 100644 index 0000000..e40c15b --- /dev/null +++ b/plans/archive/material-input-per-item-mapping-plan.md @@ -0,0 +1,482 @@ +# 개소별 자재 투입 매핑 계획 + +> **작성일**: 2026-02-12 +> **목적**: Worker Screen 자재 투입 시 개소(work_order_item)별 매핑 추적 기능 구현 +> **기준 문서**: `docs/specs/database-schema.md`, `docs/standards/api-rules.md` +> **상태**: 🔄 진행중 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | Phase 1~3 전체 구현 완료 | +| **다음 작업** | 테스트 및 검증 | +| **진행률** | 8/8 (100%) | +| **마지막 업데이트** | 2026-02-12 | + +--- + +## 1. 개요 + +### 1.1 배경 + +현재 자재 투입은 **작업지시(WorkOrder) 단위**로만 처리됨: +- `POST /api/v1/work-orders/{id}/material-inputs` → `{inputs: [{stock_lot_id, qty}]}` +- `stock_transactions.reference_id` = `work_order_id` (개소 정보 없음) +- 어떤 개소(work_order_item)에 어떤 자재가 투입되었는지 추적 불가 + +**필요**: 개소별로 자재 투입을 추적하여: +- 개소별 투입 완료 여부 확인 +- 개소별 필요 자재 vs 실투입 비교 +- 검사서에 개소별 투입 자재 LOT 번호 기록 + +### 1.2 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────┤ +│ 1. 신규 테이블(work_order_material_inputs)로 개소별 매핑 추적 │ +│ 2. 기존 stock_transactions 구조 변경 없음 (재고 이력은 그대로) │ +│ 3. 기존 작업지시 단위 API는 유지, 개소별 API를 추가 │ +│ 4. BOM 기반 필요 자재 계산은 기존 로직 재활용 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | 타입 정의 추가, 프론트 UI 변경 | 불필요 | +| ⚠️ 컨펌 필요 | 새 마이그레이션, 새 API 엔드포인트, 서비스 로직 변경 | **필수** | +| 🔴 금지 | 기존 stock_transactions 구조 변경, 기존 API 삭제 | 별도 협의 | + +### 1.4 준수 규칙 +- `docs/standards/api-rules.md` - Service-First, FormRequest, ApiResponse::handle() +- `docs/standards/quality-checklist.md` - 품질 체크리스트 +- `docs/specs/database-schema.md` - DB 스키마 규칙 +- MEMORY.md: 멀티테넌시 원칙 (FK/조인키만 컬럼, 나머지 options JSON) + +--- + +## 2. 대상 범위 + +### 2.1 Phase 1: Database & Model (백엔드 기반) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1.1 | `work_order_material_inputs` 마이그레이션 생성 | ✅ | api/ 프로젝트에서 | +| 1.2 | `WorkOrderMaterialInput` 모델 생성 | ✅ | BelongsToTenant 필수 | +| 1.3 | 관계 설정 (WorkOrderItem, WorkOrder) | ✅ | | + +### 2.2 Phase 2: Backend API (서비스 + 컨트롤러) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | `getMaterialsForItem()` 서비스 메서드 | ✅ | 개소별 BOM 자재 조회 | +| 2.2 | `registerMaterialInputForItem()` 서비스 메서드 | ✅ | 개소별 투입 + 매핑 저장 | +| 2.3 | `getMaterialInputsForItem()` 서비스 메서드 | ✅ | 개소별 투입 이력 조회 | +| 2.4 | 컨트롤러 엔드포인트 추가 | ✅ | 3개 엔드포인트 | +| 2.5 | FormRequest 생성 | ✅ | 투입 요청 검증 | +| 2.6 | 라우트 등록 | ✅ | production.php | + +### 2.3 Phase 3: Frontend (React) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 3.1 | Server Actions 추가 | ✅ | 개소별 API 호출 함수 | +| 3.2 | MaterialInputModal props 확장 | ✅ | workOrderItemId 추가 | +| 3.3 | 자재투입 버튼 → 개소별 호출 연결 | ✅ | WorkerScreen에서 | +| 3.4 | 투입 이력/상태 표시 | ✅ | 개소 카드에 투입 완료 표시 | + +--- + +## 3. 상세 설계 + +### 3.1 신규 테이블: `work_order_material_inputs` + +```sql +CREATE TABLE work_order_material_inputs ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + tenant_id BIGINT UNSIGNED NOT NULL, + work_order_id BIGINT UNSIGNED NOT NULL COMMENT '작업지시 ID', + work_order_item_id BIGINT UNSIGNED NOT NULL COMMENT '개소(작업지시품목) ID', + stock_lot_id BIGINT UNSIGNED NOT NULL COMMENT '투입 로트 ID', + item_id BIGINT UNSIGNED NOT NULL COMMENT '자재 품목 ID', + qty DECIMAL(12,3) NOT NULL COMMENT '투입 수량', + input_by BIGINT UNSIGNED NULL COMMENT '투입자 ID', + input_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '투입 시각', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + + -- FK + FOREIGN KEY (work_order_id) REFERENCES work_orders(id) ON DELETE CASCADE, + FOREIGN KEY (work_order_item_id) REFERENCES work_order_items(id) ON DELETE CASCADE, + + -- Index + INDEX idx_womi_tenant (tenant_id), + INDEX idx_womi_wo_item (work_order_id, work_order_item_id), + INDEX idx_womi_lot (stock_lot_id) +) COMMENT='개소별 자재 투입 이력'; +``` + +**설계 근거**: +- `work_order_id`: 작업지시 단위 조회용 (기존 호환) +- `work_order_item_id`: 개소별 매핑 핵심 +- `stock_lot_id`: 어떤 LOT에서 투입했는지 +- `item_id`: 어떤 자재(품목)인지 +- `qty`: 투입 수량 +- `input_by`, `input_at`: 투입자/시간 추적 + +### 3.2 API 엔드포인트 + +#### GET `/api/v1/work-orders/{workOrderId}/items/{itemId}/materials` +- **용도**: 특정 개소의 BOM 기반 필요 자재 + 재고 LOT 조회 +- **응답**: 기존 `MaterialForInput[]`과 동일 구조 +- **로직**: 기존 `getMaterials()` 중 해당 item_id의 BOM만 추출 + +#### POST `/api/v1/work-orders/{workOrderId}/items/{itemId}/material-inputs` +- **용도**: 특정 개소에 자재 투입 등록 +- **요청**: +```json +{ + "inputs": [ + { "stock_lot_id": 456, "qty": 100 } + ] +} +``` +- **처리 순서**: + 1. `StockService::decreaseFromLot()` 호출 (기존 재고 차감 로직 재사용) + 2. `work_order_material_inputs` 레코드 생성 (개소 매핑) + 3. 감사 로그 기록 +- **응답**: +```json +{ + "work_order_id": 123, + "work_order_item_id": 789, + "material_count": 2, + "input_results": [...], + "input_at": "2026-02-12T14:30:00" +} +``` + +#### GET `/api/v1/work-orders/{workOrderId}/items/{itemId}/material-inputs` +- **용도**: 특정 개소의 투입 이력 조회 +- **응답**: +```json +{ + "data": [ + { + "id": 1, + "stock_lot_id": 456, + "lot_no": "LOT-2026-001", + "item_id": 100, + "material_code": "MAT-001", + "material_name": "내화실", + "qty": 100, + "unit": "EA", + "input_by": 5, + "input_by_name": "홍길동", + "input_at": "2026-02-12T14:30:00" + } + ] +} +``` + +### 3.3 서비스 메서드 설계 + +#### WorkOrderService::getMaterialsForItem(int $workOrderId, int $itemId): array + +``` +1. WorkOrderItem 조회 (workOrderId + itemId 검증) +2. 해당 item의 BOM 추출 +3. BOM child_item별 required_qty = bom_qty × item.quantity +4. 각 자재의 StockLot 조회 (FIFO) +5. 이미 투입된 수량 차감 계산 (work_order_material_inputs에서 SUM) +6. 반환: MaterialForInput[] (remaining_required_qty 포함) +``` + +#### WorkOrderService::registerMaterialInputForItem(int $workOrderId, int $itemId, array $inputs): array + +``` +DB::transaction { + 1. WorkOrderItem 조회 + 검증 + 2. foreach (inputs as input): + a. StockService::decreaseFromLot() (기존 로직 재사용) + b. WorkOrderMaterialInput::create({ + tenant_id, work_order_id, work_order_item_id, + stock_lot_id, item_id (로트의 품목), + qty, input_by, input_at + }) + 3. 감사 로그 기록 + 4. 결과 반환 +} +``` + +### 3.4 프론트엔드 변경 + +#### MaterialInputModal Props 확장 +```typescript +interface MaterialInputModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + order: WorkOrder | null; + workOrderItemId?: number; // ← 추가: 개소 ID + workOrderItemName?: string; // ← 추가: 개소명 (모달 헤더용) + isCompletionFlow?: boolean; + onComplete?: () => void; + onSaveMaterials?: (...) => void; + savedMaterials?: MaterialInput[]; +} +``` + +#### Server Actions 추가 +```typescript +// 개소별 자재 조회 +getMaterialsForItem(workOrderId: string, itemId: number): Promise<{ + success: boolean; + data: MaterialForInput[]; +}> + +// 개소별 자재 투입 +registerMaterialInputForItem(workOrderId: string, itemId: number, inputs: ...): Promise<{ + success: boolean; +}> + +// 개소별 투입 이력 +getMaterialInputsForItem(workOrderId: string, itemId: number): Promise<{ + success: boolean; + data: MaterialInputHistory[]; +}> +``` + +#### MaterialInputModal 로직 변경 +``` +useEffect에서: + if (workOrderItemId) { + getMaterialsForItem(order.id, workOrderItemId) // 개소별 조회 + } else { + getMaterialsForWorkOrder(order.id) // 기존 전체 조회 (하위호환) + } + +handleSubmit에서: + if (workOrderItemId) { + registerMaterialInputForItem(order.id, workOrderItemId, inputs) + } else { + registerMaterialInput(order.id, inputs) + } +``` + +### 3.5 기존 API와의 관계 + +``` +기존 API (유지, 하위 호환): + GET /work-orders/{id}/materials → 전체 자재 조회 + POST /work-orders/{id}/material-inputs → 전체 단위 투입 + +신규 API (추가): + GET /work-orders/{id}/items/{itemId}/materials → 개소별 자재 조회 + POST /work-orders/{id}/items/{itemId}/material-inputs → 개소별 투입 + GET /work-orders/{id}/items/{itemId}/material-inputs → 개소별 투입 이력 +``` + +--- + +## 4. 작업 절차 + +### Step 1: 마이그레이션 + 모델 (Phase 1) +``` +1.1 api/ 프로젝트에서 마이그레이션 파일 생성 + - 파일: api/database/migrations/2026_02_12_XXXXXX_create_work_order_material_inputs_table.php + - 테이블: work_order_material_inputs (섹션 3.1 참조) + +1.2 WorkOrderMaterialInput 모델 생성 + - 파일: api/app/Models/Production/WorkOrderMaterialInput.php + - traits: BelongsToTenant, SoftDeletes (선택) + - $fillable: tenant_id, work_order_id, work_order_item_id, stock_lot_id, item_id, qty, input_by, input_at + - 관계: belongsTo(WorkOrder), belongsTo(WorkOrderItem), belongsTo(StockLot) + +1.3 기존 모델에 역관계 추가 + - WorkOrderItem: hasMany(WorkOrderMaterialInput) + - WorkOrder: hasMany(WorkOrderMaterialInput) + +검증: docker exec sam-api-1 php artisan migrate → 테이블 생성 확인 +``` + +### Step 2: Backend Service (Phase 2.1-2.3) +``` +2.1 WorkOrderService에 getMaterialsForItem() 추가 + - 기존 getMaterials() 로직 재활용 + - 해당 item의 BOM만 필터링 + - 이미 투입된 수량 차감 표시 + +2.2 WorkOrderService에 registerMaterialInputForItem() 추가 + - 기존 registerMaterialInput() 로직 기반 + - work_order_material_inputs 레코드 추가 생성 + - 트랜잭션 내에서 처리 + +2.3 WorkOrderService에 getMaterialInputsForItem() 추가 + - work_order_material_inputs 조회 + - lot_no, material_name 등 조인 + +검증: API 테스트 (curl 또는 Swagger) +``` + +### Step 3: Controller + Route (Phase 2.4-2.6) +``` +2.4 WorkOrderController에 3개 메서드 추가 + - materialsForItem(int $workOrderId, int $itemId) + - registerMaterialInputForItem(Request, int $workOrderId, int $itemId) + - materialInputsForItem(int $workOrderId, int $itemId) + +2.5 MaterialInputForItemRequest FormRequest 생성 (투입 검증) + - inputs: required|array|min:1 + - inputs.*.stock_lot_id: required|integer + - inputs.*.qty: required|numeric|gt:0 + +2.6 라우트 등록: api/routes/api/v1/production.php + - Route::get('work-orders/{id}/items/{itemId}/materials', ...) + - Route::post('work-orders/{id}/items/{itemId}/material-inputs', ...) + - Route::get('work-orders/{id}/items/{itemId}/material-inputs', ...) + +검증: php artisan route:list | grep material +``` + +### Step 4: Frontend (Phase 3) +``` +3.1 actions.ts에 3개 Server Action 추가 + - getMaterialsForItem() + - registerMaterialInputForItem() + - getMaterialInputsForItem() + +3.2 MaterialInputModal 수정 + - workOrderItemId prop 추가 + - useEffect에서 조건부 API 호출 + - handleSubmit에서 조건부 API 호출 + - 모달 헤더에 개소명 표시 + +3.3 WorkerScreen에서 개소별 자재투입 연결 + - 자재투입 버튼 클릭 시 workOrderItemId 전달 + +3.4 개소 카드에 투입 상태 표시 + - 투입 완료/미완료 뱃지 + +검증: dev.sam.kr에서 실제 플로우 테스트 +``` + +--- + +## 5. 핵심 파일 참조 + +### Backend (api/) +| 파일 | 역할 | +|------|------| +| `app/Services/WorkOrderService.php` | getMaterials() (line 1117), registerMaterialInput() (line 1264) | +| `app/Services/StockService.php` | decreaseFromLot() (line 618) - 재고 차감 | +| `app/Http/Controllers/Api/V1/WorkOrderController.php` | materials(), registerMaterialInput() | +| `routes/api/v1/production.php` (line 67-70) | 자재 관련 라우트 | +| `app/Models/Production/WorkOrderItem.php` | 작업지시 품목 모델 | + +### Frontend (react/) +| 파일 | 역할 | +|------|------| +| `src/components/production/WorkerScreen/MaterialInputModal.tsx` | 자재 투입 모달 UI | +| `src/components/production/WorkerScreen/actions.ts` | getMaterialsForWorkOrder(), registerMaterialInput() | +| `src/components/production/WorkerScreen/types.ts` | MaterialForInput, MaterialInput 타입 | + +### Database +| 테이블 | 역할 | +|--------|------| +| `work_order_items` | 작업지시 품목(개소). options JSON에 공정별 상세 | +| `stock_lots` | 재고 LOT. available_qty, fifo_order | +| `stock_transactions` | 재고 거래 이력. reference_type='work_order_input' | +| `work_order_material_inputs` | **신규** - 개소별 투입 매핑 | + +--- + +## 6. 컨펌 대기 목록 + +| # | 항목 | 변경 내용 | 영향 범위 | 상태 | +|---|------|----------|----------|------| +| 1 | 마이그레이션 | work_order_material_inputs 테이블 생성 | DB | ⚠️ 컨펌 필요 | +| 2 | API 엔드포인트 3개 추가 | 개소별 자재 조회/투입/이력 | api | ⚠️ 컨펌 필요 | +| 3 | 기존 API 유지 여부 | 작업지시 단위 API 유지 (하위호환) | api | ✅ 유지 | + +--- + +## 7. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-02-12 | - | 문서 초안 작성 | - | - | + +--- + +## 8. 참고 문서 + +- **API 규칙**: `docs/standards/api-rules.md` +- **DB 스키마**: `docs/specs/database-schema.md` +- **품질 체크리스트**: `docs/standards/quality-checklist.md` +- **기존 분석**: Explore Agent 분석 결과 (세션 내) +- **품목 정책**: `docs/rules/item-policy.md` (BOM, lot_managed 등) +- **MEMORY.md**: 멀티테넌시 원칙, 품목 options 체계 + +--- + +## 9. 검증 결과 + +### 9.1 테스트 케이스 + +| # | 시나리오 | 예상 결과 | 실제 결과 | 상태 | +|---|---------|----------|----------|------| +| 1 | 개소별 자재 조회 (BOM 있는 품목) | 해당 개소 BOM의 자재 + LOT 목록 반환 | | ✅ | +| 2 | 개소별 자재 조회 (BOM 없는 품목) | 품목 자체를 자재로 반환 | | ✅ | +| 3 | 개소별 자재 투입 | stock_lot 차감 + material_inputs 레코드 생성 | | ✅ | +| 4 | 이미 투입된 자재 재조회 | remaining_required_qty 감소 확인 | | ✅ | +| 5 | 가용수량 초과 투입 시도 | 에러 반환 (재고 부족) | | ✅ | +| 6 | 투입 이력 조회 | lot_no, 자재명, 수량, 투입자 확인 | | ✅ | +| 7 | 프론트 자재투입 모달에서 개소별 투입 | 해당 개소 자재만 표시, 투입 성공 | | ✅ | + +### 9.2 성공 기준 + +| 기준 | 달성 | 비고 | +|------|------|------| +| 개소별 자재 조회 API 동작 | ✅ | BOM 기반 필터링 | +| 개소별 자재 투입 API 동작 | ✅ | 재고 차감 + 매핑 저장 | +| 프론트에서 개소별 투입 플로우 | ✅ | MaterialInputModal 연동 | +| 기존 작업지시 단위 API 호환 유지 | ✅ | 기존 기능 미파손 | + +--- + +## 10. 자기완결성 점검 결과 + +### 10.1 체크리스트 검증 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | 개소별 자재 투입 매핑 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 | +| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3, 8개 작업 항목 | +| 4 | 의존성이 명시되어 있는가? | ✅ | 기존 API, StockService 재사용 | +| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 5 핵심 파일 참조 | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 Step 1-4 | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 | +| 8 | 모호한 표현이 없는가? | ✅ | SQL, API 스키마 구체적 명시 | + +### 10.2 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | +| Q2. 어디서부터 시작해야 하는가? | ✅ | 4. 작업 절차 Step 1 | +| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5. 핵심 파일 참조 | +| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | +| Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 | + +**결과**: 5/5 통과 → ✅ 자기완결성 확보 + +--- + +*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/mng-item-formula-integration-plan.md b/plans/archive/mng-item-formula-integration-plan.md new file mode 100644 index 0000000..54261a4 --- /dev/null +++ b/plans/archive/mng-item-formula-integration-plan.md @@ -0,0 +1,837 @@ +# MNG 품목관리 - 견적수식 엔진(FormulaEvaluatorService) 연동 계획 + +> **작성일**: 2026-02-19 +> **목적**: 가변사이즈 완제품(FG) 선택 시 오픈사이즈(W,H) 입력 → FormulaEvaluatorService로 동적 자재 산출 → 중앙 패널에 트리 표시 +> **기준 문서**: docs/plans/mng-item-management-plan.md, api/app/Services/Quote/FormulaEvaluatorService.php +> **선행 작업**: 3-Panel 품목관리 페이지 구현 완료 (Phase 1~2 of mng-item-management-plan.md) +> **상태**: 🔄 진행중 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | Phase 2.5: 로딩/에러 상태 처리 (전체 구현 완료) | +| **다음 작업** | 검증 (브라우저 테스트) | +| **진행률** | 8/8 (100%) | +| **마지막 업데이트** | 2026-02-19 | + +--- + +## 1. 개요 + +### 1.1 배경 + +MNG 품목관리 페이지(`mng.sam.kr/item-management`)에서 완제품(FG) `FG-KQTS01-벽면형-SUS`를 선택하면 `items.bom` JSON에 등록된 정적 BOM(PT 2개: 가이드레일, 하단마감재)만 표시된다. +그러나 견적관리(`dev.sam.kr/sales/quote-management/46`)에서는 `FormulaEvaluatorService`가 W=3000, H=3000 입력으로 17종 51개의 자재를 동적 산출한다. + +**핵심 문제**: items.bom(정적)과 FormulaEvaluatorService(동적) 두 시스템이 분리되어 있어, 품목관리 페이지에서 실제 필요 자재를 볼 수 없다. + +**해결**: 가변사이즈 품목(`item_details.is_variable_size = true`) 선택 시 오픈사이즈(W, H) 입력 UI를 제공하고, API의 기존 엔드포인트(`POST /api/v1/quotes/calculate/bom`)를 MNG에서 HTTP로 호출하여 산출 결과를 중앙 패널에 표시한다. + +### 1.2 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────┤ +│ - MNG에서 마이그레이션 파일 생성 금지 (API에서만) │ +│ - MNG ↔ API 통신은 HTTP API 호출 (코드 공유/직접 서비스 호출 X) │ +│ - 기존 API 엔드포인트 재사용 (POST /api/v1/quotes/calculate/bom)│ +│ - Docker nginx 내부 라우팅 + SSL 우회 패턴 사용 │ +│ - Blade + HTMX + Tailwind + Vanilla JS (Alpine.js 미사용) │ +│ - 정적 BOM과 수식 산출 결과를 탭으로 전환 가능하게 │ +│ - Controller에서 직접 DB 쿼리 금지 (Service-First) │ +│ - Controller에서 직접 validate() 금지 (FormRequest 필수) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | 서비스 생성, 컨트롤러 메서드 추가, Blade 수정, JS 추가 | 불필요 | +| ⚠️ 컨펌 필요 | API 라우트 추가, 기존 Blade 구조 변경 | **필수** | +| 🔴 금지 | mng에서 마이그레이션 생성, API 소스 수정 | 별도 협의 | + +### 1.4 MNG 절대 금지 규칙 + +``` +❌ mng/database/migrations/ 에 파일 생성 금지 +❌ docker exec sam-mng-1 php artisan migrate 실행 금지 +❌ php artisan db:seed --class=*MenuSeeder 실행 금지 +❌ Controller에서 직접 DB 쿼리 금지 (Service-First) +❌ Controller에서 직접 validate() 금지 (FormRequest 필수) +❌ api/ 프로젝트 소스 코드 수정 금지 +``` + +--- + +## 2. 대상 범위 + +### 2.1 Phase 1: MNG 백엔드 (HTTP API 호출 서비스) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1.1 | FormulaApiService 생성 (MNG→API HTTP 호출 래퍼) | ✅ | 신규 파일 | +| 1.2 | ItemManagementApiController에 calculateFormula 메서드 추가 | ✅ | 기존 파일 수정 | +| 1.3 | API 라우트 추가 (POST /api/admin/items/{id}/calculate-formula) | ✅ | 기존 파일 수정 | + +### 2.2 Phase 2: MNG 프론트엔드 (UI 연동) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | 중앙 패널 헤더에 탭 UI 추가 (정적 BOM / 수식 산출) | ✅ | index.blade.php 수정 | +| 2.2 | 오픈사이즈 입력 폼 (W, H, 수량) + 산출 버튼 | ✅ | index.blade.php 수정 | +| 2.3 | 수식 산출 결과 트리 렌더링 (카테고리 그룹별) | ✅ | JS 추가 | +| 2.4 | 가변사이즈 품목 감지 → 자동 탭 전환 | ✅ | item-detail.blade.php + JS 수정 | +| 2.5 | 로딩/에러 상태 처리 | ✅ | JS 추가 | + +--- + +## 3. 이미 구현된 코드 (선행 작업 - 수정 대상) + +> 새 세션에서 현재 코드 상태를 파악할 수 있도록 이미 존재하는 파일 전체 목록과 핵심 구조를 기록. + +### 3.1 파일 구조 (이미 존재) + +``` +mng/ +├── app/ +│ ├── Http/Controllers/ +│ │ ├── ItemManagementController.php # Web (HX-Redirect 패턴) +│ │ └── Api/Admin/ +│ │ └── ItemManagementApiController.php # API (index, bomTree, detail) +│ ├── Models/ +│ │ ├── Items/ +│ │ │ ├── Item.php # BelongsToTenant, 관계, 스코프, 상수 +│ │ │ └── ItemDetail.php # 1:1 확장 (is_variable_size 필드 포함) +│ │ └── Commons/ +│ │ └── File.php # 파일 모델 +│ ├── Services/ +│ │ └── ItemManagementService.php # getItemList, getBomTree, getItemDetail +│ └── Traits/ +│ └── BelongsToTenant.php # 테넌트 격리 Trait +├── resources/views/item-management/ +│ ├── index.blade.php # 3-Panel 메인 (★ 수정 대상) +│ └── partials/ +│ ├── item-list.blade.php # 좌측 패널 (변경 없음) +│ ├── bom-tree.blade.php # 중앙 패널 초기 상태 (변경 없음) +│ └── item-detail.blade.php # 우측 패널 (★ 수정 대상) +├── routes/ +│ ├── web.php # Route: GET /item-management (변경 없음) +│ └── api.php # Route: items group (★ 수정 대상 - 라우트 추가) +└── config/ + └── api-explorer.php # FLOW_TESTER_API_KEY 설정 참조 +``` + +### 3.2 현재 ItemManagementApiController 전체 (수정 대상) + +```php +service->getItemList([ + 'search' => $request->input('search'), + 'item_type' => $request->input('item_type'), + 'per_page' => $request->input('per_page', 50), + ]); + return view('item-management.partials.item-list', compact('items')); + } + + public function bomTree(int $id, Request $request): JsonResponse + { + $maxDepth = $request->input('max_depth', 10); + $tree = $this->service->getBomTree($id, $maxDepth); + return response()->json($tree); + } + + public function detail(int $id): View + { + $data = $this->service->getItemDetail($id); + return view('item-management.partials.item-detail', [ + 'item' => $data['item'], + 'bomChildren' => $data['bom_children'], + ]); + } +} +``` + +### 3.3 현재 API 라우트 (items 그룹, mng/routes/api.php:866~) + +```php +Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/items')->name('api.admin.items.')->group(function () { + Route::get('/search', [ItemApiController::class, 'search'])->name('search'); + + // 품목관리 페이지 API + Route::get('/', [ItemManagementApiController::class, 'index'])->name('index'); + Route::get('/{id}/bom-tree', [ItemManagementApiController::class, 'bomTree'])->name('bom-tree'); + Route::get('/{id}/detail', [ItemManagementApiController::class, 'detail'])->name('detail'); + // ★ 여기에 calculate-formula 라우트 추가 예정 +}); +``` + +### 3.4 현재 index.blade.php 중앙 패널 (수정 대상 부분) + +```html + +
+
+

BOM 구성 (재귀 트리)

+
+
+

좌측에서 품목을 선택하세요.

+
+
+``` + +### 3.5 현재 JS 구조 (index.blade.php @push('scripts')) + +핵심 함수: +- `loadItemList()` - 좌측 품목 리스트 HTMX 로드 +- `selectItem(itemId, updateTree)` - 품목 선택 (좌측 하이라이트 + 중앙 트리 fetch + 우측 상세 HTMX) +- `selectTreeNode(itemId)` - 중앙 트리 노드 클릭 (우측만 갱신, 트리 유지) +- `renderBomTree(node, container)` - BOM 트리 재귀 렌더링 +- `getTypeBadgeClass(type)` - 유형별 뱃지 CSS 클래스 + +### 3.6 테넌트 필터링 패턴 (중요) + +MNG의 HQ 관리자는 헤더에서 테넌트를 선택하며, `session('selected_tenant_id')`에 저장된다. +그러나 `BelongsToTenant`의 `TenantScope`는 `request->attributes`, `X-TENANT-ID 헤더`, `auth user`에서 tenant_id를 읽으므로 **세션 값과 불일치**할 수 있다. + +**따라서 Service에서는 `Item::withoutGlobalScopes()->where('tenant_id', session('selected_tenant_id'))` 패턴을 사용한다.** + +```php +// ✅ 올바른 패턴 (현재 ItemManagementService에서 사용 중) +Item::withoutGlobalScopes() + ->where('tenant_id', session('selected_tenant_id')) + ->findOrFail($id); + +// ❌ 잘못된 패턴 (HQ 관리자 세션과 불일치) +Item::findOrFail($id); // TenantScope가 auth user의 tenant_id 사용 +``` + +--- + +## 4. 상세 작업 내용 + +### 4.1 Phase 1: MNG 백엔드 + +#### 1.1 FormulaApiService 생성 + +**파일 경로**: `mng/app/Services/FormulaApiService.php` (신규 생성) + +**역할**: MNG에서 API 프로젝트의 `POST /api/v1/quotes/calculate/bom` 엔드포인트를 HTTP로 호출하는 래퍼 + +**호출 대상 API 엔드포인트 상세**: + +``` +POST /api/v1/quotes/calculate/bom +라우트 정의: api/routes/api/v1/sales.php:64 +미들웨어: 글로벌(ApiKeyMiddleware, CorsMiddleware, ApiRateLimiter) +FormRequest: QuoteBomCalculateRequest (authorize = true, 제한 없음) +``` + +**API 인증 요구사항** (확인 완료): + +| 헤더 | 필수 | 설명 | +|------|:----:|------| +| `X-API-KEY` | ✅ 필수 | `api_keys` 테이블에 `is_active=true`로 등록된 키 | +| `Authorization: Bearer {token}` | ❌ 선택 | Sanctum 토큰, 있으면 tenant_id 자동 설정 | +| `X-TENANT-ID` | ❌ 선택 | 테넌트 식별 (Bearer 없을 때 대안) | + +**API Key 취득 방법**: `env('FLOW_TESTER_API_KEY')` (mng/.env에 설정됨, `config/api-explorer.php:26`에서 참조) + +**요청 페이로드**: +```json +{ + "finished_goods_code": "FG-KQTS01", + "variables": { + "W0": 3000, + "H0": 3000, + "QTY": 1 + }, + "tenant_id": 287 +} +``` + +**응답 구조** (FormulaEvaluatorService::calculateBomWithDebug 반환값): +```json +{ + "success": true, + "finished_goods": { "code": "FG-KQTS01", "name": "벽면형-SUS", "id": 123 }, + "variables": { "W0": 3000, "H0": 3000, "QTY": 1 }, + "items": [ + { + "item_code": "PT-강재-C형강", + "item_name": "C형강 65×32×10t", + "specification": "65×32×10t", + "unit": "mm", + "quantity": 6038, + "unit_price": 1.0, + "total_price": 6038, + "category_group": "steel" + } + ], + "grouped_items": { + "steel": [ ... ], + "part": [ ... ], + "motor": [ ... ] + }, + "subtotals": { "steel": 123456, "part": 78900, "motor": 50000 }, + "grand_total": 252356, + "debug_steps": [ ... ] +} +``` + +**구현 코드**: +```php +withoutVerifying() + ->withHeaders([ + 'Host' => 'api.sam.kr', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'X-API-KEY' => $apiKey, + 'X-TENANT-ID' => (string) $tenantId, + ]) + ->post('https://nginx/api/v1/quotes/calculate/bom', [ + 'finished_goods_code' => $finishedGoodsCode, + 'variables' => $variables, + 'tenant_id' => $tenantId, + ]); + + if ($response->successful()) { + $json = $response->json(); + // ApiResponse::handle()는 {success, message, data} 구조로 래핑 + return $json['data'] ?? $json; + } + + Log::warning('FormulaApiService: API 호출 실패', [ + 'status' => $response->status(), + 'body' => $response->body(), + 'code' => $finishedGoodsCode, + ]); + + return [ + 'success' => false, + 'error' => 'API 응답 오류: HTTP ' . $response->status(), + ]; + } catch (\Exception $e) { + Log::error('FormulaApiService: 예외 발생', [ + 'message' => $e->getMessage(), + 'code' => $finishedGoodsCode, + ]); + + return [ + 'success' => false, + 'error' => '수식 계산 서버 연결 실패: ' . $e->getMessage(), + ]; + } + } +} +``` + +**트러블슈팅 가이드**: +- `401 Unauthorized` → API Key 확인: `docker exec sam-mng-1 php artisan tinker --execute="echo env('FLOW_TESTER_API_KEY');"` +- `Connection refused` → nginx 컨테이너 확인: `docker ps | grep nginx` +- `SSL certificate problem` → `withoutVerifying()` 누락 확인 +- `422 Validation` → finished_goods_code가 items 테이블에 존재하는지 확인 + +#### 1.2 ItemManagementApiController::calculateFormula 추가 + +**파일**: `mng/app/Http/Controllers/Api/Admin/ItemManagementApiController.php` + +**변경**: 기존 컨트롤러에 메서드 1개 추가 + use 문 추가 + +```php +// 파일 상단 use 추가 +use App\Services\FormulaApiService; + +// 기존 메서드 아래에 추가 +/** + * 수식 기반 BOM 산출 (API 서버의 FormulaEvaluatorService HTTP 호출) + */ +public function calculateFormula(Request $request, int $id): JsonResponse +{ + $item = \App\Models\Items\Item::withoutGlobalScopes() + ->where('tenant_id', session('selected_tenant_id')) + ->findOrFail($id); + + $width = (int) $request->input('width', 1000); + $height = (int) $request->input('height', 1000); + $qty = (int) $request->input('qty', 1); + + $variables = [ + 'W0' => $width, + 'H0' => $height, + 'QTY' => $qty, + ]; + + $formulaService = new FormulaApiService(); + $result = $formulaService->calculateBom( + $item->code, + $variables, + (int) session('selected_tenant_id') + ); + + return response()->json($result); +} +``` + +#### 1.3 API 라우트 추가 + +**파일**: `mng/routes/api.php` (라인 866~ 기존 items 그룹 내) + +**추가 위치**: 기존 detail 라우트 아래 + +```php +// 기존 라우트 아래에 추가 +Route::post('/{id}/calculate-formula', [ItemManagementApiController::class, 'calculateFormula'])->name('calculate-formula'); +``` + +--- + +### 4.2 Phase 2: MNG 프론트엔드 + +#### 2.1 중앙 패널 탭 UI + +**수정 파일**: `mng/resources/views/item-management/index.blade.php` + +**변경 대상 (현재 HTML)**: +```html +
+

BOM 구성 (재귀 트리)

+
+
+``` + +**변경 후**: +```html +
+
+ + +
+
+ + + + + +
+

좌측에서 품목을 선택하세요.

+
+ + + +``` + +#### 2.2 item-detail.blade.php에 메타 데이터 추가 + +**수정 파일**: `mng/resources/views/item-management/partials/item-detail.blade.php` + +**파일 맨 위에 추가** (기존 `
` 앞): +```html + + +``` + +#### 2.3 JS 추가 (index.blade.php @push('scripts')) + +**기존 IIFE 내부에 추가할 변수와 함수**: + +```javascript +// ── 추가 변수 ── +let currentBomTab = 'static'; // 'static' | 'formula' +let currentItemId = null; +let currentItemCode = null; + +// ── 탭 전환 ── +window.switchBomTab = function(tab) { + currentBomTab = tab; + + // 탭 버튼 스타일 + document.querySelectorAll('.bom-tab').forEach(btn => { + btn.classList.remove('bg-blue-100', 'text-blue-800'); + btn.classList.add('bg-gray-100', 'text-gray-600'); + }); + const activeBtn = document.getElementById(tab === 'static' ? 'tab-static-bom' : 'tab-formula-bom'); + if (activeBtn) { + activeBtn.classList.remove('bg-gray-100', 'text-gray-600'); + activeBtn.classList.add('bg-blue-100', 'text-blue-800'); + } + + // 콘텐츠 영역 전환 + document.getElementById('bom-tree-container').style.display = (tab === 'static') ? '' : 'none'; + document.getElementById('formula-input-panel').style.display = (tab === 'formula') ? '' : 'none'; + document.getElementById('formula-result-container').style.display = (tab === 'formula') ? '' : 'none'; +}; + +// ── 가변사이즈 탭 표시/숨김 ── +function showFormulaTab() { + document.getElementById('tab-formula-bom').style.display = ''; + switchBomTab('formula'); // 자동으로 수식 산출 탭으로 전환 +} + +function hideFormulaTab() { + document.getElementById('tab-formula-bom').style.display = 'none'; + document.getElementById('formula-input-panel').style.display = 'none'; + document.getElementById('formula-result-container').style.display = 'none'; + switchBomTab('static'); +} + +// ── 상세 로드 완료 후 가변사이즈 감지 ── +document.body.addEventListener('htmx:afterSwap', function(event) { + if (event.detail.target.id === 'item-detail') { + const meta = document.getElementById('item-meta-data'); + if (meta) { + currentItemId = meta.dataset.itemId; + currentItemCode = meta.dataset.itemCode; + if (meta.dataset.isVariableSize === 'true') { + showFormulaTab(); + } else { + hideFormulaTab(); + } + } + } +}); + +// ── 수식 산출 API 호출 ── +window.calculateFormula = function() { + if (!currentItemId) return; + + const width = parseInt(document.getElementById('input-width').value) || 1000; + const height = parseInt(document.getElementById('input-height').value) || 1000; + const qty = parseInt(document.getElementById('input-qty').value) || 1; + + // 입력값 범위 검증 + if (width < 100 || width > 10000 || height < 100 || height > 10000) { + alert('폭과 높이는 100~10000 범위로 입력하세요.'); + return; + } + + const container = document.getElementById('formula-result-container'); + container.innerHTML = '
'; + + fetch(`/api/admin/items/${currentItemId}/calculate-formula`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken, + }, + body: JSON.stringify({ width, height, qty }), + }) + .then(res => res.json()) + .then(data => { + if (data.success === false) { + container.innerHTML = ` +
+

${data.error || '산출 실패'}

+ +
`; + return; + } + renderFormulaTree(data, container); + }) + .catch(err => { + container.innerHTML = ` +
+

서버 연결 실패

+ +
`; + }); +}; + +// ── 수식 산출 결과 트리 렌더링 ── +function renderFormulaTree(data, container) { + container.innerHTML = ''; + + // 카테고리 그룹 한글 매핑 + const groupLabels = { steel: '강재', part: '부품', motor: '모터/컨트롤러' }; + const groupIcons = { steel: '🏗️', part: '🔧', motor: '⚡' }; + const groupedItems = data.grouped_items || {}; + + // 합계 영역 + if (data.grand_total) { + const totalDiv = document.createElement('div'); + totalDiv.className = 'mb-4 p-3 bg-blue-50 rounded-lg flex justify-between items-center'; + totalDiv.innerHTML = ` + + ${data.finished_goods?.name || ''} (${data.finished_goods?.code || ''}) + W:${data.variables?.W0} H:${data.variables?.H0} + + 합계: ${Number(data.grand_total).toLocaleString()}원 + `; + container.appendChild(totalDiv); + } + + // 카테고리 그룹별 렌더링 + Object.entries(groupedItems).forEach(([group, items]) => { + if (!items || items.length === 0) return; + + const groupDiv = document.createElement('div'); + groupDiv.className = 'mb-3'; + + const subtotal = data.subtotals?.[group] || 0; + + // 그룹 헤더 + const header = document.createElement('div'); + header.className = 'flex items-center gap-2 py-2 px-3 bg-gray-50 rounded-t-lg cursor-pointer'; + header.innerHTML = ` + + ${groupIcons[group] || '📦'} + ${groupLabels[group] || group} + (${items.length}건) + 소계: ${Number(subtotal).toLocaleString()}원 + `; + + const listDiv = document.createElement('div'); + listDiv.className = 'border border-gray-100 rounded-b-lg divide-y divide-gray-50'; + + // 그룹 접기/펼치기 + header.onclick = function() { + const toggle = header.querySelector('.text-gray-400'); + if (listDiv.style.display === 'none') { + listDiv.style.display = ''; + toggle.textContent = '▼'; + } else { + listDiv.style.display = 'none'; + toggle.textContent = '▶'; + } + }; + + // 아이템 목록 + items.forEach(item => { + const row = document.createElement('div'); + row.className = 'flex items-center gap-2 py-1.5 px-3 hover:bg-gray-50 cursor-pointer text-sm'; + row.innerHTML = ` + PT + ${item.item_code || ''} + ${item.item_name || ''} + ${item.quantity || 0} ${item.unit || ''} + ${Number(item.total_price || 0).toLocaleString()}원 + `; + // 아이템 클릭 시 items 테이블에서 해당 코드로 검색하여 상세 표시 + row.onclick = function() { + // item_code로 좌측 검색 → 해당 품목 상세 로드 + const searchInput = document.getElementById('item-search'); + searchInput.value = item.item_code; + loadItemList(); + }; + listDiv.appendChild(row); + }); + + groupDiv.appendChild(header); + groupDiv.appendChild(listDiv); + container.appendChild(groupDiv); + }); + + if (Object.keys(groupedItems).length === 0) { + container.innerHTML = '

산출된 자재가 없습니다.

'; + } +} +``` + +--- + +## 5. 컨펌 대기 목록 + +| # | 항목 | 변경 내용 | 영향 범위 | 상태 | +|---|------|----------|----------|------| +| 1 | ~~API 인증 방식~~ | X-API-KEY 필수 (FLOW_TESTER_API_KEY) | MNG→API 통신 | ✅ 확인 완료 | +| 2 | API 라우트 추가 | POST /api/admin/items/{id}/calculate-formula | mng/routes/api.php | ✅ 즉시 가능 | + +--- + +## 6. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-02-19 | - | 계획 문서 초안 작성 | - | - | +| 2026-02-19 | - | 자기완결성 보강 (기존 코드 현황, API 인증 분석, 트러블슈팅) | - | - | +| 2026-02-19 | 1.1~1.3 | Phase 1 백엔드 구현 완료 (FormulaApiService, Controller, Route) | FormulaApiService.php, ItemManagementApiController.php, api.php | ✅ | +| 2026-02-19 | 2.1~2.5 | Phase 2 프론트엔드 구현 완료 (탭 UI, 입력 폼, 트리 렌더링, 감지, 에러) | index.blade.php, item-detail.blade.php | ✅ | + +--- + +## 7. 참고 문서 + +- **기존 품목관리 계획**: `docs/plans/mng-item-management-plan.md` +- **FormulaEvaluatorService**: `api/app/Services/Quote/FormulaEvaluatorService.php` + - 메서드: `calculateBomWithDebug(string $finishedGoodsCode, array $inputVariables, ?int $tenantId): array` + - tenant_id=287 자동 감지 → KyungdongFormulaHandler 라우팅 +- **KyungdongFormulaHandler**: `api/app/Services/Quote/Handlers/KyungdongFormulaHandler.php` + - `calculateDynamicItems(array $inputs)` → steel(10종), part(3종), motor/controller 산출 +- **API 라우트**: `api/routes/api/v1/sales.php:64` → `QuoteController::calculateBom` +- **QuoteBomCalculateRequest**: `api/app/Http/Requests/V1/QuoteBomCalculateRequest.php` + - `finished_goods_code` (required|string) + - `variables` (required|array), `variables.W0` (required|numeric), `variables.H0` (required|numeric) + - `tenant_id` (nullable|integer) +- **MNG-API HTTP 패턴**: `mng/app/Services/FlowTester/HttpClient.php` +- **API Key 설정**: `mng/config/api-explorer.php:26` → `env('FLOW_TESTER_API_KEY')` +- **현재 품목관리 뷰**: `mng/resources/views/item-management/index.blade.php` +- **MNG 프로젝트 규칙**: `mng/CLAUDE.md` + +--- + +## 8. 세션 및 메모리 관리 정책 + +### 8.1 세션 시작 시 (Load Strategy) +``` +1. 이 문서 읽기 (docs/plans/mng-item-formula-integration-plan.md) +2. 📍 현재 진행 상태 확인 → 다음 작업 파악 +3. 섹션 3 "이미 구현된 코드" 확인 → 수정 대상 파일 파악 +4. 필요시 Serena 메모리 로드: + read_memory("item-formula-state") + read_memory("item-formula-snapshot") + read_memory("item-formula-active-symbols") +``` + +### 8.2 작업 중 관리 (Context Defense) +| 컨텍스트 잔량 | Action | 내용 | +|--------------|--------|------| +| **30% 이하** | Snapshot | `write_memory("item-formula-snapshot", "코드변경+논의요약")` | +| **20% 이하** | Context Purge | `write_memory("item-formula-active-symbols", "주요 수정 파일/함수")` | +| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 | + +--- + +## 9. 검증 결과 + +### 9.1 테스트 케이스 + +| # | 테스트 | 입력 | 예상 결과 | 실제 결과 | 상태 | +|---|--------|------|----------|----------|------| +| 1 | FG 가변사이즈 품목 선택 | FG-KQTS01 클릭 | 수식 산출 탭 자동 표시, 입력 폼 노출 | | ⏳ | +| 2 | 오픈사이즈 입력 후 산출 | W:3000, H:3000, QTY:1 | 17종 자재 트리 (steel/part/motor 그룹별), 소계/합계 표시 | | ⏳ | +| 3 | 비가변사이즈 품목 선택 | PT 품목 클릭 | 수식 산출 탭 숨김, 정적 BOM만 표시 | | ⏳ | +| 4 | 정적 BOM ↔ 수식 산출 탭 전환 | 탭 클릭 | 각 탭 콘텐츠 전환, 입력 폼 표시/숨김 | | ⏳ | +| 5 | 산출 결과에서 품목 클릭 | 트리 노드 클릭 | 좌측 검색에 품목코드 입력 → 품목 리스트 필터링 | | ⏳ | +| 6 | API Key 미설정 | FLOW_TESTER_API_KEY 없음 | 에러 메시지 "API 응답 오류: HTTP 401" + 재시도 버튼 | | ⏳ | +| 7 | 입력값 범위 초과 | W:0, H:-1 | alert 표시, API 호출 안 함 | | ⏳ | +| 8 | 서버 연결 실패 | nginx 중지 상태 | 에러 메시지 "서버 연결 실패" + 재시도 버튼 | | ⏳ | + +### 9.2 성공 기준 달성 현황 + +| 기준 | 달성 | 비고 | +|------|------|------| +| FG 가변사이즈 품목에서 수식 산출 가능 | ⏳ | | +| 산출 결과가 견적관리와 동일한 17종 자재 표시 | ⏳ | | +| 정적 BOM과 수식 산출 탭 전환 작동 | ⏳ | | +| 비가변사이즈 품목은 기존 정적 BOM만 표시 | ⏳ | | +| 에러 처리 및 로딩 상태 표시 | ⏳ | | + +--- + +## 10. 자기완결성 점검 결과 + +### 10.1 체크리스트 검증 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | 가변사이즈 FG 품목의 동적 자재 산출 표시 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 성공 기준 5개 항목 | +| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1 백엔드 3건, Phase 2 프론트 5건 | +| 4 | 의존성이 명시되어 있는가? | ✅ | API 엔드포인트 인증 분석 완료, Docker 라우팅 패턴 | +| 5 | 참고 파일 경로가 정확한가? | ✅ | 모든 파일 경로 검증 완료 | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | 전체 구현 코드 포함 | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 8개 | +| 8 | 모호한 표현이 없는가? | ✅ | 구체적 수치/코드로 기술 | +| 9 | 기존 코드 현황이 명시되어 있는가? | ✅ | 섹션 3에 전체 파일 구조 + 핵심 코드 인라인 | +| 10 | API 인증 방식이 확정되었는가? | ✅ | X-API-KEY 필수 (FLOW_TESTER_API_KEY) | +| 11 | 트러블슈팅 가이드가 있는가? | ✅ | 4.1 FormulaApiService 트러블슈팅 섹션 | + +### 10.2 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | +| Q2. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 + 4.1 Phase 1 | +| Q3. 어떤 파일을 수정/생성해야 하는가? | ✅ | 3.1 파일 구조 + 2. 대상 범위 | +| Q4. 현재 코드 상태는 어떤가? | ✅ | 3.2~3.6 기존 코드 현황 | +| Q5. API 인증은 어떻게 하는가? | ✅ | 4.1 FormulaApiService (인증 테이블) | +| Q6. 테넌트 필터링은 어떻게 동작하는가? | ✅ | 3.6 테넌트 필터링 패턴 | +| Q7. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | +| Q8. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 + 4.1 트러블슈팅 | + +**결과**: 8/8 통과 → ✅ 자기완결성 확보 + +--- + +*이 문서는 /sc:plan 스킬로 생성되었습니다. 자기완결성 보강: 2026-02-19* diff --git a/plans/archive/mng-item-management-plan.md b/plans/archive/mng-item-management-plan.md new file mode 100644 index 0000000..172f216 --- /dev/null +++ b/plans/archive/mng-item-management-plan.md @@ -0,0 +1,1447 @@ +# MNG 품목관리 페이지 계획 + +> **작성일**: 2026-02-19 +> **목적**: MNG 관리자 패널에 3-Panel 품목관리 페이지 추가 (좌측 리스트 + 중앙 BOM 트리 + 우측 상세) +> **기준 문서**: docs/rules/item-policy.md, docs/specs/item-master-integration.md +> **상태**: ✅ 기본 구현 완료 (미커밋) → Phase 3 수식 연동은 별도 계획 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | Phase 1~2 전체 구현 완료 (미커밋 상태) | +| **다음 작업** | 수식 엔진 연동 → `docs/plans/mng-item-formula-integration-plan.md` 참조 | +| **진행률** | 12/12 (100%) - 기본 3-Panel 구현 완료 | +| **마지막 업데이트** | 2026-02-19 | +| **후속 작업** | FormulaEvaluatorService 연동 (별도 계획 문서) | + +--- + +## 1. 개요 + +### 1.1 배경 + +MNG 관리자 패널에 품목(Items)을 관리하고 BOM 연결관계를 시각적으로 파악할 수 있는 페이지가 필요하다. +현재 items 테이블은 products + materials 통합 구조로, `items.bom` JSON 필드에 BOM 구성을 저장한다. + +### 1.2 기준 원칙 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 🎯 핵심 원칙 │ +├─────────────────────────────────────────────────────────────────┤ +│ - MNG에서 마이그레이션 파일 생성 금지 (API에서만) │ +│ - Service-First (비즈니스 로직은 Service 클래스에만) │ +│ - FormRequest 필수 (Controller 검증 금지) │ +│ - BelongsToTenant (테넌트 격리) │ +│ - Blade + HTMX + Tailwind (Alpine.js 미사용) │ +│ - 세션 기반 테넌트 필터링: session('selected_tenant_id') │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 1.3 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | 모델/서비스/뷰/컨트롤러/라우트 생성 | 불필요 | +| ⚠️ 컨펌 필요 | 기존 라우트 수정, 사이드바 메뉴 추가 | **필수** | +| 🔴 금지 | mng에서 마이그레이션 생성, 테이블 구조 변경 | 별도 협의 | + +### 1.4 MNG 절대 금지 규칙 (인라인) + +``` +❌ mng/database/migrations/ 에 파일 생성 금지 +❌ docker exec sam-mng-1 php artisan migrate 실행 금지 +❌ php artisan db:seed --class=*MenuSeeder 실행 금지 +❌ 메뉴 시더 파일 생성/실행 금지 (부서별 권한 초기화됨) +❌ Controller에서 직접 DB 쿼리 금지 (Service-First) +❌ Controller에서 직접 validate() 금지 (FormRequest 필수) +``` + +--- + +## 2. 기능 설계 + +### 2.1 3-Panel 레이아웃 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Header (64px) - 테넌트 선택 (session 기반 필터링) │ +├──────────┬─────────────────────────────┬────────────────────────────┤ +│ 좌측 │ 중앙 │ 우측 │ +│ (280px) │ (flex-1) │ (380px) │ +│ │ │ │ +│ [검색] │ │ ┌──────────────────────┐ │ +│ ________│ │ │ 기본정보 │ │ +│ │ BOM 재귀 트리 │ │ 코드: P-001 │ │ +│ 품목 1 ◀│ ┌ 완제품A │ │ 이름: 스크린 제품 │ │ +│ 품목 2 │ ├─ 부품B │ │ 유형: FG │ │ +│ 품목 3 │ │ ├─ 원자재C │ │ 단위: EA │ │ +│ 품목 4 │ │ └─ 부자재D │ │ 카테고리: ... │ │ +│ 품목 5 │ ├─ 부품E │ ├──────────────────────┤ │ +│ ... │ │ ├─ 원자재F │ │ BOM 구성 (1depth) │ │ +│ │ │ └─ 소모품G │ │ - 부품B (2ea) │ │ +│ │ └─ 원자재H │ │ - 부품E (1ea) │ │ +│ │ │ │ - 원자재H (0.5kg) │ │ +│ │ ← 전체 재귀 트리 → │ ├──────────────────────┤ │ +│ │ (좌측 선택 품목 기준) │ │ 절곡 정보 │ │ +│ │ │ │ (bending_details) │ │ +│ │ │ ├──────────────────────┤ │ +│ │ │ │ 이미지/파일 │ │ +│ │ │ │ 📎 도면.pdf │ │ +│ │ │ │ 📎 인증서.pdf │ │ +│ │ │ └──────────────────────┘ │ +├──────────┴─────────────────────────────┴────────────────────────────┤ +│ ← 클릭 시 어디서든 → 우측 상세 갱신 │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 패널별 상세 동작 + +#### 좌측 패널 (품목 리스트) +- **상단 검색**: `` debounce 300ms, 코드+이름 동시 검색 +- **리스트**: 스크롤 가능, 선택된 항목 하이라이트 +- **표시 정보**: 품목코드, 품목명, 유형(FG/PT/SM/RM/CS) 뱃지 +- **테넌트 필터**: 헤더에서 선택된 테넌트 자동 적용 (BelongsToTenant) +- **클릭 시**: 중앙 트리 갱신 + 우측 상세 갱신 + +#### 중앙 패널 (BOM 재귀 트리) +- **데이터 소스**: `items.bom` JSON → child_item_id 재귀 탐색 +- **트리 깊이**: 전체 재귀 (BOM → BOM → BOM ...) +- **노드 표시**: 품목코드 + 품목명 + 수량 + 유형 뱃지 +- **펼침/접힘**: 노드별 토글 가능 +- **클릭 시**: 해당 품목으로 우측 상세 갱신 (좌측 선택은 변경 안 함) + +#### 우측 패널 (선택 품목 상세) +- **기본정보**: 코드, 이름, 유형, 단위, 카테고리, 활성 여부, options +- **BOM 구성 (1depth)**: 직접 연결된 자식 품목만 (재귀 X) +- **절곡 정보**: item_details.bending_details JSON (해당 시) +- **파일/이미지**: 연결된 files 목록 +- **scope**: 선택된 품목에 직접 연결된 정보만 (1depth) + +### 2.3 데이터 흐름 + +``` +[좌측 검색/선택] + │ + ├──→ HTMX GET /api/admin/items?search=xxx + │ → 좌측 리스트 갱신 + │ + ├──→ fetch GET /api/admin/items/{id}/bom-tree + │ → 중앙 트리 갱신 (재귀 JSON 반환 → Vanilla JS 렌더링) + │ + └──→ HTMX GET /api/admin/items/{id}/detail + → 우측 상세 갱신 + +[중앙 트리 노드 클릭] + │ + └──→ HTMX GET /api/admin/items/{id}/detail + → 우측 상세만 갱신 (중앙 트리 유지) +``` + +--- + +## 3. 기술 설계 + +### 3.1 DB 스키마 (기존 테이블 활용, 변경 없음) + +```sql +-- items (통합 품목) - 이미 존재하는 테이블 +-- item_type: FG(완제품), PT(부품), SM(부자재), RM(원자재), CS(소모품) +-- item_category: SCREEN, STEEL, BENDING, ALUMINUM 등 +CREATE TABLE items ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + item_type VARCHAR(10) NOT NULL, -- FG/PT/SM/RM/CS + item_category VARCHAR(50) NULL, -- SCREEN/STEEL/BENDING/ALUMINUM 등 + code VARCHAR(50) NOT NULL, + name VARCHAR(200) NOT NULL, + unit VARCHAR(20) NULL, + category_id BIGINT UNSIGNED NULL, -- FK → categories.id + bom JSON NULL, -- [{child_item_id: 5, quantity: 2.5}, ...] + attributes JSON NULL, -- 동적 필드 (migration 등에서 가져온 데이터) + attributes_archive JSON NULL, -- 아카이브 + options JSON NULL, -- {lot_managed, consumption_method, ...} + description TEXT NULL, + is_active TINYINT(1) DEFAULT 1, + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_at TIMESTAMP NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX (tenant_id), INDEX (item_type), INDEX (code), INDEX (category_id) +); + +-- item_details (1:1 확장) - 이미 존재하는 테이블 +CREATE TABLE item_details ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + item_id BIGINT UNSIGNED NOT NULL UNIQUE, -- FK → items.id (1:1) + -- Products 전용 + is_sellable TINYINT(1) DEFAULT 0, + is_purchasable TINYINT(1) DEFAULT 0, + is_producible TINYINT(1) DEFAULT 0, + safety_stock DECIMAL(10,2) NULL, + lead_time INT NULL, + is_variable_size TINYINT(1) DEFAULT 0, + product_category VARCHAR(50) NULL, + part_type VARCHAR(50) NULL, + bending_diagram VARCHAR(255) NULL, -- 절곡 도면 파일 경로 + bending_details JSON NULL, -- 절곡 상세 정보 JSON + specification_file VARCHAR(255) NULL, + specification_file_name VARCHAR(255) NULL, + certification_file VARCHAR(255) NULL, + certification_file_name VARCHAR(255) NULL, + certification_number VARCHAR(100) NULL, + certification_start_date DATE NULL, + certification_end_date DATE NULL, + -- Materials 전용 + is_inspection CHAR(1) NULL, -- 'Y'/'N' + item_name VARCHAR(200) NULL, + specification VARCHAR(500) NULL, + search_tag VARCHAR(500) NULL, + remarks TEXT NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +); + +-- files (폴리모픽) - 이미 존재하는 테이블 +-- 품목 파일: document_id = items.id, document_type = '1' (ITEM_GROUP_ID) +CREATE TABLE files ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NULL, + document_id BIGINT UNSIGNED NOT NULL, -- 연결 대상 ID (items.id) + document_type VARCHAR(10) NOT NULL, -- '1' = ITEM_GROUP_ID + original_name VARCHAR(255) NOT NULL, + stored_name VARCHAR(255) NOT NULL, + path VARCHAR(500) NOT NULL, + mime_type VARCHAR(100) NULL, + size BIGINT UNSIGNED NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +); + +-- categories - 이미 존재하는 테이블 +-- 품목 카테고리 (code_group으로 구분, 계층 구조) +CREATE TABLE categories ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + tenant_id BIGINT UNSIGNED NOT NULL, + parent_id BIGINT UNSIGNED NULL, -- 자기 참조 (트리) + code_group VARCHAR(50) NOT NULL, -- 카테고리 그룹 + profile_code VARCHAR(50) NULL, + code VARCHAR(50) NOT NULL, + name VARCHAR(200) NOT NULL, + is_active TINYINT(1) DEFAULT 1, + sort_order INT DEFAULT 0, + description TEXT NULL, + created_by BIGINT UNSIGNED NULL, + updated_by BIGINT UNSIGNED NULL, + deleted_by BIGINT UNSIGNED NULL, + deleted_at TIMESTAMP NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +); +``` + +### 3.2 BOM 트리 재귀 로직 + +```php +// ItemManagementService::getBomTree(int $itemId, int $maxDepth = 10): array +public function getBomTree(int $itemId, int $maxDepth = 10): array +{ + $item = Item::with('details')->findOrFail($itemId); + return $this->buildBomNode($item, 0, $maxDepth, []); +} + +private function buildBomNode(Item $item, int $depth, int $maxDepth, array $visited): array +{ + // 순환 참조 방지: visited 배열 + maxDepth 이중 안전장치 + if (in_array($item->id, $visited) || $depth >= $maxDepth) { + return $this->formatNode($item, $depth, []); + } + + $visited[] = $item->id; + $children = []; + + $bomData = $item->bom ?? []; + if (!empty($bomData)) { + $childIds = array_column($bomData, 'child_item_id'); + $childItems = Item::whereIn('id', $childIds)->get()->keyBy('id'); + + foreach ($bomData as $bom) { + $childItem = $childItems->get($bom['child_item_id']); + if ($childItem) { + $childNode = $this->buildBomNode($childItem, $depth + 1, $maxDepth, $visited); + $childNode['quantity'] = $bom['quantity'] ?? 1; + $children[] = $childNode; + } + } + } + + return $this->formatNode($item, $depth, $children); +} + +private function formatNode(Item $item, int $depth, array $children): array +{ + return [ + 'id' => $item->id, + 'code' => $item->code, + 'name' => $item->name, + 'item_type' => $item->item_type, + 'unit' => $item->unit, + 'depth' => $depth, + 'has_children' => count($children) > 0, + 'children' => $children, + ]; +} +``` + +### 3.3 API 엔드포인트 설계 + +| Method | Endpoint | 설명 | 반환 | +|--------|----------|------|------| +| GET | `/api/admin/items` | 품목 목록 (검색, 페이지네이션) | HTML partial | +| GET | `/api/admin/items/{id}/bom-tree` | BOM 재귀 트리 | JSON | +| GET | `/api/admin/items/{id}/detail` | 품목 상세 (1depth BOM, 파일, 절곡) | HTML partial | + +#### GET /api/admin/items + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| search | string | 코드+이름 검색 (LIKE) | +| item_type | string | 유형 필터 (FG,PT,SM,RM,CS 쉼표 구분) | +| per_page | int | 페이지 크기 (default: 50) | +| page | int | 페이지 번호 | + +#### GET /api/admin/items/{id}/bom-tree + +| 파라미터 | 타입 | 설명 | +|---------|------|------| +| max_depth | int | 최대 재귀 깊이 (default: 10) | + +**응답 (JSON)**: +```json +{ + "id": 1, + "code": "SCREEN-001", + "name": "스크린 제품", + "item_type": "FG", + "unit": "EA", + "depth": 0, + "has_children": true, + "children": [ + { + "id": 5, + "code": "SLAT-001", + "name": "슬랫", + "item_type": "PT", + "quantity": 2.5, + "depth": 1, + "has_children": true, + "children": [ + { + "id": 12, + "code": "STEEL-001", + "name": "강판", + "item_type": "RM", + "quantity": 1.0, + "depth": 2, + "has_children": false, + "children": [] + } + ] + } + ] +} +``` + +#### GET /api/admin/items/{id}/detail + +**응답 (HTML partial)**: 기본정보 + BOM 1depth + 절곡정보 + 파일 목록 + +### 3.4 파일 구조 + +``` +mng/ +├── app/ +│ ├── Http/ +│ │ └── Controllers/ +│ │ ├── ItemManagementController.php # Web (Blade 화면) +│ │ └── Api/Admin/ +│ │ └── ItemManagementApiController.php # API (HTMX) +│ ├── Models/ +│ │ ├── Category.php # ⚠️ 이미 존재 (수정 불필요) +│ │ └── Items/ +│ │ ├── Item.php # ⚠️ 이미 존재 → 보완 필요 +│ │ └── ItemDetail.php # 신규 생성 +│ ├── Services/ +│ │ └── ItemManagementService.php # BOM 트리, 검색, 상세 +│ └── Traits/ +│ └── BelongsToTenant.php # ⚠️ 이미 존재 (수정 불필요) +├── resources/ +│ └── views/ +│ └── item-management/ +│ ├── index.blade.php # 메인 (3-Panel) +│ └── partials/ +│ ├── item-list.blade.php # 좌측 리스트 +│ ├── bom-tree.blade.php # 중앙 트리 (JS 렌더링) +│ └── item-detail.blade.php # 우측 상세 +└── routes/ + ├── web.php # + items 라우트 추가 + └── api.php # + items API 라우트 추가 +``` + +### 3.5 트리 렌더링 방식 + +**Vanilla JS + Tailwind (라이브러리 미사용)** - MNG 기존 패턴 유지 + +```javascript +// BOM 트리 JSON → HTML 변환 +function renderBomTree(node, container) { + const li = document.createElement('li'); + li.className = 'ml-4'; + + // 노드 렌더링 + const nodeEl = document.createElement('div'); + nodeEl.className = 'flex items-center gap-2 py-1 px-2 rounded cursor-pointer hover:bg-blue-50'; + nodeEl.onclick = () => selectTreeNode(node.id); + + // 펼침/접힘 토글 + if (node.has_children) { + const toggle = document.createElement('span'); + toggle.className = 'text-gray-400 cursor-pointer'; + toggle.textContent = '▶'; + toggle.onclick = (e) => { e.stopPropagation(); toggleNode(toggle, childList); }; + nodeEl.appendChild(toggle); + } else { + // 빈 공간 (들여쓰기 맞춤) + const spacer = document.createElement('span'); + spacer.className = 'w-4 inline-block'; + nodeEl.appendChild(spacer); + } + + // 유형 뱃지 + 코드 + 이름 + 수량 + nodeEl.innerHTML += ` + ${node.item_type} + ${node.code} + ${node.name} + ${node.quantity ? `(${node.quantity})` : ''} + `; + li.appendChild(nodeEl); + + // 자식 노드 재귀 렌더링 + if (node.children && node.children.length > 0) { + const childList = document.createElement('ul'); + childList.className = 'border-l border-gray-200'; + node.children.forEach(child => renderBomTree(child, childList)); + li.appendChild(childList); + } + + container.appendChild(li); +} + +// 트리 노드 펼침/접힘 +function toggleNode(toggle, childList) { + if (childList.style.display === 'none') { + childList.style.display = ''; + toggle.textContent = '▼'; + } else { + childList.style.display = 'none'; + toggle.textContent = '▶'; + } +} +``` + +--- + +## 4. 대상 범위 + +### Phase 1: 백엔드 (모델 + 서비스 + API) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1.1 | Item 모델 보완 (mng/app/Models/Items/Item.php) | ✅ | BelongsToTenant, 관계, 스코프, 상수, 헬퍼 추가 | +| 1.2 | ItemDetail 모델 생성 (mng/app/Models/Items/ItemDetail.php) | ✅ | 1:1 관계, is_variable_size 포함 | +| 1.3 | ItemManagementService 생성 | ✅ | getItemList, getBomTree(재귀), getItemDetail | +| 1.4 | ItemManagementApiController 생성 | ✅ | index(HTML), bomTree(JSON), detail(HTML) | +| 1.5 | API 라우트 등록 (routes/api.php) | ✅ | /api/admin/items/* (3개 라우트) | +| 1.6 | File 모델 생성 (mng/app/Models/Commons/File.php) | ✅ | Item.files() 관계용 | + +### Phase 2: 프론트엔드 (Blade + JS) + +| # | 작업 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 2.1 | 메인 페이지 (index.blade.php) - 3-Panel 레이아웃 | ✅ | Tailwind flex, 3-Panel | +| 2.2 | 좌측 패널 (item-list.blade.php) + 실시간 검색 | ✅ | HTMX + debounce 300ms + 유형 필터 | +| 2.3 | 중앙 패널 (bom-tree.blade.php) + JS 트리 렌더링 | ✅ | Vanilla JS 재귀 렌더링 | +| 2.4 | 우측 패널 (item-detail.blade.php) | ✅ | 기본정보+BOM 1depth+절곡+파일 | +| 2.5 | ItemManagementController (Web) 생성 | ✅ | HX-Redirect 패턴 | +| 2.6 | Web 라우트 등록 (routes/web.php) | ✅ | GET /item-management | +| 2.7 | 유형별 뱃지 스타일 + 트리 라인 CSS | ✅ | Tailwind inline + JS getTypeBadgeClass | + +### Phase 3: 수식 엔진 연동 (후속 작업) + +> 별도 계획 문서: `docs/plans/mng-item-formula-integration-plan.md` +> +> 가변사이즈 FG 품목 선택 시 오픈사이즈(W,H) 입력 → FormulaEvaluatorService 동적 산출 → 중앙 패널 탭 전환 표시 + +--- + +## 5. 작업 절차 + +### Step 1: 모델 보완/생성 (Phase 1.1, 1.2) +``` +├── mng/app/Models/Items/Item.php 보완 (기존 파일 존재) +│ 현재 상태: SoftDeletes만 있음, BelongsToTenant 없음, 관계 없음 +│ 추가 필요: +│ - use App\Traits\BelongsToTenant 추가 +│ - $fillable에 category_id, bom, attributes, options, description 추가 +│ - $casts에 bom→array, options→array 추가 +│ - 관계: details(), category(), files() +│ - 스코프: type(), active(), search() +│ - 상수: TYPE_FG 등, PRODUCT_TYPES, MATERIAL_TYPES +│ - 헬퍼: isProduct(), isMaterial(), getBomChildIds() +│ +└── mng/app/Models/Items/ItemDetail.php 생성 (신규) + - item() belongsTo 관계 + - $fillable: 전체 필드 (섹션 A.3 참고) + - $casts: bending_details→array, is_sellable→boolean 등 +``` + +### Step 2: 서비스 생성 (Phase 1.3) +``` +├── mng/app/Services/ItemManagementService.php 생성 +│ - getItemList(array $filters): LengthAwarePaginator +│ └ Item::query()->search($search)->active()->orderBy('code')->paginate($perPage) +│ - getBomTree(int $itemId, int $maxDepth = 10): array +│ └ 재귀 buildBomNode() (섹션 3.2 코드) +│ - getItemDetail(int $itemId): array +│ └ Item::with(['details', 'category', 'files'])->findOrFail($id) +│ └ BOM 1depth: items.bom JSON에서 child_item_id 추출 → Item::whereIn() +│ +└── 테넌트 스코프 자동 적용 (BelongsToTenant가 글로벌 스코프 등록) +``` + +### Step 3: API 컨트롤러 + 라우트 (Phase 1.4, 1.5) +``` +├── mng/app/Http/Controllers/Api/Admin/ItemManagementApiController.php +│ - __construct(private readonly ItemManagementService $service) +│ - index(Request $request): View +│ └ HTMX 요청 시 HTML partial 반환 (Blade view render) +│ - bomTree(int $id): JsonResponse +│ └ JSON 반환 (JS에서 트리 렌더링) +│ - detail(int $id): View +│ └ HTML partial 반환 (item-detail.blade.php) +│ +└── routes/api.php에 라우트 추가 (기존 그룹 내) + // 기존 Route::middleware(['web', 'auth', 'hq.member']) + // ->prefix('admin')->name('api.admin.')->group(function () { ... }); + // 내부에 추가: + Route::prefix('items')->name('items.')->group(function () { + Route::get('/', [ItemManagementApiController::class, 'index'])->name('index'); + Route::get('/{id}/bom-tree', [ItemManagementApiController::class, 'bomTree'])->name('bom-tree'); + Route::get('/{id}/detail', [ItemManagementApiController::class, 'detail'])->name('detail'); + }); +``` + +### Step 4: Blade 뷰 생성 (Phase 2.1~2.4) +``` +├── index.blade.php: 3-Panel 메인 레이아웃 +│ @extends('layouts.app'), @section('content'), @push('scripts') +│ HTMX 페이지이므로 HX-Redirect 필요 (JS가 @push('scripts')에 있음) +│ +├── partials/item-list.blade.php: 좌측 품목 리스트 +│ @foreach($items as $item) → 품목코드, 품목명, 유형 뱃지 +│ data-item-id="{{ $item->id }}" onclick="selectItem({{ $item->id }})" +│ +├── partials/bom-tree.blade.php: 중앙 트리 (빈 컨테이너) +│
품목을 선택하세요
+│ +└── partials/item-detail.blade.php: 우측 상세정보 + 기본정보 테이블 + BOM 1depth 리스트 + 절곡 정보 + 파일 목록 +``` + +### Step 5: Web 컨트롤러 + 라우트 (Phase 2.5, 2.6) +``` +├── mng/app/Http/Controllers/ItemManagementController.php +│ - __construct(private readonly ItemManagementService $service) +│ - index(Request $request): View|Response +│ └ HX-Request 체크 → HX-Redirect (JS 포함 페이지이므로) +│ └ return view('item-management.index') +│ +└── routes/web.php에 라우트 추가 + // 기존 인증 미들웨어 그룹 내에 추가: + Route::get('/item-management', [ItemManagementController::class, 'index']) + ->name('item-management.index'); +``` + +### Step 6: 스타일 + 트리 인터랙션 (Phase 2.7) +``` +├── 유형별 뱃지 색상 (Tailwind inline) +│ FG: bg-blue-100 text-blue-800 (완제품) +│ PT: bg-green-100 text-green-800 (부품) +│ SM: bg-yellow-100 text-yellow-800 (부자재) +│ RM: bg-orange-100 text-orange-800 (원자재) +│ CS: bg-gray-100 text-gray-800 (소모품) +│ +└── 트리 라인 CSS (border-l + ml-4 indent) +``` + +--- + +## 6. 상세 구현 명세 + +### 6.1 Item 모델 보완 (기존 파일 수정) + +**기존 파일**: `mng/app/Models/Items/Item.php` + +**현재 상태 (보완 전)**: +```php + 'boolean', + 'attributes' => 'array', + ]; +} +``` + +**보완 후 (목표 상태)**: +```php + 'array', + 'attributes' => 'array', + 'attributes_archive' => 'array', + 'options' => 'array', + 'is_active' => 'boolean', + ]; + + // 유형 상수 + const TYPE_FG = 'FG'; // 완제품 + const TYPE_PT = 'PT'; // 부품 + const TYPE_SM = 'SM'; // 부자재 + const TYPE_RM = 'RM'; // 원자재 + const TYPE_CS = 'CS'; // 소모품 + + const PRODUCT_TYPES = ['FG', 'PT']; + const MATERIAL_TYPES = ['SM', 'RM', 'CS']; + + // ── 관계 ── + + public function details() + { + return $this->hasOne(ItemDetail::class, 'item_id'); + } + + public function category() + { + return $this->belongsTo(Category::class, 'category_id'); + } + + /** + * 파일 (document_id/document_type 기반) + * document_id = items.id, document_type = '1' (ITEM_GROUP_ID) + */ + public function files() + { + return $this->hasMany(\App\Models\Commons\File::class, 'document_id') + ->where('document_type', '1'); + } + + // ── 스코프 ── + + public function scopeType($query, string $type) + { + return $query->where('items.item_type', strtoupper($type)); + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + public function scopeSearch($query, ?string $search) + { + if (!$search) return $query; + return $query->where(function ($q) use ($search) { + $q->where('code', 'like', "%{$search}%") + ->orWhere('name', 'like', "%{$search}%"); + }); + } + + // ── 헬퍼 ── + + public function isProduct(): bool + { + return in_array($this->item_type, self::PRODUCT_TYPES); + } + + public function isMaterial(): bool + { + return in_array($this->item_type, self::MATERIAL_TYPES); + } + + public function getBomChildIds(): array + { + return collect($this->bom ?? [])->pluck('child_item_id')->filter()->toArray(); + } +} +``` + +> **주의**: files() 관계에서 `\App\Models\Commons\File::class` 경로를 사용한다. +> 만약 mng에 File 모델이 없다면, 단순 모델로 신규 생성해야 한다. +> 확인 필요: `mng/app/Models/Commons/File.php` 존재 여부. 없으면 생성. + +### 6.2 ItemDetail 모델 (신규 생성) + +```php + 'boolean', + 'is_purchasable' => 'boolean', + 'is_producible' => 'boolean', + 'is_variable_size' => 'boolean', + 'bending_details' => 'array', + 'certification_start_date' => 'date', + 'certification_end_date' => 'date', + ]; + + public function item() + { + return $this->belongsTo(Item::class); + } +} +``` + +### 6.3 좌측 검색 - Debounce + HTMX + +```javascript +// index.blade.php @push('scripts') +let searchTimer = null; +const searchInput = document.getElementById('item-search'); + +searchInput.addEventListener('input', function() { + clearTimeout(searchTimer); + searchTimer = setTimeout(() => { + const search = this.value.trim(); + htmx.ajax('GET', `/api/admin/items?search=${encodeURIComponent(search)}&per_page=50`, { + target: '#item-list', + swap: 'innerHTML', + headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content} + }); + }, 300); // 300ms debounce +}); +``` + +### 6.4 품목 선택 시 중앙+우측 갱신 + +```javascript +// 품목 선택 함수 (좌측/중앙 공용) +function selectItem(itemId, updateTree = true) { + // 선택 하이라이트 + document.querySelectorAll('.item-row').forEach(el => el.classList.remove('bg-blue-50', 'border-blue-300')); + const selected = document.querySelector(`[data-item-id="${itemId}"]`); + if (selected) selected.classList.add('bg-blue-50', 'border-blue-300'); + + // 중앙 트리 갱신 (좌측에서 클릭 시에만) + if (updateTree) { + fetch(`/api/admin/items/${itemId}/bom-tree`, { + headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content} + }) + .then(res => res.json()) + .then(tree => { + const container = document.getElementById('bom-tree-container'); + container.innerHTML = ''; + if (tree.has_children) { + const ul = document.createElement('ul'); + renderBomTree(tree, ul); + container.appendChild(ul); + } else { + container.innerHTML = '

BOM 구성이 없습니다.

'; + } + }); + } + + // 우측 상세 갱신 (항상) + htmx.ajax('GET', `/api/admin/items/${itemId}/detail`, { + target: '#item-detail', + swap: 'innerHTML', + headers: {'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content} + }); +} + +// 중앙 트리 노드 클릭 (트리는 유지, 우측만 갱신) +function selectTreeNode(itemId) { + selectItem(itemId, false); // updateTree = false +} +``` + +--- + +## 7. 컨펌 대기 목록 + +| # | 항목 | 변경 내용 | 영향 범위 | 상태 | +|---|------|----------|----------|------| +| 1 | 사이드바 메뉴 추가 | "품목관리" 메뉴 항목 추가 | menus 테이블 (DB) | ⏳ tinker 안내 필요 | + +--- + +## 8. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-02-19 | - | 문서 초안 작성 | - | - | +| 2026-02-19 | - | 자기완결성 보강 (Appendix A~C 추가) | - | - | +| 2026-02-19 | Phase 1 | Item 모델 보완, ItemDetail/File 모델 생성 | Item.php, ItemDetail.php, File.php | ✅ | +| 2026-02-19 | Phase 1 | ItemManagementService 생성 | ItemManagementService.php | ✅ | +| 2026-02-19 | Phase 1 | ItemManagementApiController 생성 + API 라우트 | ItemManagementApiController.php, api.php | ✅ | +| 2026-02-19 | Phase 2 | 3-Panel Blade 뷰 전체 생성 | index.blade.php + 3 partials | ✅ | +| 2026-02-19 | Phase 2 | Web 컨트롤러 + 라우트 등록 | ItemManagementController.php, web.php | ✅ | +| 2026-02-19 | - | Phase 1~2 완료, Phase 3 수식 연동 계획 별도 문서 분리 | mng-item-formula-integration-plan.md | - | + +--- + +## 9. 참고 문서 + +- **품목 정책**: `docs/rules/item-policy.md` +- **품목 연동 설계**: `docs/specs/item-master-integration.md` +- **MNG 절대 규칙**: `mng/docs/MNG_CRITICAL_RULES.md` +- **MNG 프로젝트 문서**: `mng/docs/INDEX.md` +- **DB 스키마**: `docs/specs/database-schema.md` +- **API Item 모델**: `api/app/Models/Items/Item.php` +- **API ItemDetail 모델**: `api/app/Models/Items/ItemDetail.php` + +--- + +## 10. 검증 결과 + +### 10.1 테스트 케이스 + +| 입력값 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| 좌측 검색: "스크린" | "스크린" 포함 품목만 표시 | 정상 동작 | ✅ | +| FG 품목 클릭 | 중앙에 BOM 트리, 우측에 상세 | 정상 동작 (정적 BOM 2개 표시) | ✅ | +| BOM 없는 품목 클릭 | 중앙 "BOM 없음", 우측 상세 표시 | 정상 동작 | ✅ | +| 중앙 트리 노드 클릭 | 우측 상세만 변경 (트리 유지) | 정상 동작 | ✅ | +| 테넌트 전환 | 좌측 리스트가 해당 테넌트 품목으로 변경 | 확인 필요 | ⏳ | +| 순환 참조 BOM | 무한 루프 없이 maxDepth에서 중단 | 로직 구현 완료, 실제 데이터 미검증 | ⏳ | + +### 10.2 성공 기준 + +| 기준 | 달성 | 비고 | +|------|------|------| +| 3-Panel 레이아웃 정상 렌더링 | ✅ | 좌측 280px + 중앙 flex-1 + 우측 384px | +| 실시간 검색 (debounce 300ms) | ✅ | 코드+이름 동시 검색 | +| BOM 재귀 트리 정상 표시 (전체 depth) | ✅ | 펼침/접힘 토글 포함 | +| 어디서든 클릭 → 우측 상세 갱신 | ✅ | selectItem + selectTreeNode | +| 테넌트 필터링 정상 동작 | ⏳ | withoutGlobalScopes + session 패턴 사용 | +| 순환 참조 방지 (maxDepth) | ✅ | visited 배열 + maxDepth 이중 안전장치 | + +--- + +## 11. 자기완결성 점검 결과 + +### 11.1 체크리스트 검증 + +| # | 검증 항목 | 상태 | 비고 | +|---|----------|:----:|------| +| 1 | 작업 목적이 명확한가? | ✅ | 3-Panel 품목관리 페이지 | +| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 10.2 | +| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4 (12개 작업 항목) | +| 4 | 의존성이 명시되어 있는가? | ✅ | items 테이블 존재 전제 | +| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 + Appendix | +| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 (6 Step) | +| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 10.1 | +| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/구조 명시 | + +### 11.2 새 세션 시뮬레이션 테스트 + +| 질문 | 답변 가능 | 참조 섹션 | +|------|:--------:|----------| +| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | +| Q2. 어디서부터 시작해야 하는가? | ✅ | 5. 작업 절차 Step 1 | +| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 3.4 파일 구조 + 6.1 기존 파일 현황 | +| Q4. 작업 완료 확인 방법은? | ✅ | 10. 검증 결과 | +| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 + Appendix A~C | +| Q6. MNG 코딩 패턴은 무엇인가? | ✅ | Appendix A (인라인 패턴) | +| Q7. 테넌트 필터링은 어떻게 동작하는가? | ✅ | Appendix B (BelongsToTenant 전문) | +| Q8. API 모델의 정확한 필드는? | ✅ | Appendix C (API 모델 전문) | + +**결과**: 8/8 통과 → ✅ 자기완결성 확보 + +--- + +## Appendix A: MNG 코딩 패턴 레퍼런스 + +> 새 세션에서 외부 파일을 읽지 않고도 MNG 패턴을 따를 수 있도록 인라인화한 레퍼런스. + +### A.1 Web Controller 패턴 + +Web Controller는 Blade 뷰 렌더링만 담당한다. 비즈니스 로직은 Service에 위임. + +```php +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('item-management.index')); + } + return view('item-management.index'); +} +``` + +### A.2 API Controller 패턴 + +API Controller는 HTMX 요청 시 HTML partial, 일반 요청 시 JSON 반환. + +```php +departmentService->getDepartments( + $request->all(), + $request->integer('per_page', 10) + ); + + // HTMX 요청 시 HTML partial 반환 + if ($request->header('HX-Request')) { + return view('departments.partials.table', compact('departments')); + } + + // 일반 요청 시 JSON + return response()->json([ + 'success' => true, + 'data' => $departments->items(), + 'meta' => [ + 'current_page' => $departments->currentPage(), + 'last_page' => $departments->lastPage(), + 'per_page' => $departments->perPage(), + 'total' => $departments->total(), + ], + ]); + } +} +``` + +### A.3 Service 패턴 + +모든 DB 쿼리 로직은 Service에서 처리. `session('selected_tenant_id')`로 테넌트 격리. + +```php +with('parent'); + + // 검색 필터 + if (!empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('code', 'like', "%{$search}%"); + }); + } + + return $query->orderBy('sort_order')->paginate($perPage); + } +} +``` + +> **중요**: BelongsToTenant trait이 모델에 있으면 tenant_id 필터가 자동 적용된다. +> Service에서 수동으로 `where('tenant_id', ...)` 할 필요 없음. + +### A.4 Blade + HTMX 패턴 + +Index 페이지는 빈 셸이고, 데이터는 HTMX `hx-get` + `hx-trigger="load"`로 로드. + +```blade +{{-- 참고: mng/resources/views/departments/index.blade.php 패턴 --}} +@extends('layouts.app') + +@section('title', '부서 관리') + +@section('content') +
+

부서 관리

+
+ + {{-- HTMX 테이블: 초기 로드 + 이벤트 재로드 --}} +
+ {{-- 로딩 스피너 --}} +
+
+
+
+@endsection + +@push('scripts') + +@endpush +``` + +### A.5 라우트 패턴 + +**routes/web.php** 구조: +```php +// 인증 필요 라우트 그룹 +Route::middleware(['auth', 'hq.member', 'password.changed'])->group(function () { + // ... 기존 라우트들 ... + + // 품목관리 (신규 추가할 위치) + Route::get('/item-management', [ItemManagementController::class, 'index']) + ->name('item-management.index'); +}); +``` + +**routes/api.php** 구조: +```php +// MNG API는 세션 기반 (token 아님) +Route::middleware(['web', 'auth', 'hq.member']) + ->prefix('admin') + ->name('api.admin.') + ->group(function () { + // ... 기존 API 라우트들 ... + + // 품목관리 API (신규 추가할 위치) + Route::prefix('items')->name('items.')->group(function () { + Route::get('/', [ItemManagementApiController::class, 'index'])->name('index'); + Route::get('/{id}/bom-tree', [ItemManagementApiController::class, 'bomTree'])->name('bom-tree'); + Route::get('/{id}/detail', [ItemManagementApiController::class, 'detail'])->name('detail'); + }); + }); +``` + +> **주의**: MNG API는 `['web', 'auth', 'hq.member']` 미들웨어 사용 (세션 기반, Sanctum 아님). +> 고정 라우트(`/all`, `/summary`)를 `/{id}` 파라미터 라우트보다 먼저 정의해야 충돌 방지. + +### A.6 모델 패턴 + +```php +// 참고: mng/app/Models/Category.php 패턴 +use App\Traits\BelongsToTenant; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; + +class Category extends Model +{ + use BelongsToTenant, SoftDeletes; + + protected $fillable = [ + 'tenant_id', 'parent_id', 'code_group', 'profile_code', + 'code', 'name', 'is_active', 'sort_order', 'description', + 'created_by', 'updated_by', 'deleted_by', + ]; + + protected $casts = [ + 'is_active' => 'boolean', + 'sort_order' => 'integer', + ]; + + // 자기 참조 트리 + public function parent() { return $this->belongsTo(self::class, 'parent_id'); } + public function children() { return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order'); } + + // 스코프 + public function scopeActive($query) { return $query->where('is_active', true); } +} +``` + +--- + +## Appendix B: BelongsToTenant 동작 방식 + +### B.1 Trait (mng/app/Traits/BelongsToTenant.php) + +```php +runningInConsole()) { + return; + } + + // 요청당 1회만 tenant_id 조회 (캐시) + if (!self::$cacheInitialized) { + $request = app(Request::class); + self::$cachedTenantId = $request->attributes->get('tenant_id') + ?? $request->header('X-TENANT-ID') + ?? auth()->user()?->tenant_id; + self::$cacheInitialized = true; + } + + if (self::$cachedTenantId !== null) { + $builder->where($model->getTable() . '.tenant_id', self::$cachedTenantId); + } + } + + public static function clearCache(): void + { + self::$cachedTenantId = null; + self::$cacheInitialized = false; + } +} +``` + +**동작 요약**: +1. 모델에 `use BelongsToTenant` 선언하면 자동으로 TenantScope 등록 +2. 모든 쿼리에 `WHERE items.tenant_id = ?` 조건 자동 추가 +3. tenant_id 결정 우선순위: request attributes → X-TENANT-ID 헤더 → auth user +4. console 환경(migrate 등)에서는 스킵 +5. **Service에서 수동 tenant_id 필터 불필요** (자동 적용) + +--- + +## Appendix C: API 모델 전문 (참조용) + +> 구현 시 API 모델의 정확한 필드 목록과 관계를 참고하기 위한 인라인 전문. + +### C.1 api/app/Models/Items/Item.php (전체) + +```php + 'array', + 'attributes' => 'array', + 'attributes_archive' => 'array', + 'options' => 'array', + 'is_active' => 'boolean', + ]; + + const TYPE_FINISHED_GOODS = 'FG'; + const TYPE_PARTS = 'PT'; + const TYPE_SUB_MATERIALS = 'SM'; + const TYPE_RAW_MATERIALS = 'RM'; + const TYPE_CONSUMABLES = 'CS'; + const PRODUCT_TYPES = ['FG', 'PT']; + const MATERIAL_TYPES = ['SM', 'RM', 'CS']; + + public function details() { return $this->hasOne(ItemDetail::class); } + public function stock() { return $this->hasOne(\App\Models\Tenants\Stock::class); } + public function category() { return $this->belongsTo(Category::class, 'category_id'); } + + // files: document_id = item_id, document_type = '1' (ITEM_GROUP_ID) + public function files() + { + return $this->hasMany(File::class, 'document_id')->where('document_type', '1'); + } + + public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } + + // BOM 자식 조회 (JSON bom 필드에서 child_item_id 추출) + public function bomChildren() + { + $childIds = collect($this->bom ?? [])->pluck('child_item_id')->filter()->toArray(); + return self::whereIn('id', $childIds); + } + + // 스코프 + public function scopeType($query, string $type) + { + return $query->where('items.item_type', strtoupper($type)); + } + public function scopeProducts($query) { return $query->whereIn('items.item_type', self::PRODUCT_TYPES); } + public function scopeMaterials($query) { return $query->whereIn('items.item_type', self::MATERIAL_TYPES); } + public function scopeActive($query) { return $query->where('is_active', true); } + + // 헬퍼 + public function isProduct(): bool { return in_array($this->item_type, self::PRODUCT_TYPES); } + public function isMaterial(): bool { return in_array($this->item_type, self::MATERIAL_TYPES); } + public function getBomChildIds(): array + { + return collect($this->bom ?? [])->pluck('child_item_id')->filter()->toArray(); + } +} +``` + +### C.2 api/app/Models/Items/ItemDetail.php (전체) + +```php + 'boolean', + 'is_purchasable' => 'boolean', + 'is_producible' => 'boolean', + 'is_variable_size' => 'boolean', + 'bending_details' => 'array', + 'certification_start_date' => 'date', + 'certification_end_date' => 'date', + ]; + + public function item() { return $this->belongsTo(Item::class); } + public function isSellable(): bool { return $this->is_sellable ?? false; } + public function isPurchasable(): bool { return $this->is_purchasable ?? false; } + public function isProducible(): bool { return $this->is_producible ?? false; } + public function isCertificationValid(): bool + { + return $this->certification_end_date?->isFuture() ?? false; + } + public function requiresInspection(): bool { return $this->is_inspection === 'Y'; } +} +``` + +--- + +## Appendix D: 구현 시 확인 사항 + +### D.1 File 모델 존재 여부 확인 + +구현 시작 전 `mng/app/Models/Commons/File.php` 존재 여부를 확인해야 한다. +없으면 다음과 같이 간단한 모델 생성 필요: + +```php + 1, + 'parent_id' => <부모메뉴ID>, + 'name' => '품목관리', + 'url' => '/item-management', + 'icon' => 'heroicon-o-cube', + 'sort_order' => 1, + 'is_active' => true, +]); +" +``` + +### D.3 품목 유형 정리 + +| 코드 | 이름 | 설명 | BOM 자식 가능 | +|------|------|------|:------------:| +| FG | 완제품 (Finished Goods) | 최종 판매 제품 | ✅ 주로 있음 | +| PT | 부품 (Parts) | 조립/가공 부품 | ✅ 있을 수 있음 | +| SM | 부자재 (Sub Materials) | 보조 자재 | ❌ 일반적으로 없음 | +| RM | 원자재 (Raw Materials) | 원재료 | ❌ 리프 노드 | +| CS | 소모품 (Consumables) | 소모성 자재 | ❌ 리프 노드 | + +### D.4 items.bom JSON 구조 + +```json +// items.bom 필드 예시 (FG 완제품) +[ + {"child_item_id": 5, "quantity": 2.5}, + {"child_item_id": 8, "quantity": 1}, + {"child_item_id": 12, "quantity": 0.5} +] +// child_item_id는 같은 items 테이블의 다른 행을 참조 +// quantity는 소수점 가능 (단위에 따라 kg, m, EA 등) +``` + +### D.5 items.options JSON 구조 + +```json +{ + "lot_managed": true, // LOT 추적 여부 + "consumption_method": "auto", // auto/manual/none + "production_source": "self_produced", // purchased/self_produced/both + "input_tracking": true // 원자재 투입 추적 +} +``` + +--- + +*이 문서는 /plan 스킬로 생성되었습니다. 자기완결성 보강: 2026-02-19* \ No newline at end of file diff --git a/plans/order-location-management-plan.md b/plans/archive/order-location-management-plan.md similarity index 100% rename from plans/order-location-management-plan.md rename to plans/archive/order-location-management-plan.md diff --git a/plans/order-workorder-shipment-integration-plan.md b/plans/archive/order-workorder-shipment-integration-plan.md similarity index 100% rename from plans/order-workorder-shipment-integration-plan.md rename to plans/archive/order-workorder-shipment-integration-plan.md diff --git a/plans/quote-v2-auto-calculation-fix-plan.md b/plans/archive/quote-v2-auto-calculation-fix-plan.md similarity index 100% rename from plans/quote-v2-auto-calculation-fix-plan.md rename to plans/archive/quote-v2-auto-calculation-fix-plan.md diff --git a/plans/sam-stat-database-design-plan.md b/plans/archive/sam-stat-database-design-plan.md similarity index 100% rename from plans/sam-stat-database-design-plan.md rename to plans/archive/sam-stat-database-design-plan.md diff --git a/plans/simulator-calculation-logic-mapping.md b/plans/archive/simulator-calculation-logic-mapping.md similarity index 100% rename from plans/simulator-calculation-logic-mapping.md rename to plans/archive/simulator-calculation-logic-mapping.md diff --git a/plans/stock-integration-plan.md b/plans/archive/stock-integration-plan.md similarity index 100% rename from plans/stock-integration-plan.md rename to plans/archive/stock-integration-plan.md diff --git a/plans/welfare-section-plan.md b/plans/archive/welfare-section-plan.md similarity index 100% rename from plans/welfare-section-plan.md rename to plans/archive/welfare-section-plan.md diff --git a/plans/index_plans.md b/plans/index_plans.md index fc4fedb..9cd0f4d 100644 --- a/plans/index_plans.md +++ b/plans/index_plans.md @@ -1,7 +1,7 @@ # 기획 문서 인덱스 > SAM 시스템 개발 계획 및 기획 문서 모음 -> **최종 업데이트**: 2026-01-20 +> **최종 업데이트**: 2026-02-22 --- @@ -9,14 +9,12 @@ | 분류 | 개수 | 설명 | |------|------|------| -| 진행중/대기 계획서 | 16개 | 기능별 API 개발 계획 | -| 완료 아카이브 | 15개 | `archive/` 폴더에 보관 | +| 진행중/대기 계획서 | 44개 | 기능별 개발 계획 | +| 완료 아카이브 | 37개 | `archive/` 폴더에 보관 | | 스토리보드 | 1개 | ERP 화면 설계 (D1.0) | | 플로우 테스트 | 32개 | API 검증용 JSON 테스트 케이스 | -> **Note**: D0.8 스토리보드는 `docs/history/2025-12/`로 아카이브됨 -> **Note**: E2E 버그 수정 계획은 `docs/history/2026-01/`로 아카이브됨 (2026-01-15 완료) -> **Note**: 완료된 계획 15개는 `archive/` 폴더로 이동됨 (2026-01-20) +> **Note**: 완료된 계획 37개는 `archive/` 폴더로 이동됨 (최종 정리: 2026-02-22) --- @@ -24,72 +22,138 @@ ### ERP API 개발 -| 문서 | 상태 | 설명 | -|------|------|------| -| [erp-api-development-plan.md](./erp-api-development-plan.md) | 🟡 Phase 3 진행중 | SAM ERP API 전체 개발 계획 (D0.8 기준) | +| 문서 | 상태 | 진행률 | 설명 | +|------|------|--------|------| +| [erp-api-development-plan.md](./erp-api-development-plan.md) | 🟡 진행중 | Phase 3/L | SAM ERP API 전체 개발 계획, L-2 React 연동 대기 | -### 기능별 계획 +### 견적/수주 (Quote/Order) -| 문서 | 상태 | 설명 | -|------|------|------| -| [simulator-calculation-logic-mapping.md](./simulator-calculation-logic-mapping.md) | 📚 참조 | 견적 시뮬레이터 계산 로직 매핑 분석 | -| [mng-item-field-management-plan.md](./mng-item-field-management-plan.md) | ⚪ 계획수립 | 품목 필드 관리 (미착수) | -| [items-table-unification-plan.md](./items-table-unification-plan.md) | ⚪ 계획수립 | items 테이블 통합 (롤백 후 대기) | -| [mng-menu-system-plan.md](./mng-menu-system-plan.md) | 🔵 계획수립 | mng 메뉴 시스템 (설계 완료, 구현 대기) | +| 문서 | 상태 | 진행률 | 설명 | +|------|------|--------|------| +| [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) | 🟡 진행중 | 4/5 (80%) | 경동 견적 로직, Phase 5 통합 테스트 미완 | +| [quote-management-url-migration-plan.md](./quote-management-url-migration-plan.md) | 🟡 진행중 | 11/12 (92%) | URL 마이그레이션, 사용자 테스트 잔여 | +| [quote-management-8issues-plan.md](./quote-management-8issues-plan.md) | ⚪ 대기 | 0/8 (0%) | 견적관리 8개 이슈, 컨펌 대기 | +| [quote-calculation-api-plan.md](./quote-calculation-api-plan.md) | ⚪ 대기 | 0/12 (0%) | 견적 계산 API, 미착수 | +| [quote-order-sync-improvement-plan.md](./quote-order-sync-improvement-plan.md) | ⚪ 대기 | 0/4 (0%) | 견적-수주 동기화 개선, 미착수 | +| [quote-system-development-plan.md](./quote-system-development-plan.md) | ⚪ 대기 | - | 견적 시스템 개발, 계획 수립 | + +### 생산/절곡 (Production/Bending) + +| 문서 | 상태 | 진행률 | 설명 | +|------|------|--------|------| +| [bending-preproduction-stock-plan.md](./bending-preproduction-stock-plan.md) | 🟡 진행중 | 14/14 코드 | 선재고, 마이그레이션 실행/검증 잔여 | +| [bending-info-auto-generation-plan.md](./bending-info-auto-generation-plan.md) | ⚪ 대기 | 0/7 (0%) | 절곡 정보 자동 생성, 분석만 완료 | +| [bending-material-input-mapping-plan.md](./bending-material-input-mapping-plan.md) | ⚪ 대기 | 분석 | 절곡 자재투입 매핑, GAP 분석 완료 | + +### 품목/BOM (Item/BOM) + +| 문서 | 상태 | 진행률 | 설명 | +|------|------|--------|------| +| [bom-item-mapping-plan.md](./bom-item-mapping-plan.md) | 🟡 진행중 | 2/3 (66%) | BOM 품목 매핑, Phase 3 검증 잔여 | +| [item-master-data-alignment-plan.md](./item-master-data-alignment-plan.md) | 🟡 진행중 | - | 품목 마스터 정합, 섀도잉 정리 잔여 | +| [mng-item-field-management-plan.md](./mng-item-field-management-plan.md) | ⚪ 대기 | 0% | 품목 필드 관리, 미착수 | +| [item-inventory-management-plan.md](./item-inventory-management-plan.md) | ⚪ 대기 | 설계 | 품목 재고 관리, 설계 확정/구현 대기 | +| [fg-code-consolidation-plan.md](./fg-code-consolidation-plan.md) | ⚪ 대기 | 0/8 (0%) | FG 코드 통합, 미착수 | + +### 문서/서식 (Document System) + +| 문서 | 상태 | 진행률 | 설명 | +|------|------|--------|------| +| [document-management-system-plan.md](./document-management-system-plan.md) | 🟡 진행중 | 16/20 (80%) | 문서관리 시스템, Phase 4.4 잔여 | +| [document-system-master.md](./document-system-master.md) | 🟡 진행중 | Phase 4-5 | 마스터 문서, 일부 Phase 잔여 | +| [document-system-mid-inspection.md](./document-system-mid-inspection.md) | 🟡 진행중 | 5/6 | 중간검사, 1개 미완 | +| [document-system-work-log.md](./document-system-work-log.md) | 🟡 진행중 | 3/4+α | 작업일지, React 연동 잔여 | +| [incoming-inspection-document-integration-plan.md](./incoming-inspection-document-integration-plan.md) | ⚪ 대기 | 0/8 (0%) | 수입검사 서류 연동, 분석만 완료 | +| [incoming-inspection-templates-plan.md](./incoming-inspection-templates-plan.md) | 🟡 진행중 | 19/23 (83%) | 수입검사 템플릿, 4종 품목 대기 | +| [intermediate-inspection-report-plan.md](./intermediate-inspection-report-plan.md) | ⚪ 대기 | 0/14 (0%) | 중간검사 보고서, 검토 대기 | ### 마이그레이션 & 연동 -| 문서 | 상태 | 설명 | -|------|------|------| -| [5130-to-mng-migration-plan.md](./5130-to-mng-migration-plan.md) | 🟡 진행중 | 5130 → mng 마이그레이션 (5/38 완료) | -| [react-api-integration-plan.md](./react-api-integration-plan.md) | 🟡 진행중 | React ↔ API 연동 테스트 | -| [react-mock-to-api-migration-plan.md](./react-mock-to-api-migration-plan.md) | 🟡 진행중 | Mock → API 전환 (Phase A 부분 완료) | -| [dashboard-api-integration-plan.md](./dashboard-api-integration-plan.md) | 🟡 Phase 1 대기 | CEO Dashboard API 연동 | +| 문서 | 상태 | 진행률 | 설명 | +|------|------|--------|------| +| [5130-to-mng-migration-plan.md](./5130-to-mng-migration-plan.md) | 🟡 진행중 | 5/38 (13%) | 5130→mng 마이그레이션 | +| [react-api-integration-plan.md](./react-api-integration-plan.md) | 🟡 진행중 | - | React↔API 연동 | +| [react-mock-to-api-migration-plan.md](./react-mock-to-api-migration-plan.md) | 🟡 진행중 | - | Mock→API 전환, 별도 문서 추적 | +| [dashboard-api-integration-plan.md](./dashboard-api-integration-plan.md) | 🟡 진행중 | 5/11 (45%) | CEO Dashboard API 연동 | +| [kd-orders-migration-plan.md](./kd-orders-migration-plan.md) | ⚪ 대기 | 0/2 (0%) | 경동 수주 마이그레이션, 선행조건 미충족 | +| [items-migration-kyungdong-plan.md](./items-migration-kyungdong-plan.md) | 📚 참조 | ARCHIVED | 후속 문서로 이관됨 | -### 영업/생산 (Sales/Production) +### 시스템/인프라 -| 문서 | 상태 | 설명 | -|------|------|------| -| [quote-management-8issues-plan.md](./quote-management-8issues-plan.md) | 🟡 진행중 | 견적관리 8개 이슈 | -| [quote-calculation-api-plan.md](./quote-calculation-api-plan.md) | 🟡 진행중 | 견적 계산 API | -| [order-workorder-shipment-integration-plan.md](./order-workorder-shipment-integration-plan.md) | 🔵 계획수립 | 수주-작업지시-출하 연동 | -| [quote-system-development-plan.md](./quote-system-development-plan.md) | 🟡 진행중 | 견적 시스템 개발 | -| [simulator-ui-enhancement-plan.md](./simulator-ui-enhancement-plan.md) | 🔵 계획수립 | 시뮬레이터 UI 개선 | +| 문서 | 상태 | 진행률 | 설명 | +|------|------|--------|------| +| [db-trigger-audit-system-plan.md](./db-trigger-audit-system-plan.md) | 🟡 진행중 | 15/16 (94%) | DB 트리거 감사, 옵션 3건 잔여 | +| [db-backup-system-plan.md](./db-backup-system-plan.md) | 🟡 진행중 | 11/14 (79%) | DB 백업, 서버 작업 3건 잔여 | +| [tenant-id-compliance-plan.md](./tenant-id-compliance-plan.md) | ⚪ 대기 | 0/4 (0%) | 테넌트 ID 정합, 실행 대기 | +| [tenant-numbering-system-plan.md](./tenant-numbering-system-plan.md) | ⚪ 대기 | 0/8 (0%) | 테넌트 채번, 미착수 | +| [mng-numbering-rule-management-plan.md](./mng-numbering-rule-management-plan.md) | ⚪ 대기 | 0% | 채번 규칙 관리, 미착수 | -### 시스템/기타 +### 프론트엔드 & UI -| 문서 | 상태 | 설명 | -|------|------|------| -| [dummy-data-seeding-plan.md](./dummy-data-seeding-plan.md) | ⚪ 계획수립 | 더미 데이터 시딩 (2025-12-23 작성) | -| [api-explorer-development-plan.md](./api-explorer-development-plan.md) | 🔵 계획수립 | API Explorer 개발 (설계 완료, 구현 대기) | -| [employee-user-linkage-plan.md](./employee-user-linkage-plan.md) | 🔵 계획수립 | 사원-회원 연결 기능 (2025-12-25 작성) | -| [docs-update-plan.md](./docs-update-plan.md) | 🟡 진행중 | 문서 업데이트 계획 (Phase 4 진행중) | -| [react-mock-remaining-tasks.md](./react-mock-remaining-tasks.md) | 📚 참조 | Mock 전환 잔여 작업 목록 | -| [hotfix-20260119-action-plan.md](./hotfix-20260119-action-plan.md) | 🟡 진행중 | Hotfix 액션 플랜 (2026-01-19) | +| 문서 | 상태 | 진행률 | 설명 | +|------|------|--------|------| +| [simulator-ui-enhancement-plan.md](./simulator-ui-enhancement-plan.md) | 🟡 진행중 | 6/10 (60%) | 시뮬레이터 UI 개선 | +| [card-management-section-plan.md](./card-management-section-plan.md) | 🟡 진행중 | 6/12 (50%) | 카드 관리 섹션 | +| [dev-toolbar-plan.md](./dev-toolbar-plan.md) | 🟡 진행중 | 3/8 (38%) | 개발 툴바 | + +### 기타 + +| 문서 | 상태 | 진행률 | 설명 | +|------|------|--------|------| +| [hotfix-20260119-action-plan.md](./hotfix-20260119-action-plan.md) | 🟡 진행중 | API 완료 | Hotfix, React P0 2건 대기 | +| [mng-menu-system-plan.md](./mng-menu-system-plan.md) | 🟡 진행중 | 구현 완료 | 메뉴 시스템, Phase 3 테스트 잔여 | +| [monthly-expense-integration-plan.md](./monthly-expense-integration-plan.md) | ⚪ 대기 | 0/8 (0%) | 월별 경비 연동, 미착수 | +| [receiving-management-analysis-plan.md](./receiving-management-analysis-plan.md) | ⚪ 대기 | 분석 | 입고 관리, 분석 완료/개발 대기 | +| [api-explorer-development-plan.md](./api-explorer-development-plan.md) | ⚪ 대기 | 0% | API Explorer, 미착수 | +| [employee-user-linkage-plan.md](./employee-user-linkage-plan.md) | ⚪ 대기 | 0% | 사원-회원 연결, 미착수 | +| [dummy-data-seeding-plan.md](./dummy-data-seeding-plan.md) | ⚪ 대기 | - | 더미 데이터 시딩, 미착수 | +| [react-mock-remaining-tasks.md](./react-mock-remaining-tasks.md) | 📚 참조 | - | Mock 전환 잔여 작업 목록 | --- -## 완료 아카이브 (archive/) +## 완료 아카이브 (archive/) - 37개 > 완료된 계획 문서들 - 참조용으로 보관 | 문서 | 완료일 | 설명 | |------|--------|------| -| [erp-api-development-plan-d1.0-changes.md](./archive/erp-api-development-plan-d1.0-changes.md) | 2025-12 | D1.0 변경사항 (Phase 5-8) | +| [bending-lot-pipeline-dev-plan.md](./archive/bending-lot-pipeline-dev-plan.md) | 2026-02 | 절곡 LOT 매핑 파이프라인 | +| [bending-worklog-reimplementation-plan.md](./archive/bending-worklog-reimplementation-plan.md) | 2026-02 | 절곡 작업일지 재구현 | +| [document-system-product-inspection.md](./archive/document-system-product-inspection.md) | 2026-02 | 제품검사 서식 | +| [formula-engine-real-data-plan.md](./archive/formula-engine-real-data-plan.md) | 2026-02 | 수식 엔진 실데이터 | +| [material-input-per-item-mapping-plan.md](./archive/material-input-per-item-mapping-plan.md) | 2026-02 | 품목별 자재투입 매핑 | +| [mng-item-formula-integration-plan.md](./archive/mng-item-formula-integration-plan.md) | 2026-02 | mng 품목 수식 연동 | +| [mng-item-management-plan.md](./archive/mng-item-management-plan.md) | 2026-02 | mng 품목 관리 | +| [fcm-user-targeted-notification-plan.md](./archive/fcm-user-targeted-notification-plan.md) | 2026-01 | 사용자 타겟 FCM 알림 | +| [docs-update-plan.md](./archive/docs-update-plan.md) | 2026-01 | 문서 업데이트 계획 | +| [order-location-management-plan.md](./archive/order-location-management-plan.md) | 2026-01 | 수주 현장 관리 | +| [quote-v2-auto-calculation-fix-plan.md](./archive/quote-v2-auto-calculation-fix-plan.md) | 2026-01 | 견적 V2 자동계산 수정 | +| [sam-stat-database-design-plan.md](./archive/sam-stat-database-design-plan.md) | 2026-01 | 통계 DB 설계 | +| [stock-integration-plan.md](./archive/stock-integration-plan.md) | 2026-01 | 재고 연동 | +| [welfare-section-plan.md](./archive/welfare-section-plan.md) | 2026-01 | 복리후생 섹션 | +| [order-workorder-shipment-integration-plan.md](./archive/order-workorder-shipment-integration-plan.md) | 2026-01 | 수주-작업지시-출하 연동 | +| [document-management-system-changelog.md](./archive/document-management-system-changelog.md) | 2026-01 | 문서관리 변경 이력 | +| [items-table-unification-plan.md](./archive/items-table-unification-plan.md) | 2025-12 | items 테이블 통합 | +| [kd-items-migration-plan.md](./archive/kd-items-migration-plan.md) | 2025-12 | 경동 품목 마이그레이션 | +| [simulator-calculation-logic-mapping.md](./archive/simulator-calculation-logic-mapping.md) | 2025-12 | 시뮬레이터 로직 매핑 | +| [AI_리포트_키워드_색상체계_가이드_v1.4.md](./archive/AI_리포트_키워드_색상체계_가이드_v1.4.md) | 2025-12 | AI 리포트 색상 가이드 | +| [SEEDERS_LIST.md](./archive/SEEDERS_LIST.md) | 2025-12 | 시더 참조 목록 | +| [api-analysis-report.md](./archive/api-analysis-report.md) | 2025-12 | API 분석 보고서 | +| [erp-api-development-plan-d1.0-changes.md](./archive/erp-api-development-plan-d1.0-changes.md) | 2025-12 | D1.0 변경사항 | | [mng-quote-formula-development-plan.md](./archive/mng-quote-formula-development-plan.md) | 2025-12 | mng 견적 수식 관리 | -| [quote-auto-calculation-development-plan.md](./archive/quote-auto-calculation-development-plan.md) | 2025-12-22 | 견적 자동 계산 | -| [order-management-plan.md](./archive/order-management-plan.md) | 2025-01-09 | 수주관리 API 연동 | -| [work-order-plan.md](./archive/work-order-plan.md) | 2025-01-11 | 작업지시 검증 | +| [quote-auto-calculation-development-plan.md](./archive/quote-auto-calculation-development-plan.md) | 2025-12 | 견적 자동 계산 | +| [order-management-plan.md](./archive/order-management-plan.md) | 2025-01 | 수주관리 API 연동 | +| [work-order-plan.md](./archive/work-order-plan.md) | 2025-01 | 작업지시 검증 | | [process-management-plan.md](./archive/process-management-plan.md) | 2025-12 | 공정관리 API 연동 | | [construction-api-integration-plan.md](./archive/construction-api-integration-plan.md) | 2026-01 | 시공사 API 연동 | -| [notification-sound-system-plan.md](./archive/notification-sound-system-plan.md) | 2025-01-07 | 알림음 시스템 | +| [notification-sound-system-plan.md](./archive/notification-sound-system-plan.md) | 2025-01 | 알림음 시스템 | | [l2-permission-management-plan.md](./archive/l2-permission-management-plan.md) | 2025-12 | L2 권한 관리 | | [react-fcm-push-notification-plan.md](./archive/react-fcm-push-notification-plan.md) | 2025-12 | FCM 푸시 알림 | | [react-server-component-audit-plan.md](./archive/react-server-component-audit-plan.md) | 2025-12 | Server Component 점검 | | [5130-bom-migration-plan.md](./archive/5130-bom-migration-plan.md) | 2025-12 | 5130 BOM 마이그레이션 | | [5130-sam-data-migration-plan.md](./archive/5130-sam-data-migration-plan.md) | 2025-12 | 5130 데이터 마이그레이션 | | [bidding-api-implementation-plan.md](./archive/bidding-api-implementation-plan.md) | 2025-12 | 입찰 API 구현 | -| [mes-integration-analysis-plan.md](./archive/mes-integration-analysis-plan.md) | 2025-01-09 | MES 연동 분석 | +| [mes-integration-analysis-plan.md](./archive/mes-integration-analysis-plan.md) | 2025-01 | MES 연동 분석 | --- @@ -103,8 +167,6 @@ **내용**: D0.8 대비 변경/추가된 화면 (D1.0 버전) -**변경 사항**: [erp-api-development-plan-d1.0-changes.md](./erp-api-development-plan-d1.0-changes.md) 참조 - --- ## 플로우 테스트 @@ -175,55 +237,14 @@ --- -## 디렉토리 구조 - -``` -docs/plans/ -├── index_plans.md # 이 파일 -│ -├── erp-api-development-plan.md # ERP API 개발 계획 (D0.8) -├── erp-api-development-plan-d1.0-changes.md # D1.0 변경사항 -├── mng-quote-formula-development-plan.md # 견적 수식 관리 -├── quote-auto-calculation-development-plan.md # 견적 자동 계산 -├── simulator-calculation-logic-mapping.md # 시뮬레이터 로직 매핑 -├── mng-item-field-management-plan.md # 품목 필드 관리 -├── items-table-unification-plan.md # items 테이블 통합 -├── mng-menu-system-plan.md # mng 메뉴 시스템 -├── 5130-to-mng-migration-plan.md # 5130 마이그레이션 -├── react-api-integration-plan.md # React-API 연동 -├── react-mock-to-api-migration-plan.md # Mock→API 전환 -├── dummy-data-seeding-plan.md # 더미 데이터 시딩 -├── api-explorer-development-plan.md # API Explorer -├── employee-user-linkage-plan.md # 사원-회원 연결 -├── docs-update-plan.md # 문서 업데이트 계획 -│ -├── SAM_ERP_Storyboard_D1.0_251218/ # 스토리보드 D1.0 (38장) -│ └── 슬라이드*.jpeg -│ -└── flow-tests/ # 플로우 테스트 JSON (32개) - ├── auth-*.json - ├── items-*.json - ├── client-*.json - └── ... - -# 아카이브됨 -# docs/history/2025-12/SAM_ERP_Storyboard_D0.8_251216/ # D0.8 (113장) -``` - ---- - ## 관련 문서 - [docs/INDEX.md](../INDEX.md) - 전체 문서 인덱스 - [docs/projects/index_projects.md](../projects/index_projects.md) - 프로젝트 문서 인덱스 -- [docs/specs/erp-analysis/](../specs/erp-analysis/) - ERP 분석 명세서 -- [CURRENT_WORKS.md](../../CURRENT_WORKS.md) - 현재 작업 --- **범례**: -- 🟢 완료: 구현 완료 -- 🟡 진행중: 현재 작업 중 -- 🔵 계획수립: 설계/계획 완료, 구현 대기 -- ⚪ 미착수: 계획만 수립, 작업 대기 -- 📚 참조: 분석/참조용 문서 \ No newline at end of file +- 🟡 진행중: 현재 작업 중 또는 일부 완료 +- ⚪ 대기: 미착수 또는 선행조건 대기 +- 📚 참조: 분석/참조용 문서