diff --git a/plans/GUIDE.md b/plans/GUIDE.md new file mode 100644 index 0000000..3d0ed3a --- /dev/null +++ b/plans/GUIDE.md @@ -0,0 +1,127 @@ +# docs/plans 문서 가이드 (최소 원칙) + +> **작성일**: 2026-02-26 +> **상태**: 최소 원칙 (정리 완료 후 보강 예정) +> **참조**: `docs/INDEX.md`, `CLAUDE.md`에 링크 예정 + +--- + +## 1. 파일 명명 규칙 + +``` +[도메인]-[기능]-plan.md + +예시: + bending-preproduction-stock-plan.md + quote-order-sync-improvement-plan.md + document-system-work-log-plan.md +``` + +- 영문 소문자, 하이픈(`-`) 구분 +- 접미사 `-plan.md` 고정 +- 도메인 접두사 통일: + +| 도메인 | 접두사 | 예시 | +|--------|--------|------| +| 견적 | `quote-` | quote-calculation-api-plan.md | +| 수주 | `order-` | order-location-management-plan.md | +| 품목/BOM | `item-`, `bom-` | item-master-data-alignment-plan.md | +| 절곡/생산 | `bending-` | bending-preproduction-stock-plan.md | +| 문서/서식 | `document-` | document-system-master-plan.md | +| 관리자(mng) | `mng-` | mng-menu-system-plan.md | +| 시스템/인프라 | `db-`, `tenant-` | db-backup-system-plan.md | +| 프론트엔드 | `react-` | react-api-integration-plan.md | +| 마이그레이션 | `[출처]-migration-` | kd-orders-migration-plan.md | + +> 도메인 분류는 정리 완료 후 실제 남은 문서 기반으로 확정 예정 + +--- + +## 2. 문서 필수 섹션 + +| 섹션 | 필수 | 내용 | +|------|:----:|------| +| **목적** (상단 1줄) | ✅ | 왜 이 작업이 필요한가 | +| **현재 진행 상태** | ✅ | 마지막 완료 작업, 다음 작업, 진행률 | +| **대상 범위** | ✅ | Phase별 작업 항목 테이블 | +| **변경 이력** | ✅ | 날짜 + 변경 내용 | +| 참고 문서 | ⚪ | 관련 문서 링크 | +| 검증 결과 | ⚪ | 완료 시 작성 | + +--- + +## 3. 상태 표기법 + +### 문서 상태 (인덱스용) + +| 표기 | 의미 | +|------|------| +| 🟡 진행중 | 현재 작업중 | +| ⚪ 대기 | 미착수 / 선행조건 대기 | +| ✅ 완료 | 개발 완료 | + +### 항목 상태 (문서 내부용) + +| 표기 | 의미 | +|------|------| +| ⏳ | 대기 | +| 🔄 | 진행중 | +| ✅ | 완료 | +| ⚠️ | 컨펌 필요 | + +### 진행률 표기 + +``` +완료/전체 (%) +예: 5/8 (63%) +``` + +--- + +## 4. 문서 생명주기 + +``` +생성 (PLANNED) ← 개발 계획 수립 + ↓ 착수 +진행 (ACTIVE) ← 인덱스에 노출, 진행 상태 추적 + ↓ 개발 완료 +완료 (COMPLETED) ← 인덱스에서 완료 표기 + ↓ docs/ 구조화 시 +정식 문서에 반영 ← plan의 설계 결정/구현 상세를 docs/ 정식 문서로 이관 +``` + +- **plan 문서**: 개발 계획 수립 및 진행 추적 용도 +- **완료 후**: 유용한 내용(설계 결정, 구현 상세)은 `docs/` 정식 문서에 반영 +- **plan 파일 보관/삭제**: `docs/` 구조화 시 확정 + +--- + +## 5. 폴더 구조 + +``` +docs/plans/ +├── GUIDE.md ← 이 가이드 +├── index_plans.md ← ACTIVE + PLANNED 문서 인덱스 +├── [도메인]-*-plan.md ← 현행 계획 문서 +├── archive/ +│ └── HISTORY.md ← 완료 작업 요약 (기능별 섹션) +├── flow-tests/ ← JSON 테스트 케이스 (별도 관리) +└── SAM_ERP_Storyboard*/ ← 디자인 참조 (별도 관리) +``` + +--- + +## 6. 인덱스 관리 + +- 문서 생성/삭제 시 `index_plans.md` **동시 업데이트** +- **ACTIVE + PLANNED** 문서만 인덱스에 포함 +- 도메인별 섹션으로 그룹핑 +- 각 문서의 상태/진행률 표기 + +--- + +> **TODO (정리 완료 후 보강)** +> - 도메인 분류 체계 확정 (실제 남은 문서 기반) +> - 문서 간 관계 규칙 (상위/하위, 참조 관계) +> - 인덱스 관리 주기 및 방법 +> - docs/ 전체 구조와의 연계 정책 \ No newline at end of file diff --git a/plans/archive/5130-bom-migration-plan.md b/plans/archive/5130-bom-migration-plan.md deleted file mode 100644 index a970d91..0000000 --- a/plans/archive/5130-bom-migration-plan.md +++ /dev/null @@ -1,446 +0,0 @@ -# 5130 → SAM BOM 데이터 마이그레이션 계획 - -> **작성일**: 2025-01-20 -> **목적**: 5130 레거시 시스템의 BOM 데이터를 SAM items 테이블의 bom 컬럼에 마이그레이션 -> **기준 문서**: `api/app/Services/Quote/FormulaEvaluatorService.php` -> **상태**: ✅ 완료 (Serena ID: 5130-bom-migration-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | BOM 마이그레이션 실행 완료 (61건) | -| **다음 작업** | 견적 페이지에서 실제 테스트 (사용자 수동 확인) | -| **진행률** | 4/4 (100%) | -| **마지막 업데이트** | 2025-01-20 | - ---- - -## 1. 개요 - -### 1.1 배경 - -5130 레거시 시스템에서 SAM으로 품목(items) 마이그레이션이 완료되었으나, 완제품(FG)의 BOM 데이터가 마이그레이션되지 않아 다음 문제가 발생: - -``` -문제 현상: -- 견적 페이지에서 "국민방화스크린 (일체형) (S0001)" 선택 후 자동 견적 산출 → 합계 0원 -- 원인: S0001의 bom 컬럼이 NULL -- items 테이블에서 확인: SELECT bom FROM items WHERE code = 'S0001' → NULL -``` - -**기존 마이그레이션 상태:** -- Items: 608건 (KDunitprice → items) -- Orders: 24,424건 -- Order Items: 43,900건 -- ❌ BOM 데이터: 마이그레이션 안됨 - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. FormulaEvaluatorService 호환 BOM JSON 형식 생성 │ -│ 2. 동적 수량 계산을 위한 quantityFormula 필드 지원 │ -│ 3. childItemCode 기반 참조 (child_item_id 아님) │ -│ 4. 기존 SAM BOM 패턴과 일관성 유지 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | BOM JSON 데이터 추가, 매핑 테이블 생성 | 불필요 | -| ⚠️ 컨펌 필요 | 기존 items 데이터 수정, 새 마이그레이션 스크립트 | **필수** | -| 🔴 금지 | items 테이블 구조 변경, 기존 BOM 삭제 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `api/app/Services/Quote/FormulaEvaluatorService.php` - BOM 계산 로직 - ---- - -## 2. 데이터 구조 분석 - -### 2.1 5130 BOM 구조 - -``` -5130 DB (chandj) -├── KDunitprice (품목 마스터) -│ ├── prodcode: 품목 코드 -│ ├── item_name: 품목명 -│ └── item_div: [제품], [상품], [부재료], [원재료], [반제품] -│ -├── models (모델 마스터) -│ ├── model_id: PK -│ ├── model_name: KSS01, KSE01, KWE01... (모델 코드) -│ ├── major_category: 스크린 | 철재 -│ ├── finishing_type: SUS마감 | EGI마감 -│ └── guiderail_type: 벽면형 | 측면형 -│ -├── parts (1단계 BOM - 모델별 부품) -│ ├── part_id: PK -│ ├── model_id: FK → models -│ ├── part_name: 가이드레일, 하단마감재 등 -│ ├── spec: 120*70, 60*40 등 -│ ├── quantity: 수량 -│ ├── unit: SET, EA 등 -│ └── unitprice: 단가 (문자열, 콤마 포함) -│ -└── parts_sub (2단계 BOM - 부품별 원자재) - ├── subpart_id: PK - ├── part_id: FK → parts - ├── subpart_name: 1번(마감제), 2번(본체) 등 - ├── material: SUS 1.2T, EGI 1.55T 등 - ├── quantity: 수량 - ├── bendSum, plateSum, finalSum: 가공 관련 - └── unitPrice, computedPrice, lineTotal: 금액 -``` - -**5130 model_id별 데이터 현황:** -| model_id | model_name | category | finishing | guiderail | parts 수 | -|----------|------------|----------|-----------|-----------|----------| -| 12 | KSS01 | 스크린 | SUS마감 | 벽면형 | 2 | -| 13 | KSS01 | 스크린 | SUS마감 | 측면형 | 2 | -| 14 | KSE01 | 스크린 | SUS마감 | 벽면형 | 2 | -| ... | ... | ... | ... | ... | ... | - -**5130 KDunitprice item_div 분포:** -| item_div | 건수 | SAM item_type 매핑 | -|----------|------|-------------------| -| [제품] | 194건 | FG (완제품) | -| [상품] | 260건 | SM (부자재) | -| [부재료] | 48건 | SM (부자재) | -| [원재료] | 24건 | RM (원자재) | -| [반제품] | 73건 | SF (반제품) | -| [무형상품] | 4건 | CS (서비스) | - -### 2.2 SAM BOM 구조 - -```sql --- SAM items 테이블 BOM 컬럼 -items.bom: JSON -``` - -**SAM BOM JSON 형식 (FormulaEvaluatorService 호환):** -```json -[ - { - "childItemCode": "SF-SCR-F01", // 필수: 하위 품목 코드 - "quantity": 1, // 필수: 기본 수량 - "quantityFormula": "W*H/1000000", // 선택: 동적 수량 계산식 - "unit": "M2", // 선택: 단위 - "note": "스크린 원단" // 선택: 비고 - }, - { - "childItemCode": "SF-SCR-M01", - "quantity": 1, - "quantityFormula": "", - "unit": "EA", - "note": "소형용 모터" - } -] -``` - -**기존 SAM BOM 예시 (FG-SCR-001):** -```json -[ - {"unit":"M2","quantity":1,"childItemCode":"SF-SCR-F01","quantityFormula":"W*H/1000000"}, - {"unit":"M","quantity":1,"childItemCode":"SF-SCR-F02","quantityFormula":"H/1000"}, - {"unit":"EA","quantity":1,"childItemCode":"SF-SCR-M01","quantityFormula":"","note":"소형용"}, - {"unit":"EA","quantity":20,"childItemCode":"SM-B002","quantityFormula":"","note":"조립용"} -] -``` - -### 2.3 핵심 차이점 - -| 항목 | 5130 | SAM | -|------|------|-----| -| **BOM 저장 위치** | parts/parts_sub 테이블 | items.bom JSON 컬럼 | -| **연결 기준** | model_id (모델 기준) | childItemCode (품목 코드 기준) | -| **수량 계산** | 고정값 + estimate.detailJson | quantityFormula 동적 계산 | -| **단가 계산** | parts.unitprice 고정 | FormulaEvaluatorService 동적 | -| **계층 구조** | 2단계 (parts → parts_sub) | 1단계 (flat JSON array) | - ---- - -## 3. 마이그레이션 전략 - -### 3.1 접근 방식: 수동 매핑 + 템플릿 기반 - -5130의 BOM 구조와 SAM의 BOM 구조가 근본적으로 다르기 때문에, 자동 변환이 아닌 **수동 매핑 + 템플릿 기반** 접근 필요: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 전략: 완제품(FG) 유형별 BOM 템플릿 정의 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. SCREEN 완제품 → screen_bom_template │ -│ 2. STEEL 완제품 → steel_bom_template │ -│ 3. BENDING 완제품 → bending_bom_template │ -│ │ -│ 각 템플릿은 FormulaEvaluatorService 호환 JSON 형식으로 정의 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 3.2 완제품-모델 매핑 - -**매핑 대상 (SAM items WHERE item_type='FG' AND source='5130'):** -```sql --- SAM에서 5130에서 마이그레이션된 완제품 목록 -SELECT id, code, name, item_category -FROM items -WHERE item_type = 'FG' - AND (legacy_code IS NOT NULL OR code LIKE 'S%'); -``` - -**주요 완제품 매핑 예시:** -| SAM code | SAM name | item_category | 5130 model | -|----------|----------|---------------|------------| -| S0001 | 국민방화스크린(일체형) | SCREEN | KSS01 (스크린/SUS/벽면형) | -| S0002 | 국민방화스크린(분리형) | SCREEN | KSE01 (스크린/SUS/벽면형) | -| ... | ... | ... | ... | - -### 3.3 BOM 템플릿 정의 - -**SCREEN 완제품 BOM 템플릿:** -```json -[ - {"childItemCode": "RM-SCR-FABRIC", "quantity": 1, "quantityFormula": "W*H/1000000", "unit": "M2", "note": "스크린 원단"}, - {"childItemCode": "PT-SCR-GUIDE", "quantity": 1, "quantityFormula": "H/1000", "unit": "M", "note": "가이드레일"}, - {"childItemCode": "PT-SCR-BOTTOM", "quantity": 1, "quantityFormula": "W/1000", "unit": "M", "note": "하단바"}, - {"childItemCode": "PT-SCR-CASE", "quantity": 1, "quantityFormula": "W/1000", "unit": "M", "note": "케이스"}, - {"childItemCode": "PT-SCR-MOTOR", "quantity": 1, "quantityFormula": "", "unit": "EA", "note": "모터"} -] -``` - ---- - -## 4. 작업 절차 - -### 4.1 Phase 1: 하위 품목 확인 및 생성 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | BOM에 필요한 하위 품목(SF, PT, RM) 목록 정의 | ✅ | 52개 품목 정의됨 | -| 1.2 | SAM items 테이블에 하위 품목 존재 여부 확인 | ✅ | 52개 모두 존재 확인 | -| 1.3 | 누락된 하위 품목 생성 (필요시) | ✅ | 누락 품목 없음 (생성 불필요) | - -### 4.2 Phase 2: BOM 템플릿 정의 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | SCREEN 완제품용 BOM 템플릿 정의 | ✅ | FG-SCR-001 (14개 항목) | -| 2.2 | STEEL 완제품용 BOM 템플릿 정의 | ✅ | FG-STL-001 (12개 항목) | -| 2.3 | BENDING 완제품용 BOM 템플릿 정의 | ✅ | FG-BND-001 (6개 항목) | - -### 4.3 Phase 3: 마이그레이션 스크립트 작성 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | Migrate5130Bom 커맨드 생성 | ✅ | `api/app/Console/Commands/Migrate5130Bom.php` | -| 3.2 | 완제품-템플릿 매핑 로직 구현 | ✅ | item_category 기반 매핑 | -| 3.3 | items.bom 컬럼 업데이트 로직 구현 | ✅ | DB::table 직접 업데이트 | -| 3.4 | 검증 로직 구현 | ✅ | dry-run, verbose 옵션 지원 | - -### 4.4 Phase 4: 검증 및 테스트 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | Migrate5130Bom 커맨드 실행 | ✅ | 61건 처리 완료 | -| 4.2 | 견적 페이지에서 실제 테스트 | ⏳ | 사용자 수동 확인 필요 | -| 4.3 | 결과 문서화 | ✅ | 본 문서 업데이트 | - ---- - -## 5. 기술 상세 - -### 5.1 FormulaEvaluatorService BOM 처리 로직 - -```php -// api/app/Services/Quote/FormulaEvaluatorService.php - -// BOM JSON 필드 사용 위치: -// 1. getBomItems() - bom JSON 파싱 -// 2. calculateBomQuantity() - quantityFormula 평가 -// 3. childItemCode로 하위 품목 조회 - -// 주요 변수: -// - W0, H0: 개구부 치수 (입력값) -// - W1, H1: 제작 치수 (계산값) -// - W, H: W1, H1과 동일 -// - M: 면적 (m²) -// - K: 중량 (kg) -``` - -### 5.2 마이그레이션 스크립트 구조 - -```php -// api/app/Console/Commands/Migrate5130Bom.php - -class Migrate5130Bom extends Command -{ - protected $signature = 'migration:migrate-5130-bom - {--dry-run : 실제 변경 없이 시뮬레이션} - {--code= : 특정 품목 코드만 처리}'; - - // 1. item_category별 BOM 템플릿 정의 - private array $bomTemplates = [ - 'SCREEN' => [...], - 'STEEL' => [...], - 'BENDING' => [...] - ]; - - // 2. 완제품 조회 (5130 마이그레이션된 FG) - // 3. 템플릿 기반 BOM JSON 생성 - // 4. items.bom 컬럼 업데이트 -} -``` - -### 5.3 검증 쿼리 - -```sql --- 마이그레이션 전: BOM이 NULL인 완제품 -SELECT code, name, item_category -FROM items -WHERE item_type = 'FG' - AND item_category IN ('SCREEN', 'STEEL', 'BENDING') - AND (bom IS NULL OR bom = '[]'); - --- 마이그레이션 후: BOM이 있는 완제품 -SELECT code, name, item_category, JSON_LENGTH(bom) as bom_count -FROM items -WHERE item_type = 'FG' - AND item_category IN ('SCREEN', 'STEEL', 'BENDING') - AND bom IS NOT NULL - AND JSON_LENGTH(bom) > 0; -``` - ---- - -## 6. 컨펌 대기 목록 - -> 모든 승인 항목 완료 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | BOM 템플릿 확정 | SCREEN/STEEL/BENDING별 템플릿 | 견적 계산 | ✅ 완료 | -| 2 | 하위 품목 코드 확정 | childItemCode 명명 규칙 | items 테이블 | ✅ 완료 | -| 3 | 마이그레이션 실행 | items.bom 업데이트 | 완제품 61건 | ✅ 완료 | - ---- - -## 7. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-20 | 초안 | 계획 문서 작성 | - | - | -| 2025-01-20 | 분석 | 5130/SAM BOM 구조 분석 완료 | - | - | -| 2025-01-20 | 스크립트 | Migrate5130Bom 커맨드 생성 | `api/app/Console/Commands/Migrate5130Bom.php` | ✅ | -| 2025-01-20 | 실행 | BOM 마이그레이션 실행 (61건) | items.bom 컬럼 | ✅ | -| 2025-01-20 | 문서화 | 결과 문서화 완료 | 본 문서 | ✅ | - ---- - -## 8. 참고 문서 - -- **FormulaEvaluatorService**: `api/app/Services/Quote/FormulaEvaluatorService.php` -- **기존 마이그레이션**: `api/app/Console/Commands/Migrate5130PriceItems.php` -- **검증 커맨드**: `api/app/Console/Commands/Verify5130Calculation.php` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - ---- - -## 9. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 9.1 세션 시작 시 (Load Strategy) -```javascript -// 순차적 로드 -read_memory("5130-bom-migration-state") // 1. 상태 파악 -read_memory("5130-bom-migration-rules") // 2. 규칙 확인 -read_memory("5130-bom-migration-mappings") // 3. 매핑 확인 -``` - -### 9.2 Serena 메모리 구조 -- `5130-bom-migration-state`: { phase, progress, next_step, last_decision } -- `5130-bom-migration-rules`: BOM 템플릿 정의, 변환 규칙 -- `5130-bom-migration-mappings`: 완제품-모델 매핑 테이블 - ---- - -## 10. 검증 결과 - -> 2025-01-20 마이그레이션 실행 완료 - -### 10.1 마이그레이션 실행 결과 - -``` -📊 카테고리별 BOM 적용 현황 (tenant_id=287): - SCREEN: 35건 - STEEL: 11건 - BENDING: 15건 - -✅ BOM 적용 완료: 61건 -⏳ BOM 미적용: 0건 -``` - -### 10.2 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| S0001 BOM JSON 확인 | childItemCode 5개 이상 | 14개 항목 적용됨 | ✅ | -| S0001 + W0=2500, H0=2000 | 견적 금액 > 0 | 사용자 확인 필요 | ⏳ | - -### 10.3 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 완제품 BOM NULL → JSON 변환 | ✅ | 61건 변환 완료 | -| BOM JSON 형식 호환 | ✅ | FormulaEvaluatorService 호환 형식 | -| 견적 계산 정상 동작 | ⏳ | 사용자 수동 확인 필요 | - -### 10.4 BOM 템플릿 상세 - -| 카테고리 | 소스 템플릿 | BOM 항목 수 | 적용 완제품 수 | -|----------|------------|------------|--------------| -| SCREEN | FG-SCR-001 | 14개 | 35건 | -| STEEL | FG-STL-001 | 12개 | 11건 | -| BENDING | FG-BND-001 | 6개 | 15건 | - ---- - -## 11. 자기완결성 점검 결과 - -> Phase 5.5에서 수행된 자기완결성 점검 결과 - -### 11.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | S0001 등 BOM NULL → 견적 0원 문제 해결 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 10.2 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | SCREEN/STEEL/BENDING 완제품 대상 | -| 4 | 의존성이 명시되어 있는가? | ✅ | FormulaEvaluatorService, 하위 품목 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 8 참조 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 참조 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 10.1 참조 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 수치/조건 명시 | - -### 11.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 4. 작업 절차 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5.2 마이그레이션 스크립트 | -| Q4. 작업 완료 확인 방법은? | ✅ | 10. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/5130-sam-data-migration-plan.md b/plans/archive/5130-sam-data-migration-plan.md deleted file mode 100644 index 5451064..0000000 --- a/plans/archive/5130-sam-data-migration-plan.md +++ /dev/null @@ -1,828 +0,0 @@ -# 5130 → SAM 자재/수주 데이터 마이그레이션 계획 - -> **작성일**: 2025-01-19 -> **목적**: 5130 레거시 시스템의 품목(KDunitprice, price_*) 및 수주(output, output_extra) 데이터를 SAM 구조(items, orders, order_items)로 마이그레이션 -> **기준 문서**: 5130/output/_row.php, 5130/KDunitprice/_row.php, api/database/migrations/* -> **상태**: ✅ 마이그레이션 완료 (Phase 1-4 완료) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 - 전체 데이터 마이그레이션 실행 완료 | -| **다음 작업** | 완료 (운영 검증 후 문서 아카이브) | -| **진행률** | 14/14 (100%) | -| **마지막 업데이트** | 2026-01-20 | - ---- - -## 1. 개요 - -### 1.1 배경 - -5130 레거시 시스템에서 운영 중인 자재/수주 데이터를 SAM 신규 시스템으로 마이그레이션해야 합니다. -- 5130: 플랫 테이블 구조 + JSON 컬럼으로 데이터 저장 -- SAM: 정규화된 관계형 테이블 구조 + JSON attributes 필드 - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 📊 데이터 (값): 5130 우선 - 실제 운영 중인 사이트 │ -│ 🏗️ 구조: SAM 우선 - 신규 정규화 설계 │ -│ 🧮 견적 수식: 동일성 유지 - 5130과 SAM 결과값 일치 필수 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------:| -| ✅ 즉시 가능 | 필드 추가/변경, 마이그레이션 스크립트 작성, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 테이블 구조 변경, 새 컬럼 추가, 데이터 타입 변경 | **필수** | -| 🔴 금지 | 기존 데이터 삭제, 운영 DB 직접 수정, 스키마 파괴적 변경 | 별도 협의 | - -### 1.4 준수 규칙 - -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/specs/database-schema.md` - 데이터베이스 스키마 -- `api/CLAUDE.md` - API 개발 규칙 - ---- - -## 2. 테이블 매핑 개요 - -### 2.1 5130 소스 테이블 - -| 테이블 | 용도 | 주요 필드 | -|--------|------|----------| -| `KDunitprice` | 단가표 (Ecount 연동) | prodcode, item_name, item_div, spec, unit, unitprice | -| `price_raw_materials` | 원자재 단가 | JSON itemList | -| `price_bend` | 절곡 단가 | JSON itemList | -| `output` | 수주 마스터 | ~80개 필드, JSON (screenlist, slatlist, motorList 등) | -| `output_extra` | 수주 부가정보 | ~30개 필드 (parent_num으로 연결) | - -### 2.2 SAM 대상 테이블 - -| 테이블 | 용도 | item_type | -|--------|------|-----------| -| `items` | 통합 품목 마스터 | FG, PT, SM, RM, CS | -| `orders` | 수주 마스터 | - | -| `order_items` | 수주 상세 | - | -| `order_item_components` | 자재 투입 | - | - -### 2.3 매핑 관계 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 5130 → SAM │ -├─────────────────────────────────────────────────────────────────┤ -│ KDunitprice → items (SM, RM, CS) │ -│ price_raw_materials.itemList → items (RM) │ -│ price_bend.itemList → items (PT) + price tables │ -│ output → orders │ -│ output.screenlist/slatlist → order_items │ -│ output_extra → order_items.attributes │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: 품목 마스터 마이그레이션 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | KDunitprice → items 매핑 분석 | ✅ | 10개 필드 매핑 완료 | -| 1.2 | price_raw_materials → items 매핑 | ✅ | RM 타입, itemList JSON 15개 필드 매핑 | -| 1.3 | price_bend → items 매핑 | ✅ | PT 타입, itemList JSON 18개 필드 매핑 | -| 1.4 | 품목 마이그레이션 스크립트 작성 | ✅ | `Migrate5130PriceItems.php` | -| 1.5 | 품목 데이터 검증 | ✅ | dry-run 621건 성공, item_type 분류 검증 완료 | - -### 3.2 Phase 2: 수주 마스터 마이그레이션 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | output → orders 필드 매핑 | ✅ | 69개 필드 분석, 상세 매핑 완료 | -| 2.2 | output JSON → order_items 변환 | ✅ | screenlist, slatlist 구조 분석 완료 | -| 2.3 | output_extra → order_items.attributes | ✅ | 33개 필드, motorList/bendList 등 | -| 2.4 | 수주 마이그레이션 스크립트 작성 | ✅ | `Migrate5130Orders.php` + `order_id_mappings` 테이블 | -| 2.5 | 수주 데이터 검증 | ✅ | dry-run 100건 성공, 필드 매핑 검증 완료 | - -### 3.3 Phase 3: 견적 로직 검증 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 5130 견적 수식 분석 | ✅ | write_form_script.php + fetch_unitprice.php 분석 완료 | -| 3.2 | SAM 견적 수식 구현/검증 | ✅ | Legacy5130Calculator.php + Verify5130Calculation.php | -| 3.3 | 검증 테스트 실행 | ✅ | 5/5 테스트 케이스 통과, 100% 일치 | - ---- - -## 4. 상세 필드 매핑 - -### 4.1 KDunitprice → items - -| 5130 필드 | SAM 필드 | 타입 | 비고 | -|-----------|----------|------|------| -| prodcode | code | string | 품목코드 | -| item_name | name | string | 품목명 | -| item_div | item_type 판별 기준 | - | SM/RM/CS 분류 | -| spec | attributes.spec | JSON | 규격 | -| unit | unit | string | 단위 | -| unitprice | attributes.unit_price | JSON | 단가 | - -### 4.2 output → orders (상세 매핑) - -#### 4.2.1 기본 정보 매핑 - -| 5130 필드 | SAM 필드 | 타입 변환 | 비고 | -|-----------|----------|----------|------| -| num | options.legacy_num | int→JSON | 5130 원본 PK 보존 | -| - | id | auto | SAM 신규 PK | -| - | tenant_id | 287 | 경동기업 고정 | -| outdate | received_at | date→datetime | 수주일 | -| orderdate | options.order_date | date | 발주일 | -| outworkplace | site_name | varchar(50) | 현장명 | -| orderman | options.orderman | varchar(20) | 수주담당자 | -| con_num | client_id | int→FK | 거래처 (조회 필요) | -| outputplace | options.output_place | varchar(50) | 출고장소 | -| receiver | options.receiver | varchar(20) | 수령인 | -| phone | client_contact | varchar(15) | 연락처 | -| comment | memo | varchar(250) | 메모 | -| delivery | delivery_method_code | varchar(15) | 배송방법 | - -#### 4.2.2 상태 필드 매핑 - -| 5130 필드 | SAM 필드 | 변환 규칙 | 비고 | -|-----------|----------|----------|------| -| regist_state | status_code | '등록'→'REGISTERED' | 주 상태 | -| screen_state | options.screen_state | 그대로 | 방충망 상태 | -| slat_state | options.slat_state | 그대로 | 슬랫 상태 | -| bend_state | options.bend_state | 그대로 | 절곡 상태 | -| motor_state | options.motor_state | 그대로 | 모터 상태 | - -#### 4.2.3 수량/금액 필드 - -| 5130 필드 | SAM 필드 | 비고 | -|-----------|----------|------| -| screen_su | quantity (합산) | 방충망 수량 | -| slat_su | quantity (합산) | 슬랫 수량 | -| screen_m2 | options.screen_m2 | 방충망 면적 | -| slat_m2 | options.slat_m2 | 슬랫 면적 | -| output_extra.EstimateFinalSum | total_amount | 최종금액 | -| output_extra.EstimateDiscount | discount_amount | 할인금액 | -| output_extra.EstimateDiscountRate | discount_rate | 할인율 | - -#### 4.2.4 JSON → order_items 변환 대상 - -| 5130 JSON 필드 | order_items 유형 | 비고 | -|----------------|-----------------|------| -| screenlist | item_type='SCREEN' | 방충망 품목 | -| slatlist | item_type='SLAT' | 슬랫 품목 | -| output_extra.motorList | item_type='MOTOR' | 모터 품목 | -| output_extra.bendList | item_type='BEND' | 절곡 품목 | -| output_extra.etcList | item_type='ETC' | 기타 품목 | -| output_extra.controllerList | item_type='CTRL' | 컨트롤러 | -| deliveryfeeList | item_type='DELIVERY' | 배송비 | - -#### 4.2.5 options JSON에 보존할 필드 - -```json -{ - "legacy_num": "5130 num", - "legacy_extra_num": "output_extra num", - "orderman": "수주담당자", - "output_place": "출고장소", - "receiver": "수령인", - "secondord": "2차 주문처", - "secondordman": "2차 주문 담당자", - "secondordmantel": "2차 주문 연락처", - "screen_state": "방충망 상태", - "slat_state": "슬랫 상태", - "bend_state": "절곡 상태", - "motor_state": "모터 상태", - "screen_m2": "방충망 면적", - "slat_m2": "슬랫 면적", - "warranty": "보증서 여부", - "warrantyNum": "보증서 번호", - "lotNum": "로트번호", - "prodCode": "제품코드", - "ACI": { - "regDate": "인정검사 등록일", - "askDate": "인정검사 요청일", - "doneDate": "인정검사 완료일", - "memo": "인정검사 메모", - "check": "인정검사 체크", - "groupCode": "인정검사 그룹코드", - "groupName": "인정검사 그룹명" - }, - "pjnum": "프로젝트 번호", - "major_category": "대분류", - "position": "위치", - "makeWidth": "제작폭", - "makeHeight": "제작높이", - "maguriWing": "마구리날개" -} -``` - -### 4.3 screenlist/slatlist → order_items - -#### 4.3.1 screenlist JSON 구조 - -```json -{ - "floors": "층수", - "text1": "표시텍스트1", - "text2": "표시텍스트2 (요약)", - "memo": "메모 (재질)", - "cutwidth": "절단폭", - "cutheight": "절단높이", - "number": "수량", - "exititem": "출고여부", - "printside": "인쇄면", - "direction": "방향", - "intervalnum": "간격수", - "intervalnumsecond": "2차간격수", - "exitinterval": "출고간격", - "cover": "커버", - "drawbottom1": "하부도면1", - "drawbottom2": "하부도면2", - "drawbottom3": "하부도면3", - "draw": "도면파일", - "done_check": "완료체크", - "remain_check": "잔여체크", - "mid_check": "중간체크", - "left_check": "좌측체크", - "right_check": "우측체크" -} -``` - -#### 4.3.2 screenlist → order_items 매핑 - -| screenlist 필드 | order_items 필드 | 비고 | -|-----------------|-----------------|------| -| - | serial_no | 순번 (1부터) | -| cutwidth + 'x' + cutheight | specification | 규격 (예: 3260x4000) | -| floors | floor_code | 층수 | -| text1 | symbol_code | 기호 | -| number | quantity | 수량 | -| memo | remarks | 메모 (재질 등) | -| text2 | note | 요약 텍스트 | -| (전체) | attributes | 원본 JSON 보존 | - -#### 4.3.3 slatlist JSON 구조 - -```json -{ - "floors": "층수", - "text1": "기호 (FST-1 등)", - "text2": "요약텍스트", - "memo": "메모 (재질 EGI 1.6T 등)", - "cutwidth": "절단폭", - "cutheight": "절단높이 (총H)", - "number": "수량", - "exititem": "출고여부", - "intervalnum": "간격수 (매수)", - "hinge": "힌지", - "hingenum": "힌지수량", - "hinge_direction": "힌지방향", - "done_check": "완료체크" -} -``` - -### 4.4 output_extra 상세 매핑 - -#### 4.4.1 금액 관련 필드 - -| 5130 필드 | SAM 필드 | 비고 | -|-----------|----------|------| -| estimateTotal | orders.supply_amount | 공급가액 | -| EstimateFirstSum | options.estimate_first | 최초견적 | -| EstimateUpdatetSum | options.estimate_update | 변경견적 | -| EstimateDiffer | options.estimate_diff | 차액 | -| EstimateDiscountRate | orders.discount_rate | 할인율 | -| EstimateDiscount | orders.discount_amount | 할인금액 | -| EstimateFinalSum | orders.total_amount | 최종금액 | -| estimateSurang | options.estimate_quantity | 견적수량 | -| inspectionFee | options.inspection_fee | 검사비용 | - -#### 4.4.2 JSON 리스트 필드 (→ order_items) - -| 5130 필드 | 건수 | 구조 | SAM 변환 | -|-----------|------|------|----------| -| motorList | 7건 | col1~col8 | order_items (MOTOR) | -| bendList | 10건 | col1~col8 | order_items (BEND) | -| etcList | - | col1~col5 | order_items (ETC) | -| controllerList | - | col1~col4 | order_items (CTRL) | - -#### 4.4.3 motorList col 매핑 - -| col | 내용 | order_items 필드 | -|-----|------|-----------------| -| col1 | 품명 (전동개폐기_단상 220V) | item_name | -| col2 | 용량 (300kg) | specification | -| col3 | 규격 (380*180) | attributes.dimension | -| col4 | 인치 (5인치) | attributes.inch | -| col5 | 수량 | quantity | -| col6 | 형태 (신형) | attributes.type | -| col7 | 옵션 | attributes.option | -| col8 | 전원 (단상) | attributes.power | - -#### 4.4.4 bendList col 매핑 - -| col | 내용 | order_items 필드 | -|-----|------|-----------------| -| col1 | 품명 (가이드레일) | item_name | -| col2 | 재질 (EGI 1.6T) | specification | -| col3 | 길이 (3000) | attributes.length | -| col5 | 폭 (332) | attributes.width | -| col6 | 도면이미지 | attributes.drawing | -| col7 | 수량 | quantity | -| col8 | 비고 | remarks | - -### 4.5 견적 수식 분석 (Phase 3.1) - -> **분석 대상**: `5130/output/write_form_script.php` (JS), `5130/estimate/fetch_unitprice.php` (PHP) - -#### 4.5.1 절곡품 단가 계산 - -**함수**: `getBendPlatePrice(material, thickness, length, width, qty)` - -```javascript -// 5130/output/write_form_script.php (lines 5780-5822) -// item_bend 배열: { col1: 재질, col5: 두께, col17: 면적당단가(원/m²) } - -// 1. 재질/두께 정규화 -EGI: 1.15 → 1.2, 1.55 → 1.6 -SUS: 1.15 → 1.2, 1.55 → 1.5 - -// 2. 면적 계산 (mm² → m²) -areaM² = (length × width) / 1,000,000 - -// 3. 총액 계산 (절삭) -total = Math.floor(unitPricePerM² × areaM² × qty) -``` - -**데이터 소스**: `price_bend.itemList` → `window.item_bend` (JS 전역) - -#### 4.5.2 비인정 스크린 단가 계산 - -**함수**: 익명 함수 (tables 배열 내) - -```javascript -// 5130/output/write_form_script.php (lines 6794-6822) -// materialBasePrice에서 재질(material)로 단가 조회 - -// 1. 단가 조회 -unitprice = materialBasePrice[material] || 0 - -// 2. 수량 계산 (타입별 분기) -if (원단류) { - // 세로 기준 1000mm 단위 - surang = height / 1000 -} else { - // 일반 면적 기준 - surang = (width × height) / 1,000,000 × qty -} - -// 3. 총액 -total = unitprice × surang -``` - -**데이터 소스**: `price_raw_materials.itemList` → `window.materialBasePrice` (JS 전역) - -#### 4.5.3 철재 스라트 비인정 단가 - -**함수**: 익명 함수 (tables 배열 내) - -```javascript -// 5130/output/write_form_script.php (lines 6824-6881) - -// 1. 유형별 단가 조회 -type = 방화셔터/방범셔터/단열셔터/이중파이프/조인트바 -unitprice = materialBasePrice[type] || 0 - -// 2. 수량 계산 (유형별 분기) -if (면적 기준: 방화/방범/단열/이중파이프) { - surang = (width × height) / 1,000,000 × qty -} else if (수량 기준: 조인트바) { - surang = qty -} - -// 3. 총액 -total = unitprice × surang -``` - -#### 4.5.4 전동 개폐기/제어기 조회 - -**함수**: `lookupMotorPrice(row)`, `lookupControllerPrice(row)` - -```javascript -// 5130/output/write_form_script.php (lines 6886-6920) - -// KDunitprice 테이블에서 조회 -// unitInfo: { prodcode → unitprice } 매핑 - -// 전동 개폐기 -unitprice = lookupMotorPrice(row) -// → row 데이터(용량, 전원, 형태 등)로 KDunitprice 조회 - -// 제어기 -unitprice = lookupControllerPrice(row) -// → row 데이터(유형, 규격)로 KDunitprice 조회 -``` - -**데이터 소스**: `KDunitprice` → `window.unitInfo` (JS 전역) - -#### 4.5.5 모터 용량 계산 (핵심 로직) - -**함수**: `calculateMotorSpec($item, $weight, $BracketInch)` (PHP) - -```php -// 5130/estimate/fetch_unitprice.php (lines 200-350) - -// 1. 품목 유형 판별 -$ItemSel = (substr($item['col4'], 0, 2) === 'KS' || - substr($item['col4'], 0, 2) === 'KW') - ? '스크린' : '철재'; - -// 2. 용량 결정 테이블 -// 스크린: 150K ~ 600K -// 철재: 300K ~ 1000K -// Weight + BracketInch 조합으로 용량 결정 - -// 3. 브라켓 사이즈 매핑 -300-400K → 530×320 -500-600K → 600×350 -800-1000K → 690×390 -``` - -#### 4.5.6 기타 계산 함수 - -| 함수 | 용도 | 계산식 | -|------|------|--------| -| `calculateGuidrail()` | 가이드레일 수량 | `col17 / 3490` (기본 길이) | -| `calculateShaft()` | 샤프트 단가 | `col19 × 수량`, 길이별 조회 | -| `calculatePipe()` | 파이프 단가 | `col4(길이)`, `col2(규격)`으로 `col8(단가)` 조회 | -| `slatPrice()` | 인정 슬랫 단가 | `price_raw_materials.col13` | -| `unapprovedSlatPrice()` | 비인정 슬랫 단가 | `price_raw_materials.col15` | - -#### 4.5.7 전역 데이터 구조 (JS) - -```javascript -// 5130/output/write_form.php에서 PHP→JS 전달 - -// 비인정 자재 단가 (재질 → 단가) -window.materialBasePrice = { - "실리카": 12000, - "폴리에스터": 8500, - // ... -}; - -// 비인정 자재 코드 (재질 → 코드) -window.materialBaseCode = { - "실리카": "RM001", - // ... -}; - -// 절곡품 단가표 -var item_bend = [ - { col1: "EGI", col5: 1.2, col17: 45000 }, - { col1: "SUS", col5: 1.5, col17: 85000 }, - // ... -]; - -// KDunitprice 단가 (prodcode → unitprice) -window.unitInfo = { - "MOT300": 250000, - "MOT500": 380000, - // ... -}; -``` - -#### 4.5.8 SAM 구현 시 고려사항 - -| 구분 | 5130 방식 | SAM 구현 방향 | -|------|----------|--------------| -| 단가 조회 | JS 전역 변수 | Service 클래스 + DB 쿼리 | -| 면적 계산 | JS (mm² → m²) | PHP Helper 함수 | -| 두께 매핑 | JS 하드코딩 | 설정 테이블 or Enum | -| 모터 용량 | PHP 조건문 | 룰 엔진 or 매핑 테이블 | -| 반올림/절삭 | `Math.floor()` | `floor()` 동일 적용 | - ---- - -## 5. 작업 절차 - -### 5.1 단계별 절차 - -``` -Step 1: 품목 마스터 분석 (Phase 1.1-1.3) -├── KDunitprice 테이블 구조 상세 분석 -├── price_raw_materials JSON 구조 분석 -├── price_bend JSON 구조 분석 -└── SAM items 테이블과 매핑 확정 - -Step 2: 품목 마이그레이션 (Phase 1.4-1.5) -├── 마이그레이션 스크립트 작성 (Artisan Command) -├── 테스트 데이터로 검증 -└── 전체 데이터 마이그레이션 - -Step 3: 수주 마스터 분석 (Phase 2.1-2.3) -├── output 테이블 80개 필드 분석 -├── JSON 필드 (screenlist 등) 구조 분석 -├── output_extra 연결 관계 분석 -└── SAM orders/order_items 매핑 확정 - -Step 4: 수주 마이그레이션 (Phase 2.4-2.5) -├── 마이그레이션 스크립트 작성 -├── JSON → 관계형 변환 로직 구현 -├── 테스트 데이터로 검증 -└── 전체 데이터 마이그레이션 - -Step 5: 견적 로직 검증 (Phase 3) -├── 5130 견적 계산 JS 분석 -├── SAM에서 동일 로직 구현/검증 -└── 샘플 데이터로 결과 비교 -``` - -### 5.2 분석 템플릿 - -```markdown -### [테이블명] 분석 - -**현재 상태 (5130):** -- 테이블: [테이블명] -- 필드 수: [N]개 -- 레코드 수: [N]건 - -**목표 상태 (SAM):** -- 테이블: [테이블명] -- 매핑 필드: [N]개 - -**필드 매핑:** -| 5130 | SAM | 변환 로직 | -|------|-----|----------| -| | | | - -**특이사항:** -- [ ] JSON 변환 필요 여부 -- [ ] 타입 변환 필요 여부 -- [ ] 기본값 처리 방법 -``` - ---- - -## 6. 컨펌 대기 목록 - -> 테이블 구조 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| - | - | - | - | - | - ---- - -## 7. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-19 | 초안 | 문서 초안 작성 | - | - | -| 2025-01-19 | Phase 1.1 | KDunitprice → items 매핑 분석 완료 | - | - | -| 2025-01-19 | Phase 1.2 | price_raw_materials → items 매핑 분석 완료 (itemList JSON 15필드) | - | - | -| 2025-01-19 | Phase 1.3 | price_bend → items 매핑 분석 완료 (itemList JSON 18필드) | - | - | -| 2025-01-19 | Phase 1.4 | 품목 마이그레이션 스크립트 작성 완료 | `api/app/Console/Commands/Migrate5130PriceItems.php` | - | -| 2026-01-19 | Phase 2.4 | 수주 마이그레이션 스크립트 작성 완료 | `api/app/Console/Commands/Migrate5130Orders.php`, `api/database/migrations/2026_01_19_202830_create_order_id_mappings_table.php` | - | -| 2026-01-19 | Phase 3.1 | 5130 견적 수식 분석 완료 | `5130/output/write_form_script.php`, `5130/estimate/fetch_unitprice.php` | - | -| 2026-01-19 | Phase 3.2 | SAM 견적 수식 구현 완료 | `api/app/Helpers/Legacy5130Calculator.php`, `api/app/Console/Commands/Verify5130Calculation.php` | - | -| 2026-01-19 | Phase 3.3 | 견적 수식 검증 테스트 실행 | 5/5 테스트 케이스 100% 일치 | - | -| 2026-01-20 | 준비 완료 | Phase 1-3 모든 준비 작업 완료, 실행 대기 | 13/13 작업 완료 | - | -| 2026-01-20 | Phase 4 | 전체 마이그레이션 실행 완료 | items 608건, orders 24,424건, order_items 43,900건 | ✅ | - ---- - -## 8. 참고 문서 - -### 8.1 5130 소스 코드 - -- **수주 폼**: `5130/output/write_form.php` (1176줄) -- **견적 계산 JS**: `5130/output/write_form_script.php` (302KB, ~7000줄) -- **단가 조회 PHP**: `5130/estimate/fetch_unitprice.php` (875줄) -- **output 필드**: `5130/output/_row.php` (~80개 필드) -- **output_extra 필드**: `5130/output/_row_extra.php` (~30개 필드) -- **단가표 필드**: `5130/KDunitprice/_row.php` - -### 8.2 SAM 스키마 - -- **items 테이블**: `api/database/migrations/2025_12_13_152507_create_items_table.php` -- **orders 테이블**: `api/database/migrations/2024_11_19_000001_create_orders_table.php` -- **order_items 테이블**: `api/database/migrations/2024_11_19_000002_create_order_items_table.php` - -### 8.3 SAM 모델 - -- **Order 모델**: `api/app/Models/Orders/Order.php` -- **OrderItem 모델**: `api/app/Models/Orders/OrderItem.php` -- **Item 모델**: `api/app/Models/Items/Item.php` - ---- - -## 9. 세션 및 메모리 관리 정책 - -### 9.1 세션 시작 시 (Load Strategy) -```javascript -// 순차적 로드 -read_memory("5130-migration-state") // 1. 상태 파악 -read_memory("5130-migration-mappings") // 2. 매핑 정보 로드 -read_memory("5130-migration-rules") // 3. 규칙 확인 -``` - -### 9.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | 🛠 **Snapshot** | `write_memory("5130-migration-snapshot", "진행상황")` | -| **20% 이하** | 🧹 **Context Purge** | `write_memory("5130-migration-active", "현재 작업")` | -| **10% 이하** | 🛑 **Stop & Save** | 최종 상태 저장 후 세션 교체 권고 | - -### 9.3 Serena 메모리 구조 -- `5130-migration-state`: { phase, progress, next_step } (JSON 구조) -- `5130-migration-mappings`: 테이블/필드 매핑 정보 (Text) -- `5130-migration-rules`: 변환 규칙, 타입 매핑 (Text) - ---- - -## 10. 검증 결과 - -### 10.1 Phase 1 품목 마이그레이션 검증 (2025-01-19) - -#### 소스 데이터 카운트 -| 테이블 | 총 건수 | 활성 건수 | 최신 버전 | -|--------|---------|----------|----------| -| KDunitprice | 603 | 601 (NULL/0) | - | -| price_raw_materials | 14 | 6 | 2025-06-18 | -| price_bend | 3 | 3 | 2025-03-09 | - -#### dry-run 검증 결과 -| 테이블 | Total | Migrated | Skipped | 결과 | -|--------|-------|----------|---------|:----:| -| KDunitprice | 601 | 601 | 0 | ✅ | -| price_raw_materials | 13 | 13 | 0 | ✅ | -| price_bend | 7 | 7 | 0 | ✅ | -| **합계** | **621** | **621** | **0** | ✅ | - -#### item_type 분류 검증 -| item_div | 예상 | 실제 | 결과 | -|----------|------|------|:----:| -| [상품] | FG | FG | ✅ | -| [제품] | FG | FG | ✅ | -| [반제품] | PT | PT | ✅ | -| [부재료] | SM | SM | ✅ | -| [원재료] | RM | RM | ✅ | -| [무형상품] | CS | CS | ✅ | - -#### item_div 분포 (KDunitprice 601건) -| item_div | 건수 | item_type | -|----------|------|-----------| -| [상품] | 259 | FG | -| [제품] | 193 | FG | -| [반제품] | 73 | PT | -| [부재료] | 48 | SM | -| [원재료] | 24 | RM | -| [무형상품] | 4 | CS | - -### 10.2 Phase 2 수주 마이그레이션 검증 (2026-01-19) - -#### 소스 데이터 현황 -| 테이블/필드 | 총 건수 | 비고 | -|-------------|---------|------| -| output | 24,584 | 전체 수주 | -| output (screenlist 있음) | 9,392 | 방충망 포함 | -| output (slatlist 있음) | 1,955 | 슬랫 포함 | -| output_extra (motorList 있음) | 7 | 모터 포함 | -| output_extra (bendList 있음) | 10 | 절곡 포함 | - -#### dry-run 검증 결과 -| 항목 | 건수 | 결과 | 비고 | -|------|------|:----:|------| -| orders | 100 | ✅ | 100건 테스트 성공 | -| order_items (screen) | - | ⏳ | 실제 실행 후 확인 | -| order_items (slat) | - | ⏳ | 실제 실행 후 확인 | -| order_items (motor) | 0 | ✅ | motorList 없는 범위 | -| order_items (bend) | 0 | ✅ | bendList 없는 범위 | - -#### 샘플 데이터 매핑 검증 -**샘플 num=25810** -| 5130 필드 | 값 | SAM 필드 | 변환 결과 | 검증 | -|-----------|-----|----------|----------|:----:| -| outdate | 2025-12-15 | received_at | 2025-12-15 00:00:00 | ✅ | -| outworkplace | IFC | site_name | IFC | ✅ | -| regist_state | 등록 | status_code | REGISTERED | ✅ | -| phone | 010-5231-3134 | client_contact | 010-5231-3134 | ✅ | -| comment | 실리카1틀/... | memo | 실리카1틀/... | ✅ | -| delivery | 직접배차 | delivery_method_code | 직접배차 | ✅ | -| screenlist[0].cutwidth×cutheight | 3260×4000 | specification | 3260x4000 | ✅ | -| screenlist[0].number | 1 | quantity | 1 | ✅ | -| screenlist[0].memo | 실리카 | remarks | 실리카 | ✅ | - -**motorList/bendList 구조 검증** -| col | motorList 매핑 | bendList 매핑 | 검증 | -|-----|---------------|--------------|:----:| -| col1 | item_name (전동개폐기_단상 220V) | item_name (가이드레일) | ✅ | -| col2 | specification (300kg) | specification (EGI 1.6T) | ✅ | -| col3 | attributes.dimension (380*180) | attributes.length (3000) | ✅ | -| col5 | quantity (2) | attributes.width (332) | ✅ | -| col6 | attributes.type (신형) | attributes.drawing (이미지경로) | ✅ | -| col7 | attributes.option | quantity (1) | ✅ | -| col8 | attributes.power (단상) | remarks | ✅ | - -### 10.3 데이터 정합성 요약 - -| 테이블 | 5130 건수 | SAM 건수 | 일치 | 비고 | -|--------|----------|----------|:----:|------| -| KDunitprice → items | 601 | (dry-run) | ✅ | Phase 1 검증 완료 | -| price_raw_materials → items | 13 | (dry-run) | ✅ | 최신 버전만 | -| price_bend → items | 7 | (dry-run) | ✅ | 최신 버전만 | -| output → orders | 24,584 | (dry-run) | ✅ | 100건 테스트 성공 | -| screenlist → order_items | 9,392+ | (대기) | ⏳ | 실제 마이그레이션 후 확인 | -| slatlist → order_items | 1,955+ | (대기) | ⏳ | 실제 마이그레이션 후 확인 | - -### 10.4 견적 수식 검증 (2026-01-19) - -#### 검증 도구 -- **Legacy5130Calculator.php**: 5130 호환 계산 헬퍼 클래스 -- **Verify5130Calculation.php**: 검증 Artisan 커맨드 -- **실행**: `php artisan migration:verify-5130-calculation --W0=3000 --H0=2500 --type=screen` - -#### 테스트 결과 - -| 케이스 | W0×H0 | 유형 | W1 (5130/SAM) | H1 (5130/SAM) | M (m²) | K (kg) | 결과 | -|--------|-------|------|---------------|---------------|--------|--------|:----:| -| 스크린 소형 | 1500×1200 | screen | 1640/1640 | 1550/1550 | 2.542 | 26.34 | ✅ | -| 스크린 중형 | 3000×2500 | screen | 3140/3140 | 2850/2850 | 8.949 | 60.41 | ✅ | -| 스크린 대형 | 5000×4000 | screen | 5140/5140 | 4350/4350 | 22.359 | 115.57 | ✅ | -| 철재 중형 | 2000×1800 | steel | 2110/2110 | 2150/2150 | 4.5365 | 113.41 | ✅ | -| 철재 대형 | 4000×3500 | steel | 4110/4110 | 3850/3850 | 15.8235 | 395.59 | ✅ | - -#### 검증 수식 - -``` -스크린 (screen): -├── W1 = W0 + 140 (마진) -├── H1 = H0 + 350 (마진) -├── M = (W1 × H1) / 1,000,000 (m²) -└── K = (M × 2) + (W0 / 1000 × 14.17) (kg) - -철재 (steel): -├── W1 = W0 + 110 (마진) -├── H1 = H0 + 350 (마진) -├── M = (W1 × H1) / 1,000,000 (m²) -└── K = M × 25 (kg) -``` - -#### 모터 용량/브라켓 사이즈 검증 - -| 케이스 | 중량(K) | 브라켓인치 | 모터용량 | 브라켓사이즈 | -|--------|---------|-----------|---------|-------------| -| 스크린 중형 | 60.41 | 124" | 600K | 600×350 | -| 철재 중형 | 113.41 | 84" | 1000K | 690×390 | - -**결과**: 5/5 테스트 케이스 통과 → ✅ **견적 수식 100% 일치 확인** - ---- - -## 11. 자기완결성 점검 결과 - -### 11.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 5130→SAM 데이터 마이그레이션 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 데이터 정합성 + 견적 동일성 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3 정의됨 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 5130 소스 + SAM 스키마 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 8 참조 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 참조 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 10 참조 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 테이블/필드 명시 | - -### 11.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 5.1 단계별 절차 | -| Q3. 어떤 테이블을 매핑해야 하는가? | ✅ | 2. 테이블 매핑 개요 | -| Q4. 작업 완료 확인 방법은? | ✅ | 10. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md b/plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md deleted file mode 100644 index aedaf24..0000000 --- a/plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md +++ /dev/null @@ -1,406 +0,0 @@ -# SAM ERP 대시보드 -## AI 리포트 핵심 키워드 색상 체계 가이드 -### (임계값 명확화 버전 v1.4) - -> 버전: D1.4 | 작성일: 2026년 1월 - ---- - -## 1. AI 리포트 색상 체계 개요 - -AI 리포트는 각 섹션별 핵심 키워드에 색상을 적용하여 사용자가 즉시 상태를 파악할 수 있도록 합니다. 모든 기준은 명확한 수치로 정의되어 일관된 적용이 가능합니다. - -### 1.1 색상 정의 - -| 색상 | 의미 | 적용 원칙 | 우선순위 | -|:---:|:---:|:---|:---:| -| 🔴 빨간색 | 경고 | 즉각 조치 필요, 한도/기준 초과, 손실 발생 | 1순위 (최우선) | -| 🟠 주황색 | 주의 | 기준의 80~100% 도달, 기한 임박, 검토 필요 | 2순위 | -| 🟢 녹색 | 긍정 | 목표 달성, 정상 완료, 개선, 입금/회수 | 3순위 | -| 🔵 파란색 | 양호 | 안정적 유지, 정상 진행 중, 충분히 확보 | 4순위 | - -### 1.2 공통 임계값 원칙 - -| 구분 | 🔴 경고 | 🟠 주의 | 🟢 긍정 | 🔵 양호 | -|:---|:---|:---|:---|:---| -| 한도 사용률 | 100% 초과 | 85~100% | - | 85% 미만 | -| 전월/전기 대비 증감 | ±20% 이상 | ±10~20% | 개선 방향 변동 | ±10% 이내 | -| 예산 대비 | 100% 초과 | 90~100% | - | 90% 미만 | -| 연체 기간 | 90일 초과 | 30~90일 | 정상 회수 | 만기 전 | -| 운영자금 확보 | 3개월 미만 | 3~6개월 | - | 6개월 이상 | - ---- - -## 2. 일일 일보 섹션 - -일일 일보는 당일 자금 현황을 요약하여 보여주며, 현금 흐름에 대한 AI 분석 리포트가 함께 제공됩니다. - -### 2.1 현금 자산 - 출금 분석 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **출금** | 🔴 빨간색 | 7일 평균 대비 200% 이상 | 당일출금 ÷ 7일평균출금 ≥ 2.0 | -| **점검이 필요** | 🔴 빨간색 | 7일 평균 대비 200% 이상 | 출금 키워드와 함께 사용 | -| **출금 증가** | 🟠 주황색 | 7일 평균 대비 150~200% | 당일출금 ÷ 7일평균출금 1.5~2.0 | -| **정상 출금** | 🔵 파란색 | 7일 평균 대비 150% 미만 | 당일출금 ÷ 7일평균출금 < 1.5 | - -#### 적용 예시 -- 어제 🔴**3.5억원 출금**했습니다. 최근 7일 평균(1.7억원) 대비 206%로 🔴**점검이 필요**합니다. - -### 2.2 현금 자산 - 입금 분석 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **입금** | 🟢 녹색 | 입금 발생 시 (금액 무관) | 당일 입금 > 0 | -| **대규모 입금** | 🟢 녹색 | 월평균 입금의 200% 이상 | 당일입금 ÷ 월평균입금 ≥ 2.0 | -| **주요 원인** | 🟢 녹색 | 입금 원인 설명 시 | 입금 키워드와 함께 사용 | - -#### 적용 예시 -- 어제 🟢**10.2억원이 입금**되었습니다. 대한건설 선수금 🟢**입금**이 🟢**주요 원인**입니다. - -### 2.3 현금 자산 - 운영자금 안정성 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **자금 부족 우려** | 🔴 빨간색 | 월 운영비용 대비 3개월 미만 | 현금자산 ÷ 월운영비 < 3 | -| **자금 관리 필요** | 🟠 주황색 | 월 운영비용 대비 3~6개월 | 현금자산 ÷ 월운영비 3~6 | -| **확보되어 안정적** | 🔵 파란색 | 월 운영비용 대비 6개월 이상 | 현금자산 ÷ 월운영비 ≥ 6 | - -#### 적용 예시 -- 총 현금성 자산이 300.2억원입니다. 월 운영비용(16.7억원) 대비 🔵**18개월 분이 확보되어 안정적**입니다. - -### 2.4 외화 현황 - 환율 변동 - -| 키워드 | 색상 | 임계값 기준 (일일) | 임계값 기준 (주간) | -|:---|:---:|:---|:---| -| **환율 급등** | 🔴 빨간색 | 전일 대비 ±1.5% 이상 또는 ±20원 이상 | 전주 대비 ±3% 이상 | -| **환율 급락** | 🔴 빨간색 | 전일 대비 ±1.5% 이상 또는 ±20원 이상 | 전주 대비 ±3% 이상 | -| **환율 변동 주의** | 🟠 주황색 | 전일 대비 ±1.0~1.5% 또는 ±10~20원 | 전주 대비 ±2~3% | -| **환율 안정** | 🔵 파란색 | 전일 대비 ±1.0% 미만 또는 ±10원 미만 | 전주 대비 ±2% 미만 | - -### 2.5 외화 현황 - 환차손익 - -| 키워드 | 색상 | 임계값 기준 (금액) | 임계값 기준 (비율) | -|:---|:---:|:---|:---| -| **환차손 발생** | 🔴 빨간색 | 평가손실 1,000만원 이상 | 외화보유액 대비 2% 이상 손실 | -| **환리스크 주의** | 🟠 주황색 | 평가손실 500~1,000만원 | 외화보유액 대비 1~2% 손실 | -| **환차익 발생** | 🟢 녹색 | 평가이익 500만원 이상 | 외화보유액 대비 1% 이상 이익 | -| **환율 영향 미미** | 🔵 파란색 | 평가손익 ±500만원 미만 | 외화보유액 대비 ±1% 미만 | - -#### 적용 예시 -- 전일 대비 환율이 🔴**1.8% 상승(+24원)**했습니다. 외화자산 평가손실 🔴**약 1,500만원 환차손 발생**이 예상됩니다. -- 전일 대비 환율 변동 0.3%(+4원)으로 🔵**환율 안정**적인 상태입니다. 🔵**환율 영향 미미**합니다. - ---- - -## 3. 당월 예상 지출 내역 섹션 - -당월 예상되는 지출 항목(매입, 카드, 발행어음 등)을 분석하여 전월 대비 및 예산 대비 현황을 제공합니다. - -### 3.1 전월 대비 분석 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **전월 대비 N% 증가** | 🔴 빨간색 | 전월 대비 15% 이상 증가 | (당월-전월) ÷ 전월 ≥ 0.15 | -| **지출 증가 추이** | 🟠 주황색 | 전월 대비 10~15% 증가 | (당월-전월) ÷ 전월 0.10~0.15 | -| **전월 대비 N% 감소** | 🟢 녹색 | 전월 대비 5% 이상 감소 | (당월-전월) ÷ 전월 ≤ -0.05 | -| **전월과 유사** | 🔵 파란색 | 전월 대비 ±10% 이내 | |(당월-전월) ÷ 전월| < 0.10 | - -#### 적용 예시 -- 이번 달 예상 지출이 🔴**전월 대비 15% 증가**했습니다. 매입 비용 증가가 주요 원인입니다. -- 이번 달 예상 지출이 🟢**전월 대비 8% 감소**했습니다. 외주비용 절감이 주요 원인입니다. - -### 3.2 예산 대비 분석 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **예산을 N% 초과** | 🔴 빨간색 | 예산 대비 100% 초과 | 예상지출 ÷ 예산 > 1.0 | -| **예산 임박** | 🟠 주황색 | 예산 대비 90~100% | 예상지출 ÷ 예산 0.9~1.0 | -| **예산 내 운영** | 🟢 녹색 | 예산 대비 90% 미만 | 예상지출 ÷ 예산 < 0.9 | - -#### 적용 예시 -- 이번 달 예상 지출이 🔴**예산을 12% 초과**했습니다. 비용 항목별 점검이 필요합니다. -- 이번 달 예상 지출이 🟢**예산 내 운영** 중입니다. (예산 대비 82%) - -### 3.3 항목별 지출 분석 기준 - -| 지출 항목 | 🔴 경고 | 🟠 주의 | 🟢 긍정 | 🔵 양호 | -|:---|:---|:---|:---|:---| -| 매입 | 전월 대비 20% 이상 증가 | 전월 대비 10~20% 증가 | 전월 대비 감소 | ±10% 이내 | -| 카드 | 한도 100% 초과 | 한도 80~100% 사용 | - | 한도 80% 미만 | -| 발행어음 | 만기 초과 또는 부도 위험 | 만기 D-7일 이내 | 정상 결제 완료 | 만기 D-8일 이상 | -| 인건비 | 예산 대비 100% 초과 | 예산 대비 90~100% | - | 예산 대비 90% 미만 | - ---- - -## 4. 카드/가지급금 관리 섹션 - -법인카드 사용 현황과 가지급금 발생 현황을 분석하여 세무 리스크를 사전에 안내합니다. - -### 4.1 가지급금 전환 - -| 키워드 | 색상 | 임계값 기준 | 세무 영향 | -|:---|:---:|:---|:---| -| **가지급금으로 전환** | 🔴 빨간색 | 미정리 법인카드 사용 100만원 이상 | 인정이자 4.6% 발생 | -| **인정이자가 발생** | 🔴 빨간색 | 가지급금 잔액 × 4.6% | 법인세 증가 | -| **연간 N만원의 인정이자** | 🔴 빨간색 | 연간 인정이자 100만원 이상 | 가지급금 × 4.6% | -| **가지급금 정리 필요** | 🟠 주황색 | 미정리 법인카드 사용 50~100만원 | 정리 권고 | - -#### 적용 예시 -- 법인카드 사용 중 850만원이 🔴**가지급금으로 전환**되었습니다. 🔴**연 4.6% 인정이자가 발생**합니다. -- 현재 가지급금 3.5억 × 4.6% = 🔴**연간 약 1,610만원의 인정이자가 발생** 중입니다. - -### 4.2 업무관련성 소명 필요 - -| 키워드 | 색상 | 임계값 기준 | 발생 사유 | -|:---|:---:|:---|:---| -| **불인정 가맹점 결제** | 🔴 빨간색 | 유흥업소, 귀금속, 상품권 등 결제 | 가지급금 전환 대상 | -| **본인 청구 결제 감지** | 🟠 주황색 | 상품권, 귀금속, 면세점 등 1건 이상 | 소명 자료 필요 | -| **주말 사용 감지** | 🟠 주황색 | 토/일요일 결제 50만원 이상 | 업무관련성 검토 | -| **심야 사용 감지** | 🟠 주황색 | 22시~06시 결제 30만원 이상 | 업무관련성 검토 | -| **해외 사용 감지** | 🟠 주황색 | 해외 결제 발생 시 | 출장 증빙 필요 | - -#### 적용 예시 -- 상품권 구매 🟠**본인 청구 결제 감지**. 가지급금 처리 예정입니다. -- 🟠**주말 사용 감지** - 토요일 120만원 결제. 업무관련성 소명이 어려울 수 있으니 기록을 남겨주세요. - -### 4.3 법인세/종합소득세 예상 가중 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **법인세 예상 가중** | 🔴 빨간색 | 추가 법인세 100만원 이상 예상 | 가지급금 인정이자 × 법인세율 | -| **대표자 종합소득세 예상 가중** | 🔴 빨간색 | 추가 종합소득세 50만원 이상 예상 | 인정상여 × 소득세율 | -| **세무 리스크 주의** | 🟠 주황색 | 추가 세금 50만원 미만 예상 | 정리 권고 | - -#### 적용 예시 -- 가지급금으로 인한 🔴**법인세 예상 가중 약 320만원**이 발생합니다. -- 🔴**대표자 종합소득세 예상 가중 약 180만원**이 예상됩니다. (추가 사용 +10.5%) - ---- - -## 5. 접대비 현황 섹션 - -접대비 사용 현황과 한도 대비 사용률을 분석하여 세법상 한도 초과 여부를 사전에 안내합니다. - -### 5.1 한도 사용률 기준 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **한도 초과 N만원 발생** | 🔴 빨간색 | 한도 사용률 100% 초과 | 사용액 > 한도액 | -| **손금 불산입** | 🔴 빨간색 | 한도 초과액 발생 시 | 초과액 = 사용액 - 한도액 | -| **법인세 부담이 증가** | 🔴 빨간색 | 한도 초과로 인한 법인세 증가 | 초과액 × 법인세율 | -| **잔여 한도 N원** | 🟠 주황색 | 한도 사용률 85~100% | 잔여 = 한도액 - 사용액 | -| **사용 계획을 점검** | 🟠 주황색 | 한도 사용률 85% 이상 | 사용액 ÷ 한도액 ≥ 0.85 | -| **여유 있게 운영** | 🟢 녹색 | 한도 사용률 75% 미만 | 사용액 ÷ 한도액 < 0.75 | -| **정상 운영** | 🔵 파란색 | 한도 사용률 75~85% | 사용액 ÷ 한도액 0.75~0.85 | - -#### 세법상 접대비 한도 계산 -- 기본한도: 중소기업 3,600만원, 일반기업 2,400만원 (연간) -- 추가한도: 수입금액 × 적용률 (100억 이하 0.3%, 100~500억 0.2%, 500억 초과 0.03%) - -#### 적용 예시 -- (1분기) 접대비 사용 1,000만원 / 한도 4,012만원 (25%). 🟢**여유 있게 운영** 중입니다. -- 접대비 한도 85% 도달. 🟠**잔여 한도 600만원**입니다. 🟠**사용 계획을 점검**해 주세요. -- 🔴**접대비 한도 초과 320만원 발생**. 초과분은 🔴**손금 불산입**되어 🔴**법인세 부담이 증가**합니다. - -### 5.2 증빙 관리 - -| 키워드 | 색상 | 임계값 기준 | 필수 정보 | -|:---|:---:|:---|:---| -| **거래처 정보가 누락** | 🔴 빨간색 | 거래처명 또는 참석자 미입력 1건 이상 | 거래처명, 참석자, 목적 | -| **증빙 누락** | 🔴 빨간색 | 영수증 또는 카드전표 미첨부 1건 이상 | 적격증빙 필수 | -| **기록 보완 필요** | 🟠 주황색 | 상세 내용 미기재 (목적, 장소 등) | 상세 기록 권고 | -| **증빙 완비** | 🟢 녹색 | 모든 필수 정보 입력 완료 | - | - -#### 적용 예시 -- 접대비 사용 중 3건(45만원)의 🔴**거래처 정보가 누락**되었습니다. 🟠**기록 보완 필요**합니다. - ---- - -## 6. 복리후생비 현황 섹션 - -복리후생비 사용 현황을 분석하여 비과세 한도 초과 여부와 업계 평균 대비 적정성을 안내합니다. - -### 6.1 1인당 복리후생비 - -| 키워드 | 색상 | 임계값 기준 | 업계 평균 | -|:---|:---:|:---|:---| -| **과다 지출** | 🔴 빨간색 | 1인당 월 30만원 초과 | 업계 평균의 150% 초과 | -| **지출 증가 추이** | 🟠 주황색 | 1인당 월 25~30만원 | 업계 평균의 120~150% | -| **업계 평균 내 정상 운영** | 🟢 녹색 | 1인당 월 15~25만원 | 업계 평균 범위 내 | -| **적정 운영** | 🔵 파란색 | 1인당 월 15만원 미만 | 업계 평균 미만 | - -#### 적용 예시 -- 1인당 월 복리후생비 20만원. 🟢**업계 평균(15~25만원) 내 정상 운영** 중입니다. - -### 6.2 항목별 비과세 한도 - -| 항목 | 비과세 한도 | 🔴 경고 기준 | 🟢 정상 기준 | -|:---|:---|:---|:---| -| 식대 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 | -| 자가운전보조금 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 | -| 출산/보육수당 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 | -| 연구보조비 | 월 20만원 | 20만원 초과 시 초과분 과세 | 20만원 이하 | -| 야근식대/숙직비 | 실비 정산 | 과다 지급 시 과세 위험 | 실비 범위 내 | - -### 6.3 비과세 초과 시 - -| 키워드 | 색상 | 임계값 기준 | 세무 처리 | -|:---|:---:|:---|:---| -| **비과세 한도를 초과** | 🔴 빨간색 | 항목별 비과세 한도 초과 시 | 초과분 근로소득 과세 | -| **근로소득 과세됩니다** | 🔴 빨간색 | 비과세 초과분 발생 시 | 원천세 추가 징수 | -| **초과분 N만원 과세 처리** | 🔴 빨간색 | 과세 금액 명시 | 급여에 합산 | -| **한도 임박** | 🟠 주황색 | 비과세 한도의 90% 이상 사용 | 사용 주의 | - -#### 적용 예시 -- 식대가 월 25만원으로 🔴**비과세 한도(20만원)를 초과**했습니다. 🔴**초과분 5만원 근로소득 과세됩니다**. - ---- - -## 7. 미수금 현황 섹션 - -미수금 현황을 분석하여 연체 상태, 회수 필요성, 리스크 집중도를 안내합니다. - -### 7.1 연체 기간별 분류 - -| 키워드 | 색상 | 연체 기간 | 조치 수준 | -|:---|:---:|:---|:---| -| **장기 미수금 발생** | 🔴 빨간색 | 90일 초과 | 법적 조치 검토 | -| **회수 조치가 필요** | 🔴 빨간색 | 60~90일 | 적극적 독촉/추심 | -| **연체 발생** | 🟠 주황색 | 30~60일 | 독촉장 발송 | -| **연체 임박** | 🟠 주황색 | 만기 D-7일 ~ 만기 후 30일 | 사전 연락 | -| **정상 거래** | 🟢 녹색 | 만기 전 | 정상 관리 | -| **회수 완료** | 🟢 녹색 | 전액 회수 시 | 완료 처리 | - -#### 적용 예시 -- 90일 이상 🔴**장기 미수금 3건(2,500만원) 발생**. 🔴**회수 조치가 필요**합니다. - -### 7.2 리스크 집중도 - -| 키워드 | 색상 | 임계값 기준 | 계산 방식 | -|:---|:---:|:---|:---| -| **전체의 N%를 차지** | 🔴 빨간색 | 상위 1개사 미수금 비중 30% 이상 | 거래처 미수금 ÷ 총 미수금 | -| **리스크 분산이 필요** | 🔴 빨간색 | 상위 1개사 비중 30% 이상 | 집중 리스크 경고 | -| **리스크 관리 필요** | 🟠 주황색 | 상위 3개사 비중 50% 이상 | 분산 권고 | -| **리스크 분산 양호** | 🟢 녹색 | 상위 1개사 비중 20% 미만 | 정상 분산 | -| **리스크 관리 양호** | 🔵 파란색 | 상위 3개사 비중 40% 미만 | 양호한 분산 | - -#### 적용 예시 -- (주)대한전자 미수금 1,500만원으로 🔴**전체의 35%를 차지**합니다. 🔴**리스크 분산이 필요**합니다. -- 상위 3개사 미수금 비중 38%. 🔵**리스크 관리 양호**합니다. - -### 7.3 미수금 금액 기준 - -| 키워드 | 색상 | 임계값 기준 | 비고 | -|:---|:---:|:---|:---| -| **대형 미수금** | 🔴 빨간색 | 단일 건 3,000만원 이상 | 집중 관리 대상 | -| **주요 미수금** | 🟠 주황색 | 단일 건 1,000~3,000만원 | 관리 주의 | -| **일반 미수금** | 🔵 파란색 | 단일 건 1,000만원 미만 | 정상 관리 | - ---- - -## 8. 채권추심 현황 섹션 - -채권추심 진행 현황을 분석하여 법적 조치 상태와 회수 가능성을 안내합니다. - -### 8.1 추심 진행 상태 - -| 키워드 | 색상 | 임계값 기준 | 다음 단계 | -|:---|:---:|:---|:---| -| **회수 불가 판정** | 🔴 빨간색 | 채무자 무자력 확인 또는 소멸시효 완성 | 대손 처리 | -| **파산/회생 신청** | 🔴 빨간색 | 채무자 파산/회생 신청 확인 시 | 채권 신고 | -| **대손 처리 검토가 필요** | 🟠 주황색 | 회수 가능성 30% 미만 판단 시 | 세무 검토 | -| **법적 조치 진행 중** | 🔵 파란색 | 소송/강제집행 진행 중 | 결과 대기 | -| **지급명령 신청 완료** | 🟢 녹색 | 지급명령 신청 접수 완료 | 법원 결정 대기 | -| **회수 완료** | 🟢 녹색 | 채권 전액 또는 일부 회수 | 종결 처리 | - -#### 적용 예시 -- (주)대한전자 건 🟢**지급명령 신청 완료**. 법원 결정까지 약 2주 소요 예정입니다. -- (주)삼성테크 건 🔴**회수 불가 판정**. 🟠**대손 처리 검토가 필요**합니다. - -### 8.2 예상 소요 기간 - -| 키워드 | 색상 | 임계값 기준 | 비고 | -|:---|:---:|:---|:---| -| **장기 소송 예상** | 🔴 빨간색 | 예상 소요 기간 6개월 이상 | 비용/효익 검토 필요 | -| **소송 진행 중** | 🟠 주황색 | 예상 소요 기간 3~6개월 | 진행 상황 모니터링 | -| **법원 결정까지 약 N주 소요 예정** | 🔵 파란색 | 예상 소요 기간 3개월 미만 | 정상 진행 | -| **조기 회수 예상** | 🟢 녹색 | 예상 소요 기간 1개월 미만 | 신속 처리 | - -### 8.3 회수율 기준 - -| 키워드 | 색상 | 임계값 기준 | 판단 기준 | -|:---|:---:|:---|:---| -| **회수 불가** | 🔴 빨간색 | 예상 회수율 10% 미만 | 대손 처리 대상 | -| **회수 곤란** | 🔴 빨간색 | 예상 회수율 10~30% | 적극 추심 필요 | -| **부분 회수 예상** | 🟠 주황색 | 예상 회수율 30~70% | 협상 검토 | -| **회수 가능성 높음** | 🟢 녹색 | 예상 회수율 70% 이상 | 정상 추심 | -| **전액 회수 예상** | 🟢 녹색 | 예상 회수율 90% 이상 | 양호 | - ---- - -## 9. 부가세 현황 섹션 - -부가세 예정/확정 신고 현황을 분석하여 납부/환급 예상액과 세금계산서 발행 현황을 안내합니다. - -### 9.1 납부/환급 세액 - -| 키워드 | 색상 | 임계값 기준 | 판단 근거 | -|:---|:---:|:---|:---| -| **납부세액 급증** | 🔴 빨간색 | 전기 대비 30% 이상 증가 (매출 증가율 대비 초과) | 비정상 증가 | -| **매입세액 누락 의심** | 🔴 빨간색 | 매입세액 ÷ 매출세액 < 업종 평균의 70% | 누락 가능성 | -| **납부세액 증가** | 🟠 주황색 | 전기 대비 15~30% 증가 | 검토 필요 | -| **예상 환급세액** | 🟢 녹색 | 매입세액 > 매출세액 | 환급 발생 | -| **매입세액 증가가 주요 원인** | 🟢 녹색 | 설비투자 등 정당한 매입 증가 시 | 정상 사유 | -| **정상적인 증가로 판단** | 🟢 녹색 | 매출 증가율과 납부세액 증가율 유사 | 정상 범위 | -| **전기 대비 N% 증가** | 🔵 파란색 | 전기 대비 15% 미만 증가 | 정상 변동 | - -#### 적용 예시 -- 2026년 1기 예정신고 기준, 🟢**예상 환급세액**은 5,200,000원입니다. 설비투자에 따른 🟢**매입세액 증가가 주요 원인**입니다. -- 예상 납부세액 110,100,000원. 🔵**전기 대비 12.9% 증가**했으며, 매출 증가(11.5%)에 따른 🟢**정상적인 증가로 판단**됩니다. - -### 9.2 세금계산서 발행 관리 - -| 키워드 | 색상 | 임계값 기준 | 가산세 | -|:---|:---:|:---|:---| -| **세금계산서 미발행** | 🔴 빨간색 | 발행 기한 경과 후 미발행 1건 이상 | 공급가액의 2% | -| **발행 기한 초과** | 🔴 빨간색 | 공급일 다음 달 10일 경과 | 지연 발급 1% | -| **가산세 발생 위험** | 🔴 빨간색 | 미발행 또는 지연 발행 시 | 최대 2% | -| **발행 기한 임박** | 🟠 주황색 | 발행 기한 D-3일 이내 | 발행 권고 | -| **정상 발행** | 🟢 녹색 | 공급일 다음 달 10일 이내 발행 | 정상 | - -#### 적용 예시 -- 🔴**세금계산서 미발행** 3건 발생. 🔴**가산세 발생 위험**이 있습니다. 공급가액 1,500만원 × 2% = 30만원 -- 12월 매출분 세금계산서 🟠**발행 기한 임박** (D-2일). 1월 10일까지 발행 필요합니다. - ---- - -## 10. 종합 색상 적용 기준 매트릭스 - -모든 AI 리포트 섹션에 대한 색상 적용 기준을 요약한 종합 매트릭스입니다. - -| 섹션 | 🔴 경고 | 🟠 주의 | 🟢 긍정 | 🔵 양호 | -|:---|:---|:---|:---|:---| -| 일일 일보 (출금) | 7일 평균 대비 200% 이상 | 7일 평균 대비 150~200% | - | 7일 평균 대비 150% 미만 | -| 일일 일보 (입금) | - | - | 입금 발생 시 | - | -| 일일 일보 (운영자금) | 3개월 미만 확보 | 3~6개월 확보 | - | 6개월 이상 확보 | -| 일일 일보 (환율) | 일 ±1.5% 이상 | 일 ±1.0~1.5% | 환차익 발생 | 일 ±1.0% 미만 | -| 일일 일보 (환차손익) | 손실 1,000만원 이상 | 손실 500~1,000만원 | 이익 500만원 이상 | ±500만원 미만 | -| 당월 지출 (전월 대비) | 15% 이상 증가 | 10~15% 증가 | 5% 이상 감소 | ±10% 이내 | -| 당월 지출 (예산 대비) | 100% 초과 | 90~100% | - | 90% 미만 | -| 카드/가지급금 (전환) | 100만원 이상 전환 | 50~100만원 전환 | - | - | -| 카드/가지급금 (소명) | 불인정 가맹점 | 주말/심야 50만원 이상 | - | - | -| 접대비 (한도) | 100% 초과 | 85~100% | 75% 미만 | 75~85% | -| 접대비 (증빙) | 거래처 정보 누락 | 상세 내용 미기재 | 증빙 완비 | - | -| 복리후생비 | 1인당 월 30만원 초과 | 1인당 월 25~30만원 | 1인당 월 15~25만원 | 1인당 월 15만원 미만 | -| 복리후생비 (비과세) | 한도 초과 시 과세 | 한도 90% 이상 | - | 한도 이하 | -| 미수금 (연체) | 90일 초과 | 30~90일 | 회수 완료 | 만기 전 | -| 미수금 (집중도) | 1개사 30% 이상 | 3개사 50% 이상 | 1개사 20% 미만 | 3개사 40% 미만 | -| 채권추심 (상태) | 회수 불가, 파산 | 대손 검토 필요 | 지급명령 완료, 회수 | 법적 조치 진행 중 | -| 채권추심 (회수율) | 10% 미만 | 10~30% | 70% 이상 | 30~70% | -| 부가세 (납부세액) | 전기 대비 30% 이상 증가 | 전기 대비 15~30% 증가 | 환급 발생, 정상 증가 | 전기 대비 15% 미만 | -| 부가세 (세금계산서) | 미발행/기한 초과 | 기한 D-3일 이내 | 정상 발행 | - | - ---- - -*— 문서 끝 —* diff --git a/plans/archive/HISTORY.md b/plans/archive/HISTORY.md new file mode 100644 index 0000000..9693f6f --- /dev/null +++ b/plans/archive/HISTORY.md @@ -0,0 +1,88 @@ +# 완료 작업 히스토리 + +> docs/plans 완료 문서 요약. 상세 내용은 git 이력 참조. + +## 견적/수주 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 견적 자동 산출 개발 | 2025-12 | MNG 수식 설정 + React 자동산출 기능 구현 | +| MNG 수식 관리 개발 | 2025-12 | 수식 CRUD/카테고리/시뮬레이터/범위/매핑/품목 UI 완료 | +| 시뮬레이터 로직 동기화 | 2025-12 | Design/MNG 시뮬레이터 동일 결과 동기화 | +| 견적 V2 자동산출 오류 수정 | 2026-01 | 자동산출 4가지 오류 분석 및 수정 | +| 입찰관리 API 구현 | 2026-01 | 견적→입찰 전환 API 및 더미데이터 생성 | +| 시공사 페이지 API 연동 | 2026-01 | 8개 시공사 페이지 Mock→API 연동 완료 | +| 견적 URL 마이그레이션 | 2026-01 | test-new/test 경로→정식 경로 정비 | +| 수식 엔진 실제 데이터 연동 | 2026-02 | 테스트 데이터를 실제 품목으로 재구성 | + +## 수주/작업지시 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 수주관리 API 연동 | 2026-01 | 수주 목록/등록/수정/삭제 API 연동 완료 | +| 수주-작업지시-출하 연동 | 2026-01 | Order→WorkOrder→Shipment FK 연결 및 상태 동기화 | +| 작업지시 API | 2026-01 | 작업지시 목록/등록/상세 API 연동 완료 | +| 수주 하위 구조 관리 | 2026-02 | N-depth 트리 구조(개소/구역/공정) 하이브리드 설계 | + +## 품목/BOM + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| Items 테이블 통합 | 2025-12 | products/materials를 items로 통합 (Item-Master) | +| 5130 BOM 마이그레이션 | 2026-01 | 5130 레거시 BOM 61건을 SAM items.bom으로 마이그레이션 | +| 5130 자재/수주 마이그레이션 | 2026-01 | KDunitprice/output 데이터를 items/orders/order_items로 이관 | +| 경동 품목/단가 마이그레이션 | 2026-01 | 5130 ~1,500건 품목/단가/BOM 데이터 이관 | +| MNG 품목관리 페이지 | 2026-02 | 3-Panel 품목관리 (좌측 리스트+중앙 BOM+우측 상세) 구현 | +| MNG 품목-수식 연동 | 2026-02 | FormulaEvaluatorService 연동으로 동적 BOM 산출 | + +## 생산/절곡 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 공정관리 API | 2026-01 | 공정 CRUD + 분류 규칙 + 품목 연결 API 완료 | +| 재고 통합 시스템 | 2026-01 | 입고/생산/견적/출하 시 재고 자동 증감 및 FIFO 차감 | +| 절곡 작업일지 재구현 | 2026-02 | PHP 원본(~1400줄)을 React BendingWorkLogContent로 재구현 | +| 절곡 LOT 파이프라인 | 2026-02 | 절곡 세부품목 동적 BOM + LOT 추적 파이프라인 구축 | +| 개소별 자재 투입 매핑 | 2026-02 | 개소별 자재 투입 추적 및 LOT 매핑 기능 완료 | +| 절곡 선재고 관리 | 2026-02 | 선재고 입고 흐름 14/14 완료 | + +## 문서/서식 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 문서 업데이트 계획 | 2025-12 | docs/architecture 문서 동기화 (admin→mng 전환 반영) | +| 문서관리 시스템 변경이력 | 2026-02 | 검사 양식 템플릿 4종 + FQC/중간검사 구현 31개 이력 | +| 제품검사(FQC) 폼 | 2026-02 | 제품검사 양식 템플릿 설계 및 5.2 Phase 구현 | + +## 시스템/인프라 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| ERP API D1.0 개발 | 2025-12 | ERP API Phase 5~8 (12개 기능, ~71개 API) 완료 | +| API 전체 분석 보고서 | 2026-01 | 710+ API 중복/통합/미사용 분석 (React 실제 사용 ~80개) | +| 통계 DB 설계 | 2026-01 | 확장 가능한 전용 통계 DB(sam_stat) 설계 | +| MES 통합 흐름 분석 | 2026-01 | 견적→수주→작업지시 모듈 간 데이터 흐름 분석 | +| DB 트리거 감사 시스템 | 2026-02 | 감사 트리거 15/16 완료, 94% | + +## 사용자/권한 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| L2 권한관리 API | 2025-12 | React 권한관리 Mock→API 연동 (Spatie Permission) | +| 시더 목록 | 2026-01 | 사용자/부서/거래처 등 13개 시더 명령어 정리 | + +## 프론트엔드/알림 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| React FCM 푸시 알림 | 2025-12 | mng FCM.js를 React에 포팅, Capacitor 앱 지원 | +| FCM 사용자별 알림 | 2026-01 | 테넌트 전체 브로드캐스트→사용자별 타겟 발송 전환 | +| 알림음 시스템 | 2026-01 | FCM 알림 타입별 커스텀 알림음 (6개 채널) | +| React 서버컴포넌트 점검 | 2026-01 | 'use client' 정책 준수 여부 점검 (0개 오류) | + +## 기타 + +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| AI 리포트 색상체계 가이드 | 2026-01 | AI 리포트 섹션별 색상 임계값 정의 (v1.4) | +| 복리후생비 섹션 | 2026-01 | CEO 대시보드 복리후생비 현황 4개 카드 구현 | diff --git a/plans/archive/SEEDERS_LIST.md b/plans/archive/SEEDERS_LIST.md deleted file mode 100644 index b8a90b2..0000000 --- a/plans/archive/SEEDERS_LIST.md +++ /dev/null @@ -1,128 +0,0 @@ -# SAM API 시더 목록 - -> 생성일: 2025-01-05 -> 대상 테넌트: ID 287 - -## 개별 실행 방법 - -```bash -# Docker 컨테이너 접속 후 -php artisan db:seed --class=시더클래스명 - -# Dummy 폴더 시더는 네임스페이스 포함 -php artisan db:seed --class=Dummy\\DummyClientSeeder -``` - ---- - -## 1. 메인 시더 - -| # | 시더 | 설명 | 실행 명령어 | -|---|------|------|-------------| -| 1 | `DatabaseSeeder` | 기본 시더 (테스트 유저 + 메뉴) | `php artisan db:seed` | -| 2 | `DummyDataSeeder` | 전체 더미 데이터 (모든 Dummy 호출) | `php artisan db:seed --class=DummyDataSeeder` | - ---- - -## 2. 기본 데이터 시더 (Dummy) - -| # | 시더 | 테이블 | 수량 | 실행 명령어 | -|---|------|--------|------|-------------| -| 3 | `DummyUserSeeder` | users | 15 | `php artisan db:seed --class=Dummy\\DummyUserSeeder` | -| 4 | `DummyDepartmentSeeder` | departments | 11 | `php artisan db:seed --class=Dummy\\DummyDepartmentSeeder` | -| 5 | `DummyClientGroupSeeder` | client_groups | 5 | `php artisan db:seed --class=Dummy\\DummyClientGroupSeeder` | -| 6 | `DummyBankAccountSeeder` | bank_accounts | 5 | `php artisan db:seed --class=Dummy\\DummyBankAccountSeeder` | -| 7 | `DummyClientSeeder` | clients | 20 | `php artisan db:seed --class=Dummy\\DummyClientSeeder` | - ---- - -## 3. 회계 데이터 시더 (Dummy) - -| # | 시더 | 테이블 | 수량 | 실행 명령어 | -|---|------|--------|------|-------------| -| 8 | `DummyDepositSeeder` | deposits | 60 | `php artisan db:seed --class=Dummy\\DummyDepositSeeder` | -| 9 | `DummyWithdrawalSeeder` | withdrawals | 60 | `php artisan db:seed --class=Dummy\\DummyWithdrawalSeeder` | -| 10 | `DummySaleSeeder` | sales | 80 | `php artisan db:seed --class=Dummy\\DummySaleSeeder` | -| 11 | `DummyPurchaseSeeder` | purchases | 70 | `php artisan db:seed --class=Dummy\\DummyPurchaseSeeder` | -| 12 | `DummyBadDebtSeeder` | bad_debts | 18 | `php artisan db:seed --class=Dummy\\DummyBadDebtSeeder` | -| 13 | `DummyBillSeeder` | bills | 30 | `php artisan db:seed --class=Dummy\\DummyBillSeeder` | - ---- - -## 4. HR 데이터 시더 (Dummy) - -| # | 시더 | 테이블 | 수량 | 실행 명령어 | -|---|------|--------|------|-------------| -| 14 | `DummyWorkSettingSeeder` | work_settings | 1 | `php artisan db:seed --class=Dummy\\DummyWorkSettingSeeder` | -| 15 | `DummyAttendanceSettingSeeder` | attendance_settings | 1 | `php artisan db:seed --class=Dummy\\DummyAttendanceSettingSeeder` | -| 16 | `DummyAttendanceSeeder` | attendances | ~300 | `php artisan db:seed --class=Dummy\\DummyAttendanceSeeder` | -| 17 | `DummyLeaveGrantSeeder` | leave_grants | ~200 | `php artisan db:seed --class=Dummy\\DummyLeaveGrantSeeder` | -| 18 | `DummyLeaveSeeder` | leaves | ~50 | `php artisan db:seed --class=Dummy\\DummyLeaveSeeder` | -| 19 | `DummyCardSeeder` | cards | 5 | `php artisan db:seed --class=Dummy\\DummyCardSeeder` | -| 20 | `DummySalarySeeder` | salaries | 15 | `php artisan db:seed --class=Dummy\\DummySalarySeeder` | - ---- - -## 5. 기타 더미 시더 (Dummy) - -| # | 시더 | 테이블 | 수량 | 실행 명령어 | -|---|------|--------|------|-------------| -| 21 | `DummyItemSeeder` | items | 10,000 | `php artisan db:seed --class=Dummy\\DummyItemSeeder` | -| 22 | `DummyPopupSeeder` | popups | 8 | `php artisan db:seed --class=Dummy\\DummyPopupSeeder` | -| 23 | `DummyPaymentSeeder` | payments | 13 | `php artisan db:seed --class=Dummy\\DummyPaymentSeeder` | -| 24 | `ApprovalTestDataSeeder` | approvals | ~60 | `php artisan db:seed --class=ApprovalTestDataSeeder` | - ---- - -## 6. 시스템/설정 시더 - -| # | 시더 | 설명 | 실행 명령어 | -|---|------|------|-------------| -| 25 | `GlobalMenuTemplateSeeder` | 글로벌 메뉴 템플릿 | `php artisan db:seed --class=GlobalMenuTemplateSeeder` | -| 26 | `ReactMenuSeeder` | React 메뉴 | `php artisan db:seed --class=ReactMenuSeeder` | -| 27 | `CategorySeeder` | 카테고리 | `php artisan db:seed --class=CategorySeeder` | -| 28 | `ItemTypeSeeder` | 품목 유형 | `php artisan db:seed --class=ItemTypeSeeder` | -| 29 | `ItemMasterSeeder` | 품목 마스터 | `php artisan db:seed --class=ItemMasterSeeder` | -| 30 | `PositionSeeder` | 직급 | `php artisan db:seed --class=PositionSeeder` | -| 31 | `FolderSeeder` | 폴더 | `php artisan db:seed --class=FolderSeeder` | -| 32 | `CapabilityProfileSeeder` | 역량 프로필 | `php artisan db:seed --class=CapabilityProfileSeeder` | -| 33 | `StockReceivingSeeder` | 입고 | `php artisan db:seed --class=StockReceivingSeeder` | -| 34 | `ComprehensiveAnalysisSeeder` | 종합분석 | `php artisan db:seed --class=ComprehensiveAnalysisSeeder` | -| 35 | `SystemFieldDefinitionSeeder` | 시스템 필드 정의 | `php artisan db:seed --class=SystemFieldDefinitionSeeder` | -| 36 | `DemoSystemSeeder` | 데모 시스템 | `php artisan db:seed --class=DemoSystemSeeder` | -| 37 | `BpMesCategoryFieldsSeeder` | MES 카테고리 필드 | `php artisan db:seed --class=BpMesCategoryFieldsSeeder` | -| 38 | `BpMesTenantStatFieldsSeeder` | MES 테넌트 통계 필드 | `php artisan db:seed --class=BpMesTenantStatFieldsSeeder` | - ---- - -## 7. 견적 관련 시더 - -| # | 시더 | 설명 | 실행 명령어 | -|---|------|------|-------------| -| 39 | `QuoteFormulaSeeder` | 견적 계산식 | `php artisan db:seed --class=QuoteFormulaSeeder` | -| 40 | `QuoteFormulaCategorySeeder` | 견적 계산 카테고리 | `php artisan db:seed --class=QuoteFormulaCategorySeeder` | -| 41 | `QuoteFormulaItemSeeder` | 견적 계산 품목 | `php artisan db:seed --class=QuoteFormulaItemSeeder` | -| 42 | `QuoteFormulaMappingSeeder` | 견적 계산 매핑 | `php artisan db:seed --class=QuoteFormulaMappingSeeder` | - ---- - -## 요약 - -| 카테고리 | 개수 | -|----------|------| -| 메인 시더 | 2 | -| 기본 데이터 (Dummy) | 5 | -| 회계 데이터 (Dummy) | 6 | -| HR 데이터 (Dummy) | 7 | -| 기타 더미 (Dummy) | 4 | -| 시스템/설정 | 14 | -| 견적 관련 | 4 | -| **총계** | **42** | - ---- - -## 주의사항 - -1. **Dummy 시더**는 `TENANT_ID = 287` 하드코딩 -2. **의존성 순서**: 기본 데이터 → 회계 → HR → 기타 순서로 실행 권장 -3. **중복 주의**: 이미 데이터가 있는 경우 중복 생성됨 (특히 `DummyItemSeeder` 10,000개) \ No newline at end of file diff --git a/plans/archive/api-analysis-report.md b/plans/archive/api-analysis-report.md deleted file mode 100644 index ae48343..0000000 --- a/plans/archive/api-analysis-report.md +++ /dev/null @@ -1,434 +0,0 @@ -# SAM API 전체 분석 보고서 - -> **작성일**: 2026-01-29 -> **목적**: api/, mng/, react/ 프로젝트 간 API 중복/통합/미사용 분석 및 관계 정리 -> **기준 문서**: api/routes/api/v1/*.php, mng/routes/api.php, mng/routes/web.php, react/src/lib/api/* -> **상태**: ✅ 분석 완료 - ---- - -## 📍 분석 결과 요약 - -| 항목 | 수치 | -|------|------| -| **api/ 엔드포인트** | ~710+ | -| **mng/ 엔드포인트** | ~300+ | -| **React 실제 사용** | ~80+ (api/ 전체의 ~15%) | -| **중복 도메인** | 10개 | -| **즉시 정리 가능** | 3건 | -| **통합 가능 그룹** | 6개 | - ---- - -## 1. 개요 - -### 1.1 배경 - -SAM 프로젝트는 api/(REST API), mng/(관리자 패널), react/(프론트엔드) 3개 프로젝트로 구성되어 있으며, 각 프로젝트가 독립적으로 발전하면서 동일 도메인에 대한 API가 중복 생성되었다. 본 분석은 전체 API를 파악하고 정리 방안을 제시한다. - -### 1.2 분석 범위 - -| 프로젝트 | 역할 | 분석 대상 | -|---------|------|----------| -| **api/** | REST API 서버 | routes/api/v1/*.php (14개 라우트 파일), 컨트롤러 138개 | -| **mng/** | 관리자 패널 | routes/api.php, routes/web.php, 컨트롤러 90+개 | -| **react/** | 프론트엔드 | src/lib/api/*, src/hooks/*, src/app/api/* | - ---- - -## 2. 프로젝트별 API 구조 - -### 2.1 API 프로젝트 (api/) - -**라우트 파일**: `api/routes/api/v1/` - -| 도메인 | 라우트 파일 | 엔드포인트 수 | 소비자 | -|--------|-----------|:----------:|--------| -| 인증 | `auth.php` | 8 | React, MNG | -| 사용자 | `users.php` | 25 | React | -| 테넌트 | `tenants.php` | 18 | React | -| 관리자 | `admin.php` | 22 | React, MNG | -| 공통 | `common.php` | 95+ | React, MNG | -| HR | `hr.php` | 85+ | React | -| 재무 | `finance.php` | 130+ | React | -| 영업 | `sales.php` | 85+ | React | -| 재고 | `inventory.php` | 65+ | React | -| 생산 | `production.php` | 35+ | React | -| 설계 | `design.php` | 55+ | React | -| 파일 | `files.php` | 15 | React | -| 게시판 | `boards.php` | 70+ | React | -| 문서 | `documents.php` | 5+ | React | - -### 2.2 MNG 프로젝트 (mng/) - -**API 소비 방식**: 자체 모델로 DB 직접 접근 (api/ REST API를 거치지 않음) - -| 도메인 | 엔드포인트 수 | 비고 | -|--------|:----------:|------| -| 사용자/역할/권한 | 30+ | api/와 중복 | -| 메뉴/글로벌메뉴 | 25+ | api/와 중복 | -| 게시판/필드 | 20+ | api/와 중복 | -| 카테고리/글로벌 | 15+ | api/와 중복 | -| 바로빌 (전체) | 60+ | MNG 전용 (외부 서비스) | -| 프로젝트 관리 | 25+ | MNG 전용 | -| 견적 공식 | 30+ | MNG 전용 | -| 품목 필드 | 25+ | MNG 전용 | -| 문서/템플릿 | 12+ | api/와 중복 | -| 계좌/자금일정 | 18+ | api/와 중복 | -| 기타 (회의록, 신용, 영업, Lab) | 40+ | MNG 전용 | - -### 2.3 React 프론트엔드 (react/) - -**API 호출 방식**: Next.js Proxy (`/api/proxy/*`) → Backend (`/api/v1/*`) - -| 카테고리 | 주요 엔드포인트 | 사용 빈도 | -|---------|---------------|:--------:| -| 인증 | login, logout, refresh, signup | 높음 | -| 품목 CRUD | items, items/{id}, items/bom | 높음 | -| 품목기준관리 | item-master/* (pages, sections, fields) | 높음 | -| 견적 계산 | quotes/calculate, quotes/calculate/bom | 높음 | -| 공통코드 | settings/common/{group} | 높음 | -| 대시보드 | card-transactions/dashboard, loans/dashboard | 중간 | -| 알림 | today-issues/unread, unread/count | 중간 | -| 거래처 | clients, client-groups | 중간 | -| 재고 | stocks, work-results | 중간 | -| 일괄작업 | bulk-update-account-code | 낮음 | -| 내보내기 | attendances/export, salaries/export | 낮음 | - ---- - -## 3. 중복 API 분석 - -### 3.1 중복 컨트롤러 목록 (api/ vs mng/) - -| # | 도메인 | api/ 컨트롤러 | mng/ 컨트롤러 | 중복 수준 | -|---|--------|-------------|-------------|:--------:| -| 1 | 사용자 관리 | `Api\V1\Admin\AdminController` | `Api\Admin\UserController` | 🔴 높음 | -| 2 | 역할 관리 | `Api\V1\RoleController` | `Api\Admin\RoleController` | 🔴 높음 | -| 3 | 메뉴 관리 | `Api\V1\MenuController` | `Api\Admin\MenuController` | 🔴 높음 | -| 4 | 카테고리 | `Api\V1\CategoryController` | `Api\Admin\CategoryApiController` | 🔴 높음 | -| 5 | 계좌 관리 | `Api\V1\BankAccountController` | `Api\Admin\BankAccountController` | 🔴 높음 | -| 6 | 권한 관리 | `Api\V1\PermissionController` | `Api\Admin\PermissionController` | 🟡 중간 | -| 7 | 부서 관리 | `Api\V1\DepartmentController` | `Api\Admin\DepartmentController` | 🟡 중간 | -| 8 | 게시판 | `Api\V1\BoardController` | `Api\Admin\BoardController` | 🟡 중간 | -| 9 | 문서 | `Api\V1\Documents\DocumentController` | `Api\Admin\DocumentApiController` | 🟡 중간 | -| 10 | 테넌트 | `Api\V1\TenantController` | `Api\Admin\TenantController` | 🟡 중간 | - -### 3.2 상세 비교 - -#### (1) 사용자 관리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 복구 (restore) | ✅ | ✅ | 동일 | -| 비밀번호 초기화 | ✅ | ✅ | 동일 | -| 활성화/비활성화 | ✅ (PATCH /status) | ❌ | api/만 | -| 역할 부여/해제 | ✅ (POST/DELETE /roles) | ❌ | api/만 | -| 영구삭제 | ❌ | ✅ (DELETE /force) | mng/만 (슈퍼관리자) | -| 개발용 로그인토큰 | ❌ | ✅ (POST /login-token) | mng/만 | -| 모달 데이터 | ❌ | ✅ (GET /modal) | mng/만 | - -#### (2) 역할 관리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 통계 (stats) | ✅ | ❌ | api/만 | -| 활성 목록 (active) | ✅ | ❌ | api/만 | - -#### (3) 메뉴 관리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 순서변경 (reorder) | ✅ | ✅ | 동일 | -| 복구 (restore) | ✅ | ✅ | 동일 | -| 활성화 토글 | ✅ (toggle) | ✅ (toggle-active) | 동일 기능 | -| 동기화 | ✅ (sync, sync-new, sync-updates) | ❌ | api/만 | -| 트리 구조 | ❌ | ✅ (tree) | mng/만 | -| 글로벌 복사 | ❌ | ✅ (copy-from-global) | mng/만 | -| 일괄 작업 | ❌ | ✅ (bulk-delete/restore/force) | mng/만 | -| 숨김 토글 | ❌ | ✅ (toggle-hidden) | mng/만 | -| 영구삭제 | ❌ | ✅ (force) | mng/만 (슈퍼관리자) | - -#### (4) 카테고리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 트리/순서변경/이동 | ✅ | ✅ | 동일 | -| 활성화 토글 | ✅ | ✅ | 동일 | -| 필드 관리 | ✅ (fields CRUD, bulk-upsert) | ❌ | api/만 | -| 템플릿 관리 | ✅ (templates, apply, preview, diff) | ❌ | api/만 | -| 로그 조회 | ✅ (logs) | ❌ | api/만 | -| 글로벌 관리 | ❌ | ✅ (global-categories) | mng/만 | - -#### (5) 계좌 관리 🔴 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 기본 CRUD | ✅ | ✅ | 동일 | -| 활성화 토글 | ✅ | ✅ | 동일 | -| 활성 목록 (active) | ✅ | ❌ | api/만 | -| 대표계좌 설정 | ✅ (set-primary) | ❌ | api/만 | -| 전체 조회 (all) | ❌ | ✅ | mng/만 | -| 요약 (summary) | ❌ | ✅ | mng/만 | -| 거래내역 | ❌ | ✅ (transactions) | mng/만 | -| 일괄 작업 | ❌ | ✅ (bulk-*) | mng/만 | -| 영구삭제/복구 | ❌ | ✅ (force/restore) | mng/만 | - -#### (6) 권한 관리 🟡 - -| 기능 | api/ | mng/ | 차이 | -|------|:----:|:----:|------| -| 권한 매트릭스 조회 | ✅ (dept/role/user menu-matrix) | ❌ | api/만 (특화) | -| 기본 CRUD | ❌ | ✅ | mng/만 | - -> **분석**: api/는 매트릭스 조회 전용, mng/는 CRUD 전용으로 기능 분리된 상태. 완전 중복은 아님. - ---- - -## 4. 통합 가능 API - -### 4.1 통합 대상 그룹 - -| # | 대상 | 현재 상태 | 통합 방안 | 우선순위 | -|---|------|----------|----------|:--------:| -| 1 | **인증 API** | signup + register 중복 | register 제거 (signup 유지) | 🔴 | -| 2 | **사용자 관리** | api/ + mng/ 각각 CRUD | mng/ → api/ 호출로 전환 | 🔴 | -| 3 | **역할 관리** | api/ + mng/ 각각 CRUD | api/에 통합, mng/는 호출만 | 🟡 | -| 4 | **메뉴 관리** | api/ 동기화 + mng/ 관리 분리 | 관리: mng/, 조회+동기화: api/ | 🟡 | -| 5 | **대시보드 데이터** | 개별 엔드포인트 분산 | 통합 대시보드 API 제공 | 🟢 | -| 6 | **일괄 업데이트** | withdrawals/deposits/sales 각각 | 공통 bulk-update 패턴 | 🟢 | - -### 4.2 인증 API 중복 상세 - -``` -현재: - POST /v1/login → 로그인 - POST /v1/logout → 로그아웃 - POST /v1/signup → 회원가입 (1) - POST /v1/register → 회원가입 (2) ← 중복! - POST /v1/token-login → 토큰 로그인 (MNG→DEV) - POST /v1/refresh → 토큰 갱신 - POST /v1/internal/exchange-token → 내부 서버 토큰 교환 - GET /v1/debug-apikey → 디버그용 ← 프로덕션 제거 필요 - -권장: - - register 제거 (signup 유지) - - debug-apikey 프로덕션 비활성화 -``` - ---- - -## 5. 미사용 API - -### 5.1 React에서 호출하지 않는 api/ 도메인 - -| 도메인 | 엔드포인트 수 | 미사용 이유 | -|--------|:----------:|-----------| -| HR 전체 (employees, attendance, leave, approval) | ~80+ | MNG에서 직접 관리 또는 React 미구현 | -| 생산 대부분 (processes, work-orders, inspections) | ~35+ | work-results만 사용 | -| 설계 전체 (models, versions, bom-templates) | ~55+ | 견적 계산 시 간접 사용만 | -| 재무 대부분 (cards, payroll, bad-debts 등) | ~100+ | CEO 대시보드 일부만 사용 | -| 사용자 초대 (invitations) | ~5 | React 미구현 | -| 알림 설정 (notification-settings) | ~5 | React 미구현 | -| 프로필 관리 (profiles) | ~5 | React 미구현 | -| 팝업 관리 (popups) | ~5 | React 미구현 | -| AI 리포트 (reports/ai) | ~4 | React 미구현 | -| 구독/결제 (subscriptions, payments) | ~20+ | React 미구현 | -| 현장/시공 (sites, construction) | ~30+ | React 미구현 | -| 검사 관리 (inspections) | ~8 | React 미구현 | - -> **참고**: "미사용"은 React 프론트엔드 기준. MNG에서 Blade UI로 직접 사용하거나 향후 구현 예정인 경우 포함. - -### 5.2 완전 미사용 가능성 높은 API - -| 엔드포인트 | 이유 | 조치 권장 | -|-----------|------|----------| -| `GET /v1/debug-apikey` | 디버그 전용 | 프로덕션 비활성화 | -| `POST /v1/register` | signup과 중복 | 제거 | -| `GET /v1/welfare/*` | React/MNG 모두 미호출 확인 필요 | 사용 여부 확인 | -| `GET /v1/entertainment/*` | React/MNG 모두 미호출 확인 필요 | 사용 여부 확인 | -| `GET /v1/calendar/schedules` | React 미호출 | 사용 여부 확인 | -| `GET /v1/comprehensive-analysis` | React 미호출 | 사용 여부 확인 | - -### 5.3 MNG 전용 기능 (정상) - -| 기능 | 설명 | 상태 | -|------|------|:----:| -| 바로빌 (Barobill) | 전자세금계산서, 카드, 홈택스 연동 | ✅ 정상 | -| 프로젝트 관리 | 프로젝트, 태스크, 이슈 | ✅ 정상 | -| 데일리 로그 | 일일 스크럼 | ✅ 정상 | -| 견적 공식 | 견적 계산 공식 관리 | ✅ 정상 | -| 회의록 | 녹음, AI 요약 (Google Cloud) | ✅ 정상 | -| 신용 평가 | Coocon API 연동 | ✅ 정상 | -| 영업 관리 | 매니저, 전망, 기록 | ✅ 정상 | -| DevTools | API 탐색기, 흐름 테스터 | ✅ 정상 | -| Lab/R&D | AI, 전략 실험 | ✅ 정상 | - ---- - -## 6. 프로젝트 간 API 관계도 - -### 6.1 시스템 구조 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ 사용자 (브라우저) │ -│ │ -│ ┌──────────────┐ ┌──────────────────┐ │ -│ │ React App │ │ MNG Admin │ │ -│ │ (dev.sam.kr) │ │ (mng.sam.kr) │ │ -│ └──────┬───────┘ └──────┬───────────┘ │ -│ │ │ │ -│ Next.js Proxy 자체 모델 직접 사용 │ -│ (/api/proxy/*) + 일부 api/ 호출 │ -│ │ │ │ -│ ▼ │ │ -│ ┌──────────────┐ │ │ -│ │ API 서버 │◄─────────────────┘ │ -│ │ (api.sam.kr) │ token-login, │ -│ │ │ DevTools API 탐색 │ -│ └──────┬───────┘ │ -│ │ │ -│ ▼ │ -│ ┌──────────────┐ │ -│ │ Database │◄──── MNG도 동일 DB 직접 접근 │ -│ │ (MySQL) │ │ -│ └──────────────┘ │ -└─────────────────────────────────────────────────────────────┘ - -외부 API: -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ -│ Google │ │ Coocon │ │ FCM │ │ NTS │ -│ Cloud │ │ (신용) │ │ (푸시) │ │ (홈택스) │ -└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ - └────────────┴────────────┴─────────────┘ - │ - MNG에서 호출 -``` - -### 6.2 데이터 흐름 - -| 흐름 | 방식 | 설명 | -|------|------|------| -| React → API | HTTP (Proxy) | 모든 비즈니스 로직 API 호출 | -| MNG → DB | 직접 모델 | 관리 기능은 DB 직접 접근 | -| MNG → API | HTTP | token-login, DevTools, 일부 동기화 | -| MNG → 외부 | HTTP | Barobill, Google Cloud, Coocon, NTS | -| API → DB | 직접 모델 | 모든 비즈니스 로직 | - -### 6.3 중복 발생 원인 - -``` -문제: MNG가 api/를 호출하지 않고 DB 직접 접근 - → 동일 도메인에 대해 api/, mng/ 각각 독립 컨트롤러 보유 - → 비즈니스 로직 분산, 유지보수 부담 증가 - -현재: - React → api/ (REST API) → DB - MNG → DB 직접 ← 여기가 문제 - -이상적: - React → api/ (REST API) → DB - MNG → api/ (REST API) → DB (관리자 전용 엔드포인트 추가) -``` - ---- - -## 7. 개선 권장사항 - -### 7.1 즉시 정리 (Quick Wins) 🔴 - -| # | 작업 | 영향 | 노력 | -|---|------|------|:----:| -| 1 | `POST /v1/register` 제거 (signup 유지) | 코드 정리 | 소 | -| 2 | `GET /v1/debug-apikey` 프로덕션 비활성화 | 보안 강화 | 소 | -| 3 | 미사용 Swagger 문서 정리 | 문서 정확성 | 소 | - -### 7.2 중복 해소 (Medium Term) 🟡 - -| # | 작업 | 현재 | 목표 | -|---|------|------|------| -| 1 | 사용자 관리 통합 | api/ + mng/ 각각 | api/ 마스터, mng/ 관리자 기능만 추가 | -| 2 | 역할 관리 통합 | api/ + mng/ 각각 | api/ 단일 소스 | -| 3 | 카테고리 통합 | api/ + mng/ 각각 | api/ 마스터, mng/ 글로벌 관리만 유지 | -| 4 | 계좌 관리 통합 | api/ + mng/ 각각 | 하나로 통합 | -| 5 | 메뉴 관리 정리 | api/ 동기화 + mng/ 관리 | 역할 분리 명확화 | - -### 7.3 아키텍처 개선 (Long Term) 🟢 - -| # | 작업 | 설명 | -|---|------|------| -| 1 | MNG → API 호출 전환 | MNG가 DB 직접 접근 대신 api/ REST API 호출 | -| 2 | API Gateway 도입 | 인증/권한/레이트리밋 중앙 관리 | -| 3 | 미사용 API 비활성화 | deprecation 헤더 추가 후 단계적 제거 | -| 4 | API v2 전환 | 중복 정리 포함한 v2 설계 | - ---- - -## 8. 전체 엔드포인트 도메인별 수 - -### API 프로젝트 - -| 도메인 | 파일 | 수 | -|--------|------|:--:| -| 인증 | auth.php | 8 | -| 사용자 | users.php | 25 | -| 테넌트 | tenants.php | 18 | -| 관리자 | admin.php | 22 | -| 공통 | common.php | 95+ | -| HR | hr.php | 85+ | -| 재무 | finance.php | 130+ | -| 영업 | sales.php | 85+ | -| 재고 | inventory.php | 65+ | -| 생산 | production.php | 35+ | -| 설계 | design.php | 55+ | -| 파일 | files.php | 15 | -| 게시판 | boards.php | 70+ | -| 문서 | documents.php | 5+ | -| **합계** | | **~710+** | - -### MNG 프로젝트 - -| 그룹 | 수 | -|------|:--:| -| 사용자/역할/권한 | 30+ | -| 메뉴/글로벌메뉴 | 25+ | -| 게시판/필드 | 20+ | -| 카테고리/글로벌 | 15+ | -| 바로빌 | 60+ | -| 프로젝트 관리 | 25+ | -| 견적 공식 | 30+ | -| 품목 필드 | 25+ | -| 문서/템플릿 | 12+ | -| 계좌/자금일정 | 18+ | -| 기타 | 40+ | -| **합계** | **~300+** | - ---- - -## 9. 참고 문서 - -- `docs/standards/api-rules.md` - API 규칙 -- `docs/architecture/system-overview.md` - 시스템 아키텍처 -- `docs/specs/database-schema.md` - DB 스키마 -- `api/routes/api/v1/*.php` - API 라우트 파일 -- `mng/routes/api.php` - MNG API 라우트 -- `react/src/lib/api/` - React API 클라이언트 - ---- - -## 10. 결론 - -1. **api/와 mng/의 10개 도메인에서 컨트롤러 중복** 발생 - 동일 DB를 각각 직접 접근하는 구조적 문제 -2. **React는 api/ 전체의 약 15%만 사용** - 나머지는 MNG 전용이거나 미구현 기능 -3. **인증 API에 signup/register 중복** 존재 - 즉시 정리 가능 -4. **장기적으로 MNG → API 호출 전환**이 이상적이나, 현재 아키텍처도 기능적으로 동작 -5. **Quick Wins(register 제거, debug-apikey 비활성화)부터 시작** 권장 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/bending-lot-pipeline-dev-plan.md b/plans/archive/bending-lot-pipeline-dev-plan.md deleted file mode 100644 index a9d2833..0000000 --- a/plans/archive/bending-lot-pipeline-dev-plan.md +++ /dev/null @@ -1,1097 +0,0 @@ -# 절곡 자재투입 LOT 매핑 파이프라인 개발 계획 - -> **작성일**: 2026-02-22 -> **목적**: 절곡 세부품목(BD-XX-NN)의 동적 BOM 생성 및 LOT 추적 파이프라인 구축 -> **기준 문서**: `docs/plans/bending-material-input-mapping-plan.md` -> **상태**: ✅ 완료 (Serena ID: bending-lot-pipeline-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 5.2 완료 — 전체 파이프라인 완성 | -| **다음 작업** | 없음 (전체 완료) | -| **진행률** | 13/13 (100%) ✅ | -| **마지막 업데이트** | 2026-02-22 | - ---- - -## 1. 개요 - -### 1.1 배경 - -절곡 작업일지에는 4대 카테고리(가이드레일/하단마감재/셔터박스/연기차단재)의 세부품목이 표시되나, 현재 SAM에서 이 세부품목들이 items 테이블의 BOM과 연결되지 않아 **자재투입 시 세부품목별 LOT 매핑이 불가능**하다. - -**방안 B(동적 BOM 생성)** 확정: 작업지시 생성 시 BendingInfoBuilder를 확장하여 `work_order_items.options.dynamic_bom`에 세부품목 정보를 저장하고, `getMaterials()` API가 이를 우선 참조하도록 수정한다. - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 견적 로직(QuoteCalculationService) 수정 없음 │ -│ 2. DB 스키마 변경 없음 — 기존 options JSON 컬럼 활용 │ -│ 3. 하위 호환성 — dynamic_bom 없는 기존 데이터도 정상 동작 │ -│ 4. bending_info와 dynamic_bom은 동일 Builder에서 동시 생성 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | JSON 필드 추가, 새 Service 클래스 생성, 유틸 함수, 테스트 | 불필요 | -| ⚠️ 컨펌 필요 | getMaterials() 로직 변경, registerMaterialInput API 통일, 프론트 모달 동작 변경 | **필수** | -| 🔴 금지 | items.bom 컬럼 직접 수정, 견적 로직 변경, work_order_material_inputs 스키마 변경 | 별도 협의 | - -### 1.4 준수 규칙 - -- `docs/standards/api-rules.md` — Service-First, FormRequest, ApiResponse -- `docs/standards/quality-checklist.md` — 품질 체크리스트 -- `docs/rules/item-policy.md` — 품목 정책 (BD-* 명명 규칙) -- `api/CLAUDE.md` — SAM API 개발 규칙 - -### 1.5 성공 기준 - -| 기준 | 측정 방법 | -|------|----------| -| 작업지시 생성 시 dynamic_bom JSON 자동 생성 | work_order_items.options에 dynamic_bom 존재 확인 | -| getMaterials API가 세부품목(BD-RS-43 등) 반환 | API 응답에 세부품목 리스트 포함 확인 | -| 세부품목별 LOT 선택 → 재고 차감 정상 | stock_transactions + work_order_material_inputs 레코드 확인 | -| 자재투입 이력에 work_order_item_id 기록 | WorkOrderMaterialInput 레코드의 work_order_item_id NOT NULL | -| 레거시 5130과 동일한 LOT prefix 체계 | prefix × lengthCode 전체 조합 매칭 검증 | - -### 1.6 현재 구현 컨텍스트 (새 세션 필독) - -> 이 섹션은 새 세션에서 별도 파일을 읽지 않고도 작업을 시작할 수 있도록 핵심 코드 구조를 인라인합니다. - -#### 1.6.1 전체 데이터 흐름 - -``` -[견적/수주] - QuoteCalculationService.calculateBom() - → order_nodes.options.bom_result에 부모 품목 저장 - → 예: BD-가이드레일-KSS01-SUS-120*70, qty=8.5m - ↓ -[작업지시 생성] - WorkOrderService.store() (L266-316) - → salesOrder.items 순회 → work_order_items에 복사 - → nodeOptions에서 bending_info 복사: work_order_items.options.bending_info - → ⭐ [신규] dynamic_bom도 여기서 저장: work_order_items.options.dynamic_bom - ↓ - BendingInfoBuilder.build(Order, processId) (L29-69) - → 절곡 공정 확인 → rootNodes 필터링 → productCode 파싱 - → getMaterialMapping() → aggregateNodes() → assembleBendingInfo() - → ⭐ [신규] buildDynamicBom() → 길이 버킷팅 결과로 BD-XX-NN 세부품목 매핑 - ↓ -[자재투입 조회] - getMaterials(workOrderId) (L1183-1317) - → work_order_items 순회 - → ⭐ [신규] options.dynamic_bom 있으면 세부품목 사용 / 없으면 item.bom fallback - → 세부품목별 Stock → StockLot (FIFO) 조회 - ↓ -[자재투입 등록] - registerMaterialInputForItem(workOrderId, itemId, inputs) (L2821-2907) - → StockService.decreaseFromLot() — 재고 차감 - → WorkOrderMaterialInput::create() — 투입 이력 기록 - ↓ -[생산완료] - updateStatus(workOrderId, 'completed') (L520-602) - → sales_order_id 있으면: createShipmentFromWorkOrder() (출하 직행) - → sales_order_id 없으면: stockInFromProduction() → stock_lots 생성 -``` - -#### 1.6.2 BendingInfoBuilder 핵심 구조 - -**파일**: `api/app/Services/Production/BendingInfoBuilder.php` - -```php -// 진입점 -public function build(Order $order, int $processId, ?array $nodeIds = null): ?array - -// BOM 아이템 카테고리 분류 (L96-130) -private function categorizeBomItem(array $bomItem): ?string -// 반환: 'guideRail', 'shutterBox_case', 'shutterBox_finCover', 'bottomBar', -// 'smokeBarrier_rail', 'smokeBarrier_case', 'detail_lbar', 'detail_reinforce', 'motor' - -// 노드 집계 (L135-175) -private function aggregateNodes(Collection $nodes): array -// 반환: { dimensionGroups: [{height, width, qty}], totalNodeQty, bomCategories: {category => bomItem} } - -// 높이 기준 버킷팅 (L760-763) — 가이드레일용 -private function heightLengthData(array $dimGroups): array -// 반환: [{ length: 2438, quantity: 5 }, { length: 3000, quantity: 3 }] -// 표준 길이: [2438, 3000, 3500, 4000, 4300] - -// 하단마감재 배분 (L801-834) -private function bottomBarDistribution(int $openWidth): array -// 반환: [3000mm수량, 4000mm수량] -// 예: openWidth=7000 → [1, 1] (3000×1 + 4000×1) - -// 셔터박스 배분 (L411-548) -private function shutterBoxDistribution(int $openWidth): array -// 반환: [1219 => qty, 2438 => qty, 3000 => qty, 3500 => qty, 4000 => qty, 4150 => qty] - -// 가이드레일 섹션 (L251-299) -private function buildGuideRail(string $guideType, string $baseSize, array $materials, array $dimGroups, string $productCode): array -// guideType: '벽면형', '측면형', '혼합형' -// 반환: { wall: {baseSize, baseDimension, lengthData}, side: {...} | null } - -// 표준 길이 버킷팅 (L856-865) — ⚠️ 초과 시 원본 반환 -private function bucketToStandardLength(int $dimension, array $buckets): int -``` - -#### 1.6.3 getMaterials() 현재 로직 - -**파일**: `api/app/Services/WorkOrderService.php` L1183-1317 - -``` -Phase 1: 유니크 자재 수집 - for each workOrder.items: - if item.bom 존재: ← 절곡 부모 품목은 bom=null이므로 여기 안 탐 - BOM 자식 순회 → uniqueMaterials[childItemId] += qty - else: ← 현재 절곡은 여기로 빠짐 (부모 품목 자체가 자재로) - uniqueMaterials[itemId] = qty - -Phase 2: StockLot 조회 - for each uniqueMaterial: - stock = Stock.find(itemId) → StockLot.where(available) → FIFO 정렬 - -⚠️ 문제: 절곡 부모 품목(BD-가이드레일-KSS01-SUS-120*70)의 bom이 null - → 세부품목(BD-RS-43 등)이 자재 목록에 나오지 않음 - → dynamic_bom으로 해결 -``` - -#### 1.6.4 registerMaterialInput 두 메서드 차이 - -| 항목 | registerMaterialInput (L1330) | registerMaterialInputForItem (L2821) | -|------|-------------------------------|--------------------------------------| -| 파라미터 | workOrderId, inputs | workOrderId, **itemId**, inputs | -| 재고 차감 | ✅ decreaseFromLot | ✅ decreaseFromLot | -| WorkOrderMaterialInput | ❌ 미생성 | ✅ 생성 (work_order_item_id 포함) | -| 용도 | 전체 작업지시 단위 | 개소(품목) 단위 | - -#### 1.6.5 프론트엔드 현재 구조 - -**MaterialInputModal** (`react/src/components/production/WorkerScreen/MaterialInputModal.tsx`) - -```typescript -// Props — workOrderItemId 유무로 API 경로 분기 -interface MaterialInputModalProps { - order: WorkOrder | null; - workOrderItemId?: number; // 있으면 개소별 API, 없으면 전체 API - workOrderItemName?: string; -} - -// 품목 그룹핑 (L102-119): itemId 기준 Map -// FIFO 배분 (L121-138): selectedLotKeys → 가용량 순서로 자동 배분 -// 등록 (L261-307): -// workOrderItemId ? registerMaterialInputForItem() : registerMaterialInput() -``` - -**API 엔드포인트** (`react/src/components/production/WorkerScreen/actions.ts`) - -| 메서드 | 경로 | 함수명 | -|--------|------|--------| -| GET | `/api/v1/work-orders/{id}/materials` | getMaterialsForWorkOrder | -| GET | `/api/v1/work-orders/{id}/items/{itemId}/materials` | getMaterialsForItem | -| POST | `/api/v1/work-orders/{id}/material-inputs` | registerMaterialInput | -| POST | `/api/v1/work-orders/{id}/items/{itemId}/material-inputs` | registerMaterialInputForItem | -| GET | `/api/v1/work-orders/{id}/items/{itemId}/material-inputs` | getMaterialInputsForItem | -| DELETE | `/api/v1/work-orders/{id}/material-inputs/{inputId}` | deleteMaterialInput | -| PATCH | `/api/v1/work-orders/{id}/material-inputs/{inputId}` | updateMaterialInput | - -**절곡 유틸리티** (`react/.../documents/bending/utils.ts`) - -- `getSLengthCode(length, category)` — 길이→코드 변환 -- `getMaterialMapping(productCode, finishMaterial)` — 재질 매핑 -- `buildWallGuideRailRows()`, `buildSideGuideRailRows()`, `buildBottomBarRows()`, `buildShutterBoxRows()`, `buildSmokeBarrierRows()` — 각 섹션 파트 행 생성 (lotPrefix 포함) - -#### 1.6.6 LOT Prefix 전체 맵 (PrefixResolver 구현 기준) - -**가이드레일 벽면형 (Wall)** - -| 세부품목 | KSS01(SUS) | KSE01/KWE01(EGI마감) | KSE01/KWE01(SUS마감) | KTE01(철재) | -|---------|-----------|-------------------|-------------------|-----------| -| 마감재 | RS | RE | RE | RS | -| 본체 | RM | RM | RM | **RT** | -| C형 | RC | RC | RC | RC | -| D형 | RD | RD | RD | RD | -| 별도마감 | - | - | **YY** | - | -| 하부BASE | XX | XX | XX | XX | - -**가이드레일 측면형 (Side)** - -| 세부품목 | KSS01(SUS) | KSE01/KWE01(EGI마감) | KSE01/KWE01(SUS마감) | KTE01(철재) | -|---------|-----------|-------------------|-------------------|-----------| -| 마감재 | SS | SE | SE | SS | -| 본체 | SM | SM | SM | **ST** | -| C형 | SC | SC | SC | SC | -| D형 | SD | SD | SD | SD | -| 별도마감 | - | - | **YY** | - | -| 하부BASE | XX | XX | XX | XX | - -**하단마감재** - -| 세부품목 | EGI마감 | SUS마감 | 철재 | -|---------|--------|--------|------| -| 메인 | BE | BS | TS | -| L-Bar | LA | LA | LA | -| 보강평철 | HH | HH | HH | -| 별도마감 | - | YY | - | - -**셔터박스** (표준 500*380 사이즈만 개별 prefix) - -| 세부품목 | 표준 prefix | 비표준 prefix | -|---------|-----------|-------------| -| 전면부 | CF | XX | -| 린텔부 | CL | XX | -| 점검구 | CP | XX | -| 후면코너부 | CB | XX | -| 상부덮개 | XX | XX | -| 마구리 | XX | XX | - -**연기차단재**: W50, W80 모두 → GI - -#### 1.6.7 길이코드 매핑 (getSLengthCode) - -| 길이(mm) | 코드 | 비고 | -|---------|------|------| -| 1219 | 12 | 셔터박스 | -| 2438 | 24 | 셔터박스 | -| 3000 | 30 | 공통 | -| 3500 | 35 | 공통 | -| 4000 | 40 | 공통 | -| 4150 | 41 | 셔터박스 | -| 4200 | 42 | - | -| 4300 | 43 | 가이드레일 | -| 3000 | **53** | 연기차단재50 전용 | -| 4000 | **54** | 연기차단재50 전용 | -| 3000 | **83** | 연기차단재80 전용 | -| 4000 | **84** | 연기차단재80 전용 | - -**코드 생성 규칙**: `BD-{prefix}-{lengthCode}` → 예: `BD-RS-43` = 가이드레일 벽면 SUS 마감재 4300mm - -#### 1.6.8 BD-* 마스터 현황 (items 테이블, 총 148개) - -**A. 제품 마스터형 (58개)** — 부모 품목 (견적 BOM에 사용) -``` -BD-가이드레일-KSS01-SUS-120*70 등 (20개: 제품코드별) -BD-하단마감재-KSE01-EGI-60*40 등 (10개) -BD-케이스-500*380 등 (10개), BD-마구리-505*355 등 (10개) -BD-L-BAR-*, BD-보강평철-*, BD-연기차단재 (8개) -``` - -**B. LOT prefix형 (90개 등록, XX/YY/HH 미등록)** — 세부품목 (자재투입 대상) - -| prefix | 수량 | prefix | 수량 | prefix | 수량 | -|--------|:----:|--------|:----:|--------|:----:| -| BD-RS | 5 | BD-SS | 4 | BD-BE | 2 | -| BD-RM | 6 | BD-SM | 5 | BD-BS | 5 | -| BD-RC | 6 | BD-SC | 5 | BD-TS | 1 | -| BD-RD | 6 | BD-SD | 5 | BD-LA | 2 | -| BD-RT | 2 | BD-ST | 1 | BD-CF | 6 | -| | | BD-SU | 4 | BD-CL | 6 | -| | | | | BD-CP | 6 | -| | | | | BD-CB | 6 | -| | | | | BD-GI | 7 | - -**미등록**: BD-XX (하부BASE/셔터 상부/마구리), BD-YY (별도SUS마감), BD-HH (보강평철) → Phase 0.1에서 등록 - -#### 1.6.9 dynamic_bom JSON 목표 구조 - -`work_order_items.options.dynamic_bom` 에 저장: - -```json -[ - { - "child_item_id": 15812, - "child_item_code": "BD-RS-43", - "lot_prefix": "RS", - "part_type": "마감재", - "category": "guideRail", - "material_type": "SUS", - "length_mm": 4300, - "qty": 1 - }, - { - "child_item_id": 15809, - "child_item_code": "BD-RS-40", - "lot_prefix": "RS", - "part_type": "마감재", - "category": "guideRail", - "material_type": "SUS", - "length_mm": 4000, - "qty": 1 - }, - { - "child_item_id": 15826, - "child_item_code": "BD-RM-43", - "lot_prefix": "RM", - "part_type": "본체", - "category": "guideRail", - "material_type": "EGI", - "length_mm": 4300, - "qty": 1 - } -] -``` - -**필드 설명**: -- `child_item_id`: items 테이블 PK (getMaterials에서 Stock/StockLot 조회용) -- `child_item_code`: items.code (표시용) -- `lot_prefix`: LOT prefix (프론트 작업일지 매핑용) -- `part_type`: 세부품명 한글 (마감재, 본체, C형 등) -- `category`: 4대 카테고리 (guideRail, bottomBar, shutterBox, smokeBarrier) -- `material_type`: 재질 (SUS, EGI 등) -- `length_mm`: 표준 길이 (mm) -- `qty`: 수량 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 0: 선행 준비 (마스터 데이터) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 0.1 | XX/YY/HH 미등록 품목 items 등록 | ✅ | 22건 등록 (13+9 추가 누락) | -| 0.2 | 마스터 데이터 검증 스크립트 작성 | ✅ | 101/101 전체 통과 | - -### 2.2 Phase 1: GAP #1 해결 — API 통일 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | registerMaterialInput → registerMaterialInputForItem 통일 | ✅ | work_order_item_id 분기 + fallback + N+1 수정 | -| 1.2 | 프론트 workOrderItemId 전달 보장 | ✅ | actions.ts + MaterialInputModal work_order_item_id 전달 | - -### 2.3 Phase 2: 방안 B 핵심 — dynamic_bom 생성 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | PrefixResolver 클래스 구현 | ✅ | `app/Services/Production/PrefixResolver.php` | -| 2.2 | BendingInfoBuilder 확장 — dynamic_bom 생성 | ✅ | `build()` 리턴 변경 + `buildDynamicBomForItem()` 추가, OrderService 연동 | -| 2.3 | DynamicBomEntry DTO 구현 | ✅ | `app/DTOs/Production/DynamicBomEntry.php` | - -### 2.4 Phase 3: getMaterials 연동 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | getMaterials() dynamic_bom 우선 체크 | ✅ | dynamic_bom → BOM fallback, (item_id, woItem_id) 쌍 합산, 추가 필드 반환 | -| 3.2 | N+1 쿼리 최적화 + uniqueMaterials 합산 단위 변경 | ✅ | 3.1에서 함께 해결: Item/Stock/StockLot 모두 배치 조회 | - -### 2.5 Phase 4: 프론트엔드 연동 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | 자재투입 모달 세부품목 단위 표시 | ✅ | MaterialInputModal groupKey + category badge + actions.ts 필드 추가 | -| 4.2 | 작업일지 LOT NO 표시 연동 | ✅ | 4개 섹션 lotNoMap prop + WorkLogModal lotNoMap 빌드 | - -### 2.6 Phase 5: 테스트 및 검증 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 5.1 | PrefixResolver + dynamic_bom 단위 테스트 | ✅ | 58 tests / 256 assertions 통과 | -| 5.2 | getMaterials → 자재투입 통합 테스트 | ✅ | 6 tests (4 pass + 2 skip — dynamic_bom 작업지시 미생성), 마스터 품목 전체 검증 | - -### 2.7 별도 과제 (이 계획 범위 밖) - -| # | 항목 | 시점 | -|---|------|------| -| X.1 | GAP #4: 수주 연결 생산완료 → stock_lots 입고 통일 | 출하 시스템 설계 시 | -| X.2 | GAP #3: lot_genealogy (투입↔산출 LOT 직접 연결) | 향후 고도화 | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Phase 0: 선행 준비 -├── 0.1 XX/YY/HH 품목 등록 (items 테이블 INSERT) -└── 0.2 검증 스크립트 (Artisan Command) - └── 19종 prefix × 7-12 lengthCode 조합 → items 존재 확인 - -Phase 1: API 통일 (GAP #1) — Phase 0 완료 후 -├── 1.1 registerMaterialInput() 내부에서 registerMaterialInputForItem() 호출하도록 통일 -│ ├── WorkOrderService.php L1330-1388 수정 -│ └── 기존 프론트 호출 호환성 유지 -└── 1.2 프론트 workOrderItemId 전달 - └── WorkerScreen/index.tsx → MaterialInputModal Props - -Phase 2: dynamic_bom 생성 — Phase 0 완료 후 (Phase 1과 병행 가능) -├── 2.1 PrefixResolver 클래스 -│ ├── productCode + finishMaterial + guideType → prefix 결정 -│ ├── prefix + lengthMm → BD-XX-NN 코드 생성 -│ └── BD-XX-NN → items.id 조회 (캐시) -├── 2.2 BendingInfoBuilder 확장 -│ ├── build() 반환값에 dynamic_bom 추가 -│ ├── bending_info와 동시 생성 (정합성 보장) -│ └── work_order_items.options.dynamic_bom에 저장 -└── 2.3 DynamicBomValidator - └── dynamic_bom JSON 구조 검증 (child_item_id 필수 등) - -Phase 3: getMaterials 수정 — Phase 2 완료 후 -├── 3.1 dynamic_bom 우선 체크 -│ ├── WorkOrderService.php getMaterials() L1198 이후 -│ ├── options.dynamic_bom 있으면 → 세부품목 리스트 사용 -│ └── 없으면 → 기존 item.bom fallback (하위 호환) -└── 3.2 N+1 최적화 - ├── Item::whereIn() 배치 조회 - └── uniqueMaterials 합산 단위: (item_id, work_order_item_id) 쌍 - -Phase 4: 프론트엔드 — Phase 3 완료 후 -├── 4.1 자재투입 모달 수정 -│ ├── materialGroups가 세부품목 단위로 표시 (이미 itemId 기준 그룹핑) -│ └── 그룹 헤더에 세부품목명(BD-RS-43) 표시 -└── 4.2 작업일지 LOT NO 표시 - ├── dynamic_bom에서 lotPrefix + lengthCode 조합 - └── 투입 이력(getMaterialInputsForItem)에서 실제 LOT NO 반영 - -Phase 5: 테스트 — Phase 3 완료 후 (Phase 4와 병행 가능) -├── 5.1 단위 테스트 -│ ├── PrefixResolver: 7종 productCode × 3종 finishMaterial × 3종 guideType -│ ├── dynamic_bom 생성: 실제 bom_result 데이터 기반 -│ └── DynamicBomValidator: 필수/선택 필드 검증 -└── 5.2 통합 테스트 - ├── 작업지시 생성 → dynamic_bom 저장 확인 - ├── getMaterials → 세부품목 반환 확인 - └── 자재투입 → stock_transactions + work_order_material_inputs 확인 -``` - -### 3.2 의존성 맵 - -``` -Phase 0 ──→ Phase 1 (독립 진행 가능) - │ - └──→ Phase 2 ──→ Phase 3 ──→ Phase 4 - │ - └──→ Phase 5 -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 0: 선행 준비 - -#### 0.1 XX/YY/HH 미등록 품목 등록 - -**현재 상태**: BD-* 품목 148개 중 XX(하부BASE), YY(별도SUS마감), HH(보강평철) 미등록 - -**목표 상태**: BD-XX-NN, BD-YY-NN, BD-HH-NN 패턴으로 items 테이블에 등록 - -**등록 대상**: - -| prefix | 설명 | 등록할 길이코드 | 예상 수량 | -|--------|------|---------------|----------| -| BD-XX | 하부BASE, 셔터박스 상부덮개/마구리 | 12, 24, 30, 35, 40, 41, 43 | 7개 | -| BD-YY | 별도 SUS 마감 (SUS마감 시만) | 30, 35, 40, 43 | 4개 | -| BD-HH | 보강평철 | 30, 40 | 2개 | - -**수정 파일**: 없음 (DB INSERT — Seeder 또는 Artisan Command) - -**생성 파일**: -- `api/database/seeders/BendingItemSeeder.php` — BD-XX/YY/HH 품목 등록 - -**검증**: `items` 테이블에서 `code LIKE 'BD-XX-%'` 조회로 13개 확인 - ---- - -#### 0.2 마스터 데이터 검증 스크립트 - -**목적**: 19종 prefix × 가능 lengthCode 전체 조합이 items에 존재하는지 확인 - -**생성 파일**: -- `api/app/Console/Commands/ValidateBendingItems.php` - -**로직**: -``` -전체 prefix 목록 정의 (RS, RM, RC, RD, RT, SS, SM, SC, SD, ST, SU, BE, BS, TS, LA, CF, CL, CP, CB, GI, XX, YY, HH) -각 prefix별 유효 lengthCode 정의 -조합별 items.code = "BD-{prefix}-{code}" 존재 확인 -누락 항목 리스트 출력 -``` - -**실행**: `php artisan bending:validate-items` - -**검증**: 출력이 "All items registered" (누락 0건) - ---- - -### 4.2 Phase 1: GAP #1 해결 — API 통일 - -#### 1.1 registerMaterialInput → registerMaterialInputForItem 통일 - -**현재 상태**: -- `registerMaterialInput()` (L1330): 재고 차감만, WorkOrderMaterialInput 레코드 미생성 -- `registerMaterialInputForItem()` (L2821): 재고 차감 + WorkOrderMaterialInput 레코드 생성 - -**목표 상태**: 모든 자재투입이 `work_order_material_inputs`에 기록 - -**수정 파일**: -- `api/app/Services/WorkOrderService.php` - -**수정 내용**: -``` -registerMaterialInput(int $workOrderId, array $inputs) 수정: - ├── $inputs 배열에 work_order_item_id 필드 추가 지원 - │ { stock_lot_id: N, qty: N, work_order_item_id?: N } - ├── work_order_item_id가 있으면 → registerMaterialInputForItem() 위임 - └── work_order_item_id가 없으면 → 기존 동작 + WorkOrderMaterialInput 레코드 생성 추가 - (work_order_item_id = 첫 번째 work_order_item의 id로 fallback) -``` - -**N+1 개선**: `registerMaterialInputForItem()` L2860-2861의 `StockLot::find()` → `$lot->stock->item_id` 호출을 `StockLot::with('stock')->find()` Eager Loading으로 변경 - -**검증**: -- POST `/work-orders/{id}/material-inputs` 호출 후 `work_order_material_inputs` 테이블에 레코드 존재 확인 -- 기존 호출 형식(work_order_item_id 미포함)도 정상 동작 확인 - ---- - -#### 1.2 프론트 workOrderItemId 전달 보장 - -**현재 상태**: `WorkerScreen/index.tsx`에서 `MaterialInputModal`에 `workOrderItemId` Props를 전달하지만, 완료 플로우에서는 미지정 가능 - -**수정 파일**: -- `react/src/components/production/WorkerScreen/index.tsx` - -**수정 내용**: -- 자재투입 모달 호출 시 `workOrderItemId`가 항상 전달되도록 보장 -- 완료 플로우에서도 `selectedItemId` 설정 - -**검증**: MaterialInputModal이 항상 `registerMaterialInputForItem()` 경로로 호출되는지 확인 - ---- - -### 4.3 Phase 2: 방안 B 핵심 — dynamic_bom 생성 - -#### 2.1 PrefixResolver 클래스 구현 - -**목적**: 제품코드 + 마감재질 + 가이드타입 → LOT prefix 결정 로직을 단일 클래스로 집중 - -**생성 파일**: -- `api/app/Services/Production/PrefixResolver.php` - -**클래스 구조**: -```php -class PrefixResolver -{ - // 벽면형 prefix 맵 - private const WALL_PREFIXES = [ - 'finish' => ['KSS' => 'RS', 'KSE' => 'RE', 'KWE' => 'RE'], - 'body' => 'RM', - 'c_type' => 'RC', - 'd_type' => 'RD', - 'extra_finish' => 'YY', // SUS 마감 시만 - 'base' => 'XX', - ]; - - // 측면형 prefix 맵 - private const SIDE_PREFIXES = [ - 'finish' => ['KSS' => 'SS', 'KSE' => 'SE', 'KWE' => 'SE'], - 'body' => 'SM', - 'c_type' => 'SC', - 'd_type' => 'SD', - 'extra_finish' => 'YY', - 'base' => 'XX', - ]; - - // 철재형 override - private const STEEL_OVERRIDES = [ - 'wall_body' => 'RT', - 'side_body' => 'ST', - ]; - - // 하단마감재 prefix 맵 - private const BOTTOM_BAR_PREFIXES = [ - 'EGI' => 'BE', - 'SUS' => 'BS', - 'STEEL_SUS' => 'TS', - ]; - - // 셔터박스 prefix 맵 (표준 사이즈만) - private const SHUTTER_BOX_PREFIXES = [ - 'front' => 'CF', - 'lintel' => 'CL', - 'inspection' => 'CP', - 'rear_corner' => 'CB', - 'top_cover' => 'XX', - 'fin_cover' => 'XX', - ]; - - // 연기차단재 - private const SMOKE_PREFIXES = [ - 'w50' => 'GI', - 'w80' => 'GI', - ]; - - /** - * 가이드레일 세부품목의 prefix 결정 - */ - public function resolveGuideRailPrefix( - string $partType, // 'finish', 'body', 'c_type', 'd_type', 'extra_finish', 'base' - string $guideType, // 'wall', 'side' - string $productCode, // 'KSS01', 'KSE01', ... - ): string - - /** - * 하단마감재 세부품목의 prefix 결정 - */ - public function resolveBottomBarPrefix( - string $partType, // 'main', 'lbar', 'reinforce', 'extra' - string $finishMaterial, // 'EGI 1.55T', 'SUS 1.2T' - string $productCode, - ): string - - /** - * 셔터박스 세부품목의 prefix 결정 - */ - public function resolveShutterBoxPrefix( - string $partType, // 'front', 'lintel', 'inspection', 'rear_corner', 'top_cover', 'fin_cover' - bool $isStandardSize, // 500*380인지 - ): string - - /** - * 연기차단재 세부품목의 prefix 결정 - */ - public function resolveSmokeBarrierPrefix(string $partType): string - - /** - * prefix + 길이(mm) → BD-XX-NN 코드 생성 - */ - public function buildItemCode(string $prefix, int $lengthMm, ?string $smokeCategory = null): string - - /** - * BD-XX-NN 코드 → items.id 조회 (캐시 사용) - */ - public function resolveItemId(string $itemCode): ?int - - /** - * 길이(mm) → 길이코드 변환 (getSLengthCode 동일) - */ - public static function lengthToCode(int $lengthMm, ?string $smokeCategory = null): ?string -} -``` - -**의존성**: `App\Models\Items\Item` (코드→ID 조회용) - -**검증**: 단위 테스트에서 productCode × guideType × partType 전 조합 테스트 - ---- - -#### 2.2 BendingInfoBuilder 확장 — dynamic_bom 생성 - -**수정 파일**: -- `api/app/Services/Production/BendingInfoBuilder.php` - -**수정 범위**: - -1. **build() 메서드 (L29-69)**: 반환값에 `dynamic_bom` 배열 추가 - ``` - 현재: return assembleBendingInfo(...) // bending_info만 - 변경: return [ - 'bending_info' => assembleBendingInfo(...), - 'dynamic_bom' => buildDynamicBom(...) // 신규 - ] - ``` - -2. **buildDynamicBom() 신규 메서드**: bending_info 생성과 동일한 길이 버킷팅 결과를 사용 - ``` - private function buildDynamicBom( - array $aggregated, // aggregateNodes() 결과 - string $productCode, - array $materials, // getMaterialMapping() 결과 - PrefixResolver $resolver, - ): array - ``` - - **로직**: - ``` - dynamic_bom = [] - - // 1. 가이드레일 세부품목 - for each guideType (wall, side): - lengthData = heightLengthData(dimGroups) // 기존 버킷팅 재사용 - for each (length, qty) in lengthData: - for each partType in [finish, body, c_type, d_type, extra_finish, base]: - prefix = resolver.resolveGuideRailPrefix(partType, guideType, productCode) - if prefix is empty: skip - itemCode = resolver.buildItemCode(prefix, length) - itemId = resolver.resolveItemId(itemCode) - dynamic_bom[] = { - child_item_id: itemId, - child_item_code: itemCode, - lot_prefix: prefix, - part_type: partType의 한글명, - category: 'guideRail', - material_type: materials[partType], - length_mm: length, - qty: qty - } - - // 2. 하단마감재 세부품목 - for each dimGroup: - [qty3000, qty4000] = bottomBarDistribution(openWidth) - for each (length, qty) in [(3000, qty3000), (4000, qty4000)]: - if qty == 0: skip - for each partType in [main, lbar, reinforce, extra]: - prefix = resolver.resolveBottomBarPrefix(partType, finishMaterial, productCode) - ... dynamic_bom 추가 ... - - // 3. 셔터박스 세부품목 - for each dimGroup: - distribution = shutterBoxDistribution(openWidth) - for each (length, qty) in distribution: - if qty == 0: skip - isStandard = (boxSize == '500*380') - for each partType in [front, lintel, inspection, rear_corner, top_cover, fin_cover]: - prefix = resolver.resolveShutterBoxPrefix(partType, isStandard) - ... dynamic_bom 추가 ... - - // 4. 연기차단재 세부품목 - for each smokeType (w50, w80): - for each (length, qty) in smokeLengthData: - prefix = resolver.resolveSmokeBarrierPrefix(smokeType) - smokeCategory = smokeType == 'w50' ? '연기차단재50' : '연기차단재80' - itemCode = resolver.buildItemCode(prefix, length, smokeCategory) - ... dynamic_bom 추가 ... - - return dynamic_bom - ``` - -3. **work_order_items.options 저장 위치 수정**: - - `WorkOrderService.php` L275-306 (작업지시 품목 복사 로직)에서 build() 반환값의 `dynamic_bom`을 `options.dynamic_bom`에 저장 - -**주의사항**: -- `aggregateNodes()` L164의 `!isset` 체크: 첫 노드에서만 BOM 메타 추출 → 노드별 BOM이 다를 수 있으므로 주의 -- `bucketToStandardLength()` L862-864: 표준 길이 초과 시 원본 반환 → PrefixResolver.resolveItemId()에서 null 반환 시 경고 로그 + fallback -- 혼합형 가이드레일: wall + side 각각 독립 dynamic_bom 생성 - -**검증**: -- 작업지시 생성 API 호출 후 `work_order_items.options` JSON에 `dynamic_bom` 배열 존재 확인 -- dynamic_bom의 각 항목에 `child_item_id`가 NOT NULL인지 확인 -- bending_info의 lengthData와 dynamic_bom의 length_mm/qty가 일치하는지 확인 - ---- - -#### 2.3 DynamicBomValidator DTO 구현 - -**생성 파일**: -- `api/app/DTOs/Production/DynamicBomEntry.php` - -**구조**: -```php -class DynamicBomEntry -{ - public function __construct( - public readonly int $child_item_id, - public readonly string $child_item_code, - public readonly string $lot_prefix, - public readonly string $part_type, - public readonly string $category, // guideRail, bottomBar, shutterBox, smokeBarrier - public readonly string $material_type, - public readonly int $length_mm, - public readonly int|float $qty, - ) {} - - public static function fromArray(array $data): self - public function toArray(): array - public static function validate(array $data): bool // child_item_id 필수 등 -} -``` - -**검증**: 단위 테스트에서 필수 필드 누락 시 예외 발생 확인 - ---- - -### 4.4 Phase 3: getMaterials 연동 - -#### 3.1 getMaterials() dynamic_bom 우선 체크 - -**수정 파일**: -- `api/app/Services/WorkOrderService.php` - -**수정 위치**: `getMaterials()` L1198 이후 - -**수정 내용**: -``` -현재 (L1198-1238): - foreach (workOrderItems as woItem): - item = woItem.item - if (item.bom): - ... BOM 순회 ... - else: - ... item 자체를 자재로 ... - -변경: - // Phase 1: dynamic_bom 대상 item_id 일괄 수집 - allDynamicItemIds = [] - foreach (workOrderItems as woItem): - dynamicBom = woItem.options['dynamic_bom'] ?? null - if (dynamicBom): - allDynamicItemIds += array_column(dynamicBom, 'child_item_id') - - // Phase 2: 배치 조회 (N+1 방지) - dynamicItems = Item::whereIn('id', array_unique(allDynamicItemIds)) - ->get()->keyBy('id') - - // Phase 3: 유니크 자재 수집 - foreach (workOrderItems as woItem): - dynamicBom = woItem.options['dynamic_bom'] ?? null - if (dynamicBom): - foreach (dynamicBom as bomEntry): - childItem = dynamicItems[bomEntry['child_item_id']] - // 합산 키: (item_id, work_order_item_id) 쌍 - key = bomEntry['child_item_id'] . '_' . woItem.id - uniqueMaterials[key] = { - item_id: bomEntry['child_item_id'], - work_order_item_id: woItem.id, - bom_qty: bomEntry['qty'], - item: childItem, - ... - } - elseif (item.bom): - ... 기존 BOM 로직 (하위 호환) ... - else: - ... 기존 fallback ... -``` - -**반환 형식 변경**: -``` -기존: { stock_lot_id, item_id, lot_no, bom_qty, required_qty, ... } -추가: { ..., work_order_item_id, lot_prefix, part_type, category } -``` - -**검증**: -- dynamic_bom 있는 work_order → 세부품목(BD-RS-43 등) 반환 확인 -- dynamic_bom 없는 work_order → 기존 동작 그대로 (하위 호환) -- 동일 item_id가 다른 work_order_item에 속한 경우 별도 행으로 반환 - ---- - -#### 3.2 N+1 쿼리 최적화 + uniqueMaterials 합산 단위 변경 - -**수정 파일**: `api/app/Services/WorkOrderService.php` - -**수정 내용**: -1. `Item::find()` 개별 호출 → `Item::whereIn()` 배치 조회 -2. `uniqueMaterials` 합산 키를 `item_id` → `(item_id, work_order_item_id)` 쌍으로 변경 -3. StockLot 조회도 `Stock::whereIn()` 배치 처리 - -**기대 효과**: 쿼리 수 30-50회 → 3-5회로 감소 - -**검증**: Laravel Debugbar 또는 DB 쿼리 로그로 쿼리 수 확인 - ---- - -### 4.5 Phase 4: 프론트엔드 연동 - -#### 4.1 자재투입 모달 세부품목 단위 표시 - -**수정 파일**: -- `react/src/components/production/WorkerScreen/MaterialInputModal.tsx` - -**현재 상태**: `materialGroups`가 `itemId` 기준 그룹핑 (L102-119). getMaterials 응답이 세부품목을 반환하면 자동으로 세부품목 단위 그룹핑됨. - -**수정 내용**: -- 그룹 헤더에 세부품목명(BD-RS-43 등) + part_type(마감재 등) + category(가이드레일 등) 표시 -- 기존 `materialCode`/`materialName` 필드로 충분하나, 카테고리별 시각적 구분 추가 - -**수정 규모**: 소규모 — 그룹 헤더 렌더링 수정 - -**검증**: 자재투입 모달에서 세부품목별 그룹이 표시되고, 각 그룹 내 LOT 선택이 정상 동작 - ---- - -#### 4.2 작업일지 LOT NO 표시 연동 - -**수정 파일**: -- `react/src/components/production/WorkOrders/documents/bending/GuideRailSection.tsx` -- 해당 폴더의 다른 Section 컴포넌트 (BottomBarSection, ShutterBoxSection 등) - -**현재 상태**: LOT NO 컬럼이 `"-"`로 하드코딩 - -**수정 내용**: -- `getMaterialInputsForItem()` API로 투입 이력 조회 -- lotPrefix + lengthCode 매칭으로 실제 LOT NO 표시 -- 투입 전이면 "-", 투입 후이면 실제 LOT 번호 - -**수정 규모**: 중규모 — 각 Section 컴포넌트에 LOT 조회 로직 추가 - -**검증**: 자재투입 완료 후 작업일지에 실제 LOT NO 표시 - ---- - -### 4.6 Phase 5: 테스트 및 검증 - -#### 5.1 단위 테스트 - -**생성 파일**: -- `api/tests/Unit/Services/Production/PrefixResolverTest.php` -- `api/tests/Unit/Services/Production/BendingInfoBuilderDynamicBomTest.php` - -**테스트 케이스**: - -| 테스트 | 입력 | 기대 결과 | -|--------|------|----------| -| KSS01 벽면형 마감재 4300mm | ('finish', 'wall', 'KSS01') | prefix='RS', code='BD-RS-43' | -| KSE01 측면형 본체 3000mm | ('body', 'side', 'KSE01') | prefix='SM', code='BD-SM-30' | -| KTE01 벽면형 본체 (철재) | ('body', 'wall', 'KTE01') | prefix='RT' | -| 하단마감재 EGI | ('main', 'EGI 1.55T', 'KSE01') | prefix='BE' | -| 셔터박스 비표준 사이즈 | ('front', false) | prefix='XX' | -| 연기차단재 W50 3000mm | resolveSmokeBarrierPrefix('w50') | prefix='GI', code='BD-GI-53' | -| 표준 길이 초과 (4500mm) | buildItemCode('RS', 4500) | 경고 로그 + null 반환 | - ---- - -#### 5.2 통합 테스트 - -**생성 파일**: -- `api/tests/Feature/Production/BendingMaterialInputFlowTest.php` - -**테스트 시나리오**: - -``` -1. 작업지시 생성 → dynamic_bom 저장 확인 - - Order (KSS01, SUS마감, 오픈높이=4300, 오픈폭=3000) - - 작업지시 생성 → work_order_items.options.dynamic_bom 확인 - - dynamic_bom에 RS-43, RM-43, RC-43, RD-43 세부품목 존재 - -2. getMaterials → 세부품목 반환 확인 - - getMaterials(workOrderId) 호출 - - 응답에 BD-RS-43, BD-RM-43 등 세부품목 반환 - - 각 세부품목의 StockLot 정보 포함 - -3. 자재투입 → 이력 기록 확인 - - registerMaterialInputForItem() 호출 - - stock_transactions에 OUT 기록 - - work_order_material_inputs에 레코드 생성 - - stock_lots.available_qty 감소 -``` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | registerMaterialInput API 통일 | 기존 API에 WorkOrderMaterialInput 레코드 생성 추가 | 프론트 호출 호환 유지 | ⏳ | -| 2 | BendingInfoBuilder.build() 반환값 변경 | 기존 array → { bending_info, dynamic_bom } | WorkOrderService 호출처 수정 필요 | ⏳ | -| 3 | getMaterials() 로직 변경 | dynamic_bom 우선 체크 + 합산 단위 변경 | MaterialInputModal 응답 형식 변경 | ⏳ | - ---- - -## 6. 변경 이력 - -| 날짜 | 변경 내용 | -|------|----------| -| 2026-02-22 | 문서 초안 작성 | -| 2026-02-22 | Phase 0 완료: BD-* 22건 등록 + 검증 101/101 통과 | -| 2026-02-22 | Phase 2 완료: PrefixResolver, BendingInfoBuilder 확장(build→context+bending_info, buildDynamicBomForItem), DynamicBomEntry DTO, OrderService 연동 | -| 2026-02-22 | Phase 1.1 + 3.1/3.2 완료: registerMaterialInput 통일 (work_order_item_id 분기+fallback+WorkOrderMaterialInput 레코드 생성), getMaterials dynamic_bom 우선체크 + N+1 배치최적화 | - ---- - -## 7. 참고 문서 - -| 문서 | 경로 | -|------|------| -| **분석 기준 문서** | `docs/plans/bending-material-input-mapping-plan.md` | -| 선생산 재고 계획 | `docs/plans/bending-preproduction-stock-plan.md` | -| BendingInfoBuilder | `api/app/Services/Production/BendingInfoBuilder.php` | -| WorkOrderService | `api/app/Services/WorkOrderService.php` | -| StockService | `api/app/Services/StockService.php` | -| WorkOrderMaterialInput 모델 | `api/app/Models/Production/WorkOrderMaterialInput.php` | -| MaterialInputModal | `react/src/components/production/WorkerScreen/MaterialInputModal.tsx` | -| WorkerScreen actions | `react/src/components/production/WorkerScreen/actions.ts` | -| Bending types/utils | `react/src/components/production/WorkOrders/documents/bending/` | -| API 개발 규칙 | `docs/standards/api-rules.md` | -| 품질 체크리스트 | `docs/standards/quality-checklist.md` | - ---- - -## 8. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 8.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("bending-lot-pipeline-state") // 1. 상태 파악 -read_memory("bending-lot-pipeline-snapshot") // 2. 사고 흐름 복구 -read_memory("bending-lot-pipeline-active-symbols") // 3. 작업 대상 파악 -``` - -### 8.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | Snapshot | `write_memory("bending-lot-pipeline-snapshot", "코드변경+논의요약")` | -| **20% 이하** | Context Purge | `write_memory("bending-lot-pipeline-active-symbols", "주요 수정 파일/함수")` | -| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 | - -### 8.3 Serena 메모리 구조 -- `bending-lot-pipeline-state`: { phase, progress, next_step, last_decision } -- `bending-lot-pipeline-snapshot`: 현재까지의 논의 및 코드 변경점 요약 -- `bending-lot-pipeline-rules`: 해당 작업에서 결정된 불변의 규칙들 -- `bending-lot-pipeline-active-symbols`: 현재 수정 중인 파일/심볼 리스트 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| KSS01 + SUS + 벽면형 + 4300mm | BD-RS-43 (item_id 존재) | | ⏳ | -| getMaterials (dynamic_bom 있는 WO) | 세부품목 리스트 반환 | | ⏳ | -| 자재투입 등록 | work_order_material_inputs 레코드 생성 | | ⏳ | -| getMaterials (dynamic_bom 없는 WO) | 기존 동작 (하위 호환) | | ⏳ | - -### 9.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|:----:|------| -| dynamic_bom 자동 생성 | ⏳ | Phase 2 완료 후 | -| getMaterials 세부품목 반환 | ⏳ | Phase 3 완료 후 | -| 세부품목별 LOT 입력 가능 | ⏳ | Phase 4 완료 후 | -| 자재투입 이력 100% 기록 | ⏳ | Phase 1 완료 후 | -| LOT prefix 체계 일치 | ⏳ | Phase 0.2 검증 후 | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 1.5 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 대상 범위 (13개 태스크) | -| 4 | 의존성이 명시되어 있는가? | ✅ | 3.2 의존성 맵 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 코드 분석 기반 확인 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 상세 작업 내용 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 각 태스크별 검증 항목 | -| 8 | 모호한 표현이 없는가? | ✅ | 라인 번호, 메서드명, 파일 경로 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 0 + 📍 현재 진행 상태 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 섹션 4 (각 태스크별 수정/생성 파일 명시) | -| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 + 각 태스크별 검증 항목 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* diff --git a/plans/archive/bending-worklog-reimplementation-plan.md b/plans/archive/bending-worklog-reimplementation-plan.md deleted file mode 100644 index 1da3252..0000000 --- a/plans/archive/bending-worklog-reimplementation-plan.md +++ /dev/null @@ -1,860 +0,0 @@ -# 절곡 작업일지 완전 재구현 계획 - -> **작성일**: 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/archive/bidding-api-implementation-plan.md b/plans/archive/bidding-api-implementation-plan.md deleted file mode 100644 index e0c3135..0000000 --- a/plans/archive/bidding-api-implementation-plan.md +++ /dev/null @@ -1,817 +0,0 @@ -# 입찰관리(Bidding) API 구현 계획 - -> **작성일**: 2026-01-19 -> **목적**: 견적 → 입찰 전환 기능 구현 및 테스트용 더미데이터 생성 -> **기준 문서**: React 목업 타입 (`react/src/components/business/construction/bidding/types.ts`) -> **상태**: ✅ 완료 (Serena ID: bidding-api-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4.3 - Pint 코드 포맷팅 및 Swagger 재생성 | -| **다음 작업** | 사용자 수동 실행 (마이그레이션, 시더) | -| **진행률** | 12/12 (100%) | -| **마지막 업데이트** | 2026-01-19 | - ---- - -## 1. 개요 - -### 1.1 배경 - -**업무 흐름:** -``` -현장설명회 → 견적관리 → [견적완료] → 입찰관리 → 계약관리 → 기성/정산 - ↑ - 전환 기능 필요 -``` - -현재 React 프론트엔드의 입찰관리(`/construction/project/bidding`)는 **목업 데이터**를 사용 중입니다. -견적(Quote) API는 이미 구현되어 있으므로, 입찰(Bidding) API를 새로 구현하고 견적 → 입찰 전환 기능을 추가해야 합니다. - -**현재 상태:** -| 구분 | 견적(Estimate/Quote) | 입찰(Bidding) | -|------|---------------------|---------------| -| API Model | ✅ `Estimate.php` | ❌ 없음 | -| API Migration | ✅ `estimates` 테이블 | ❌ 없음 | -| API Endpoint | ✅ `/api/v1/quotes` | ❌ 없음 | -| React | ✅ API 연동 완료 | ❌ 목업 상태 | - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. SAM API Rules 엄격 준수 (Service-First, FormRequest) │ -│ 2. Multi-tenancy 필수 (BelongsToTenant) │ -│ 3. React 목업 타입과 100% 호환 │ -│ 4. 견적 데이터 참조 (복사가 아닌 FK 연결) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 새 테이블 생성, 새 API 추가, Seeder 작성 | 불필요 | -| ⚠️ 컨펌 필요 | 기존 quotes 테이블 수정, 비즈니스 로직 변경 | **필수** | -| 🔴 금지 | 기존 API 삭제, 파괴적 변경 | 별도 협의 | - -### 1.4 준수 규칙 - -- `api/CLAUDE.md` - SAM API Development Rules -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/guides/swagger-guide.md` - Swagger 문서화 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: Database & Model (Day 1) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | `biddings` 테이블 마이그레이션 생성 | ✅ | `2026_01_19_100000_create_biddings_table.php` | -| 1.2 | `Bidding` Model 생성 | ✅ | BelongsToTenant, SoftDeletes | -| 1.3 | 더미데이터 Seeder 생성 | ✅ | 10건 테스트 데이터 | - -### 2.2 Phase 2: API Implementation (Day 2) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | BiddingService 생성 | ✅ | CRUD + 통계 | -| 2.2 | BiddingController 생성 | ✅ | | -| 2.3 | FormRequest 생성 | ✅ | Filter, Update, Status, BulkDelete | -| 2.4 | Routes 등록 | ✅ | `/api/v1/biddings` | - -### 2.3 Phase 3: 견적 → 입찰 전환 (Day 2-3) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | QuoteService에 `convertToBidding()` 추가 | ✅ | 기존 코드에 메서드 추가 | -| 3.2 | 전환 API 엔드포인트 추가 | ✅ | `POST /quotes/{id}/convert-to-bidding` | - -### 2.4 Phase 4: Swagger & 검증 (Day 3) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | Swagger 문서 작성 | ✅ | `BiddingApi.php` | -| 4.2 | i18n 메시지 추가 | ✅ | message.php, error.php | -| 4.3 | Pint 코드 포맷팅 | ✅ | 9 style issues fixed | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Step 1: Database Schema -├── biddings 테이블 마이그레이션 작성 -├── 마이그레이션 실행 -└── Seeder로 더미데이터 생성 - -Step 2: Model & Service -├── Bidding Model 생성 (BelongsToTenant, SoftDeletes) -├── BiddingService 생성 (CRUD, stats, filter) -└── BiddingController 생성 - -Step 3: API Routes -├── routes/api.php에 biddings 라우트 추가 -├── FormRequest 클래스 생성 -└── API 테스트 - -Step 4: 견적 → 입찰 전환 -├── QuoteService에 convertToBidding() 추가 -├── 전환 API 엔드포인트 추가 -└── 전환 테스트 - -Step 5: Documentation -├── Swagger 문서 작성 -├── API 문서 검증 -└── Pint 실행 -``` - -### 3.2 데이터베이스 스키마 - -```sql --- biddings 테이블 -CREATE TABLE biddings ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', - - -- 기본 정보 - bidding_code VARCHAR(50) NOT NULL COMMENT '입찰번호', - quote_id BIGINT UNSIGNED NULL COMMENT '연결된 견적 ID (quotes.id)', - - -- 거래처/현장 - client_id BIGINT UNSIGNED NULL COMMENT '거래처 ID', - client_name VARCHAR(100) NULL COMMENT '거래처명 (스냅샷)', - project_name VARCHAR(200) NULL COMMENT '현장명', - - -- 입찰 정보 - bidding_date DATE NULL COMMENT '입찰일', - bid_date DATE NULL COMMENT '입찰일 (레거시 호환)', - submission_date DATE NULL COMMENT '투찰일', - confirm_date DATE NULL COMMENT '확정일', - total_count INT DEFAULT 0 COMMENT '총 개소', - bidding_amount DECIMAL(15,2) DEFAULT 0 COMMENT '입찰금액', - - -- 상태 - status VARCHAR(20) DEFAULT 'waiting' COMMENT '상태 (waiting/submitted/failed/invalid/awarded/hold)', - - -- 입찰자 - bidder_id BIGINT UNSIGNED NULL COMMENT '입찰자 ID', - bidder_name VARCHAR(50) NULL COMMENT '입찰자명 (스냅샷)', - - -- 공사기간 - construction_start_date DATE NULL COMMENT '공사 시작일', - construction_end_date DATE NULL COMMENT '공사 종료일', - vat_type VARCHAR(20) DEFAULT 'excluded' COMMENT '부가세 (included/excluded)', - - -- 비고 - remarks TEXT NULL COMMENT '비고', - - -- 견적 데이터 스냅샷 (JSON) - expense_items JSON NULL COMMENT '공과 항목 스냅샷', - estimate_detail_items JSON NULL COMMENT '견적 상세 항목 스냅샷', - - -- 감사 - created_by BIGINT UNSIGNED NULL COMMENT '생성자', - updated_by BIGINT UNSIGNED NULL COMMENT '수정자', - deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자', - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - deleted_at TIMESTAMP NULL, - - -- 인덱스 - INDEX idx_tenant_id (tenant_id), - INDEX idx_status (status), - INDEX idx_bidding_date (bidding_date), - INDEX idx_quote_id (quote_id), - UNIQUE INDEX idx_bidding_code (tenant_id, bidding_code) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -### 3.3 API 엔드포인트 설계 - -| Method | Path | 설명 | -|--------|------|------| -| GET | `/api/v1/biddings` | 목록 조회 (필터, 페이지네이션) | -| GET | `/api/v1/biddings/stats` | 통계 조회 | -| GET | `/api/v1/biddings/{id}` | 단건 조회 | -| PUT | `/api/v1/biddings/{id}` | 수정 | -| DELETE | `/api/v1/biddings/{id}` | 삭제 | -| DELETE | `/api/v1/biddings/bulk` | 일괄 삭제 | -| POST | `/api/v1/quotes/{id}/convert-to-bidding` | 견적 → 입찰 전환 | - -**참고**: 입찰은 별도 등록 없음 (견적완료 시 자동 전환) - -### 3.4 타입 매핑 (React → API) - -| React (camelCase) | API (snake_case) | DB Column | -|-------------------|------------------|-----------| -| `id` | `id` | `id` | -| `biddingCode` | `bidding_code` | `bidding_code` | -| `partnerId` | `client_id` | `client_id` | -| `partnerName` | `client_name` | `client_name` | -| `projectName` | `project_name` | `project_name` | -| `biddingDate` | `bidding_date` | `bidding_date` | -| `totalCount` | `total_count` | `total_count` | -| `biddingAmount` | `bidding_amount` | `bidding_amount` | -| `bidDate` | `bid_date` | `bid_date` | -| `submissionDate` | `submission_date` | `submission_date` | -| `confirmDate` | `confirm_date` | `confirm_date` | -| `status` | `status` | `status` | -| `bidderId` | `bidder_id` | `bidder_id` | -| `bidderName` | `bidder_name` | `bidder_name` | -| `remarks` | `remarks` | `remarks` | -| `estimateId` | `quote_id` | `quote_id` | -| `estimateCode` | `quote_number` | (join) | - -### 3.5 상태값 매핑 - -| 값 | 한글 | 설명 | -|----|------|------| -| `waiting` | 입찰대기 | 견적 전환 후 초기 상태 | -| `submitted` | 투찰 | 투찰서 제출 완료 | -| `failed` | 탈락 | 입찰 실패 | -| `invalid` | 유찰 | 입찰 무효 | -| `awarded` | 낙찰 | 입찰 성공 | -| `hold` | 보류 | 검토 대기 | - -### 3.6 기존 quotes 테이블 스키마 (연결용) - -> `biddings.quote_id` → `quotes.id` FK 연결 - -```sql --- quotes 테이블 핵심 컬럼 (api/database/migrations/2025_12_04_164542_create_quotes_table.php) -quotes ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, - quote_type ENUM('manufacturing', 'construction'), -- 'construction' 필터 - quote_number VARCHAR(50), -- 견적번호 (예: KD-SC-251204-01) - registration_date DATE, - client_id BIGINT, -- 거래처 ID - client_name VARCHAR(100), -- 거래처명 - site_name VARCHAR(200), -- 현장명 - total_amount DECIMAL(15,2), -- 최종 금액 - status ENUM('pending','draft','sent','approved','rejected','finalized','converted'), - site_briefing_id BIGINT, -- 현장설명회 연결 - options JSON, -- { summary_items, expense_items, detail_items, price_adjustment_data } - ... -) -``` - -**Quote 상태 상수** (api/app/Models/Quote/Quote.php): -- `pending` → 견적대기 (현장설명회에서 자동생성) -- `finalized` → 확정 (입찰 전환 가능) -- `converted` → 전환완료 - -### 3.7 API 응답 형식 (JSON) - -#### 목록 조회 응답 (GET /biddings) -```json -{ - "success": true, - "message": "message.fetched", - "data": { - "data": [ - { - "id": 1, - "bidding_code": "BID-2025-001", - "client_id": 1, - "client_name": "이사대표", - "project_name": "광장 아파트", - "bidding_date": "2025-01-25", - "total_count": 15, - "bidding_amount": 71000000, - "bid_date": "2025-01-20", - "submission_date": "2025-01-22", - "confirm_date": "2025-01-25", - "status": "awarded", - "bidder_id": 1, - "bidder_name": "홍길동", - "remarks": "", - "quote_id": 1, - "quote_number": "EST-2025-001", - "created_at": "2025-01-01T00:00:00.000000Z" - } - ], - "current_page": 1, - "per_page": 20, - "total": 10, - "last_page": 1 - } -} -``` - -#### 통계 응답 (GET /biddings/stats) -```json -{ - "success": true, - "message": "message.fetched", - "data": { - "total": 10, - "waiting": 3, - "awarded": 3 - } -} -``` - -#### 단건 조회 응답 (GET /biddings/{id}) -```json -{ - "success": true, - "message": "message.fetched", - "data": { - "id": 1, - "bidding_code": "BID-2025-001", - "client_id": 1, - "client_name": "이사대표", - "project_name": "광장 아파트", - "bidding_date": "2025-01-25", - "total_count": 15, - "bidding_amount": 71000000, - "status": "awarded", - "construction_start_date": "2025-02-01", - "construction_end_date": "2025-04-30", - "vat_type": "excluded", - "expense_items": [ - { "id": "1", "name": "설계비", "amount": 5000000 }, - { "id": "2", "name": "운반비", "amount": 3000000 } - ], - "estimate_detail_items": [ - { "id": "1", "no": 1, "name": "방화문", "material": "SUS304", "width": 1000, "height": 2100, "quantity": 10, ... } - ], - "quote": { - "id": 1, - "quote_number": "EST-2025-001" - } - } -} -``` - -### 3.8 convertToBidding() 상세 로직 - -```php -/** - * 견적 → 입찰 전환 - * - * @param int $quoteId 견적 ID - * @return Bidding 생성된 입찰 - */ -public function convertToBidding(int $quoteId): Bidding -{ - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - // 1. 견적 조회 (quote_type=construction, status=finalized) - $quote = Quote::where('tenant_id', $tenantId) - ->where('id', $quoteId) - ->where('quote_type', 'construction') - ->where('status', 'finalized') - ->firstOrFail(); - - // 2. 이미 입찰이 존재하는지 확인 - $existingBidding = Bidding::where('quote_id', $quoteId)->first(); - if ($existingBidding) { - throw new BadRequestHttpException(__('error.bidding_already_exists')); - } - - // 3. 입찰 데이터 생성 - $bidding = Bidding::create([ - 'tenant_id' => $tenantId, - 'bidding_code' => $this->generateBiddingCode($tenantId), - 'quote_id' => $quote->id, - - // 거래처/현장 정보 복사 - 'client_id' => $quote->client_id, - 'client_name' => $quote->client_name, - 'project_name' => $quote->site_name, - - // 금액 정보 - 'bidding_amount' => $quote->total_amount, - 'total_count' => $quote->items->count(), - - // 날짜 - 'bidding_date' => now()->toDateString(), - - // 상태 - 'status' => 'waiting', - - // 현장설명회에서 공사기간 가져오기 - 'construction_start_date' => $quote->siteBriefing?->construction_start_date, - 'construction_end_date' => $quote->siteBriefing?->construction_end_date, - 'vat_type' => $quote->siteBriefing?->vat_type ?? 'excluded', - - // 견적 옵션 데이터 스냅샷 - 'expense_items' => $quote->options['expense_items'] ?? [], - 'estimate_detail_items' => $quote->options['detail_items'] ?? [], - - 'created_by' => $userId, - ]); - - // 4. 견적 상태 업데이트 (선택적) - // $quote->update(['status' => 'converted']); - - return $bidding; -} - -/** - * 입찰번호 자동 생성 (BID-YYYY-NNN) - */ -private function generateBiddingCode(int $tenantId): string -{ - $year = now()->format('Y'); - $prefix = "BID-{$year}-"; - - $lastBidding = Bidding::where('tenant_id', $tenantId) - ->where('bidding_code', 'like', "{$prefix}%") - ->orderBy('id', 'desc') - ->first(); - - $sequence = 1; - if ($lastBidding) { - $lastNum = (int) substr($lastBidding->bidding_code, -3); - $sequence = $lastNum + 1; - } - - return $prefix . str_pad($sequence, 3, '0', STR_PAD_LEFT); -} -``` - -### 3.9 Service/Controller 패턴 (SAM 표준) - -**Controller 패턴** (api/app/Http/Controllers): -```php - $this->service->index($request->validated())); - } - - public function show(int $id) - { - return ApiResponse::handle(fn () => $this->service->show($id)); - } - - public function update(BiddingUpdateRequest $request, int $id) - { - return ApiResponse::handle(fn () => $this->service->update($id, $request->validated())); - } - - public function destroy(int $id) - { - return ApiResponse::handle(fn () => $this->service->destroy($id)); - } - - public function stats() - { - return ApiResponse::handle(fn () => $this->service->stats()); - } -} -``` - -**Service 패턴** (api/app/Services): -```php -tenantId(); // 필수 - $query = Bidding::where('tenant_id', $tenantId); - // ... 필터, 정렬, 페이지네이션 - return $query->paginate($params['size'] ?? 20); - } - - public function show(int $id): Bidding - { - $tenantId = $this->tenantId(); - return Bidding::where('tenant_id', $tenantId) - ->with(['quote']) - ->findOrFail($id); - } - - public function stats(): array - { - $tenantId = $this->tenantId(); - return [ - 'total' => Bidding::where('tenant_id', $tenantId)->count(), - 'waiting' => Bidding::where('tenant_id', $tenantId)->where('status', 'waiting')->count(), - 'awarded' => Bidding::where('tenant_id', $tenantId)->where('status', 'awarded')->count(), - ]; - } -} -``` - -### 3.10 더미데이터 (Seeder용 10건) - -> React 목업 기준 (`react/src/components/business/construction/bidding/actions.ts`) - -```php -// api/database/seeders/BiddingSeeder.php -$biddings = [ - [ - 'bidding_code' => 'BID-2025-001', - 'client_name' => '이사대표', - 'project_name' => '광장 아파트', - 'bidding_date' => '2025-01-25', - 'total_count' => 15, - 'bidding_amount' => 71000000, - 'bid_date' => '2025-01-20', - 'submission_date' => '2025-01-22', - 'confirm_date' => '2025-01-25', - 'status' => 'awarded', - 'bidder_name' => '홍길동', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-002', - 'client_name' => '야사건설', - 'project_name' => '대림아파트', - 'bidding_date' => '2025-01-20', - 'total_count' => 22, - 'bidding_amount' => 100000000, - 'bid_date' => '2025-01-18', - 'submission_date' => null, - 'confirm_date' => null, - 'status' => 'waiting', - 'bidder_name' => '김철수', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-003', - 'client_name' => '여의건설', - 'project_name' => '현장아파트', - 'bidding_date' => '2025-01-18', - 'total_count' => 18, - 'bidding_amount' => 85000000, - 'bid_date' => '2025-01-15', - 'submission_date' => '2025-01-16', - 'confirm_date' => '2025-01-18', - 'status' => 'awarded', - 'bidder_name' => '홍길동', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-004', - 'client_name' => '이사대표', - 'project_name' => '송파타워', - 'bidding_date' => '2025-01-15', - 'total_count' => 30, - 'bidding_amount' => 120000000, - 'bid_date' => '2025-01-12', - 'submission_date' => '2025-01-13', - 'confirm_date' => '2025-01-15', - 'status' => 'failed', - 'bidder_name' => '이영희', - 'remarks' => '가격 경쟁력 부족', - ], - [ - 'bidding_code' => 'BID-2025-005', - 'client_name' => '야사건설', - 'project_name' => '강남센터', - 'bidding_date' => '2025-01-12', - 'total_count' => 25, - 'bidding_amount' => 95000000, - 'bid_date' => '2025-01-10', - 'submission_date' => '2025-01-11', - 'confirm_date' => null, - 'status' => 'submitted', - 'bidder_name' => '홍길동', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-006', - 'client_name' => '여의건설', - 'project_name' => '목동센터', - 'bidding_date' => '2025-01-10', - 'total_count' => 12, - 'bidding_amount' => 78000000, - 'bid_date' => '2025-01-08', - 'submission_date' => '2025-01-09', - 'confirm_date' => '2025-01-10', - 'status' => 'invalid', - 'bidder_name' => '김철수', - 'remarks' => '입찰 조건 미충족', - ], - [ - 'bidding_code' => 'BID-2025-007', - 'client_name' => '이사대표', - 'project_name' => '서초타워', - 'bidding_date' => '2025-01-08', - 'total_count' => 35, - 'bidding_amount' => 150000000, - 'bid_date' => '2025-01-05', - 'submission_date' => null, - 'confirm_date' => null, - 'status' => 'waiting', - 'bidder_name' => '이영희', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-008', - 'client_name' => '야사건설', - 'project_name' => '청담프로젝트', - 'bidding_date' => '2025-01-05', - 'total_count' => 40, - 'bidding_amount' => 200000000, - 'bid_date' => '2025-01-03', - 'submission_date' => '2025-01-04', - 'confirm_date' => '2025-01-05', - 'status' => 'awarded', - 'bidder_name' => '홍길동', - 'remarks' => '', - ], - [ - 'bidding_code' => 'BID-2025-009', - 'client_name' => '여의건설', - 'project_name' => '잠실센터', - 'bidding_date' => '2025-01-03', - 'total_count' => 20, - 'bidding_amount' => 88000000, - 'bid_date' => '2025-01-01', - 'submission_date' => null, - 'confirm_date' => null, - 'status' => 'hold', - 'bidder_name' => '김철수', - 'remarks' => '검토 대기 중', - ], - [ - 'bidding_code' => 'BID-2025-010', - 'client_name' => '이사대표', - 'project_name' => '역삼빌딩', - 'bidding_date' => '2025-01-01', - 'total_count' => 10, - 'bidding_amount' => 65000000, - 'bid_date' => '2024-12-28', - 'submission_date' => null, - 'confirm_date' => null, - 'status' => 'waiting', - 'bidder_name' => '이영희', - 'remarks' => '', - ], -]; - -// 통계 요약: -// - total: 10건 -// - waiting: 3건 (BID-002, 007, 010) -// - awarded: 3건 (BID-001, 003, 008) -// - submitted: 1건 (BID-005) -// - failed: 1건 (BID-004) -// - invalid: 1건 (BID-006) -// - hold: 1건 (BID-009) -``` - ---- - -## 4. 상세 작업 내용 - -> 각 Phase 진행 후 이 섹션에 상세 내용 추가 - -### 4.1 Phase 1: Database & Model - -#### 1.1 마이그레이션 파일 생성 -- **상태**: ⏳ 대기 -- **파일**: `api/database/migrations/2026_01_19_XXXXXX_create_biddings_table.php` - -#### 1.2 Model 생성 -- **상태**: ⏳ 대기 -- **파일**: `api/app/Models/Bidding/Bidding.php` - -#### 1.3 Seeder 생성 -- **상태**: ⏳ 대기 -- **파일**: `api/database/seeders/BiddingSeeder.php` -- **데이터**: React 목업 기준 10건 - ---- - -## 5. 컨펌 대기 목록 - -> API 내부 로직 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | QuoteService 수정 | `convertToBidding()` 메서드 추가 | api/Quote | ⏳ 대기 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-19 | - | 문서 초안 작성 | - | - | - ---- - -## 7. 참고 문서 - -- **SAM API Rules**: `api/CLAUDE.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **Swagger 가이드**: `docs/guides/swagger-guide.md` -- **React 목업 타입**: `react/src/components/business/construction/bidding/types.ts` -- **React 목업 데이터**: `react/src/components/business/construction/bidding/actions.ts` -- **기존 견적 API**: `react/src/components/business/construction/estimates/actions.ts` - ---- - -## 8. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 8.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("bidding-api-state") // 1. 상태 파악 -read_memory("bidding-api-snapshot") // 2. 사고 흐름 복구 -``` - -### 8.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | 🛠 Snapshot | 현재까지 코드 변경점 저장 | -| **20% 이하** | 🧹 Context Purge | 활성 심볼 저장 | -| **10% 이하** | 🛑 Stop & Save | 최종 상태 저장 | - -### 8.3 Serena 메모리 구조 -- `bidding-api-state`: { phase, progress, next_step } -- `bidding-api-snapshot`: 현재까지의 코드 변경점 요약 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 API 테스트 케이스 - -| 엔드포인트 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|-----------|------|----------|----------|------| -| GET /biddings | - | 목록 반환 | | ⏳ | -| GET /biddings/stats | - | 통계 반환 | | ⏳ | -| GET /biddings/{id} | id=1 | 단건 반환 | | ⏳ | -| PUT /biddings/{id} | 수정 데이터 | 수정 성공 | | ⏳ | -| POST /quotes/{id}/convert-to-bidding | quote_id | 입찰 생성 | | ⏳ | - -### 9.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| Bidding API CRUD 동작 | ⏳ | | -| 견적 → 입찰 전환 동작 | ⏳ | | -| 더미데이터 10건 생성 | ⏳ | | -| Swagger 문서 완성 | ⏳ | | -| Pint 통과 | ⏳ | | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 견적→입찰 전환 + 더미데이터 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-4 정의 | -| 4 | 의존성이 명시되어 있는가? | ✅ | quotes API 의존 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 7. 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3.1 절차 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 파일/API 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 현재 진행 상태 + 3.1 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 2. 대상 범위 | -| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/construction-api-integration-plan.md b/plans/archive/construction-api-integration-plan.md deleted file mode 100644 index f217f7a..0000000 --- a/plans/archive/construction-api-integration-plan.md +++ /dev/null @@ -1,480 +0,0 @@ -# 시공사 페이지 API 연동 계획 - -> **작성일**: 2026-01-08 -> **목적**: 시공사 8개 페이지 Mock → API 연동 -> **기준 문서**: `docs/standards/api-rules.md`, `docs/guides/swagger-guide.md` -> **상태**: ✅ 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 3.4: 노임관리 API 연동 완료 ✅ | -| **다음 작업** | 🎉 **전체 완료** | -| **진행률** | 8/8 (100%) | -| **마지막 업데이트** | 2026-01-12 | - ---- - -## 0. 전제 조건 (Prerequisites) - -### 0.1 환경 확인 -```bash -# Docker 컨테이너 상태 확인 -docker ps | grep sam - -# API 서버 접속 확인 -curl -I http://api.sam.kr/api/health - -# React 개발 서버 확인 -curl -I http://react.sam.kr -``` - -**체크리스트:** -- [ ] Docker 컨테이너 실행 중 (api, react, mysql) -- [ ] api.sam.kr 접속 가능 (200 응답) -- [ ] react.sam.kr 접속 가능 (200 응답) -- [ ] 데이터베이스 연결 정상 - -### 0.2 권한 및 인증 -- [ ] API 개발 권한 (`api/` 디렉토리 수정 가능) -- [ ] React 개발 권한 (`react/` 디렉토리 수정 가능) -- [ ] Sanctum 토큰 발급 방법 숙지 (테스트용) - -### 0.3 필수 도구 -- PHP 8.4+, Composer -- Node.js 20+, pnpm -- Git - ---- - -## 1. 개요 - -### 1.1 배경 -시공사 메뉴의 8개 페이지가 현재 모두 Mock 데이터를 사용하고 있으며, 실제 API 연동이 필요함. -(물량검토관리는 Frontend/기획 미존재로 제외) - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - Service-First: 비즈니스 로직 → Service Layer │ -│ - Multi-tenancy: BelongsToTenant 필수 │ -│ - FormRequest: Controller 검증 금지 │ -│ - Server Actions: React에서 'use server' 패턴 사용 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | actions.ts Mock→API 변경, 타입 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 새 API 엔드포인트 생성, DB 스키마 변경 | **필수** | -| 🔴 금지 | 기존 API 삭제, 테이블 구조 변경 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/standards/api-rules.md` - API 개발 규칙 ✅ 존재 -- `docs/guides/swagger-guide.md` - Swagger 작성 가이드 ✅ 존재 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 ✅ 존재 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: 계약관리 (Contract) - -| # | 작업 항목 | 상태 | 서브 문서 | -|---|----------|:----:|----------| -| 1.1 | 계약관리 (contract) | ✅ | [contract-plan.md](./sub/contract-plan.md) | -| 1.2 | 인수인계보고서관리 (handover-report) | ✅ | [handover-report-plan.md](./sub/handover-report-plan.md) | - -### 2.2 Phase 2: 발주관리 (Order) - -| # | 작업 항목 | 상태 | 서브 문서 | -|---|----------|:----:|----------| -| 2.1 | 현장관리 (site-management) | ✅ | [site-management-plan.md](./sub/site-management-plan.md) | -| 2.2 | 구조검토관리 (structure-review) | ✅ | [structure-review-plan.md](./sub/structure-review-plan.md) | -| 2.3 | 물량검토관리 (quantity-review) | ❌ 제외 | Frontend/기획 미존재 | - -### 2.3 Phase 3: 기준정보 (Base Info) - -| # | 작업 항목 | 상태 | 서브 문서 | -|---|----------|:----:|----------| -| 3.1 | 카테고리관리 (categories) | ✅ | [categories-plan.md](./sub/categories-plan.md) | -| 3.2 | 품목관리 (items) | ✅ | [items-plan.md](./sub/items-plan.md) | -| 3.3 | 단가관리 (pricing) | ✅ | [pricing-plan.md](./sub/pricing-plan.md) | -| 3.4 | 노임관리 (labor) | ✅ | [labor-plan.md](./sub/labor-plan.md) | - ---- - -## 3. API 현황 분석 - -### 3.1 기존 API (연동 가능) - -| API | 경로 | 상태 | 대상 컴포넌트 | -|-----|------|:----:|--------------| -| categories | `/api/construction/categories` | ✅ 존재 | 카테고리관리 | -| pricing | `/api/construction/pricing` | ✅ 존재 | 단가관리 | - -### 3.2 신규 개발 필요 API - -| API | 예상 경로 | 우선순위 | 대상 컴포넌트 | -|-----|----------|:--------:|--------------| -| contracts | `/api/construction/contracts` | ✅ 완료 | 계약관리 | -| handover-reports | `/api/construction/handover-reports` | ✅ 완료 | 인수인계보고서 | -| sites | `/api/construction/sites` | ✅ 완료 | 현장관리 | -| structure-reviews | `/api/construction/structure-reviews` | ✅ 완료 | 구조검토관리 | -| quantity-reviews | `/api/construction/quantity-reviews` | ❌ 제외 | 물량검토관리 (Frontend/기획 미존재) | -| items | `/api/construction/items` | 🟢 낮음 | 품목관리 | -| labor | `/api/construction/labor` | 🟢 낮음 | 노임관리 | - ---- - -## 4. 작업 절차 - -### 4.1 단계별 절차 (상세) - -``` -Step 1: 서브 문서 확인 -├── docs/plans/sub/{module}-plan.md 읽기 -├── 현재 Mock 데이터 구조 확인 -└── 필요한 API 엔드포인트 파악 - -Step 2: API 엔드포인트 확인/생성 -├── api/routes/api.php에서 기존 API 확인 -├── 없으면: -│ ├── Controller 생성: php artisan make:controller Api/Construction/{Name}Controller -│ ├── Service 생성: app/Services/Construction/{Name}Service.php -│ ├── FormRequest 생성: php artisan make:request Api/Construction/{Name}Request -│ └── Model 확인/생성 -└── Swagger 문서 작성 - -Step 3: React actions.ts 수정 -├── react/src/components/business/construction/{module}/actions.ts 열기 -├── Mock 데이터 상수 제거 (MOCK_XXX) -├── API 호출 로직 구현: -│ └── const response = await fetch('/api/construction/{endpoint}', {...}) -└── 에러 핸들링 추가 - -Step 4: 타입 정합성 확인 -├── API 응답과 프론트엔드 타입 매칭 -├── types.ts 수정 (snake_case → camelCase 변환 등) -└── 컴포넌트 수정 (필요시) - -Step 5: 테스트 및 검증 -├── API 직접 호출 테스트 (curl/Postman) -├── UI 동작 확인 (브라우저) -└── 에러 케이스 테스트 -``` - -### 4.2 첫 번째 작업 시작점 - -**Phase 1.1 계약관리 시작:** -```bash -# 1. 서브 문서 읽기 -cat docs/plans/sub/contract-plan.md - -# 2. 현재 Mock 확인 -cat react/src/components/business/construction/contract/actions.ts - -# 3. API 존재 여부 확인 -grep -n "contracts" api/routes/api.php - -# 4. 없으면 Controller 생성 -cd api && php artisan make:controller Api/Construction/ContractController --resource -``` - ---- - -## 5. 환경 정보 - -### 5.1 프로젝트 구조 - -``` -SAM/ -├── api/ # Laravel 12 REST API -│ ├── app/Http/Controllers/Api/Construction/ -│ ├── app/Services/Construction/ -│ └── routes/api.php -│ -├── react/ # Next.js 15 Frontend -│ └── src/ -│ ├── app/[locale]/(protected)/construction/ -│ │ ├── project/contract/ # 계약관리 -│ │ ├── project/contract/handover-report/ # 인수인계 -│ │ ├── order/site-management/ # 현장관리 -│ │ ├── order/structure-review/ # 구조검토 -│ │ ├── order/order-management/ # 발주관리 -│ │ └── order/base-info/ # 기준정보 -│ │ ├── categories/ -│ │ ├── items/ -│ │ ├── pricing/ -│ │ └── labor/ -│ └── components/business/construction/ -│ -└── docs/plans/ # 계획 문서 - ├── construction-api-integration-plan.md # 메인 (현재 문서) - └── sub/ # 서브 문서 (9개) -``` - -### 5.2 개발 환경 - -| 항목 | 값 | -|------|-----| -| 도메인 | sam.kr (로컬) | -| API | api.sam.kr | -| React | react.sam.kr | -| PHP | 8.4+ | -| Laravel | 12 | -| Next.js | 15 | - ---- - -## 6. 컴포넌트 분석 요약 - -### 6.1 계약관리 (Contract) - -| 컴포넌트 | Mock 상태 | 주요 기능 | -|----------|:--------:|----------| -| ContractListClient | ✅ Mock | 목록, 검색, 삭제, 필터 | -| 인수인계보고서 | ✅ Mock | 목록, 상세, 삭제 | - -### 6.2 발주관리 (Order) - -| 컴포넌트 | Mock 상태 | 주요 기능 | -|----------|:--------:|----------| -| SiteManagementListClient | ✅ Mock | 현장 목록, 통계, 삭제 | -| StructureReviewListClient | ✅ Mock | 구조검토 목록, 상태 관리 | -| OrderManagementClient | ✅ Mock | 발주 목록, 필터, 삭제 | - -### 6.3 기준정보 (Base Info) - -| 컴포넌트 | Mock 상태 | API 존재 | 주요 기능 | -|----------|:--------:|:-------:|----------| -| CategoryManagementClient | ✅ Mock | ✅ | 카테고리 CRUD, 순서 변경 | -| ItemManagementClient | ✅ Mock | ❌ | 품목 CRUD, 카테고리 연결 | -| PricingListClient | ✅ Mock | ✅ | 단가 CRUD, 버전 관리 | -| LaborManagementClient | ✅ Mock | ❌ | 노임 CRUD, 단가 관리 | - ---- - -## 7. 성공 기준 - -### 7.1 각 페이지 완료 조건 - -| # | 조건 | 확인 방법 | -|---|------|----------| -| 1 | Mock 데이터 완전 제거 | `grep -r "MOCK_" actions.ts` 결과 없음 | -| 2 | API 호출 성공 | 네트워크 탭에서 200 응답 확인 | -| 3 | UI에서 데이터 정상 표시 | 목록에 실제 데이터 표시 | -| 4 | CRUD 동작 정상 | 생성/조회/수정/삭제 모두 동작 | -| 5 | 에러 핸들링 동작 | 네트워크 끊김 시 에러 메시지 표시 | - -### 7.2 전체 완료 조건 - -- [ ] 8개 페이지 모두 API 연동 완료 (4/8) -- [ ] Swagger 문서 작성 완료 -- [ ] 기본 동작 테스트 통과 -- [ ] 코드 리뷰 완료 - -### 7.3 품질 기준 - -- API 응답 시간: < 500ms -- 에러 발생 시 사용자 친화적 메시지 표시 -- TypeScript 타입 에러 0개 -- ESLint 경고 0개 - ---- - -## 8. 검증 방법 - -### 8.1 API 테스트 (curl) - -```bash -# 1. 인증 토큰 획득 (테스트용) -TOKEN=$(curl -s -X POST "http://api.sam.kr/api/auth/login" \ - -H "Content-Type: application/json" \ - -d '{"email":"test@test.com","password":"password"}' | jq -r '.token') - -# 2. 계약 목록 조회 -curl -X GET "http://api.sam.kr/api/construction/contracts" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Accept: application/json" - -# 3. 계약 상세 조회 -curl -X GET "http://api.sam.kr/api/construction/contracts/1" \ - -H "Authorization: Bearer $TOKEN" - -# 4. 계약 생성 -curl -X POST "http://api.sam.kr/api/construction/contracts" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"title":"테스트 계약","partner_id":1}' -``` - -### 8.2 UI 테스트 체크리스트 - -``` -□ 페이지 접속 시 로딩 스피너 표시 -□ 데이터 로딩 완료 후 목록 표시 -□ 검색 기능 동작 -□ 필터 기능 동작 -□ 페이지네이션 동작 -□ 상세 보기 동작 -□ 생성 폼 동작 -□ 수정 폼 동작 -□ 삭제 확인 및 동작 -□ 에러 발생 시 메시지 표시 -``` - -### 8.3 에러 케이스 테스트 - -| 케이스 | 예상 동작 | 확인 방법 | -|--------|----------|----------| -| 네트워크 끊김 | 에러 메시지 표시 | 네트워크 탭에서 Offline 모드 | -| 401 인증 오류 | 로그인 페이지 리다이렉트 | 토큰 만료 상태에서 접속 | -| 404 데이터 없음 | "데이터 없음" 표시 | 존재하지 않는 ID 접근 | -| 500 서버 오류 | 에러 메시지 표시 | API 강제 에러 발생 | - ---- - -## 9. 세션 관리 - -### 9.1 새 세션 시작 시 - -```bash -# 1. 메인 문서 읽기 (현재 진행 상태 확인) -cat docs/plans/construction-api-integration-plan.md | head -30 - -# 2. "다음 작업" 확인 -grep "다음 작업" docs/plans/construction-api-integration-plan.md - -# 3. 해당 서브 문서 읽기 -cat docs/plans/sub/{다음작업}-plan.md - -# 4. 작업 시작 -``` - -### 9.2 작업 중 체크포인트 - -| 시점 | 행동 | -|------|------| -| 작업 완료 시 | 메인 문서 "현재 진행 상태" 업데이트 | -| 서브 작업 완료 시 | 서브 문서 상태 (⏳→✅) 업데이트 | -| 컨펌 필요 시 | "컨펌 대기 목록"에 추가 | -| 세션 종료 전 | 변경 이력에 기록 | - -### 9.3 세션 종료 시 - -```bash -# 1. 진행 상태 업데이트 -# - 📍 현재 진행 상태 섹션의 "마지막 완료 작업", "다음 작업" 수정 -# - 대상 범위의 상태 아이콘 수정 (⏳ → ✅ 또는 🔄) - -# 2. 변경 이력 추가 -# | 2026-01-08 | 1.1 | 계약관리 API 연동 완료 | contract/actions.ts | - | - -# 3. 커밋 (승인 후) -git add . && git commit -m "feat: [시공사] 1.1 계약관리 - API 연동" -``` - -### 9.4 컨텍스트 관리 (Serena 메모리) - -```javascript -// 세션 시작 시 로드 -read_memory("construction-api-state") - -// 작업 중 저장 (30분마다 또는 주요 완료 시) -write_memory("construction-api-state", { - phase: "1.1", - status: "진행중", - lastCompleted: "Controller 생성", - nextStep: "Service 로직 구현" -}) - -// 컨텍스트 30% 이하 시 -write_memory("construction-api-snapshot", "현재까지 진행 상황 요약...") -``` - ---- - -## 10. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-08 | 초안 | 문서 초안 작성, 9개 컴포넌트 분석 | - | - | -| 2026-01-08 | 보완 | 전제조건, 성공기준, 검증방법, 세션관리 추가 | - | - | -| 2026-01-09 | 1.1 | 계약관리 API 연동 완료 (Backend + Frontend) | api/, react/ | ✅ | -| 2026-01-09 | 1.2 | 인수인계보고서 Frontend API 연동 완료 | react/ | ✅ | -| 2026-01-09 | 2.1 | 현장관리 API 연동 완료 (Backend + Frontend) | api/, react/ | ✅ | -| 2026-01-09 | 2.2 | 구조검토관리 API 연동 완료 (Backend + Frontend) | api/, react/ | ✅ | -| 2026-01-09 | 2.3 | 물량검토관리 제외 (Frontend/기획 미존재) | docs/ | ✅ | -| 2026-01-09 | 3.1 | 카테고리관리 API 연동 완료 (HTTP 메서드 수정) | react/ | ✅ | -| 2026-01-09 | 3.2 | 품목관리 API 연동 완료 (apiClient.delete body 지원 추가) | react/ | ✅ | -| 2026-01-09 | 3.3 | 단가관리 Backend API 보완 (stats, bulkDestroy 추가) | api/ | ✅ | - ---- - -## 11. 참고 문서 - -| 문서 | 경로 | 용도 | -|------|------|------| -| API 규칙 | `docs/standards/api-rules.md` | API 개발 표준 | -| Swagger 가이드 | `docs/guides/swagger-guide.md` | API 문서화 | -| 품질 체크리스트 | `docs/standards/quality-checklist.md` | 완료 전 점검 | -| 빠른 시작 | `docs/quickstart/quick-start.md` | 환경 설정 | -| 개발 명령어 | `docs/quickstart/dev-commands.md` | 자주 쓰는 명령어 | - ---- - -## 12. 서브 문서 링크 - -| Phase | 문서 | 경로 | API 상태 | -|-------|------|------|:--------:| -| 1.1 | 계약관리 | [./sub/contract-plan.md](./sub/contract-plan.md) | ✅ 완료 | -| 1.2 | 인수인계보고서 | [./sub/handover-report-plan.md](./sub/handover-report-plan.md) | ❌ 신규 | -| 2.1 | 현장관리 | [./sub/site-management-plan.md](./sub/site-management-plan.md) | ⚠️ 확인필요 | -| 2.2 | 구조검토관리 | [./sub/structure-review-plan.md](./sub/structure-review-plan.md) | ❌ 신규 | -| 2.3 | 발주관리 | [./sub/order-management-plan.md](./sub/order-management-plan.md) | ❌ 신규 | -| 3.1 | 카테고리관리 | [./sub/categories-plan.md](./sub/categories-plan.md) | ✅ 존재 | -| 3.2 | 품목관리 | [./sub/items-plan.md](./sub/items-plan.md) | ❌ 신규 | -| 3.3 | 단가관리 | [./sub/pricing-plan.md](./sub/pricing-plan.md) | ✅ 존재 | -| 3.4 | 노임관리 | [./sub/labor-plan.md](./sub/labor-plan.md) | ❌ 신규 | - ---- - -## 13. 자기완결성 점검 결과 - -### 13.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 참조 섹션 | -|---|----------|:----:|----------| -| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 7. 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 2. 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 0. 전제 조건 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 11. 참고 문서 (검증됨) | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 4. 작업 절차 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 8. 검증 방법 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 명령어 포함 | - -### 13.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 4.2 첫 번째 작업 시작점 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5.1 프로젝트 구조 + 서브 문서 | -| Q4. 작업 완료 확인 방법은? | ✅ | 7. 성공 기준, 8. 검증 방법 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 11. 참고 문서 | - -**결과: 5/5 통과 → ✅ 자기완결성 확보** - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* -*보완일: 2026-01-08* \ No newline at end of file diff --git a/plans/archive/docs-update-plan.md b/plans/archive/docs-update-plan.md deleted file mode 100644 index 1713e06..0000000 --- a/plans/archive/docs-update-plan.md +++ /dev/null @@ -1,309 +0,0 @@ -# docs/architecture 문서 업데이트 계획 - -> **작성일**: 2025-12-26 -> **목적**: 현재 시스템 상태와 문서 동기화 -> **기준 문서**: docs/INDEX.md -> **상태**: 🔄 진행중 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 전체 완료 | -| **다음 작업** | 없음 (완료) | -| **진행률** | 13/13 (100%) ✅ | -| **마지막 업데이트** | 2025-12-26 | - ---- - -## 1. 개요 - -### 1.1 배경 -- 2025-12-13 admin 프로젝트 → mng 프로젝트 전환 완료 -- 문서에 아직 admin 참조가 남아있어 동기화 필요 -- 기술 스택 버전 업데이트 반영 필요 - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 문서 업데이트 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - 현재 시스템 상태와 100% 동기화 │ -│ - admin → mng 전환 완전 반영 │ -│ - 버전 정보 최신화 (React 19.2.1, Next.js 15.5.7) │ -│ - 상호 참조 링크 일관성 유지 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 날짜 갱신, 오타 수정, 버전 업데이트 | 불필요 | -| ⚠️ 컨펌 필요 | 구조 변경, 새 섹션 추가, 문서 삭제 | **필수** | -| 🔴 금지 | 비즈니스 로직 변경, 정책 변경 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/INDEX.md` - 문서 인덱스 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: 핵심 문서 업데이트 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | system-overview.md - admin→mng 전환 | ✅ | 완료 | -| 1.2 | dev-commands.md - admin→mng 변경 | ✅ | 완료 | -| 1.3 | quick-start.md - claudedocs→docs 경로 수정 | ✅ | 완료 | - -### 2.2 Phase 2: 보조 문서 업데이트 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | INDEX.md - 프로젝트 구조 미세 조정 | ✅ | Admin 참조 제거 | -| 2.2 | quality-checklist.md - 날짜 갱신 | ✅ | 2025-12-26 | -| 2.3 | swagger-guide.md - 날짜 갱신 | ✅ | 2025-12-26 | - -### 2.3 Phase 3: 검증 및 정리 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | security-policy.md - 날짜 갱신 | ✅ | 2025-12-26 | -| 3.2 | database-schema.md - 테이블 수 업데이트 | ✅ | 92개→171개 | - -### 2.4 Phase 4: 오래된 파일 정리/아카이브 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | history/2025-09/ 문서 검토 | ✅ | 참조용 유지 | -| 4.2 | history/2025-11/ 문서 검토 | ✅ | 아카이브로 적절 | -| 4.3 | admin 참조 파일 식별 및 정리 | ✅ | 4개 파일 수정 완료 | -| 4.4 | 완료된 plans/ 문서 정리 | ✅ | D0.8→history, index 업데이트 | -| 4.5 | 중복/불필요 문서 정리 | ✅ | 빈 디렉토리 6개 삭제 | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Step 1: Phase 1 - 핵심 문서 업데이트 -├── 1.1 system-overview.md 전면 업데이트 -│ ├── admin/ 설명 → mng/ 설명 -│ ├── Filament v4 → Pure Blade + Tailwind -│ ├── Docker 서비스 구성 업데이트 -│ └── 저장소 구조 업데이트 -├── 1.2 dev-commands.md 수정 -│ ├── Admin Application → MNG Application -│ └── admin/ 경로 → mng/ 경로 -└── 1.3 quick-start.md 수정 - ├── claudedocs/ → docs/ 경로 - └── 프로젝트 구조 업데이트 - -Step 2: Phase 2 - 보조 문서 업데이트 -├── 2.1 INDEX.md 미세 조정 -├── 2.2 quality-checklist.md 날짜 갱신 -└── 2.3 swagger-guide.md 날짜 갱신 - -Step 3: Phase 3 - 검증 및 정리 -├── 3.1 security-policy.md 날짜 갱신 -├── 3.2 database-schema.md 테이블 수 확인 -└── 3.3 모든 문서 일관성 검증 - -Step 4: Phase 4 - 오래된 파일 정리/아카이브 -├── 4.1 history/2025-09/ 문서 검토 -│ └── 구버전 스키마, 체크포인트 확인 -├── 4.2 history/2025-11/ 문서 검토 -│ └── item-master 관련 아카이브 정리 -├── 4.3 admin 참조 파일 정리 -│ └── mng로 미전환된 파일 식별/수정 -├── 4.4 완료된 plans/ 문서 정리 -│ └── 완료된 계획 문서 삭제/아카이브 -└── 4.5 중복/불필요 문서 정리 - └── 통합 가능 문서 식별 및 처리 -``` - -### 3.2 문서 업데이트 템플릿 - -```markdown -### [항목 ID] 항목명 - -**현재 상태:** -- [현재 상태 설명] - -**목표 상태:** -- [목표 상태 설명] - -**변경 사항:** -- [ ] ✅ [즉시 가능 항목] -- [ ] ⚠️ [컨펌 필요 항목] -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1: 핵심 문서 업데이트 - -#### 1.1 system-overview.md -- **상태**: ⏳ 대기 -- **주요 변경**: - - [ ] admin/ 섹션 → mng/ 섹션으로 전환 - - [ ] 기술 스택: Filament v4 → Pure Blade + Tailwind CSS 3.x - - [ ] Docker 서비스: design, php73 추가 - - [ ] React 버전: 19.2.0 → 19.2.1 - - [ ] Next.js 버전: 15 → 15.5.7 - - [ ] 도메인 매핑: admin.sam.kr → mng 서비스 설명 - - [ ] 저장소 구조: admin → mng - -#### 1.2 dev-commands.md -- **상태**: ⏳ 대기 -- **주요 변경**: - - [ ] "Admin Application (admin/)" → "MNG Application (mng/)" - - [ ] admin/ 경로 → mng/ 경로 - - [ ] 업데이트 날짜 갱신 - -#### 1.3 quick-start.md -- **상태**: ⏳ 대기 -- **주요 변경**: - - [ ] claudedocs/SAM/ 경로 → docs/ 경로 - - [ ] 프로젝트 구조에 mng, design, planning 추가 - - [ ] admin/ 참조 → mng/ 참조 - - [ ] 업데이트 날짜 갱신 - -### 4.2 Phase 4: 오래된 파일 정리/아카이브 - -#### 4.1 history/2025-09/ 문서 검토 -- **상태**: ⏳ 대기 -- **대상 파일**: - - `history/2025-09/checkpoint.md` - 구버전 체크포인트 - - `history/2025-09/database-schema.md` - 구버전 스키마 (참조용 유지 검토) -- **조치**: 아카이브 적합성 검토, 불필요시 삭제 - -#### 4.2 history/2025-11/ 문서 검토 -- **상태**: ⏳ 대기 -- **대상 파일**: - - `history/2025-11/item-master-gap-analysis.md` - - `history/2025-11/item-master-spec.md` - - `history/2025-11/front-requests/` 디렉토리 - - `history/2025-11/item-master-archived/` 디렉토리 -- **조치**: 현재 유효성 검토, 아카이브 정리 - -#### 4.3 admin 참조 파일 식별 및 정리 -- **상태**: ⏳ 대기 -- **검색 대상**: docs/ 전체에서 "admin" 키워드 포함 파일 -- **조치**: mng로 전환 또는 deprecated 표시 - -#### 4.4 완료된 plans/ 문서 정리 -- **상태**: ⏳ 대기 -- **대상 파일**: - - 완료된 계획 문서 식별 - - 현재 진행중인 문서 유지 -- **조치**: 완료된 계획은 삭제 또는 history/로 이동 - -#### 4.5 중복/불필요 문서 정리 -- **상태**: ⏳ 대기 -- **검토 대상**: - - 내용이 중복된 문서 - - 더 이상 유효하지 않은 문서 - - 통합 가능한 문서 -- **조치**: 통합, 삭제, 또는 아카이브 - ---- - -## 5. 컨펌 대기 목록 - -> 구조 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| - | - | - | - | - | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-12-26 | - | 계획 문서 초안 작성 | - | - | -| 2025-12-26 | Phase 4 | 오래된 파일 정리/아카이브 작업 추가 | docs-update-plan.md | - | -| 2025-12-26 | Phase 1 | 핵심 문서 3개 업데이트 완료 | system-overview.md, dev-commands.md, quick-start.md | ✅ | -| 2025-12-26 | Phase 2 | 보조 문서 3개 업데이트 완료 | INDEX.md, quality-checklist.md, swagger-guide.md | ✅ | -| 2025-12-26 | Phase 3 | 검증 및 정리 완료 | security-policy.md, database-schema.md | ✅ | -| 2025-12-26 | Phase 4.1-4.2 | history/ 문서 검토 완료 | - | ✅ | -| 2025-12-26 | Phase 4.4 | plans/ 정리 완료 | D0.8→history, index_plans.md 업데이트 | ✅ | -| 2025-12-26 | Phase 4.3 | admin 참조 파일 정리 | docker-setup, git-conventions, project-launch-roadmap, remote-work-setup | ✅ | - ---- - -## 7. 참고 문서 - -- **문서 인덱스**: `docs/INDEX.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **Serena 메모리**: `docs-update-analysis.md` - ---- - -## 8. 세션 관리 정책 - -### 8.1 세션 시작 시 -``` -list_memories() → 기존 상태 확인 -read_memory("docs-update-analysis") → 분석 결과 로드 -이 계획 문서 읽기 → 컨텍스트 로드 -``` - -### 8.2 작업 중 -- 변경 이력 실시간 업데이트 -- Phase/항목별 상태 업데이트 -- 컨펌 필요 시 대기 목록 추가 - -### 8.3 세션 종료 시 -``` -변경 이력에 최종 업데이트 기록 -write_memory("docs-update-progress") → Serena에 저장 -``` - -### 8.4 Serena 메모리 구조 -``` -docs-update-analysis.md # 분석 결과 (완료) -docs-update-progress.md # 진행 상황 (작업 중 업데이트) -``` - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 문서 일관성 체크 - -| 문서 | admin 참조 | mng 반영 | 날짜 최신화 | 링크 유효 | -|------|:----------:|:--------:|:-----------:|:---------:| -| system-overview.md | | | | | -| dev-commands.md | | | | | -| quick-start.md | | | | | -| INDEX.md | | | | | -| quality-checklist.md | | | | | -| swagger-guide.md | | | | | -| security-policy.md | | | | | -| database-schema.md | | | | | - -### 9.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| admin 참조 완전 제거 | | | -| mng 반영 완료 | | | -| 버전 정보 최신화 | | | -| 상호 참조 링크 유효 | | | - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* diff --git a/plans/archive/document-management-system-changelog.md b/plans/archive/document-management-system-changelog.md deleted file mode 100644 index ee81f29..0000000 --- a/plans/archive/document-management-system-changelog.md +++ /dev/null @@ -1,31 +0,0 @@ -# 문서관리 시스템 - 변경 이력 - -> **본 문서**: `docs/plans/document-management-system-plan.md`의 변경 이력 -> **최종 업데이트**: 2026-02-12 - ---- - -## 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 관련 섹션 | 승인 | -|------|------|----------|----------|------| -| 2026-01-31 | 초안 | 기존 시스템 분석 기반 계획 문서 전면 재작성 | 본 문서 | - | -| 2026-01-31 | Phase 1.1 완료 | 양식 편집 UI 5개 탭 전체 CRUD 확인 (사실상 완료) | 섹션 3.1, 11.1 | - | -| 2026-01-31 | Phase 1.2 완료 | viewJS.php 라우팅 분석 + EGI/SUS 대표 2종 상세 분석 + 공통패턴 추출 | 섹션 3.1, 11.2 | - | -| 2026-01-31 | Phase 1.3 완료 | IncomingInspectionTemplateSeeder 생성. EGI(ID:7), SUS(ID:8) 2종 시드 완료. 결재2+기본필드10+섹션+항목+컬럼 전체 | 섹션 3.1 | - | -| 2026-01-31 | Phase 1.4 완료 | 미리보기 기능 기존 구현 확인. 모달로 결재란+기본정보+검사이미지+검사테이블(complex)+Footer 모두 렌더링 | 섹션 3.1 | - | -| 2026-01-31 | Phase 1.5 완료 | 양식 복제 기능. duplicate() 메서드 + 라우트 + 테이블 버튼 + JS 함수 추가 | 섹션 3.1 | - | -| 2026-01-31 | Phase 2.1 완료 | 문서 생성 기능 보완. ①문서번호 카테고리별 prefix(IQC/PRD/SLS/PUR, YYMMDD-순번) ②결재라인 초기화(template.approvalLines→document_approvals) ③기본필드 뷰 속성 불일치 수정(field_type/label/default_value 매핑, Str::slug로 field_key 생성) ④섹션 title 참조 수정 | 섹션 3.2 | - | -| 2026-01-31 | Phase 2.2 완료 | 문서 데이터 입력 UI. ①섹션별 동적 검사 테이블 렌더링(complex/select/check/measurement/text 컬럼 타입 지원) ②서브 라벨 행(complex 컬럼의 n1/n2/n3) ③정적 컬럼 자동 매핑(NO/검사항목/검사기준/검사방식/검사주기→item속성) ④종합판정+비고 Footer ⑤JS 폼 데이터 수집(기본필드+섹션데이터+체크박스) ⑥백엔드 saveDocumentData() 공통 메서드(section_id/column_id/row_index EAV 저장) | 섹션 3.2 | - | -| 2026-01-31 | Phase 2.3 완료 | 결재 워크플로우. ①API: submit(DRAFT→PENDING), approve(단계별 승인, 전체 완료 시 APPROVED), reject(반려 사유 필수, REJECTED) ②edit.blade: 결재 제출 버튼 + JS ③show.blade: 승인/반려 버튼, 반려 모달, 결재 현황 속성 수정(step/role/acted_at), 상태 배지 CSS ④재제출 시 결재라인 상태 초기화 ⑤라우트: submit/approve/reject 3개 추가 | 섹션 3.2 | - | -| 2026-01-31 | Phase 2.4 완료 | 문서 목록/검색/필터. ①날짜 범위 필터(date_from/date_to) API + UI 추가 ②DRAFT 문서 삭제 버튼 + deleteDocument() JS (showDeleteConfirm + fetch DELETE) ③기존 구현 확인: 상태/템플릿/검색/페이징 정상 동작 | 섹션 3.2 | - | -| 2026-01-31 | Phase 3.1 완료 | 중간검사 양식 구조 설계. ①5130 레거시 4종(절곡/스크린/슬랫/조인트바) viewMidInspect*.php 전체 분석 ②검사항목·기준·판정방식·공차·이미지 문서화 ③컬럼 구조(check/complex/select) 매핑 설계 ④4종 비교표 + 양식 시스템 매핑 전략(Option A/B/C) ⑤공통 구조(결재3단계, 기본필드7개, Footer) 정의 | 섹션 5.2 | - | -| 2026-01-31 | Phase 3.2 완료 | 5130 중간검사 데이터 이관 설계. ①JSON 공통 배열 구조 분석([0]결재/[1]입력값/[2]num/[3]table/[4]log/[5]checkbox) ②JSON→EAV 매핑 테이블(결재→document_approvals, 기본필드/측정값/체크박스→document_data) ③데이터 변환 규칙(날짜mm/dd→datetime, boolean→string, 이름→user_id) ④6단계 이관 프로세스 설계 ⑤절곡품 inputValue named object vs 나머지 flat array 차이 문서화 ⑥주의사항 5건 | 섹션 5.3 | - | -| 2026-01-31 | Phase 3.3 완료 | 중간검사 양식 시드 데이터. MidInspectionTemplateSeeder 생성. ①조인트바(ID:10, 1섹션6항목8컬럼, 고정기준값4개) ②슬랫(ID:11, 1섹션5항목7컬럼, 고정2+도면1) ③스크린(ID:12, 1섹션6항목8컬럼, 겉모양3+치수3) ④절곡품(ID:13, 4섹션11항목7컬럼, 구성품별 분리) ⑤공통: 결재3단계(판매→생산→품질), 기본필드7개, Footer(부적합+종합판정) | 섹션 3.3 | - | -| 2026-01-31 | Phase 3.4 완료 | 검사 기준 이미지 이관. 5130/img/inspection/ → mng/public/img/inspection/ (27개 파일). 가이드레일(벽면/측면×6변형), 하단마감재(4), 케이스(4), 절곡기준서(2), 스크린/슬랫/조인트바(각1), L-BAR(1), 연기차단재(1) | 섹션 5.4 | - | -| 2026-01-31 | Phase 4.1 완료 | API 엔드포인트 설계. ①DocumentTemplate 모델 6개(Template+ApprovalLine+BasicField+Section+SectionItem+Column) ②DocumentTemplateService(list+show) ③DocumentTemplateController(index+show) ④IndexRequest FormRequest ⑤라우트 2개(GET /v1/document-templates, GET /v1/document-templates/{id}) ⑥DocumentTemplateApi.php Swagger(7개 스키마) ⑦Document 결재 워크플로우 활성화(submit/approve/reject/cancel 4개 엔드포인트) ⑧ApproveRequest+RejectRequest FormRequest ⑨DocumentApi.php Swagger에 결재 4개 추가 ⑩Document.template() 참조 경로 수정 | 섹션 3.4, 4.1, 7 | - | -| 2026-01-31 | Phase 4.2 완료 | mng JSON 기반 문서 화면. ①show.blade.php 섹션 테이블 읽기전용 렌더링(complex/select/check/measurement/text 5가지 컬럼 타입) ②select 판정값 배지(적합=초록, 부적합=빨강) ③check 체크마크 SVG ④measurement mono 폰트 ⑤정적 컬럼 매핑(NO/검사항목/기준/방식/주기/규격/분류) ⑥종합판정+비고 Footer(마지막 섹션에 표시) ⑦검사 기준 이미지 표시 ⑧버그 3건 수정: field_key→Str::slug, field_type→field_type, section.name→title | 섹션 3.4 | - | -| 2026-01-31 | Phase 4.3 완료 | 문서 데이터 입력/저장 연동 검증. Phase 2.2~2.3에서 이미 완전 구현 확인: ①edit.blade.php JS 폼 수집(기본필드+섹션데이터+체크박스) ②fetch POST/PATCH→DocumentApiController ③saveDocumentData() EAV 저장(section_id/column_id/row_index) ④판정(적합/부적합) select+종합판정 Footer 저장 정상 ⑤6.2 결정사항 #2(프론트 입력, 결과만 저장) 적용됨. 추가 코드 작업 없음 | 섹션 3.4 | - | -| 2026-02-10 | Phase 5 계획 수립 | Phase 5 확장 계획 수립. ①마스터 진행 관리 문서 신규 생성(document-system-master.md) ②중간검사(PQC) 상세 계획(document-system-mid-inspection.md) ③제품검사(FQC) 상세 계획(document-system-product-inspection.md) ④작업일지 상세 계획(document-system-work-log.md) ⑤핵심 결정사항 5건: 조인트바=슬랫하위유지, 제품검사=개소별1문서, 작업일지=하이브리드, 제품검사=품질검사 동일, 기타문서=추후정의 ⑥기존 plan 문서 Phase 5 섹션 업데이트 | 섹션 3.5, 마스터 문서 | - | -| 2026-02-10 | 방안1 채택 | 검사기준서↔테이블컬럼 연동 분석 및 방안1 결정. ①edit.blade.php 분석(검사기준서 탭=section_fields+items, 테이블컬럼 탭=columns, 완전 독립) ②이슈 수정: 스키마 불일치→section_fields 누락이 실제 원인(컬럼은 모두 존재) ③방안1 채택: items.measurement_type→columns 자동 파생, 테이블컬럼 탭은 확인/미세조정용 ④Phase 5.0 신설(3개 작업: 자동파생 JS, 시더 section_fields 추가, 탭 모드 전환) ⑤결정사항 #9/#10 추가 ⑥4개 문서 업데이트(master, mid-inspection, product-inspection, changelog) | 마스터 섹션 7.5, 결정사항 | - | -| 2026-02-12 | Phase 5.2 전체 완료 | 제품검사(FQC) 폼 구현 5/5 완료. ①5.2.1 ProductInspectionTemplateSeeder(template_id:65, 결재3+기본필드7+섹션2+항목11) ②5.2.2 mng 양식 편집/미리보기 검증 ③5.2.3 API bulk-create-fqc+fqc-status 엔드포인트(DocumentService.bulkCreateFqc/fqcStatus) ④5.2.4 React fqcActions.ts+FqcDocumentContent.tsx 신규, InspectionReportModal/ProductInspectionInputModal 듀얼모드(FQC양식/legacy하드코딩) 전환 ⑤5.2.5 InspectionDetail FQC 진행현황 통계바+개소별 상태뱃지(합격/불합격/진행중/미생성)+조회버튼. OrderSettingItem.orderId 기반 자동 활성화, 없으면 legacy fallback | Phase 5.2, 마스터 문서 | - | \ No newline at end of file diff --git a/plans/archive/document-system-product-inspection.md b/plans/archive/document-system-product-inspection.md deleted file mode 100644 index e43682b..0000000 --- a/plans/archive/document-system-product-inspection.md +++ /dev/null @@ -1,375 +0,0 @@ -# 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/archive/erp-api-development-plan-d1.0-changes.md b/plans/archive/erp-api-development-plan-d1.0-changes.md deleted file mode 100644 index d0920b6..0000000 --- a/plans/archive/erp-api-development-plan-d1.0-changes.md +++ /dev/null @@ -1,559 +0,0 @@ -# SAM ERP API 개발 작업 계획 - D1.0 변경사항 - -> **작성일**: 2025-12-19 -> **기준 문서**: SAM_ERP_Storyboard_D1.0_251218 (38페이지) -> **이전 버전**: SAM_ERP_Storyboard_D0.8_251216 (85페이지) -> **상태**: ✅ Phase 5 완료 | ✅ Phase 6 완료 | ✅ Phase 7 완료 | ✅ Phase 8 완료 - ---- - -## 📚 참고 문서 - -### 핵심 참고 문서 -| 문서 | 경로 | 용도 | -|------|------|------| -| **기존 개발 계획** | [`erp-api-development-plan.md`](./erp-api-development-plan.md) | D0.8 기준 Phase 1-4 | -| **개발 공통 정책** | [`../guides/PROJECT_DEVELOPMENT_POLICY.md`](../guides/PROJECT_DEVELOPMENT_POLICY.md) | 개발 표준 및 정책 | -| **D0.8 스토리보드** | [`SAM_ERP_Storyboard_D0.8_251216/`](./SAM_ERP_Storyboard_D0.8_251216/) | 이전 버전 UI 참조 | -| **D1.0 스토리보드** | [`SAM_ERP_Storyboard_D1.0_251218/`](./SAM_ERP_Storyboard_D1.0_251218/) | 최신 UI/UX 참조 | - -### 기존 코드 참조 -| 항목 | 경로 | 상태 | -|------|------|------| -| `Board` 모델 | `api/app/Models/Boards/Board.php` | ✅ 존재 | -| `BoardSetting` 모델 | `api/app/Models/Boards/BoardSetting.php` | ✅ 존재 | -| `BoardComment` 모델 | `api/app/Models/Boards/BoardComment.php` | ✅ 존재 | -| `Plan` 모델 | `api/app/Models/Tenants/Plan.php` | ✅ 존재 | -| `Subscription` 모델 | `api/app/Models/Tenants/Subscription.php` | ✅ 존재 | -| `PushNotificationSetting` | `api/app/Models/PushNotificationSetting.php` | ✅ 존재 | - ---- - -## 📊 D1.0 개발 범위 요약 - -| Phase | 구분 | 항목수 | 신규 테이블 | API 수 | 상태 | -|-------|------|--------|------------|--------|------| -| Phase 5 | 기본 확장 | 4개 | 1개 | ~14개 | ✅ 완료 | -| Phase 6 | 핵심 신규 | 2개 | 4개 | ~17개 | ✅ 완료 | -| Phase 7 | 게시판 연동 | 2개 | 0개 | ~15개 | ✅ 완료 | -| Phase 8 | SaaS 확장 | 3개 | 1개 | ~10개 | ✅ 완료 | -| **합계** | | **12개** | **~5개** | **~71개** | | - ---- - -## 🚀 Phase 5: D1.0 기본 확장 ✅ 완료 - -> 기존 테이블/모델 활용, API 추가 중심 -> **완료일: 2025-12-22** (기존 구현 확인) - -### 5.1 사용자 초대 기능 ✅ -> 슬라이드: 2 | 경로: 인사관리 > 사원관리 > 사용자 초대 -> **완료일: 2025-12-19** - -- [x] **테이블 생성** - - [x] `user_invitations` 마이그레이션 (2025_12_19_100001) - - [x] 마이그레이션 실행 및 검증 - -- [x] **모델 생성** - - [x] `UserInvitation` 모델 (BelongsToTenant) - - [x] 관계 정의 (inviter, role, tenant) - - [x] 토큰 생성 헬퍼 (`generateToken()`) - - [x] 상태 상수 (pending, accepted, expired, cancelled) - -- [x] **서비스 구현** - - [x] `UserInvitationService` 생성 - - [x] 이메일 초대 발송 로직 (`invite()`) - - [x] 초대 수락 로직 (`accept()`) - - [x] 토큰 만료 처리 (`expirePendingInvitations()`) - - [x] 초대 재발송 로직 (`resend()`) - -- [x] **API 엔드포인트** (5개) - - [x] `POST /v1/users/invite` - 사용자 초대 (이메일 발송) - - [x] `GET /v1/users/invitations` - 초대 목록 - - [x] `POST /v1/users/invitations/{token}/accept` - 초대 수락 - - [x] `DELETE /v1/users/invitations/{id}` - 초대 취소 - - [x] `POST /v1/users/invitations/{id}/resend` - 초대 재발송 - -- [x] **Swagger 문서** - - [x] `UserInvitationApi.php` 작성 - - [x] 스키마 정의 (UserInvitation, InviteRequest, AcceptRequest) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 5.2 알림설정 확장 ✅ -> 슬라이드: 19-22 | 경로: 기준정보 > 알림설정 -> **완료일: 2025-12-19** - -- [x] **테이블 확장** - - [x] `notification_settings` 테이블 확인/생성 - -- [x] **모델 생성/수정** - - [x] `NotificationSetting` 모델 (BelongsToTenant) - - [x] 카테고리별 그룹화 메서드 - -- [x] **서비스 구현** - - [x] `NotificationSettingService` 생성 - - [x] 카테고리별 조회/수정 로직 - - [x] 사용자별 기본값 생성 로직 - -- [x] **API 엔드포인트** (3개) - - [x] `GET /v1/users/me/notification-settings` - 알림 설정 조회 - - [x] `PUT /v1/users/me/notification-settings` - 알림 설정 수정 - - [x] `PUT /v1/users/me/notification-settings/bulk` - 알림 일괄 설정 - -- [x] **Swagger 문서** - - [x] `NotificationSettingApi.php` 작성 - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 5.3 계정정보 수정 (탈퇴/사용중지) ✅ -> 슬라이드: 24 | 경로: 계정정보 -> **완료일: 2025-12-19** - -- [x] **서비스 구현** - - [x] `AccountService` 생성/확장 - - [x] 회원 탈퇴 로직 (`withdraw()`) - - [x] 사용 중지 로직 (`suspend()`) - - [x] 약관 동의 정보 관리 (`getAgreements()`, `updateAgreements()`) - -- [x] **API 엔드포인트** (4개) - - [x] `POST /v1/account/withdraw` - 회원 탈퇴 - - [x] `POST /v1/account/suspend` - 사용 중지 (특정 테넌트) - - [x] `GET /v1/account/agreements` - 약관 동의 정보 조회 - - [x] `PUT /v1/account/agreements` - 약관 동의 정보 수정 - -- [x] **Swagger 문서** - - [x] `AccountApi.php` 확장 - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 5.4 매출 상세 확장 (거래명세서) ✅ -> 슬라이드: 9 | 경로: 회계관리 > 매출관리 > 매출 상세 -> **완료일: 2025-12-19** - -**기존 구성요소:** -- `Sale` 모델, `SaleService` 존재 -- `TaxInvoice` 모델 존재 (세금계산서) - -- [x] **서비스 확장** - - [x] `SaleService` 확장 - - [x] 거래명세서 조회 로직 (`getStatement()`) - - [x] 거래명세서 발행 로직 (`issueStatement()`) - - [x] 거래명세서 이메일 발송 로직 (`sendStatement()`) - -- [x] **API 엔드포인트** (3개) - - [x] `GET /v1/sales/{id}/statement` - 거래명세서 조회 - - [x] `POST /v1/sales/{id}/statement/issue` - 거래명세서 발행 - - [x] `POST /v1/sales/{id}/statement/send` - 거래명세서 이메일 발송 - -- [x] **Swagger 문서** - - [x] `SaleApi.php` 확장 (거래명세서 관련 추가) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -## 🔨 Phase 6: D1.0 핵심 신규 개발 (예상 2-3주) - -> 신규 테이블 + API 전체 신규 구현 - -### 6.1 악성채권 추심관리 ✅ -> 슬라이드: 10-13 | 경로: 회계관리 > 악성채권 추심관리 -> **완료일: 2025-12-19** (commit: c0af888) - -- [x] **테이블 생성** (3개) - - [x] `bad_debts` 마이그레이션 (2025_12_19_160001) - - [x] `bad_debt_documents` 마이그레이션 (2025_12_19_160002) - - [x] `bad_debt_memos` 마이그레이션 (2025_12_19_160003) - - [x] 마이그레이션 실행 및 검증 - -- [x] **모델 생성** (3개) - - [x] `BadDebt` 모델 (BelongsToTenant, SoftDeletes) - - 상태 상수: collecting, legal_action, recovered, bad_debt - - 관계: client, assignedUser, creator, documents, memos - - [x] `BadDebtDocument` 모델 - - 문서 유형: business_license, tax_invoice, additional - - [x] `BadDebtMemo` 모델 - -- [x] **서비스 구현** - - [x] `BadDebtService` 생성 (307줄) - - [x] 악성채권 등록/수정/삭제 로직 - - [x] 상태 전이 로직 (추심중→법적조치→회수완료/대손처리) - - [x] 요약 통계 (총 채권, 상태별 금액) - - [x] 서류 첨부/삭제 로직 - - [x] 메모 추가/삭제 로직 - -- [x] **API 엔드포인트** (11개) - - [x] `GET /v1/bad-debts` - 악성채권 목록 - - [x] `POST /v1/bad-debts` - 악성채권 등록 - - [x] `GET /v1/bad-debts/summary` - 상단 요약 (총 채권, 상태별 금액) - - [x] `GET /v1/bad-debts/{id}` - 악성채권 상세 - - [x] `PUT /v1/bad-debts/{id}` - 악성채권 수정 - - [x] `DELETE /v1/bad-debts/{id}` - 악성채권 삭제 - - [x] `PATCH /v1/bad-debts/{id}/toggle` - 설정 ON/OFF - - [x] `POST /v1/bad-debts/{id}/documents` - 서류 첨부 - - [x] `DELETE /v1/bad-debts/{id}/documents/{docId}` - 서류 삭제 - - [x] `POST /v1/bad-debts/{id}/memos` - 메모 추가 - - [x] `DELETE /v1/bad-debts/{id}/memos/{memoId}` - 메모 삭제 - -- [x] **Swagger 문서** - - [x] `BadDebtApi.php` 작성 (433줄) - - [x] 스키마 정의 (BadDebt, BadDebtDocument, BadDebtMemo, Summary) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 6.2 팝업관리 ✅ -> 슬라이드: 15-16 | 경로: 기준정보 > 팝업관리 -> **완료일: 2025-12-19** - -- [x] **테이블 생성** (1개) - - [x] `popups` 마이그레이션 - ```sql - -- popups (팝업) - id, tenant_id, target_type, target_id, - title, content, status, - started_at, ended_at, options, - created_by, updated_by, deleted_by, - created_at, updated_at, deleted_at - ``` - - [x] 마이그레이션 실행 및 검증 - -- [x] **모델 생성** - - [x] `Popup` 모델 (BelongsToTenant, SoftDeletes) - - target_type: all, department - - status: active, inactive - - 활성 팝업 스코프 (기간 + 상태 체크) - -- [x] **서비스 구현** - - [x] `PopupService` 생성 - - [x] 팝업 CRUD 로직 - - [x] 활성 팝업 조회 로직 (로그인 후 노출용) - - [x] 기간 유효성 검사 로직 - -- [x] **API 엔드포인트** (6개) - - [x] `GET /v1/popups` - 팝업 목록 (관리자용) - - [x] `POST /v1/popups` - 팝업 등록 - - [x] `GET /v1/popups/active` - 활성 팝업 목록 (사용자용) - - [x] `GET /v1/popups/{id}` - 팝업 상세 - - [x] `PUT /v1/popups/{id}` - 팝업 수정 - - [x] `DELETE /v1/popups/{id}` - 팝업 삭제 - -- [x] **Swagger 문서** - - [x] `PopupApi.php` 작성 - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -## 📋 Phase 7: D1.0 게시판 연동 ✅ 완료 -> **완료일: 2025-12-19** - -> 기존 Board 모델 활용, API 엔드포인트 추가 - -**기존 구성요소 (api 프로젝트):** -- `Board` 모델: is_system, board_type, board_code, name, extra_settings -- `BoardSetting` 모델: 커스텀 필드 정의 -- `BoardComment` 모델: 댓글 -- `Post` 모델: 게시글 - -### 7.1 게시판관리 ✅ -> 슬라이드: 17-18 | 경로: 기준정보 > 게시판관리 -> **완료일: 2025-12-19** (기존 구현 활용) - -- [x] **기존 모델 확인/확장** - - [x] `Board` 모델 확인 - - [x] `BoardSetting` 모델 확인 - - [x] 필요 필드 이미 존재 - -- [x] **서비스 구현** - - [x] `BoardService` 존재 (테넌트별 게시판 CRUD 로직) - -- [x] **API 엔드포인트** (5개) - - [x] `GET /v1/boards` - 게시판 목록 - - [x] `POST /v1/boards` - 게시판 생성 - - [x] `GET /v1/boards/{id}` - 게시판 상세 - - [x] `PUT /v1/boards/{id}` - 게시판 수정 - - [x] `DELETE /v1/boards/{id}` - 게시판 삭제 - -- [x] **Swagger 문서** - - [x] `BoardApi.php` 작성 완료 - ---- - -### 7.2 게시판 (사용자용) ✅ -> 슬라이드: 3-7 | 경로: 게시판 -> **완료일: 2025-12-19** - -- [x] **기존 모델 확인/확장** - - [x] `Post` 모델 확인 - - [x] 상단 노출 필드 (is_notice) - - [x] 조회수 필드 (views) - -- [x] **서비스 구현** - - [x] `PostService` 존재 - - [x] 게시글 CRUD 로직 - - [x] 상단 노출 로직 - - [x] 조회수 증가 로직 - - [x] 나의 게시글 조회 로직 ✅ 추가됨 - -- [x] **API 엔드포인트** (10개) - - [x] `GET /v1/boards` - 게시판 목록 (탭용) - - [x] `GET /v1/boards/{code}/posts` - 게시글 목록 - - [x] `POST /v1/boards/{code}/posts` - 게시글 등록 - - [x] `GET /v1/boards/{code}/posts/{id}` - 게시글 상세 - - [x] `PUT /v1/boards/{code}/posts/{id}` - 게시글 수정 - - [x] `DELETE /v1/boards/{code}/posts/{id}` - 게시글 삭제 - - [x] `GET /v1/posts/my` - 나의 게시글 ✅ 신규 추가 - - [x] `GET /v1/boards/{code}/posts/{id}/comments` - 댓글 목록 - - [x] `POST /v1/boards/{code}/posts/{id}/comments` - 댓글 등록 - - [x] `PUT /v1/boards/{code}/posts/{id}/comments/{commentId}` - 댓글 수정 - - [x] `DELETE /v1/boards/{code}/posts/{id}/comments/{commentId}` - 댓글 삭제 - -- [x] **Swagger 문서** - - [x] `BoardApi.php` 작성 완료 - - [x] `PostApi.php` 작성 완료 - ---- - -### 7.3 고객센터 → 게시판관리로 대체 ⏭️ -> 슬라이드: 30-38 | 경로: 고객센터 - -**결정사항:** 고객센터 기능은 기존 게시판관리 시스템으로 구현 -- 공지사항, 이벤트, FAQ, 1:1 문의 → 게시판 유형(board_code)으로 관리 -- 별도 SupportAPI 불필요, 기존 Board/Post API 활용 - ---- - -## 💼 Phase 8: D1.0 SaaS 확장 (예상 1-2주) - -> 기존 Plan/Subscription/Payment 모델 활용 - -### 8.1 구독관리 ✅ -> 슬라이드: 28 | 경로: 구독관리 -> **완료일: 2025-12-22** (기존 구현 확인) - -**기존 구성요소:** -- `Plan` 모델: name, code, price, features(json) -- `Subscription` 모델: tenant_id, plan_id, started_at, ended_at, status -- `DataExport` 모델: 데이터 내보내기 - -- [x] **서비스 확장** - - [x] `SubscriptionService` 확장 (432줄) - - [x] 현재 구독 정보 조회 로직 (`current()`) - - [x] 사용량 조회 로직 (`usage()`) - - [x] 자료 내보내기 로직 (`createExport()`, `getExport()`) - - [x] 서비스 해지 로직 (`cancel()`) - -- [x] **API 엔드포인트** (5개 + 추가 6개) - - [x] `GET /v1/subscriptions/current` - 현재 구독 정보 - - [x] `GET /v1/subscriptions/usage` - 사용량 조회 - - [x] `POST /v1/subscriptions/export` - 자료 내보내기 요청 - - [x] `GET /v1/subscriptions/export/{id}` - 내보내기 상태 조회 - - [x] `POST /v1/subscriptions/{id}/cancel` - 서비스 해지 - - [x] `GET /v1/subscriptions` - 구독 목록 (추가) - - [x] `POST /v1/subscriptions` - 구독 등록 (추가) - - [x] `GET /v1/subscriptions/{id}` - 구독 상세 (추가) - - [x] `POST /v1/subscriptions/{id}/renew` - 구독 갱신 (추가) - - [x] `POST /v1/subscriptions/{id}/suspend` - 일시정지 (추가) - - [x] `POST /v1/subscriptions/{id}/resume` - 재개 (추가) - -- [x] **Swagger 문서** - - [x] `SubscriptionApi.php` 작성 (526줄) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- -### 8.2 결제내역 ✅ -> 슬라이드: 29 | 경로: 결제내역 -> **완료일: 2025-12-22** (기존 구현 확인) - -**기존 구성요소:** -- `Payment` 모델: subscription_id, amount, payment_method, paid_at, status - -- [x] **서비스 확장** - - [x] `PaymentService` 확장 (357줄) - - [x] 결제 내역 목록 조회 로직 (`index()`) - - [x] 거래명세서 생성 로직 (`statement()`) - - [x] 결제 요약 통계 (`summary()`) - -- [x] **API 엔드포인트** (2개 + 추가 6개) - - [x] `GET /v1/payments` - 결제 내역 목록 - - [x] `GET /v1/payments/{id}/statement` - 거래명세서 조회 - - [x] `GET /v1/payments/summary` - 결제 요약 통계 (추가) - - [x] `GET /v1/payments/{id}` - 결제 상세 (추가) - - [x] `POST /v1/payments` - 결제 등록 (추가) - - [x] `POST /v1/payments/{id}/complete` - 완료 처리 (추가) - - [x] `POST /v1/payments/{id}/cancel` - 취소 (추가) - - [x] `POST /v1/payments/{id}/refund` - 환불 (추가) - -- [x] **Swagger 문서** - - [x] `PaymentApi.php` 작성 (455줄) - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -### 8.3 회사 추가 ✅ -> 슬라이드: 25-27 | 경로: 회사정보 -> **완료일: 2025-12-22** - -- [x] **테이블 생성** (1개) - - [x] `company_requests` 마이그레이션 - ```sql - -- company_requests (회사 추가 신청) - id, user_id, business_number, company_name, ceo_name, - address, phone, email, status, message, reject_reason, - barobill_response(json), approved_by, created_tenant_id, - processed_at, created_at, updated_at - ``` - - [x] 마이그레이션 실행 및 검증 - -- [x] **모델 생성** - - [x] `CompanyRequest` 모델 - - 상태 상수: pending, approved, rejected - - 관계: user, approver, createdTenant - - 스코프: pending(), approved(), rejected() - -- [x] **서비스 구현** - - [x] `CompanyService` 생성 - - [x] 사업자등록번호 유효성 검사 (바로빌 연동 + 체크섬 검증) - - [x] 회사 추가 신청 로직 - - [x] 신청 승인 로직 (테넌트 자동 생성 + 사용자 연결) - - [x] 신청 반려 로직 - - [x] 신청 목록 조회 (관리자용/사용자용) - -- [x] **API 엔드포인트** (7개) - - [x] `POST /v1/companies/check` - 사업자등록번호 유효성 검사 - - [x] `POST /v1/companies/request` - 회사 추가 신청 - - [x] `GET /v1/companies/requests` - 신청 목록 (관리자용) - - [x] `GET /v1/companies/requests/{id}` - 신청 상세 - - [x] `POST /v1/companies/requests/{id}/approve` - 승인 - - [x] `POST /v1/companies/requests/{id}/reject` - 반려 - - [x] `GET /v1/companies/my-requests` - 내 신청 목록 - -- [x] **Swagger 문서** - - [x] `CompanyApi.php` 작성 - -- [ ] **테스트** - - [ ] Feature 테스트 작성 - - [ ] 수동 API 테스트 - ---- - -## 📋 기획 확인 필요 항목 - -> ⚠️ API 구현 전 비즈니스 로직 확정 필요 - -### D1.0 신규 확인 필요 -- [ ] 사용자 초대 시 권한 범위 (테넌트 단위 vs 전사) -- [ ] 악성채권 자동 판정 조건 (연체일수 기준, 기본 90일?) -- [ ] 팝업 노출 우선순위 (복수 팝업 시) -- [ ] 서비스 해지 시 데이터 보관 기간 -- [ ] 자료 내보내기 포맷 (Excel, CSV, JSON) -- [ ] 상단 노출 게시글 최대 개수 (기본 5개) -- [ ] 1:1 문의 상담분류 목록 (문의하기, 신고하기, 건의사항, 서비스 오류) - -### 기존 확인 사항 (D0.8) -- [ ] 테넌트: 신청→승인→만료→해지 전이 조건 -- [ ] 전자결재→회계: 지출결의서 승인 시 출금 자동 생성? -- [ ] 바로빌 API 비용 확인 - ---- - -## 📝 작업 일지 - -### 2025-12-19 -- [x] D1.0 스토리보드 분석 완료 (38페이지) -- [x] D0.8 대비 변경사항 식별 (신규 8개, 수정 4개) -- [x] D1.0 개발 계획 문서 작성 (Phase 5-8) -- [x] 기존 코드베이스 분석 (Board, Plan, Subscription 모델 확인) -- [x] Phase 6.1 악성채권 추심관리 API 개발 완료 (commit: c0af888) -- [x] Phase 6.2 팝업관리 API 개발 완료 -- [x] Phase 7.1 게시판관리 - 기존 구현 확인 완료 -- [x] Phase 7.2 게시판(사용자용) - 기존 구현 확인 + `/posts/my` API 추가 (commit: c15a245) -- [x] Phase 7.3 고객센터 → 게시판관리로 대체 결정 -- [x] Phase 8 SaaS 확장 분석 시작 - -### 2025-12-22 -- [x] Phase 8.1 구독관리 - 기존 구현 확인 완료 -- [x] Phase 8.2 결제내역 - 기존 구현 확인 완료 -- [x] Phase 8.3 회사 추가 API 개발 완료 (commit: 7781253) - - company_requests 테이블 생성 - - CompanyRequest 모델 생성 - - CompanyService 생성 (바로빌 연동 + 테넌트 생성) - - 7개 API 엔드포인트 구현 - - Swagger 문서 작성 -- [x] Phase 5 전체 기존 구현 확인 완료 - - 5.1 사용자 초대: 5개 API (invite, invitations, accept, cancel, resend) - - 5.2 알림설정: 3개 API (notification-settings, update, bulk) - - 5.3 계정정보: 4개 API (withdraw, suspend, agreements) - - 5.4 매출 거래명세서: 3개 API (statement, issue, send) -- [x] D1.0 Phase 5-8 전체 API 개발 완료! - ---- - -## ✅ 완료 기준 - -### Phase 5 완료 조건 (기본 확장) ✅ -- [x] 사용자 초대 API 구현 완료 ✅ 2025-12-19 -- [x] 알림설정 API 확장 완료 ✅ 2025-12-19 -- [x] 계정정보 API 확장 완료 ✅ 2025-12-19 -- [x] 매출 거래명세서 API 구현 완료 ✅ 2025-12-19 -- [x] Swagger 문서 완성 ✅ 2025-12-19 -- [x] Pint 코드 포맷팅 완료 ✅ - -### Phase 6 완료 조건 (핵심 신규) -- [x] 악성채권 추심관리 전체 구현 ✅ 2025-12-18 -- [x] 팝업관리 전체 구현 ✅ 2025-12-19 -- [x] 마이그레이션 검증 완료 -- [x] Swagger 문서 완성 - -### Phase 7 완료 조건 (게시판 연동) ✅ -- [x] 게시판관리 API 구현 완료 ✅ 2025-12-19 -- [x] 게시판 (사용자용) API 구현 완료 ✅ 2025-12-19 -- [x] 고객센터 → 게시판관리로 대체 결정 ✅ 2025-12-19 - -### Phase 8 완료 조건 (SaaS 확장) ✅ -- [x] 구독관리 API 구현 완료 ✅ 2025-12-22 -- [x] 결제내역 API 구현 완료 ✅ 2025-12-22 -- [x] 회사 추가 API 구현 완료 ✅ 2025-12-22 -- [x] 자료 내보내기 기능 구현 ✅ (SubscriptionService에 포함) - -### 전체 완료 조건 -- [ ] 모든 D1.0 API 구현 완료 (~71개) -- [ ] Swagger 문서 100% -- [ ] 통합 테스트 통과 -- [ ] 프론트엔드 연동 준비 완료 - ---- - -## 🔗 관련 링크 - -- **기존 개발 계획**: [`erp-api-development-plan.md`](./erp-api-development-plan.md) -- **API Swagger UI**: http://sam.kr/api-docs/index.html -- **개발 공통 정책**: [`../guides/PROJECT_DEVELOPMENT_POLICY.md`](../guides/PROJECT_DEVELOPMENT_POLICY.md) -- **D1.0 스토리보드**: [`SAM_ERP_Storyboard_D1.0_251218/`](./SAM_ERP_Storyboard_D1.0_251218/) \ No newline at end of file diff --git a/plans/archive/fcm-user-targeted-notification-plan.md b/plans/archive/fcm-user-targeted-notification-plan.md deleted file mode 100644 index 59389e2..0000000 --- a/plans/archive/fcm-user-targeted-notification-plan.md +++ /dev/null @@ -1,369 +0,0 @@ -# FCM 사용자별 알림 발송 계획 - -> **작성일**: 2026-01-28 -> **목적**: FCM 푸시 알림을 테넌트 전체 브로드캐스트에서 사용자별 타겟 발송으로 변경 -> **상태**: ✅ 구현 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 - FCM 발송 로직 수정 완료 | -| **다음 작업** | 테스트 검증 | -| **진행률** | 8/8 (100%) | -| **마지막 업데이트** | 2026-01-28 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 TodayIssue 생성 시 FCM 푸시 알림이 **테넌트 전체 사용자** 중 알림 설정이 켜진 모든 사용자에게 발송됨. - -**문제점**: -- 결재요청 알림이 결재자가 아닌 사람에게도 발송됨 -- 기안 승인/반려/완료 알림이 기안자가 아닌 사람에게도 발송됨 -- 불필요한 알림으로 사용자 경험 저하 - -### 1.2 목표 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 목표 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 이슈 타입에 따라 특정 대상자에게만 FCM 발송 │ -│ 2. 사용자별 알림 설정(ON/OFF)이 정상 동작하도록 보장 │ -│ 3. 근태 알림은 제외 (정책 미확정) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 발송 대상 정책 - -| 이슈 타입 | 현재 | 변경 후 대상 | -|-----------|------|-------------| -| **결재요청** | 테넌트 전체 | **결재자(나)** - ApprovalStep.user_id | -| **기안 승인** | 테넌트 전체 | **기안자** - Approval.drafter_id | -| **기안 반려** | 테넌트 전체 | **기안자** - Approval.drafter_id | -| **기안 완료** | 테넌트 전체 | **기안자** - Approval.drafter_id | -| 수주등록 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 추심이슈 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 안전재고 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 지출승인 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 세금신고 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 신규업체 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 입금 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| 출금 | 테넌트 전체 | 테넌트 전체 (변경 없음) | -| **근태 알림** | - | **제외** (정책 미확정) | - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 필드 추가, 로직 수정, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 마이그레이션, 새 테이블/컬럼 | **필수** | -| 🔴 금지 | 기존 테이블 구조 변경, 파괴적 변경 | 별도 협의 | - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: 데이터베이스 변경 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | TodayIssue 테이블에 `target_user_id` 컬럼 추가 | ✅ | nullable, FK | -| 1.2 | 마이그레이션 파일 생성 | ✅ | 2026_01_28_132426 | - -### 2.2 Phase 2: 모델 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | TodayIssue 모델에 target_user_id 추가 | ✅ | fillable, relation, scopes | -| 2.2 | TodayIssue::createIssue() 메서드에 targetUserId 파라미터 추가 | ✅ | | - -### 2.3 Phase 3: Observer 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | handleApprovalStepChange() - 결재요청 시 결재자 지정 | ✅ | step->user_id 전달 | -| 3.2 | 기안 승인/반려/완료 알림 추가 (기안자 지정) | ✅ | ApprovalIssueObserver 신규 | - -### 2.4 Phase 4: FCM 발송 로직 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | sendFcmNotification() - target_user_id 있으면 해당 사용자만 | ✅ | | -| 4.2 | getEnabledUserTokens() - 특정 사용자 필터링 로직 추가 | ✅ | | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Step 1: 데이터베이스 변경 -├── today_issues 테이블에 target_user_id 컬럼 추가 -├── 마이그레이션 실행 -└── 검증: 테이블 구조 확인 - -Step 2: TodayIssue 모델 수정 -├── target_user_id fillable 추가 -├── targetUser() relation 추가 -└── createIssue() 파라미터 추가 - -Step 3: TodayIssueObserverService 수정 -├── createIssueWithFcm() 파라미터 추가 -├── handleApprovalStepChange() 수정 - 결재자 지정 -├── 기안 상태 변경 알림 추가 (신규) -└── 근태 알림 비활성화 - -Step 4: FCM 발송 로직 수정 -├── sendFcmNotification() 수정 -├── getEnabledUserTokens() 수정 - targetUserId 파라미터 추가 -└── 검증: 대상자만 수신 확인 -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1: 데이터베이스 변경 - -**마이그레이션 파일**: -```php -// database/migrations/xxxx_add_target_user_id_to_today_issues_table.php - -Schema::table('today_issues', function (Blueprint $table) { - $table->unsignedBigInteger('target_user_id') - ->nullable() - ->after('source_id') - ->comment('특정 대상 사용자 ID (null이면 테넌트 전체)'); - - $table->foreign('target_user_id') - ->references('id') - ->on('users') - ->onDelete('cascade'); - - $table->index(['tenant_id', 'target_user_id']); -}); -``` - -### 4.2 Phase 2: TodayIssue 모델 수정 - -```php -// app/Models/Tenants/TodayIssue.php - -protected $fillable = [ - // ... 기존 필드 - 'target_user_id', // 추가 -]; - -public function targetUser(): BelongsTo -{ - return $this->belongsTo(User::class, 'target_user_id'); -} - -public static function createIssue( - int $tenantId, - string $sourceType, - ?int $sourceId, - string $badge, - string $content, - ?string $path = null, - bool $needsApproval = false, - ?\DateTime $expiresAt = null, - ?int $targetUserId = null // 추가 -): self { - // ... 기존 로직 + target_user_id 저장 -} -``` - -### 4.3 Phase 3: Observer 수정 - -**결재요청 - 결재자에게만**: -```php -// handleApprovalStepChange() 수정 - -$this->createIssueWithFcm( - tenantId: $approval->tenant_id, - sourceType: TodayIssue::SOURCE_APPROVAL, - sourceId: $step->id, - badge: TodayIssue::BADGE_APPROVAL_REQUEST, - content: __('message.today_issue.approval_pending', [...]), - path: '/approval/inbox', - needsApproval: true, - expiresAt: null, - targetUserId: $step->user_id // 결재자 -); -``` - -**기안 승인/반려/완료 - 기안자에게만** (신규): -```php -// handleApprovalStatusChange() 신규 메서드 - -public function handleApprovalStatusChange(Approval $approval): void -{ - $badge = match($approval->status) { - 'approved' => TodayIssue::BADGE_DRAFT_APPROVED, - 'rejected' => TodayIssue::BADGE_DRAFT_REJECTED, - 'completed' => TodayIssue::BADGE_DRAFT_COMPLETED, - default => null, - }; - - if (!$badge) return; - - $this->createIssueWithFcm( - tenantId: $approval->tenant_id, - sourceType: TodayIssue::SOURCE_APPROVAL, - sourceId: $approval->id, - badge: $badge, - content: __('message.today_issue.'.$approval->status, [...]), - path: '/approval/draft', - needsApproval: false, - expiresAt: Carbon::now()->addDays(7), - targetUserId: $approval->drafter_id // 기안자 - ); -} -``` - -### 4.4 Phase 4: FCM 발송 로직 수정 - -```php -// sendFcmNotification() 수정 - -public function sendFcmNotification(TodayIssue $issue): void -{ - // target_user_id가 있으면 해당 사용자만, 없으면 테넌트 전체 - $tokens = $this->getEnabledUserTokens( - $issue->tenant_id, - $issue->notification_type, - $issue->target_user_id // 추가 - ); - - // ... 기존 발송 로직 -} - -// getEnabledUserTokens() 수정 - -private function getEnabledUserTokens( - int $tenantId, - string $notificationType, - ?int $targetUserId = null // 추가 -): array { - $query = PushDeviceToken::withoutGlobalScopes() - ->where('tenant_id', $tenantId) - ->where('is_active', true) - ->whereNull('deleted_at'); - - // 특정 대상자가 지정된 경우 - if ($targetUserId !== null) { - $query->where('user_id', $targetUserId); - } - - $tokens = $query->get(); - - // 알림 설정 확인 후 필터링 - $enabledTokens = []; - foreach ($tokens as $token) { - if ($this->isNotificationEnabledForUser($tenantId, $token->user_id, $notificationType)) { - $enabledTokens[] = $token->token; - } - } - - return $enabledTokens; -} -``` - ---- - -## 5. 제외 항목 - -### 5.1 근태 알림 (정책 미확정) - -다음 알림 타입은 이번 작업에서 **제외**: -- 연차 알림 -- 출근 알림 -- 지각 알림 -- 결근 알림 - -**사유**: 정책이 모호하여 추후 별도 작업 - -### 5.2 알림 소리 커스터마이징 - -현재는 **하드코딩된 채널별 알림음** 사용: -- `push_urgent`: 긴급 (신규업체) -- `push_payment`: 결재 -- `push_sales_order`: 수주 -- `push_default`: 기타 - -**추후 작업**: 사용자별 알림 설정의 `soundType` 값 기준으로 발송 - ---- - -## 6. 영향받는 파일 - -### API (api/) - -| 파일 | 변경 내용 | -|------|----------| -| `database/migrations/2026_01_28_132426_add_target_user_id_to_today_issues_table.php` | 신규 - 마이그레이션 | -| `app/Models/Tenants/TodayIssue.php` | target_user_id 추가, 신규 뱃지 상수, targetUser 관계, forUser/targetedTo 스코프 | -| `app/Services/TodayIssueObserverService.php` | createIssueWithFcm, sendFcmNotification, getEnabledUserTokens 수정, handleApprovalStatusChange 추가 | -| `app/Observers/TodayIssue/ApprovalIssueObserver.php` | 신규 - 기안 상태 변경 Observer | -| `app/Providers/AppServiceProvider.php` | ApprovalIssueObserver 등록 | -| `lang/ko/message.php` | 신규 메시지 키 추가 (draft_approved/rejected/completed) | - -### React (react/) - 변경 없음 - -프론트엔드 알림 설정 UI는 이미 사용자별로 구현되어 있음. - ---- - -## 7. 검증 방법 - -### 7.1 테스트 시나리오 - -| # | 시나리오 | 예상 결과 | -|---|----------|----------| -| 1 | A가 B에게 결재 요청 | B에게만 FCM 발송 | -| 2 | B가 A의 기안 승인 | A에게만 FCM 발송 | -| 3 | B가 A의 기안 반려 | A에게만 FCM 발송 | -| 4 | 수주 등록 | 테넌트 전체 (알림 ON인 사용자만) | -| 5 | A가 알림 OFF → 수주 등록 | A에게는 발송 안됨 | - -### 7.2 성공 기준 - -- [ ] 결재요청 알림이 결재자에게만 발송됨 -- [ ] 기안 상태 변경 알림이 기안자에게만 발송됨 -- [ ] 사용자별 알림 설정(ON/OFF)이 정상 동작함 -- [ ] 기존 브로드캐스트 이슈(수주, 입금 등)는 정상 동작함 - ---- - -## 8. 참고 문서 - -- `api/app/Services/TodayIssueObserverService.php` - 현재 발송 로직 -- `api/app/Models/NotificationSetting.php` - 알림 설정 모델 -- `react/src/components/settings/NotificationSettings/types.ts` - 프론트엔드 알림 설정 타입 - ---- - -## 9. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-28 | - | 계획 문서 초안 작성 | - | - | -| 2026-01-28 | Phase 1 | target_user_id 컬럼 추가 마이그레이션 | migrations/2026_01_28_132426_* | ✅ | -| 2026-01-28 | Phase 2 | TodayIssue 모델 수정 (fillable, relation, scopes) | TodayIssue.php | ✅ | -| 2026-01-28 | Phase 3 | Observer 수정 (결재자/기안자 타겟팅) | TodayIssueObserverService.php, ApprovalIssueObserver.php | ✅ | -| 2026-01-28 | Phase 4 | FCM 발송 로직 수정 | TodayIssueObserverService.php | ✅ | -| 2026-01-28 | 신규 | ApprovalIssueObserver 생성 | ApprovalIssueObserver.php | ✅ | -| 2026-01-28 | i18n | 기안 상태 알림 메시지 추가 | lang/ko/message.php | ✅ | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/formula-engine-real-data-plan.md b/plans/archive/formula-engine-real-data-plan.md deleted file mode 100644 index 7114c42..0000000 --- a/plans/archive/formula-engine-real-data-plan.md +++ /dev/null @@ -1,1077 +0,0 @@ -# 수식 엔진 실제 데이터 연동 계획 - -> **작성일**: 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/archive/items-table-unification-plan.md b/plans/archive/items-table-unification-plan.md deleted file mode 100644 index eee1f67..0000000 --- a/plans/archive/items-table-unification-plan.md +++ /dev/null @@ -1,589 +0,0 @@ -# Items 테이블 통합 마이그레이션 계획 - -## 참조 문서 - -### 필수 확인 - -| 문서 | 경로 | 내용 | -|------|------|------| -| **ItemMaster 연동 설계서** | [specs/item-master-integration.md](../specs/item-master-integration.md) | source_table, EntityRelationship 구조 | -| **DB 스키마** | [specs/database-schema.md](../specs/database-schema.md) | 테이블 구조, Multi-tenant 아키텍처 | - -### 참고 문서 - -| 문서 | 경로 | 내용 | -|------|------|------| -| **품목관리 마이그레이션 가이드** | [projects/mes/ITEM_MANAGEMENT_MIGRATION_GUIDE.md](../projects/mes/ITEM_MANAGEMENT_MIGRATION_GUIDE.md) | 프론트엔드 마이그레이션 | -| **API 품목 분석 요약** | [projects/mes/00_baseline/docs_breakdown/api_item_analysis_summary.md](../projects/mes/00_baseline/docs_breakdown/api_item_analysis_summary.md) | 기존 API 분석, price_histories | -| **Swagger 가이드** | [guides/swagger-guide.md](../guides/swagger-guide.md) | API 문서화 규칙 | - -### 관련 코드 - -| 파일 | 경로 | 역할 | -|------|------|------| -| ItemPage 모델 | `api/app/Models/ItemMaster/ItemPage.php` | source_table 매핑 | -| EntityRelationship 모델 | `api/app/Models/ItemMaster/EntityRelationship.php` | 엔티티 관계 관리 | -| ItemMasterService | `api/app/Services/ItemMaster/ItemMasterService.php` | init API, 메타데이터 조회 | -| ProductService | `api/app/Services/ProductService.php` | 기존 Products API (제거 예정) | -| MaterialService | `api/app/Services/MaterialService.php` | 기존 Materials API (제거 예정) | - ---- - -## 개요 - -### 목적 -`products`/`materials` 테이블을 `items` 테이블로 통합하여: -- BOM 관리 시 `child_item_type` 불필요 (ID만으로 유일 식별) -- 단일 쿼리로 모든 품목 조회 가능 -- Item-Master 시스템과 일관된 구조 - -### 현재 상황 -- **개발 단계**: 미오픈 (레거시 호환 불필요) -- **Item-Master**: 메타데이터 시스템 운영 중 (pages, sections, fields) -- **이전 시도**: 12/11 items 생성 → 12/12 롤백 (정책 정리 필요) - -### 현재 시스템 구조 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Item-Master (메타데이터) │ -├─────────────────────────────────────────────────────────────┤ -│ item_pages (source_table: 'products'|'materials') │ -│ ↓ EntityRelationship │ -│ item_sections → item_fields, item_bom_items │ -└─────────────────────────────────────────────────────────────┘ - ↓ 참조 -┌─────────────────────────────────────────────────────────────┐ -│ 실제 데이터 테이블 │ -├─────────────────────────────────────────────────────────────┤ -│ products (808건) ← ProductController, ProductService │ -│ materials (417건) ← MaterialController, MaterialService │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 목표 구조 - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Item-Master (메타데이터) │ -├─────────────────────────────────────────────────────────────┤ -│ item_pages (source_table: 'items') │ -│ ↓ EntityRelationship │ -│ item_sections → item_fields, item_bom_items │ -└─────────────────────────────────────────────────────────────┘ - ↓ 참조 -┌─────────────────────────────────────────────────────────────┐ -│ 통합 데이터 테이블 │ -├─────────────────────────────────────────────────────────────┤ -│ items ← ItemController, ItemService │ -│ item_type: FG, PT, SM, RM, CS │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Phase 0: 데이터 정규화 - -### 0.1 item_type 표준화 - -개발 중이므로 비표준 데이터는 삭제 처리. 품목관리 완료 후 경동기업 데이터 전체 재세팅 예정. - -**표준 item_type 체계**: - -| 코드 | 설명 | 출처 | -|------|------|------| -| FG | 완제품 (Finished Goods) | products | -| PT | 부품 (Parts) | products | -| SM | 부자재 (Sub-materials) | materials | -| RM | 원자재 (Raw Materials) | materials | -| CS | 소모품 (Consumables) | materials만 | - -**비표준 데이터 삭제**: -```sql --- products에서 비표준 타입 삭제 (PRODUCT, SUBASSEMBLY, PART, CS) -DELETE FROM products WHERE product_type NOT IN ('FG', 'PT'); - --- materials는 이미 표준 타입만 사용 (SM, RM, CS) -``` - -### 0.2 BOM 데이터 정리 - -통합 시 문제되는 BOM 데이터 삭제: -```sql --- 삭제될 products/materials를 참조하는 BOM 항목 제거 --- (Phase 1 이관 전에 실행) -``` - -### 0.3 체크리스트 - -- [x] products 비표준 타입 삭제 -- [x] 관련 BOM 데이터 정리 -- [x] 삭제 건수 확인 - ---- - -## Phase 1: items 테이블 생성 + 데이터 이관 - -### 1.1 items 테이블 - -```sql -CREATE TABLE items ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - - -- 기본 정보 - item_type VARCHAR(15) NOT NULL COMMENT 'FG, PT, SM, RM, CS', - code VARCHAR(100) NOT NULL, - name VARCHAR(255) NOT NULL, - unit VARCHAR(20) NULL, - category_id BIGINT UNSIGNED NULL, - - -- BOM (JSON) - bom JSON NULL COMMENT '[{child_item_id, quantity}, ...]', - - -- 상태 - is_active TINYINT(1) DEFAULT 1, - - -- 감사 필드 - created_by BIGINT UNSIGNED NULL, - updated_by BIGINT UNSIGNED NULL, - deleted_by BIGINT UNSIGNED NULL, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - deleted_at TIMESTAMP NULL, - - -- 인덱스 - INDEX idx_items_tenant_type (tenant_id, item_type), - INDEX idx_items_tenant_code (tenant_id, code), - INDEX idx_items_tenant_category (tenant_id, category_id), - UNIQUE KEY uq_items_tenant_code (tenant_id, code, deleted_at), - - FOREIGN KEY (tenant_id) REFERENCES tenants(id), - FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -### 1.2 item_details 테이블 (확장 필드) - -```sql -CREATE TABLE item_details ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - item_id BIGINT UNSIGNED NOT NULL, - - -- Products 전용 필드 - is_sellable TINYINT(1) DEFAULT 1, - is_purchasable TINYINT(1) DEFAULT 0, - is_producible TINYINT(1) DEFAULT 0, - safety_stock INT NULL, - lead_time INT NULL, - is_variable_size TINYINT(1) DEFAULT 0, - product_category VARCHAR(50) NULL, - part_type VARCHAR(50) NULL, - - -- Materials 전용 필드 - is_inspection VARCHAR(1) DEFAULT 'N', - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uq_item_details_item_id (item_id), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -### 1.3 item_attributes 테이블 (동적 속성) - -```sql -CREATE TABLE item_attributes ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - item_id BIGINT UNSIGNED NOT NULL, - - attributes JSON NULL, - options JSON NULL, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uq_item_attributes_item_id (item_id), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -``` - -### 1.4 데이터 이관 스크립트 - -```php -// Products → Items -DB::statement(" - INSERT INTO items (tenant_id, item_type, code, name, unit, category_id, bom, - is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at) - SELECT tenant_id, product_type, code, name, unit, category_id, bom, - is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at - FROM products -"); - -// Materials → Items -DB::statement(" - INSERT INTO items (tenant_id, item_type, code, name, unit, category_id, - is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at) - SELECT tenant_id, material_type, material_code, name, unit, category_id, - is_active, created_by, updated_by, deleted_by, created_at, updated_at, deleted_at - FROM materials -"); -``` - -### 1.5 체크리스트 - -- [x] items 마이그레이션 생성 -- [x] item_details 마이그레이션 생성 -- [x] item_attributes 마이그레이션 생성 -- [x] 데이터 이관 스크립트 실행 -- [x] 건수 검증 (1,225건) - ---- - -## Phase 2: Item 모델 + Service 생성 - -### 2.1 Item 모델 - -```php -// app/Models/Item.php -class Item extends Model -{ - use BelongsToTenant, ModelTrait, SoftDeletes; - - protected $fillable = [ - 'tenant_id', 'item_type', 'code', 'name', 'unit', - 'category_id', 'bom', 'is_active', - ]; - - protected $casts = [ - 'bom' => 'array', - 'is_active' => 'boolean', - ]; - - // 1:1 관계 - public function details() { return $this->hasOne(ItemDetail::class); } - public function attributes() { return $this->hasOne(ItemAttribute::class); } - - // 타입별 스코프 - public function scopeProducts($q) { - return $q->whereIn('item_type', ['FG', 'PT']); - } - public function scopeMaterials($q) { - return $q->whereIn('item_type', ['SM', 'RM', 'CS']); - } -} -``` - -### 2.2 ItemService - -```php -// app/Services/ItemService.php -class ItemService extends Service -{ - public function index(array $params): LengthAwarePaginator - { - $query = Item::where('tenant_id', $this->tenantId()); - - // item_type 필터 - if ($itemType = $params['item_type'] ?? null) { - $query->where('item_type', strtoupper($itemType)); - } - - // 검색 - if ($search = $params['search'] ?? null) { - $query->where(fn($q) => $q - ->where('code', 'like', "%{$search}%") - ->orWhere('name', 'like', "%{$search}%") - ); - } - - return $query->with(['details', 'attributes'])->paginate($params['per_page'] ?? 15); - } -} -``` - -### 2.3 체크리스트 - -- [x] Item 모델 생성 -- [x] ItemDetail 모델 생성 -- [x] ItemAttribute 모델 생성 -- [x] ItemService 생성 -- [x] ItemRequest 생성 - ---- - -## Phase 3: Item-Master 연동 수정 - -### 3.1 ItemPage.source_table 변경 - -```php -// app/Models/ItemMaster/ItemPage.php - -// 기존 -$mapping = [ - 'products' => \App\Models\Product::class, - 'materials' => \App\Models\Material::class, -]; - -// 변경 -$mapping = [ - 'items' => \App\Models\Item::class, -]; -``` - -### 3.2 item_pages 데이터 업데이트 - -```sql --- source_table 통합 -UPDATE item_pages SET source_table = 'items' WHERE source_table IN ('products', 'materials'); -``` - -### 3.3 체크리스트 - -- [x] ItemPage 모델 수정 (getTargetModelClass) -- [x] item_pages.source_table 마이그레이션 -- [x] ItemMasterService 연동 테스트 - ---- - -## Phase 4: API 통합 - -### 4.1 API 구조 변경 - -``` -기존 (분리): - /api/v1/products → ProductController - /api/v1/products/materials → MaterialController - -통합 후: - /api/v1/items → ItemController - /api/v1/items?item_type=FG → Products 조회 - /api/v1/items?item_type=SM → Materials 조회 -``` - -### 4.2 ItemController - -```php -// app/Http/Controllers/Api/V1/ItemController.php -class ItemController extends Controller -{ - public function __construct(private ItemService $service) {} - - public function index(ItemIndexRequest $request) - { - return ApiResponse::handle(fn() => [ - 'data' => $this->service->index($request->validated()), - ], __('message.fetched')); - } - - public function store(ItemStoreRequest $request) - { - return ApiResponse::handle(fn() => [ - 'data' => $this->service->store($request->validated()), - ], __('message.created')); - } -} -``` - -### 4.3 라우트 - -```php -// routes/api_v1.php -Route::prefix('items')->group(function () { - Route::get('/', [ItemController::class, 'index']); - Route::post('/', [ItemController::class, 'store']); - Route::get('/{id}', [ItemController::class, 'show']); - Route::patch('/{id}', [ItemController::class, 'update']); - Route::delete('/{id}', [ItemController::class, 'destroy']); -}); -``` - -### 4.4 체크리스트 - -- [x] ItemController 생성 -- [x] ItemIndexRequest, ItemStoreRequest 등 생성 -- [x] 라우트 등록 -- [x] Swagger 문서 작성 -- [x] 기존 ProductController, MaterialController 제거 - ---- - -## Phase 5: 참조 테이블 마이그레이션 - -### 5.1 변경 대상 - -| 테이블 | 기존 | 변경 | -|--------|------|------| -| product_components | ref_type + ref_id | child_item_id | -| bom_template_items | ref_type + ref_id | item_id | -| orders | product_id | item_id | -| order_items | product_id | item_id | -| material_receipts | material_id | item_id | -| lots | material_id | item_id | -| price_histories | item_type + item_id | item_id | -| item_fields | source_table 'products'\|'materials' | source_table 'items' | - -### 5.2 체크리스트 - -- [x] 각 참조 테이블 마이그레이션 작성 -- [x] 관련 모델 관계 업데이트 -- [x] 데이터 검증 - ---- - -## Phase 6: 정리 - -### 6.1 체크리스트 - -- [x] CRUD 테스트 (전체 item_type) -- [x] BOM 계산 테스트 -- [x] Item-Master 연동 테스트 -- [x] 참조 무결성 테스트 -- [x] products 테이블 삭제 -- [x] materials 테이블 삭제 -- [x] 기존 Product, Material 모델 삭제 -- [x] 기존 ProductService, MaterialService 삭제 - ---- - -## 테이블 구조 요약 - -``` -┌─────────────────────────────────────────────────────┐ -│ items (핵심) │ -├─────────────────────────────────────────────────────┤ -│ id, tenant_id, item_type, code, name, unit │ -│ category_id, bom (JSON), is_active │ -│ timestamps + soft deletes │ -└─────────────────────┬───────────────────────────────┘ - │ 1:1 - ┌───────────────┴───────────────┐ - ▼ ▼ -┌─────────────┐ ┌─────────────┐ -│item_details │ │item_attrs │ -├─────────────┤ ├─────────────┤ -│ is_sellable │ │ attributes │ -│ is_purch... │ │ options │ -│ safety_stk │ └─────────────┘ -│ lead_time │ -│ is_inspect │ -└─────────────┘ -``` - ---- - -## BOM 계산 로직 - -### 통합 전 -```php -foreach ($bom as $item) { - if ($item['child_item_type'] === 'product') { - $child = Product::find($item['child_item_id']); - } else { - $child = Material::find($item['child_item_id']); - } -} -``` - -### 통합 후 -```php -$childIds = collect($bom)->pluck('child_item_id'); -$children = Item::whereIn('id', $childIds)->get()->keyBy('id'); -``` - ---- - -## 프론트엔드 전달 사항 - -### API 엔드포인트 변경 - -| 기존 | 통합 | -|------|------| -| `GET /api/v1/products` | `GET /api/v1/items?item_type=FG` | -| `GET /api/v1/products?product_type=PART` | `GET /api/v1/items?item_type=PART` | -| `GET /api/v1/products/materials` | `GET /api/v1/items?item_type=SM` | - -### 응답 필드 변경 - -| 기존 | 통합 | -|------|------| -| `product_type` | `item_type` | -| `material_type` | `item_type` | -| `material_code` | `code` | - -### BOM 요청/응답 변경 - -**요청 (Request)**: -```json -// 기존: BOM 저장 시 ref_type 지정 필요 -{ - "bom": [ - { "ref_type": "PRODUCT", "ref_id": 5, "quantity": 2 }, - { "ref_type": "MATERIAL", "ref_id": 10, "quantity": 1 } - ] -} - -// 통합: item_id만 사용 -{ - "bom": [ - { "child_item_id": 5, "quantity": 2 }, - { "child_item_id": 10, "quantity": 1 } - ] -} -``` - -**응답 (Response)**: -```json -// 기존 -{ "child_item_type": "product", "child_item_id": 5, "quantity": 2 } - -// 통합 -{ "child_item_id": 5, "quantity": 2 } -``` - -**프론트엔드 수정 포인트**: -- BOM 구성품 추가 시 `ref_type` 선택 UI 제거 -- 품목 검색 시 `/api/v1/items` 단일 엔드포인트 사용 -- BOM 저장 payload에서 `ref_type`, `ref_id` → `child_item_id`로 변경 - ---- - -## 일정 - -| Phase | 작업 | 상태 | -|-------|------|------| -| 0 | 데이터 정규화 (비표준 item_type/BOM 삭제) | ✅ 완료 | -| 1 | items 테이블 생성 + 데이터 이관 | ✅ 완료 | -| 2 | Item 모델 + Service 생성 | ✅ 완료 | -| 3 | Item-Master 연동 수정 | ✅ 완료 | -| 4 | API 통합 | ✅ 완료 | -| 5 | 참조 테이블 마이그레이션 | ✅ 완료 | -| 6 | 정리 | ✅ 완료 | - -> **완료일**: 2025-12-15 -> **관련 커밋**: `039fd62` (products/materials 테이블 삭제), `a93dfe7` (Phase 6 완료) - ---- - -## 리스크 - -| 리스크 | 대응 | -|--------|------| -| 데이터 이관 누락 | 이관 전후 건수 검증 | -| Item-Master 연동 오류 | source_table 변경 전 테스트 | -| BOM 순환 참조 | 저장 시 검증 로직 추가 | -| Code 중복 (products↔materials) | 개발 중이므로 품목관리 완료 후 경동기업 데이터 전체 삭제 후 재세팅 예정. 중복 데이터는 삭제 처리 | - ---- - -## 롤백 계획 - -각 Phase는 독립적 마이그레이션으로 구성: -```bash -# Phase 1 롤백 -php artisan migrate:rollback --step=3 - -# 데이터 복구 (products/materials 테이블 유지 상태에서) -# 신규 테이블만 삭제하면 됨 -``` \ No newline at end of file diff --git a/plans/archive/kd-items-migration-plan.md b/plans/archive/kd-items-migration-plan.md deleted file mode 100644 index 7710c32..0000000 --- a/plans/archive/kd-items-migration-plan.md +++ /dev/null @@ -1,1293 +0,0 @@ -# 경동기업(5130) 품목/단가 마이그레이션 계획 - -> **작성일**: 2026-01-28 -> **목적**: 경동기업 레거시 시스템(5130/)의 **품목(items), 단가(prices), BOM** 데이터를 SAM으로 이관 -> **기준 문서**: `5130/` 폴더 분석 결과 -> **상태**: 🔄 분석 완료, 구현 대기 -> **데이터 규모**: ~1,500 레코드 (items ~800 + prices ~500 + BOM ~200) - ---- - -## 🚀 새 세션 시작 가이드 (Quick Start) - -### 이 문서만 보고 작업을 재개하려면: - -```bash -# 1. Docker 서비스 확인 -docker ps | grep sam - -# 2. 레거시 DB (chandj) 접속 테스트 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM KDunitprice;" - -# 3. 현재 진행 상태 확인 -# → 아래 "📍 현재 진행 상태" 섹션 참조 - -# 4. 다음 작업 시작 -# → "📍 현재 진행 상태" > "다음 작업" 참조 -``` - -### 환경 정보 - -| 항목 | 값 | -|------|-----| -| **프로젝트 루트** | `/Users/kent/Works/@KD_SAM/SAM` | -| **레거시 소스** | `5130/` (프로젝트 루트 직하) | -| **API 프로젝트** | `api/` | -| **Docker 컨테이너** | `sam-mysql-1` | -| **레거시 DB** | `chandj` (MySQL) | -| **SAM DB** | `samdb` (MySQL) ⚠️ | -| **대상 테넌트 ID** | `287` (경동기업) | -| **생성자 사용자 ID** | `1` | - -### DB 접속 명령어 - -```bash -# 레거시 DB (chandj) 접속 -docker exec -it sam-mysql-1 mysql -uroot -proot chandj - -# SAM DB 접속 -docker exec -it sam-mysql-1 mysql -uroot -proot samdb - -# 레거시 테이블 목록 확인 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SHOW TABLES;" - -# SAM items 테이블 확인 -docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" -``` - -### 전제 조건 (작업 전 확인) - -- [x] Docker 서비스 실행 중 -- [x] `sam-mysql-1` 컨테이너 실행 중 -- [x] chandj 데이터베이스 접근 가능 -- [ ] SAM items 마이그레이션 실행 완료 (`php artisan migrate`) -- [ ] SAM prices 마이그레이션 실행 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | ✅ **정적 데이터 마이그레이션 완료** | -| **다음 작업** | 동적 BOM/견적 로직 구현 → [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) | -| **진행률** | 4/4 (100%) - 정적 데이터 완료 | -| **마지막 업데이트** | 2026-01-28 | - -> ⚠️ **주의**: 이 문서는 **정적 품목/단가 데이터 이관**만 다룹니다. -> 동적 BOM 계산, 모터/제어기/부자재 자동 추가 등 **견적 로직**은 별도 문서 참조: -> → [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) - -### Phase 1~3 실행 결과 ✅ - -| 소스 | 타입 | 건수 | -|------|------|------| -| KDunitprice | FG/PT/SM/RM/CS | 601건 | -| models | FG | +18건 | -| item_list | PT | +9건 | -| BDmodels.seconditem | PT (누락 부품) | +6건 | -| price_motor | SM (누락 품목) | +13건 | -| price_raw_materials | RM (누락 품목) | +4건 | -| **items 합계** | | **651건** | -| **prices 합계** | | **651건** | -| **BOM 연결** | items.bom JSON | **18건** | - -**Phase 2 상세:** -- Phase 2.1: BDmodels.seconditem → PT items 6건 추가 - - L-BAR, 보강평철, 케이스, 하단마감재, 가이드레일용 연기차단재, 케이스용 연기차단재 -- Phase 2.2: BDmodels → items.bom JSON 연결 18건 - - FG items (models 기반) ↔ PT items (seconditem) 연결 - -**Phase 3 상세:** -- Phase 3.1: price_motor → SM items 13건 추가 - - PM-020~PM-032: 제어기 (6P~18P, 20회선~100회선) - - PM-033~PM-035: 방화/방범 콘트롤박스, 스위치 -- Phase 3.2: price_raw_materials → RM items 4건 추가 - - RM-007: 신설비상문 (3x2 300*200) - - RM-008~RM-009: 제연커튼 (연기차단원단, 불투명) - - RM-010~RM-011: 화이바원단, 와이어원단 -- 중복 확인: KDunitprice 기존 품목과 명칭 비교로 중복 제외 - -### Phase 4 검증 결과 ✅ - -**로컬 검증 완료 (2026-01-28):** - -| 검증 항목 | 기대값 | 실제값 | 상태 | -|-----------|--------|--------|------| -| items 총 건수 | 651건 | 651건 | ✅ | -| prices 총 건수 | 651건 | 651건 | ✅ | -| BOM 연결 | 18건 | 18건 | ✅ | -| code 중복 | 0건 | 0건 | ✅ | - -**item_type 분포:** -| item_type | 건수 | -|-----------|------| -| FG (완제품) | 470건 | -| PT (부품) | 88건 | -| SM (부자재) | 61건 | -| RM (원자재) | 28건 | -| CS (소모품) | 4건 | - -### 후속 작업 - -**이 문서 범위 (정적 데이터):** -- ✅ 완료 - 개발서버 배포 대기 중 - -**별도 문서 (동적 로직):** -- → [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) -- 5130 견적 로직 분석 -- 동적 BOM 계산 (모터/제어기/부자재) -- 파라미터 기반 절곡품 산출 - -### Seeder 재실행 방법 - -```bash -# Docker 컨테이너 내부에서 실행 -docker exec sam-api-1 php artisan db:seed --class="Database\\Seeders\\Kyungdong\\KyungdongItemSeeder" -``` - ---- - -## 0. 성공 기준 - -| 기준 | 목표값 | 확인 방법 | -|------|-------|----------| -| **items 합계** | **~800건** | `SELECT COUNT(*) FROM items WHERE tenant_id=287` | -| items (FG) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='FG'` | -| items (PT) | ~250건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='PT'` | -| items (SM) | ~300건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='SM'` | -| items (RM) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='RM'` | -| items (CS) | ~50건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='CS'` | -| **prices 합계** | **~500건** | `SELECT COUNT(*) FROM prices WHERE tenant_id=287` | -| **BOM 관계** | ~300건 | `SELECT COUNT(*) FROM item_bom_items WHERE tenant_id=287` | -| code 유일성 | 100% | `SELECT code, COUNT(*) FROM items WHERE tenant_id=287 GROUP BY code HAVING COUNT(*) > 1` (0건) | -| API 테스트 | 100% | `/api/v1/items` 목록 조회 성공 | - ---- - -## 1. 개요 - -### 1.1 배경 - -경동기업은 **모델(Model) + 제품(Product)** 분리 구조를 사용하지만, SAM은 **통합 items 테이블** 구조로 기획됨. 레거시 5130/ 폴더의 데이터를 SAM 구조에 맞게 변환하여 이관 필요. - -### 1.2 핵심 차이점 - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ 레거시 (chandj) → SAM (samdb) │ -├────────────────────────────────────────────────────────────────────────────┤ -│ 📦 품목 마스터 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ KDunitprice (603건, 핵심!) → items (마스터, code로 구분) │ -│ models (18건) → items (FG) │ -│ parts, parts_sub (170건) → item_bom_items │ -│ category_l1~l4 → items 카테고리 참조 │ -│ guiderail, bottombar, bending 등 → item_details │ -│ │ -│ 💰 단가 정보 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ price_* (10개 테이블) → prices │ -│ KDunitprice.출고가/입고가 → prices (기본가) │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.2.1 중복 제거 전략 ⭐ - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심: KDunitprice가 마스터, code 필드로 중복 방지 │ -├────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 1️⃣ KDunitprice (603건) → items 먼저 생성 │ -│ - item_div로 item_type 결정 │ -│ - code = prodcode 그대로 사용 ⭐ │ -│ │ -│ 2️⃣ price_* 테이블 → items 중복 확인 후 prices만 생성 │ -│ - code로 items 조회 │ -│ - 존재하면 → prices만 추가 (item_id 연결) │ -│ - 없으면 → items 생성 후 prices 추가 │ -│ │ -│ 3️⃣ 매핑 테이블 불필요 │ -│ - item_id_mappings ❌ (양방향 조회 불필요) │ -│ - chandj는 손 안댐, samdb에만 밀어 넣음 │ -│ │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 SAM items 구조 (Target) - -```sql --- items 테이블 (tenant_id=287 for 경동기업) --- ⚠️ 실제 컬럼명 (2026-01-28 확인됨) -CREATE TABLE items ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, -- 287 (경동기업) - item_type VARCHAR(15) NOT NULL, -- FG, PT, SM, RM, CS - code VARCHAR(100) NOT NULL, -- 품목코드 (← KDunitprice.prodcode) - name VARCHAR(255) NOT NULL, -- 품목명 (← KDunitprice.item_name) - unit VARCHAR(20), -- 단위 (← KDunitprice.unit) - category_id BIGINT, -- 카테고리 ID - process_type VARCHAR(50), -- 공정 타입 - item_category VARCHAR(50), -- 품목 분류 - bom JSON, -- BOM 정보 - attributes JSON, -- 동적 필드 값 (spec 등) - attributes_archive JSON, -- 속성 아카이브 - options JSON, -- 추가 옵션 - description TEXT, -- 설명 - is_active BOOLEAN DEFAULT TRUE, - created_by BIGINT, - updated_by BIGINT, - deleted_by BIGINT, - created_at TIMESTAMP, - updated_at TIMESTAMP, - deleted_at TIMESTAMP -- Soft Delete -); -``` - -### 1.4 item_type 분류 - -| SAM item_type | 설명 | 레거시 소스 | -|---------------|------|-------------| -| **FG** | 완제품 (Finished Goods) | KDunitprice[제품], KDunitprice[상품], models | -| **PT** | 부품 (Parts) | KDunitprice[반제품], parts, parts_sub, category_l4 | -| **SM** | 부자재 (Sub-Materials) | KDunitprice[부재료], price_* 테이블들 | -| **RM** | 원자재 (Raw Materials) | KDunitprice[원재료], price_raw_materials | -| **CS** | 소모품 (Consumables) | KDunitprice[무형상품], 기타 | - -### 1.4.1 KDunitprice.item_div → item_type 매핑 ⭐ - -```sql --- KDunitprice.item_div 값 목록 (603건 중) --- [제품], [상품], [부재료], [원재료], [반제품], [무형상품] - -CASE item_div - WHEN '[제품]' THEN 'FG' -- 완제품 - WHEN '[상품]' THEN 'FG' -- 상품도 완제품으로 분류 - WHEN '[반제품]' THEN 'PT' -- 반제품 = 부품 - WHEN '[부재료]' THEN 'SM' -- 부자재 - WHEN '[원재료]' THEN 'RM' -- 원자재 - WHEN '[무형상품]' THEN 'CS' -- 소모품/무형 - ELSE 'SM' -- 기본값 -END AS item_type -``` - ---- - -## 2. 레거시 DB 구조 분석 - -### 2.1 핵심 테이블 및 레코드 수 - -#### 📦 품목 마스터 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| **`KDunitprice`** ⭐ | **603** | **품목 마스터 (핵심!)** | **items (마스터)** | -| `models` | 18 | 모델 마스터 (스크린/철재) | items (FG) | -| `BDmodels` | 59 | 모델별 BOM + 단가 (JSON) | item_bom_items + prices | -| `parts` | 36 | 부품 | item_bom_items | -| `parts_sub` | 134 | 하위 부품 | item_bom_items | -| `category_l1` | 2 | 1단계 카테고리 (스크린/철재) | 참조용 | -| `category_l2` | 14 | 2단계 카테고리 | 참조용 | -| `category_l3` | 24 | 3단계 카테고리 | 참조용 | -| `category_l4` | 37 | 4단계 카테고리 (부품) | items (PT) | -| `item_list` | 5+ | 품목 마스터 | items (PT) | - -#### 💰 단가 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| `price_motor` | 2 (JSON) | 모터 단가 | prices | -| `price_shaft` | 2 (JSON) | 감기샤프트 단가 | prices | -| `price_pipe` | 2 (JSON) | 파이프 단가 | prices | -| `price_angle` | 2 (JSON) | 앵글 단가 | prices | -| `price_raw_materials` | 6 (JSON) | 주자재 단가 | prices | -| `price_bend` | 3 (JSON) | 절곡 단가 | prices | -| `price_pole` | 2 (JSON) | 폴 단가 | prices | -| `price_screenplate` | 2 (JSON) | 스크린플레이트 단가 | prices | -| `price_smokeban` | 2 (JSON) | 연기차단 단가 | prices | - -### 2.2 KDunitprice 테이블 구조 ⭐ (핵심 마스터) - -```sql --- KDunitprice: 품목 마스터 (603건) - 가장 중요한 테이블! --- ⚠️ 실제 컬럼명 (2026-01-28 확인됨) -num INT PRIMARY KEY, -- PK -is_deleted INT, -- 삭제 여부 -prodcode VARCHAR(50), -- items.code (유니크 키!) ⭐ -item_name VARCHAR(255), -- items.name ⭐ -item_div VARCHAR(20), -- [제품]/[상품]/[부재료]/[원재료]/[반제품]/[무형상품] → item_type ⭐ -spec VARCHAR(100), -- items.attributes.spec -unit VARCHAR(20), -- items.unit -unitprice DECIMAL, -- prices.sales_price (단일 컬럼, 입고가/출고가 구분 없음!) ⭐ -searchtag TEXT, -- 검색 태그 -update_log TEXT -- 변경 이력 -``` - -**item_div 분포 확인 쿼리**: -```sql -SELECT item_div, COUNT(*) FROM KDunitprice WHERE is_deleted=0 GROUP BY item_div; --- [제품] ~100건 → FG --- [상품] ~50건 → FG --- [반제품] ~100건 → PT --- [부재료] ~200건 → SM --- [원재료] ~100건 → RM --- [무형상품] ~53건 → CS -``` - -### 2.3 BDmodels 테이블 구조 (BOM + 단가) - -```sql --- BDmodels: 모델별 BOM 및 단가 정보 -num INT PRIMARY KEY, -major_category VARCHAR(10), -- 스크린/철재 -spec VARCHAR(30), -- 규격 (60*40, 120*70 등) -model_name VARCHAR(255), -- 모델명 -finishing_type ENUM('SUS마감','EGI마감'), -check_type VARCHAR(20), -- 벽면형/측면형/혼합형 -seconditem VARCHAR(30), -- 부품명 (가이드레일, 하단마감재, L-BAR 등) -unitprice TEXT, -- 단가 (문자열) -savejson TEXT, -- BOM 상세 JSON -description TEXT, -is_deleted, priceDate DATE -``` - -**savejson 예시** (가이드레일 BOM): -```json -[ - {"col1":"1번(마감재)","col2":"SUS 1.2T","col3":"-3","col4":"203","col5":"206","col6":"51,000","col7":"10,506","col8":"2","col9":"21,012","col10":"삭제"}, - {"col1":"2번(본체)","col2":"EGI 1.55T","col3":"-5","col4":"294","col5":"299","col6":"27,000","col7":"8,073","col8":"1","col9":"8,073","col10":"삭제"} -] -``` - -### 2.4 단가 시스템 상세 분석 ⭐ - -#### 2.4.1 레거시 단가 테이블 전체 목록 (10개) - -| 테이블명 | 레코드 수 | 최신 날짜 | 용도 | -|---------|----------|----------|------| -| `price_motor` | 2 | 2024-08-25 | 전동개폐기, 제어기 단가 | -| `price_shaft` | 2 | 2024-08-25 | 감기샤프트 단가 | -| `price_pipe` | 2 | 2024-08-26 | 파이프 단가 | -| `price_angle` | 2 | 2024-08-26 | 앵글 단가 | -| `price_raw_materials` | 6 | 2025-06-18 | 슬랫, 스크린 원자재 단가 | -| `price_bend` | 3 | 2025-03-09 | 절곡 단가 | -| `price_pole` | 2 | 2024-08-26 | 폴 단가 | -| `price_screenplate` | 2 | 2024-08-26 | 스크린플레이트 단가 | -| `price_smokeban` | 2 | 2024-08-26 | 연기차단 단가 | -| `price_etc` | 0 | - | 기타 (개별 컬럼 방식, 비활성) | - -#### 2.4.2 SAM prices 테이블 구조 (Target) - -```sql -CREATE TABLE prices ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, -- 287 (경동기업) - - -- 품목 연결 - item_type_code VARCHAR(20), -- FG/PT/SM/RM/CS - item_id BIGINT, -- items.id FK - client_group_id BIGINT NULL, -- NULL = 기본가 - - -- 원가 정보 - purchase_price DECIMAL(15,4), -- 매입단가 (원가) - processing_cost DECIMAL(15,4), -- 가공비 - loss_rate DECIMAL(5,2), -- LOSS율 (%) - - -- 판매가 정보 - margin_rate DECIMAL(5,2), -- 마진율 (%) - sales_price DECIMAL(15,4), -- 판매단가 ⭐ - rounding_rule ENUM('round','ceil','floor'), - rounding_unit INT DEFAULT 1, -- 반올림 단위 - - -- 메타 정보 - supplier VARCHAR(255), -- 공급업체 - effective_from DATE, -- 적용 시작일 ⭐ - effective_to DATE NULL, -- 적용 종료일 - note TEXT, - - -- 상태 관리 - status ENUM('draft','active','inactive','finalized'), - is_final BOOLEAN DEFAULT FALSE, - - -- 감사 컬럼 - created_by, updated_by, deleted_by, timestamps, soft_deletes -); -``` - ---- - -## 3. 매핑 설계 - -### 3.1 models → items (FG 완제품) - -| 레거시 (models) | SAM (items) | 비고 | -|----------------|-------------|------| -| model_id | (신규 생성) | | -| model_name | code | KSS01 → FG-KSS01 | -| - | name | 모델명 + 마감타입 + 가이드타입 조합 | -| major_category | attributes.major_category | 스크린/철재 | -| finishing_type | attributes.finishing_type | SUS마감/EGI마감 | -| guiderail_type | attributes.guiderail_type | 벽면형/측면형/혼합형 | -| - | item_type | 'FG' | -| - | tenant_id | 287 | - -**코드 생성 규칙**: -``` -FG-{model_name}-{guiderail_type}-{finishing_type} -예: FG-KSS01-벽면형-SUS -``` - -### 3.2 price_* → prices 테이블 (단가 연동) ⭐ - -> **중요**: 단가 데이터는 items.attributes가 아닌 **prices 테이블**에 별도 관리 - -| 레거시 (price_*) | SAM (prices) | 비고 | -|-----------------|--------------|------| -| registedate | effective_from | 적용 시작일 | -| itemList.col13 (판매가) | sales_price | | -| itemList.col11 (원가) | purchase_price | | -| - | item_type_code | FG/PT/SM/RM/CS | -| - | item_id | items.id FK | -| - | client_group_id | NULL (기본가) | -| - | status | 'active' | - ---- - -## 4. 대상 범위 - -### 4.1 Phase 1: 마스터 데이터 이관 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| **1.0** | **KDunitprice → items (마스터)** ⭐ | ⏳ | **603건 (최우선!)** | -| 1.1 | models → items (FG) INSERT 쿼리 작성 | ⏳ | 18건 (중복 확인 후) | -| 1.2 | item_list → items (PT) INSERT 쿼리 작성 | ⏳ | 5건+ (중복 확인 후) | -| 1.3 | category_l4 → items (PT) INSERT 쿼리 작성 | ⏳ | 37건 (중복 확인 후) | -| 1.4 | price_motor 파싱 → prices 연결 | ⏳ | code로 items 조회 후 prices 생성 | -| 1.5 | price_shaft 파싱 → prices 연결 | ⏳ | ~15건 | -| 1.6 | price_raw_materials 파싱 → prices 연결 | ⏳ | ~20건 | -| 1.7 | ⚠️ **사용자 승인**: Phase 1 INSERT 실행 | ⏳ | | - -### 4.2 Phase 2: BOM 및 상세 데이터 이관 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | BDmodels.savejson → item_bom_items | ⏳ | 59건 | -| 2.2 | parts → item_bom_items | ⏳ | 36건 | -| 2.3 | parts_sub → item_bom_items | ⏳ | 134건 | -| 2.4 | guiderail/bottombar/bending 등 → item_details | ⏳ | 제품 상세 | -| 2.5 | parent_item_id, child_item_id 매핑 | ⏳ | code 기반 조회 | - -### 4.3 Phase 3: 단가 데이터 이관 ⭐ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | price_motor → items (SM) + prices | ⏳ | ~35건 품목 + 단가 | -| 3.2 | price_shaft → items (SM) + prices | ⏳ | ~15건 | -| 3.3 | price_pipe → items (SM) + prices | ⏳ | ~10건 | -| 3.4 | price_angle → items (SM) + prices | ⏳ | ~10건 | -| 3.5 | price_raw_materials → items (RM) + prices | ⏳ | ~20건 | -| 3.6 | price_bend → items (SM) + prices | ⏳ | ~10건 | -| 3.7 | price_pole → items (SM) + prices | ⏳ | ~5건 | -| 3.8 | price_screenplate → items (SM) + prices | ⏳ | ~5건 | -| 3.9 | price_smokeban → items (SM) + prices | ⏳ | ~5건 | -| 3.10 | 단가 버전 이력 정리 | ⏳ | effective_from/to 설정 | -| 3.11 | ⚠️ **사용자 승인**: 단가 INSERT 실행 | ⏳ | | - -### 4.4 Phase 4: 검증 및 배포 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | 로컬 테스트 | ⏳ | | -| 4.2 | API 테스트 | ⏳ | | -| 4.3 | 개발서버 배포 | ⏳ | ⚠️ 컨펌 필요 | - ---- - -## 5. Seeder 파일 - -### 5.0 Seeder 구조 및 실행 방법 - -**파일 위치**: `api/database/seeders/Kyungdong/KyungdongItemSeeder.php` - -**실행 명령어**: -```bash -# 로컬 실행 (tenant_id=287만 삭제 후 INSERT) -cd /Users/kent/Works/@KD_SAM/SAM/api -php artisan db:seed --class=Database\\Seeders\\Kyungdong\\KyungdongItemSeeder - -# 개발서버 실행 (TRUNCATE 후 INSERT) - ⚠️ 컨펌 필요 -php artisan db:seed --class=Database\\Seeders\\Kyungdong\\KyungdongItemSeeder --env=development -``` - -**환경별 삭제 전략**: -| 환경 | 삭제 방식 | 비고 | -|------|----------|------| -| 로컬 (local) | `DELETE WHERE tenant_id=287` | 다른 테넌트 데이터 보존 | -| 개발 (development) | `TRUNCATE` | 전체 초기화 | - ---- - -### 5.1 KyungdongItemSeeder.php (전체 코드) - -```php -command->info('🚀 경동기업 품목/단가 마이그레이션 시작...'); - - // 1. 기존 데이터 삭제 - $this->cleanupExistingData(); - - // 2. KDunitprice → items - $itemCount = $this->migrateItems(); - - // 3. KDunitprice → prices - $priceCount = $this->migratePrices(); - - $this->command->info("✅ 완료: items {$itemCount}건, prices {$priceCount}건"); - } - - /** - * 기존 데이터 삭제 - */ - private function cleanupExistingData(): void - { - if (App::environment('local')) { - // 로컬: tenant_id=287만 삭제 - $this->command->info(' 🧹 로컬 환경: tenant_id=287 데이터 삭제...'); - DB::table('prices')->where('tenant_id', self::TENANT_ID)->delete(); - DB::table('items')->where('tenant_id', self::TENANT_ID)->delete(); - } else { - // 개발/운영: TRUNCATE (⚠️ 주의) - $this->command->info(' 🧹 개발 환경: TRUNCATE...'); - DB::statement('SET FOREIGN_KEY_CHECKS=0'); - DB::table('prices')->truncate(); - DB::table('items')->truncate(); - DB::statement('SET FOREIGN_KEY_CHECKS=1'); - } - } - - /** - * KDunitprice → items 마이그레이션 - */ - private function migrateItems(): int - { - $this->command->info(' 📦 KDunitprice → items 마이그레이션...'); - - // chandj.KDunitprice에서 데이터 조회 - $kdItems = DB::connection('legacy') // config/database.php에 'legacy' 연결 필요 - ->table('KDunitprice') - ->where('is_deleted', 0) - ->whereNotNull('prodcode') - ->where('prodcode', '!=', '') - ->get(); - - $items = []; - $now = now(); - - foreach ($kdItems as $kd) { - $items[] = [ - 'tenant_id' => self::TENANT_ID, - 'item_type' => $this->mapItemType($kd->item_div), - 'code' => $kd->prodcode, - 'name' => $kd->item_name, - 'unit' => $kd->unit, - 'attributes' => json_encode([ - 'spec' => $kd->spec, - 'item_div' => $kd->item_div, - 'legacy_source' => 'KDunitprice', - 'legacy_num' => $kd->num, - ]), - 'is_active' => true, - 'created_by' => self::USER_ID, - 'updated_by' => self::USER_ID, - 'created_at' => $now, - 'updated_at' => $now, - ]; - - // 500건씩 배치 INSERT - if (count($items) >= 500) { - DB::table('items')->insert($items); - $items = []; - } - } - - // 남은 데이터 INSERT - if (!empty($items)) { - DB::table('items')->insert($items); - } - - return $kdItems->count(); - } - - /** - * KDunitprice → prices 마이그레이션 - */ - private function migratePrices(): int - { - $this->command->info(' 💰 KDunitprice → prices 마이그레이션...'); - - // items와 KDunitprice 조인하여 prices 생성 - $count = DB::statement(" - INSERT INTO prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, - effective_from, status, - created_by, updated_by, created_at, updated_at - ) - SELECT - ? AS tenant_id, - i.item_type AS item_type_code, - i.id AS item_id, - NULL AS client_group_id, - 0 AS purchase_price, - COALESCE(k.unitprice, 0) AS sales_price, - CURDATE() AS effective_from, - 'active' AS status, - ? AS created_by, - ? AS updated_by, - NOW(), NOW() - FROM items i - JOIN " . config('database.connections.legacy.database') . ".KDunitprice k - ON k.prodcode = i.code - WHERE i.tenant_id = ? - AND k.is_deleted = 0 - AND k.prodcode IS NOT NULL - AND k.prodcode != '' - ", [self::TENANT_ID, self::USER_ID, self::USER_ID, self::TENANT_ID]); - - return DB::table('prices')->where('tenant_id', self::TENANT_ID)->count(); - } - - /** - * item_div → item_type 매핑 - */ - private function mapItemType(?string $itemDiv): string - { - return match ($itemDiv) { - '[제품]', '[상품]' => 'FG', - '[반제품]' => 'PT', - '[부재료]' => 'SM', - '[원재료]' => 'RM', - '[무형상품]' => 'CS', - default => 'SM', - }; - } -} -``` - ---- - -### 5.2 Legacy DB 연결 설정 - -**config/database.php에 추가**: -```php -'connections' => [ - // ... 기존 연결들 - - 'legacy' => [ - 'driver' => 'mysql', - 'host' => env('LEGACY_DB_HOST', '127.0.0.1'), - 'port' => env('LEGACY_DB_PORT', '3306'), - 'database' => env('LEGACY_DB_DATABASE', 'chandj'), - 'username' => env('LEGACY_DB_USERNAME', 'root'), - 'password' => env('LEGACY_DB_PASSWORD', 'root'), - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - ], -], -``` - -**.env에 추가**: -```env -LEGACY_DB_HOST=127.0.0.1 -LEGACY_DB_PORT=3306 -LEGACY_DB_DATABASE=chandj -LEGACY_DB_USERNAME=root -LEGACY_DB_PASSWORD=root -``` - ---- - -### 5.3 참고: SQL 쿼리 (직접 실행용) - -#### 5.3.1 KDunitprice → items (마스터) - -```sql --- ⚠️ 참고용 SQL (Seeder 사용 권장) --- KDunitprice: 품목 마스터 (603건) → SAM items - -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, description, is_active, - created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - -- item_div → item_type 매핑 - CASE item_div - WHEN '[제품]' THEN 'FG' - WHEN '[상품]' THEN 'FG' - WHEN '[반제품]' THEN 'PT' - WHEN '[부재료]' THEN 'SM' - WHEN '[원재료]' THEN 'RM' - WHEN '[무형상품]' THEN 'CS' - ELSE 'SM' - END AS item_type, - prodcode AS code, -- 유니크 키! ⭐ - item_name AS name, -- ⭐ - unit AS unit, - JSON_OBJECT( - 'spec', spec, -- ⭐ - 'item_div', item_div, - 'legacy_source', 'KDunitprice', - 'legacy_num', num - ) AS attributes, - NULL AS description, -- 비고 컬럼 없음 - 1 AS is_active, - 1 AS created_by, - NOW(), NOW() -FROM chandj.KDunitprice -WHERE is_deleted = 0 - AND prodcode IS NOT NULL AND prodcode != ''; - --- 결과 확인 -SELECT item_type, COUNT(*) -FROM samdb.items -WHERE tenant_id = 287 -GROUP BY item_type; -``` - -#### 5.3.2 KDunitprice → prices (기본 단가) - -```sql --- ⚠️ 참고용 SQL (Seeder 사용 권장) --- unitprice 단일 컬럼 → sales_price, purchase_price는 0 -INSERT INTO samdb.prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, - effective_from, status, - created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - i.item_type AS item_type_code, - i.id AS item_id, - NULL AS client_group_id, -- 기본가 - 0 AS purchase_price, -- 입고가 컬럼 없음, 0으로 설정 - COALESCE(k.unitprice, 0) AS sales_price, -- ⭐ unitprice 사용 - CURDATE() AS effective_from, -- 적용일 - 'active' AS status, - 1 AS created_by, - NOW(), NOW() -FROM chandj.KDunitprice k -JOIN samdb.items i ON i.code = k.prodcode AND i.tenant_id = 287 -- ⭐ prodcode 사용 -WHERE k.is_deleted = 0 - AND k.prodcode IS NOT NULL AND k.prodcode != ''; -``` - -### 5.4 models → items (FG) - 추가 SQL 참고용 - -```sql --- ⚠️ 참고용 SQL (Seeder 확장 시 사용) --- 레거시 chandj.models → SAM items (FG) --- KDunitprice에 없는 것만 추가 (중복 확인 필요) -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - 'FG' AS item_type, - CONCAT('FG-', model_name, '-', - COALESCE(guiderail_type, 'STD'), '-', - CASE finishing_type - WHEN 'SUS마감' THEN 'SUS' - WHEN 'EGI마감' THEN 'EGI' - ELSE 'STD' - END - ) AS code, - CONCAT(model_name, ' ', major_category, ' ', finishing_type, ' ', COALESCE(guiderail_type, '')) AS name, - 'EA' AS unit, - JSON_OBJECT( - 'major_category', major_category, - 'finishing_type', finishing_type, - 'guiderail_type', guiderail_type, - 'legacy_model_id', model_id - ) AS attributes, - CASE WHEN is_deleted = 0 THEN 1 ELSE 0 END AS is_active, - 1 AS created_by, - created_at, - updated_at -FROM chandj.models -WHERE is_deleted = 0; -``` - -### 5.5 category_l4 → items (PT) - 추가 SQL 참고용 - -```sql --- ⚠️ 참고용 SQL (Seeder 확장 시 사용) --- 레거시 4단계 카테고리 → SAM items (PT) -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - 'PT' AS item_type, - CONCAT('PT-', l1.name, '-', l2.name, '-', l3.name, '-', l4.name) AS code, - l4.name AS name, - 'EA' AS unit, - JSON_OBJECT( - 'category_l1', l1.name, - 'category_l2', l2.name, - 'category_l3', l3.name, - 'category_l4', l4.name, - 'legacy_l4_id', l4.id - ) AS attributes, - 1 AS is_active, - 1 AS created_by, - NOW(), NOW() -FROM chandj.category_l4 l4 -JOIN chandj.category_l3 l3 ON l4.parent_id = l3.id -JOIN chandj.category_l2 l2 ON l3.parent_id = l2.id -JOIN chandj.category_l1 l1 ON l2.parent_id = l1.id; -``` - -### 5.6 price_motor → items (SM) + prices - PHP 스크립트 참고용 - -```php -query(" - SELECT num, registedate, itemList - FROM price_motor - WHERE is_deleted = 0 - ORDER BY registedate DESC -"); -$priceRecords = $stmt->fetchAll(PDO::FETCH_ASSOC); - -// 최신 단가의 itemList 파싱 → items 생성 -$latestRecord = $priceRecords[0]; -$itemList = json_decode($latestRecord['itemList'], true); - -foreach ($itemList as $idx => $item) { - $voltage = $item['col1']; // 220, 380, 제어기, 방화, 방범 - $capacity = $item['col2']; // 150K(S), 300K, 노출형, 매립형... - $purchasePrice = (float)str_replace(',', '', $item['col11'] ?? '0'); - $salesPrice = (float)str_replace(',', '', $item['col13'] ?? '0'); - - // 품목 코드 생성 - $code = "SM-MOTOR-" . preg_replace('/[^A-Za-z0-9가-힣]/', '', $voltage) - . "-" . preg_replace('/[^A-Za-z0-9가-힣()]/', '', $capacity); - - // 품목명 생성 - if (in_array($voltage, ['220', '380'])) { - $name = "전동개폐기 {$voltage}V {$capacity}"; - $itemType = 'SM'; - } elseif ($voltage === '제어기') { - $name = "연동제어기 {$capacity}"; - $itemType = 'SM'; - } else { - $name = "{$voltage} {$capacity}"; - $itemType = 'SM'; - } - - // 1단계: items INSERT - $itemStmt = $pdo->prepare(" - INSERT INTO items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at - ) VALUES (?, ?, ?, ?, 'EA', ?, 1, ?, NOW(), NOW()) - ON DUPLICATE KEY UPDATE name = VALUES(name) - "); - $attributes = json_encode([ - 'voltage' => $voltage, - 'capacity' => $capacity, - 'legacy_source' => 'price_motor', - 'legacy_col_index' => $idx - ]); - $itemStmt->execute([$tenantId, $itemType, $code, $name, $attributes, $userId]); - $itemId = $pdo->lastInsertId(); - - // 2단계: prices INSERT (모든 버전) - foreach ($priceRecords as $priceIdx => $priceRecord) { - $priceItemList = json_decode($priceRecord['itemList'], true); - if (!isset($priceItemList[$idx])) continue; - - $priceItem = $priceItemList[$idx]; - $pPrice = (float)str_replace(',', '', $priceItem['col11'] ?? '0'); - $sPrice = (float)str_replace(',', '', $priceItem['col13'] ?? '0'); - $effectiveFrom = $priceRecord['registedate']; - - // 다음 레코드가 있으면 effective_to 설정 - $effectiveTo = isset($priceRecords[$priceIdx + 1]) - ? date('Y-m-d', strtotime($effectiveFrom . ' -1 day')) - : null; - - $status = ($priceIdx === 0) ? 'active' : 'inactive'; - - $priceStmt = $pdo->prepare(" - INSERT INTO prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, effective_from, effective_to, - status, created_by, created_at, updated_at - ) VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NOW(), NOW()) - "); - $priceStmt->execute([ - $tenantId, $itemType, $itemId, - $pPrice, $sPrice, $effectiveFrom, $effectiveTo, - $status, $userId - ]); - } - - echo "✓ {$code} - items + prices 생성 완료\n"; -} -``` - ---- - -## 6. 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 📦 데이터 전략 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - KDunitprice(603건)가 품목 마스터 → items 최우선 생성 │ -│ - code 필드로 중복 방지 (ON DUPLICATE KEY UPDATE) │ -│ - BOM은 item_bom_items 테이블 사용 (items.bom JSON ❌) │ -│ - 단가 정보는 prices 테이블에 별도 저장 (items.attributes ❌) │ -│ │ -│ ❌ 불필요한 것 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - item_id_mappings 테이블 (양방향 조회 불필요) │ -│ - chandj 수정 (손 안댐, samdb에만 밀어 넣음) │ -│ - 레거시 소스 확인 (마이그레이션 후 검증만) │ -│ │ -│ ✅ 필수 사항 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - 경동기업 기준으로 맞춤 (이미 사용중인 시스템) │ -│ - 전체 이관 (items + prices + BOM) │ -│ - SQL 쿼리 + PHP 스크립트 혼용 (JSON 파싱 필요) │ -│ - 로컬 검증 완료 후 개발서버 배포 │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 6.1 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | SELECT 쿼리, 분석, 매핑 설계 | 불필요 | -| ⚠️ 컨펌 필요 | INSERT 실행, TRUNCATE, 개발서버 배포 | **필수** | -| 🔴 금지 | 운영서버 직접 작업 | 별도 협의 | - ---- - -## 7. 데이터 규모 예상 - -### 7.1 items 테이블 예상 - -| 소스 | 레코드 수 | SAM item_type | 예상 items 건수 | -|------|----------|---------------|----------------| -| **KDunitprice** ⭐ | **603** | FG/PT/SM/RM/CS | **~603 (마스터)** | -| models | 18 | FG | ~0 (중복 제외) | -| category_l4 | 37 | PT | ~20 (일부 신규) | -| item_list | 5 | PT | ~0 (중복 제외) | -| price_* 테이블 | ~130 항목 | SM/RM | ~100 (신규만) | -| **items 합계** | - | - | **~700~800건** | - -**item_type별 분포 예상**: -| item_type | 설명 | 예상 건수 | -|-----------|------|----------| -| FG | 완제품 | ~100건 | -| PT | 부품 | ~250건 | -| SM | 부자재 | ~300건 | -| RM | 원자재 | ~100건 | -| CS | 소모품 | ~50건 | - -### 7.2 prices 테이블 예상 ⭐ - -| 소스 | 버전 수 | 품목당 단가 | 예상 prices 건수 | -|------|--------|------------|-----------------| -| KDunitprice | 1 | 603 | ~603 | -| price_motor | 2 | 35 | ~70 | -| price_shaft | 2 | 15 | ~30 | -| price_pipe | 2 | 10 | ~20 | -| price_angle | 2 | 10 | ~20 | -| price_raw_materials | 6 | 20 | ~120 | -| price_bend | 3 | 10 | ~30 | -| 기타 price_* | 2 | 15 | ~30 | -| **prices 합계** | - | - | **~500건** (중복 제외) | - ---- - -## 8. 체크리스트 - -### Phase 1: 마스터 데이터 이관 ✅ 완료 -- [x] 레거시 DB 구조 분석 완료 -- [x] KDunitprice 테이블 발견 및 분석 (603건, 핵심 마스터) -- [x] 중복 제거 전략 수립 (code 기반, 매핑 테이블 불필요) -- [x] Seeder 기반 마이그레이션 계획 수립 -- [x] ~~config/database.php에 'legacy' 연결 추가~~ → 기존 'chandj' 연결 사용 -- [x] ~~.env에 LEGACY_DB_* 환경변수 추가~~ → 기존 CHANDJ_DB_* 사용 -- [x] **Phase 1.0**: KDunitprice → items 601건, prices 601건 ✅ -- [x] **Phase 1.1**: models → items (FG) 18건 ✅ -- [x] **Phase 1.2**: item_list → items (PT) 9건 ✅ -- [x] ~~Phase 1.3: category_l4~~ → 스킵 (카테고리 데이터) -- [x] **Phase 1 결과**: items 628건, prices 628건 ✅ - -### Phase 2: BOM 데이터 이관 ✅ 완료 -- [x] BDmodels.seconditem → PT items 누락 부품 6건 추가 ✅ -- [x] ~~child_item_id 매핑 테이블 생성~~ → code 기반 직접 조회 -- [x] items.bom JSON 생성 (18건 FG ↔ PT 연결) ✅ -- [x] **최종 결과**: items 634건, prices 634건, BOM 18건 ✅ (2026-01-28) - -### Phase 3: 단가 데이터 이관 ✅ 완료 -- [x] 레거시 price_* 테이블 구조 분석 (10개) -- [x] 각 테이블별 JSON 스키마 분석 -- [x] SAM prices 테이블 구조 확인 -- [x] Legacy → SAM 단가 매핑 전략 수립 -- [x] price_motor → items (SM) 누락 품목 13건 추가 ✅ -- [x] price_raw_materials → items (RM) 누락 품목 4건 추가 ✅ -- [x] 기타 price_* 테이블 분석 완료 (대부분 계산 참조용, 품목 마스터 아님) - - price_shaft, price_pipe, price_angle, price_bend, price_pole, price_screenplate: 계산 참조용 - - 220V/380V 모터: KDunitprice에 "KD모터*Kg단상/삼상"으로 이미 존재 -- [x] **사용자 승인**: 완료 (2026-01-28) - -### Phase 4: 검증 및 배포 ✅ 로컬 검증 완료 -- [x] 건수 검증 ✅ (items 651건, prices 651건, BOM 18건) -- [x] 데이터 조회 테스트 ✅ (artisan tinker, MySQL 직접 쿼리) -- [x] code 중복 검증 ✅ (0건) -- [x] Phase 3 추가 품목 확인 ✅ (PM-* 13건, RM-* 4건) -- [ ] ⚠️ **사용자 승인**: 개발서버 배포 - ---- - -## 9. 참고 문서 - -- **레거시 소스**: `5130/` 폴더 -- **SAM items 마이그레이션**: `api/database/migrations/2025_12_13_152507_create_items_table.php` -- **SAM prices 마이그레이션**: `api/database/migrations/2025_12_08_154633_create_prices_table.php` -- **SAM price_revisions 마이그레이션**: `api/database/migrations/2025_12_08_154634_create_price_revisions_table.php` -- **품목 분석**: `docs/data/analysis/item-db-analysis.md` -- **DummyItemSeeder**: `api/database/seeders/Dummy/DummyItemSeeder.php` -- **DummyDataSeeder**: `api/database/seeders/DummyDataSeeder.php` (TENANT_ID=287, USER_ID=1 상수 참조) -- **prices item_type_code 마이그레이션**: `api/database/migrations/2025_12_21_165524_update_prices_item_type_code_to_actual_item_type.php` -- **연관 문서**: `docs/plans/kd-orders-migration-plan.md` (입고/재고/주문 마이그레이션) - ---- - -## 10. 세션 및 메모리 관리 정책 - -### 10.1 세션 시작 시 (Load Strategy) -```bash -# 1. Docker 확인 -docker ps | grep sam - -# 2. DB 접속 테스트 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM KDunitprice;" -docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" - -# 3. 현재 진행 상태 확인 -# → 이 문서의 "📍 현재 진행 상태" 섹션 참조 - -# 4. 마이그레이션 상태 확인 (API 프로젝트) -cd /Users/kent/Works/@KD_SAM/SAM/api && php artisan migrate:status -``` - -### 10.2 작업 중 관리 - -| 작업 완료 시 | 조치 | -|-------------|------| -| Phase 완료 | "📍 현재 진행 상태" 업데이트 | -| INSERT 실행 | "12. 변경 이력" 추가 | -| 스키마 변경 | 관련 섹션 업데이트 + 변경 이력 추가 | -| 오류 발생 | 체크리스트에 메모 추가 | - -### 10.3 컨텍스트 관리 - -| 컨텍스트 잔량 | 조치 | -|--------------|------| -| **30% 이하** | 현재 작업 중단점 문서에 기록 | -| **20% 이하** | "📍 현재 진행 상태" 최종 업데이트 | -| **10% 이하** | 세션 정리 및 다음 세션 가이드 작성 | - ---- - -## 11. 자기완결성 점검 결과 - -### 11.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 0 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Quick Start 전제 조건 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 SQL 쿼리 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 0 확인 방법 SQL | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 건수, 테이블명, 컬럼 명시 | - -### 11.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 문서 헤더 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 → 다음 작업 상세 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 9. 참고 문서 | -| Q4. 작업 완료 확인 방법은? | ✅ | 0. 성공 기준 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서, Quick Start DB 접속 명령어 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - -### 11.3 핵심 정보 요약 (새 세션용) - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 📋 핵심 정보 요약 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 🎯 목표: 경동기업 레거시(chandj) → SAM(samdb) 품목/단가 이관 │ -│ │ -│ 📊 데이터 규모 (총 ~1,500건): │ -│ - items: ~800건 (KDunitprice 603 + 추가) │ -│ - prices: ~500건 │ -│ - item_bom_items: ~200건 │ -│ │ -│ 🔑 핵심 상수: │ -│ - tenant_id = 287 (경동기업) │ -│ - user_id = 1 (생성자) │ -│ - Docker: sam-mysql-1 │ -│ - 레거시 DB: chandj / SAM DB: samdb ⚠️ │ -│ │ -│ ⭐ KDunitprice 실제 컬럼명 (2026-01-28 확인): │ -│ - prodcode (품목코드) → items.code │ -│ - item_name (품목명) → items.name │ -│ - spec (규격) → items.attributes.spec │ -│ - unit (단위) → items.unit │ -│ - item_div ([제품] 등) → items.item_type │ -│ - unitprice (단가, 단일 컬럼!) → prices.sales_price │ -│ │ -│ ⭐ 마이그레이션 순서 (Seeder 기반): │ -│ 1. config/database.php에 'legacy' 연결 추가 │ -│ 2. .env에 LEGACY_DB_* 환경변수 추가 │ -│ 3. KyungdongItemSeeder.php 파일 생성 ← 최우선! │ -│ 4. Seeder 실행 (items 603건 + prices 603건) │ -│ 5. 추가 items/BOM은 확장 Seeder로 처리 │ -│ │ -│ 📍 현재 상태: Phase 1 대기 (Seeder 파일 생성 및 실행) │ -│ │ -│ ❌ 불필요한 것: item_id_mappings (양방향 조회 불필요, chandj 손 안댐) │ -│ │ -│ ⚠️ 주의: 모든 INSERT 실행 전 사용자 승인 필요 │ -│ │ -│ 📎 연관 문서: docs/plans/kd-orders-migration-plan.md (입고/재고/주문) │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 12. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-28 | 문서 분리 | items-migration-kyungdong-plan.md에서 품목/단가 부분 분리 | - | - | -| 2026-01-28 | 문서 생성 | kd-items-migration-plan.md 신규 생성 | - | - | -| 2026-01-28 | 컬럼명 수정 | 실제 DB 컬럼명으로 업데이트 (품목코드→prodcode, 품목명→item_name 등) | - | - | -| 2026-01-28 | Seeder 전환 | SQL → Seeder 방식으로 전환, 섹션 5.0~5.6 구조 정리 | - | - | - ---- - -## 13. 트러블슈팅 가이드 - -### 13.1 일반적인 문제 - -| 문제 | 원인 | 해결책 | -|------|------|--------| -| Docker 컨테이너 없음 | Docker 미실행 | `docker-compose up -d` 실행 | -| DB 접속 실패 | 컨테이너명 변경 | `docker ps`로 정확한 컨테이너명 확인 | -| chandj DB 없음 | 레거시 DB 미설정 | Docker 볼륨 확인 또는 덤프 복원 | -| tenant_id 287 없음 | 경동기업 테넌트 미생성 | SAM에서 테넌트 생성 필요 | -| items 테이블 없음 | 마이그레이션 미실행 | `php artisan migrate` 실행 | -| **SAM DB 이름 오류** | `sam` 대신 `samdb` | 모든 쿼리에서 `samdb` 사용 확인 | -| KDunitprice 테이블 없음 | 레거시 덤프 불완전 | chandj 전체 덤프 확인 | - -### 13.2 JSON 파싱 오류 - -```php -// price_* 테이블의 itemList 파싱 시 주의사항 -$itemList = json_decode($record['itemList'], true); - -// 빈 값 또는 잘못된 JSON 처리 -if (empty($itemList) || !is_array($itemList)) { - // 스킵하고 로그 기록 - error_log("Invalid itemList in {$table} num={$record['num']}"); - continue; -} - -// 숫자 형식 변환 (콤마 제거) -$price = (float)str_replace(',', '', $item['col13'] ?? '0'); -``` - -### 13.3 중복 코드 처리 (code 기반) - -```sql --- 이미 존재하는 품목 확인 (code 유일성 검사) -SELECT code, COUNT(*) AS cnt -FROM samdb.items -WHERE tenant_id=287 -GROUP BY code -HAVING cnt > 1; - --- INSERT 시 ON DUPLICATE KEY UPDATE 사용 --- ⚠️ items 테이블에 (tenant_id, code) UNIQUE 인덱스 필요 -INSERT INTO samdb.items (...) VALUES (...) -ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW(); - --- KDunitprice와 price_* 중복 확인 (⭐ 실제 컬럼명 사용) -SELECT k.prodcode, '모터 150K' AS price_item -FROM chandj.KDunitprice k -WHERE k.item_name LIKE '%모터%150K%'; --- → KDunitprice가 마스터, price_*는 가격만 추가 -``` - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/l2-permission-management-plan.md b/plans/archive/l2-permission-management-plan.md deleted file mode 100644 index e7490a2..0000000 --- a/plans/archive/l2-permission-management-plan.md +++ /dev/null @@ -1,378 +0,0 @@ -# L-2 권한관리 Mock → API 연동 계획 - -> **작성일**: 2025-12-30 -> **목적**: React 권한관리 페이지의 Mock 데이터를 API 연동으로 전환 -> **기준 문서**: mng.sam.kr/role-permissions -> **상태**: ✅ 완료 - Phase 1~4 전체 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 React 연동 완료 | -| **다음 작업** | 완료 (테스트 후 운영 배포) | -| **진행률** | 12/12 (100%) | -| **마지막 업데이트** | 2025-12-30 - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 React의 권한관리 페이지(`/settings/permissions`)는 `localStorage`와 `defaultPermissions` Mock 데이터를 사용하고 있습니다. mng 프로젝트에는 이미 완전한 역할-권한 관리 시스템이 구현되어 있으므로, api 프로젝트에 동일한 API를 개발하고 React에서 연동해야 합니다. - -**문제점:** -- React는 `localStorage`에 권한 데이터 저장 (새로고침/브라우저 변경 시 데이터 손실) -- 실제 DB 연동 없음 -- 역할 숨김(is_hidden) 기능이 DB 스키마에 없음 - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. React → api.sam.kr만 호출 (mng 직접 호출 금지) │ -│ 2. mng의 RoleService/RolePermissionService 로직 참조하여 api에 재구현 │ -│ 3. Spatie Permission 패키지 활용 (기존 테이블 구조 유지) │ -│ 4. Multi-tenant 지원 필수 (BelongsToTenant) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | API 엔드포인트 추가, 타입 정의, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | DB 마이그레이션 (is_hidden 컬럼), 기존 API 수정 | **필수** | -| 🔴 금지 | roles 테이블 구조 대폭 변경, 기존 권한 삭제 | 별도 협의 | - -### 1.4 준수 규칙 - -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/api-rules.md` - API 개발 규칙 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/specs/database-schema.md` - DB 스키마 - ---- - -## 2. 현재 상태 분석 - -### 2.1 mng 프로젝트 (기준) - -| 파일 | 역할 | 주요 기능 | -|------|------|----------| -| `RoleController.php` | 역할 CRUD 화면 | index, create, edit | -| `RoleService.php` | 역할 비즈니스 로직 | getRoles, createRole, updateRole, deleteRole | -| `RolePermissionController.php` | 권한 매트릭스 화면 | index (테넌트별 역할 목록) | -| `RolePermissionService.php` | 권한 매트릭스 로직 | togglePermission, allowAll, denyAll, getMenuTree | -| `Role.php` (Model) | 역할 모델 | tenant, permissions, users 관계 | - -**mng의 역할 필드:** -```php -$fillable = ['tenant_id', 'name', 'description', 'guard_name']; -``` - -**⚠️ 숨김 기능 없음**: mng에도 `is_hidden` 필드가 없음 - -### 2.2 React 프로젝트 (현재) - -| 파일 | 현재 상태 | 문제점 | -|------|----------|--------| -| `index.tsx` | `localStorage` + `defaultPermissions` | 실제 DB 연동 없음 | -| `types.ts` | `Permission` 타입 정의 | `status: 'active' | 'hidden'` 있음 | -| `PermissionDetail.tsx` | 메뉴별 권한 설정 | Mock 데이터 사용 | - -**React의 Permission 타입:** -```typescript -interface Permission { - id: number; - name: string; - status: 'active' | 'hidden'; // ← DB에 없음! - menuPermissions: MenuPermission[]; - createdAt: string; -} -``` - -### 2.3 api 프로젝트 (현재) - -- **Role 관련 API 없음** (개발 필요) -- `shared/Models/Role.php` 존재 여부 확인 필요 - -### 2.4 DB 스키마 (roles 테이블) - -```sql -roles (11 컬럼): -- id (PK) -- tenant_id (FK → tenants.id) -- name -- guard_name (default: 'web') -- description -- created_by, updated_by, deleted_by -- created_at, updated_at, deleted_at - --- ⚠️ is_hidden 컬럼 없음! 추가 필요 -``` - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: DB 스키마 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | roles 테이블에 `is_hidden` 컬럼 추가 | ✅ | `2025_12_30_160802_add_is_hidden_to_roles_table.php` 생성완료, 실행대기 | -| 1.2 | 기존 역할 데이터 기본값 설정 (is_hidden = false) | ✅ | 마이그레이션에 포함 | - -### 3.2 Phase 2: api 프로젝트 - Role CRUD API - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | Role 모델 생성/수정 | ✅ | shared/Models/Role.php | -| 2.2 | RoleService 생성 | ✅ | `api/app/Services/RoleService.php` | -| 2.3 | RoleController 생성 | ✅ | `api/app/Http/Controllers/Api/V1/RoleController.php` | -| 2.4 | RoleFormRequest 생성 | ⏳ | StoreRoleRequest, UpdateRoleRequest 미생성 | -| 2.5 | routes/api.php 라우트 추가 | ✅ | 5개 CRUD 라우트 등록완료 | -| 2.6 | Swagger 문서 작성 | ✅ | `api/app/Swagger/v1/RoleApi.php` | - -### 3.3 Phase 3: api 프로젝트 - 권한 매트릭스 API - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | RolePermissionController 생성 | ✅ | `api/app/Http/Controllers/Api/V1/RolePermissionController.php` | -| 3.2 | 권한 목록 조회 API | ✅ | GET /roles/{id}/permissions | -| 3.3 | 권한 부여 API | ✅ | POST /roles/{id}/permissions | -| 3.4 | 권한 회수/동기화 API | ✅ | DELETE, PUT /roles/{id}/permissions/sync | -| 3.5 | Swagger 문서 작성 | ✅ | `api/app/Swagger/v1/RolePermissionApi.php` | - -### 3.4 Phase 4: React 연동 ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | actions.ts 생성 | ✅ | 12개 Server Actions (fetchRoles, createRole, updateRole, deleteRole 등) | -| 4.2 | types.ts 수정 | ✅ | ApiResponse, Role, RoleStats, MenuTreeItem, PermissionMatrix 타입 추가 | -| 4.3 | index.tsx 수정 (목록) | ✅ | localStorage → API 연동, 로딩/에러 상태, toast 알림 | -| 4.4 | PermissionDetailClient.tsx 수정 (상세/권한매트릭스) | ✅ | 역할 CRUD, 권한 토글, 전체 허용/거부/초기화 | -| 4.5 | Mock 데이터 제거 | ✅ | defaultPermissions 삭제, API 기반으로 전환 | - ---- - -## 4. API 설계 - -### 4.1 Role CRUD API - -| Method | Endpoint | 설명 | Request | Response | -|--------|----------|------|---------|----------| -| GET | `/api/v1/roles` | 역할 목록 | `?search=&is_hidden=` | `{ data: Role[], meta: Pagination }` | -| GET | `/api/v1/roles/{id}` | 역할 상세 | - | `{ data: Role }` | -| POST | `/api/v1/roles` | 역할 생성 | `{ name, description, is_hidden }` | `{ data: Role }` | -| PUT | `/api/v1/roles/{id}` | 역할 수정 | `{ name, description, is_hidden }` | `{ data: Role }` | -| DELETE | `/api/v1/roles/{id}` | 역할 삭제 | - | `{ message }` | - -### 4.2 권한 매트릭스 API - -| Method | Endpoint | 설명 | Request | Response | -|--------|----------|------|---------|----------| -| GET | `/api/v1/roles/{id}/menus` | 메뉴 트리 + 권한 상태 | - | `{ data: MenuWithPermissions[] }` | -| POST | `/api/v1/roles/{id}/permissions/toggle` | 권한 토글 | `{ menu_id, permission_type }` | `{ data: { value: boolean } }` | -| POST | `/api/v1/roles/{id}/permissions/allow-all` | 전체 허용 | - | `{ message }` | -| POST | `/api/v1/roles/{id}/permissions/deny-all` | 전체 거부 | - | `{ message }` | -| POST | `/api/v1/roles/{id}/permissions/reset` | 기본값 초기화 | - | `{ message }` | - -### 4.3 Role 응답 타입 - -```typescript -interface Role { - id: number; - tenant_id: number; - name: string; - description: string | null; - guard_name: string; - is_hidden: boolean; // ← 신규 필드 - permissions_count: number; // ← 권한 개수 - users_count: number; // ← 사용자 수 - created_at: string; - updated_at: string; -} - -interface MenuWithPermissions { - id: number; - name: string; - parent_id: number | null; - depth: number; - has_children: boolean; - permissions: { - view: boolean; - create: boolean; - update: boolean; - delete: boolean; - approve: boolean; - export: boolean; - manage: boolean; - }; -} -``` - ---- - -## 5. 상세 작업 내용 - -### 5.1 Phase 1: DB 스키마 수정 ✅ - -#### 1.1 roles 테이블에 is_hidden 컬럼 추가 -- **상태**: ✅ 파일 생성완료 (실행 대기) -- **마이그레이션 파일**: `2025_12_30_160802_add_is_hidden_to_roles_table.php` -- **컬럼 정의**: `boolean is_hidden default false after description` -- **영향**: api, mng 모두 적용 - -### 5.2 Phase 2: Role CRUD API ✅ - -#### 생성된 파일 -| 파일 | 경로 | -|------|------| -| RoleController | `api/app/Http/Controllers/Api/V1/RoleController.php` | -| RoleService | `api/app/Services/RoleService.php` | -| RoleApi Swagger | `api/app/Swagger/v1/RoleApi.php` | - -#### 등록된 라우트 (5개) -``` -GET /api/v1/roles → index -POST /api/v1/roles → store -GET /api/v1/roles/{id} → show -PATCH /api/v1/roles/{id} → update -DELETE /api/v1/roles/{id} → destroy -``` - -### 5.3 Phase 3: 권한 매트릭스 API ✅ - -#### 생성된 파일 -| 파일 | 경로 | -|------|------| -| RolePermissionController | `api/app/Http/Controllers/Api/V1/RolePermissionController.php` | -| RolePermissionApi Swagger | `api/app/Swagger/v1/RolePermissionApi.php` | - -#### 등록된 라우트 (4개) -``` -GET /api/v1/roles/{id}/permissions → index -POST /api/v1/roles/{id}/permissions → grant -DELETE /api/v1/roles/{id}/permissions → revoke -PUT /api/v1/roles/{id}/permissions/sync → sync -``` - ---- - -## 6. 컨펌 대기 목록 - -> API 내부 로직 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | is_hidden 컬럼 추가 | roles 테이블 마이그레이션 | api, mng | ⏳ 대기 | - ---- - -## 7. 파일 구조 (예상) - -### 7.1 api 프로젝트 - -``` -api/app/ -├── Http/ -│ ├── Controllers/ -│ │ └── RoleController.php ← 🆕 생성 -│ └── Requests/ -│ ├── StoreRoleRequest.php ← 🆕 생성 -│ └── UpdateRoleRequest.php ← 🆕 생성 -├── Models/ -│ └── Role.php ← 🔄 수정 (is_hidden 추가) -└── Services/ - ├── RoleService.php ← 🆕 생성 - └── RolePermissionService.php ← 🆕 생성 - -api/database/migrations/ -└── xxxx_add_is_hidden_to_roles_table.php ← 🆕 생성 - -api/routes/ -└── api.php ← 🔄 수정 (라우트 추가) -``` - -### 7.2 React 프로젝트 - -``` -react/src/components/settings/PermissionManagement/ -├── index.tsx ← 🔄 수정 (API 연동) -├── types.ts ← 🔄 수정 (타입 매핑) -├── actions.ts ← 🆕 생성 -├── PermissionDetail.tsx ← 🔄 수정 (API 연동) -├── PermissionDetailClient.tsx ← 🔄 수정 -└── PermissionDialog.tsx ← 🔄 수정 -``` - ---- - -## 8. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-12-30 | Phase 1~3 | API 개발 완료 (마이그레이션, Controller, Service, Swagger, 라우트) | 다수 | ✅ | -| 2025-12-30 | Phase 4 | React 연동 완료 (actions.ts, types.ts, index.tsx, PermissionDetailClient.tsx) | react 4개 파일 | ✅ | -| 2025-12-30 | 문서 | 계획 문서 초안 작성 | - | - | -| 2025-12-30 | 문서 | Phase 4 완료 반영 업데이트 | - | - | - ---- - -## 9. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **DB 스키마**: `docs/specs/database-schema.md` -- **mng 권한관리**: `mng/app/Services/RoleService.php`, `RolePermissionService.php` - ---- - -## 10. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 10.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("l2-permission-state") // 1. 상태 파악 -read_memory("l2-permission-snapshot") // 2. 사고 흐름 복구 -``` - -### 10.2 Serena 메모리 구조 -- `l2-permission-state`: { phase, progress, next_step, last_decision } -- `l2-permission-snapshot`: 현재까지의 논의 및 코드 변경점 요약 - ---- - -## 11. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 11.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| GET /api/v1/roles | 역할 목록 반환 | | ⏳ | -| POST /api/v1/roles | 역할 생성 | | ⏳ | -| PUT /api/v1/roles/{id} | 역할 수정 | | ⏳ | -| DELETE /api/v1/roles/{id} | 역할 삭제 | | ⏳ | -| GET /api/v1/roles/{id}/menus | 메뉴+권한 매트릭스 | | ⏳ | -| POST /api/v1/roles/{id}/permissions/toggle | 권한 토글 | | ⏳ | - -### 11.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| localStorage 제거 | ⏳ | | -| 역할 CRUD API 동작 | ⏳ | | -| 권한 매트릭스 API 동작 | ⏳ | | -| 숨김 기능 동작 | ⏳ | | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/material-input-per-item-mapping-plan.md b/plans/archive/material-input-per-item-mapping-plan.md deleted file mode 100644 index e40c15b..0000000 --- a/plans/archive/material-input-per-item-mapping-plan.md +++ /dev/null @@ -1,482 +0,0 @@ -# 개소별 자재 투입 매핑 계획 - -> **작성일**: 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/mes-integration-analysis-plan.md b/plans/archive/mes-integration-analysis-plan.md deleted file mode 100644 index 3b9bc28..0000000 --- a/plans/archive/mes-integration-analysis-plan.md +++ /dev/null @@ -1,525 +0,0 @@ -# MES 모듈 통합 흐름 분석 계획 - -> **작성일**: 2025-01-09 -> **목적**: 견적 → 수주 → 작업지시 + 공정관리 모듈 간 연동 상태 점검 및 문제점 분석 -> **기준 문서**: `docs/plans/process-management-plan.md`, `docs/plans/order-management-plan.md`, `docs/plans/work-order-plan.md` -> **상태**: ✅ 분석 완료 + 개선 방향 **재결정됨** (2025-01-09 추가 분석) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 공정 관리 페이지 확인 + 개념 명확화 | -| **다음 작업** | WorkOrder `process_type` → `process_id` FK 변경 구현 | -| **진행률** | 7/7 (100%) | -| **마지막 업데이트** | 2025-01-09 | - -### ✅ 결정된 개선 방향 (재결정) - -| 결정 사항 | 내용 | -|----------|------| -| **WorkOrder.process_type** | `process_type` (varchar) → `process_id` (FK) **변경** | -| **Process.process_type** | 공정 구분 → `common_codes`에서 관리 | -| **개념 정리** | 공정명(WorkOrder) ≠ 공정구분(Process) 명확히 구분 | - ---- - -## 1. 개요 - -### 1.1 배경 -MES 시스템의 핵심 모듈인 공정관리, 수주관리, 작업지시가 개별적으로 개발 완료되었으나, -모듈 간 통합 흐름이 제대로 설계되었는지 검증이 필요합니다. - -### 1.2 분석 목표 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 분석 목표 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 모듈 간 데이터 흐름 검증 │ -│ 2. API 연동 상태 점검 │ -│ 3. 프론트엔드 연동 상태 점검 │ -│ 4. 설계 문제점 및 개선 방안 도출 │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. 분석 대상 - -### 2.1 모듈 구성 - -| 모듈 | 역할 | API 상태 | Frontend 상태 | -|------|------|:--------:|:------------:| -| **견적관리 (Quote)** | 견적서 작성 및 수주 변환 | ✅ 완료 | ✅ 완료 | -| **수주관리 (Order)** | 견적→수주 변환, 생산지시 생성 | ✅ 완료 | ✅ 완료 | -| **작업지시 (WorkOrder)** | 실제 생산 작업 관리 | ✅ 완료 | ✅ 완료 | -| **공정관리 (Process)** | 공정 템플릿 및 품목 분류 규칙 관리 | ✅ 완료 | ✅ 완료 | - -### 2.2 기대 데이터 흐름 - -``` -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ -│ 견적관리 │ │ 수주관리 │ │ 작업지시 │ │ 공정관리 │ -│ (Quote) │ ──→ │ (Order) │ ──→ │ (WorkOrder) │ ? │ (Process) │ -└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ - │ │ │ │ - ▼ ▼ ▼ ▼ - - 견적서 작성 - 수주 확정 - 작업 상태 관리 - 공정 템플릿 - - 품목/단가 구성 - 생산지시 생성 - 담당자 배정 - 품목 분류 규칙 - - 고객 승인 - 납기 관리 - 공정별 진행 - 작업 단계 정의 -``` - ---- - -## 3. 분석 결과 - -### 3.0 ✅ 견적관리 → 수주관리 연동 (정상 작동) - -**API 연동 구현**: -``` -POST /api/v1/orders/from-quote/{quoteId} -→ Order 생성 + Quote 상태 변경 (finalized → converted) -``` - -**연결 관계**: -| 항목 | 내용 | -|------|------| -| FK 연결 | `orders.quote_id` → `quotes.id` | -| 상태 연동 | Quote `finalized` 시에만 수주 변환 가능 | -| 중복 방지 | 동일 Quote에 대해 중복 변환 불가 | - -**Quote 상태 흐름**: -``` -draft → sent → approved → finalized → converted -(임시저장) (발송) (승인) (확정) (수주변환) -``` - -**API 핵심 로직** (`api/app/Services/OrderService.php`): -```php -public function createFromQuote(int $quoteId): Order -{ - $quote = Quote::findOrFail($quoteId); - - // 변환 가능 상태 검증 (finalized만 가능) - if ($quote->status !== Quote::STATUS_FINALIZED) { - throw new BadRequestHttpException(__('error.quote.must_be_finalized')); - } - - // 중복 변환 방지 - $existingOrder = Order::where('quote_id', $quoteId)->first(); - if ($existingOrder) { - throw new BadRequestHttpException(__('error.order.already_exists_from_quote')); - } - - // Order 생성 + Quote 품목 자동 복사 - $order = Order::create([ - 'quote_id' => $quote->id, - 'client_id' => $quote->client_id, - 'status_code' => Order::STATUS_DRAFT, - // ... 견적 정보 복사 - ]); - - // Quote 상태 변경 - $quote->status = Quote::STATUS_CONVERTED; - $quote->save(); - - return $order; -} -``` - -**프론트엔드 구현**: -```typescript -// react/src/components/orders/actions.ts -export async function createOrderFromQuote( - quoteId: string | number -): Promise - -// react/src/components/quotes/QuotationSelectDialog.tsx -// 견적 선택 → 수주 변환 UI 컴포넌트 -``` - -**데이터 변환**: -| Quote 필드 | Order 필드 | 변환 방식 | -|-----------|-----------|----------| -| `id` | `quote_id` (FK) | 참조 | -| `client_id` | `client_id` | 복사 | -| `project_name` | `project_name` | 복사 | -| `quote_items` | `order_items` | 품목 복사 | -| `product_category` | - | 참조용 | - -**평가**: ✅ **정상 구현됨** - FK 관계, 상태 연동, 중복 방지 모두 정상 - ---- - -### 3.1 ✅ 수주관리 → 작업지시 연동 (정상 작동) - -**API 연동 구현**: -``` -POST /api/v1/orders/{id}/production-order -→ WorkOrder 생성 + Order 상태 변경 (CONFIRMED → IN_PROGRESS) -``` - -**연결 관계**: -| 항목 | 내용 | -|------|------| -| FK 연결 | `work_orders.sales_order_id` → `orders.id` | -| 상태 연동 | Order CONFIRMED 시에만 생산지시 가능 | -| 중복 방지 | 동일 Order에 대해 중복 생성 불가 | - -**프론트엔드 구현**: -```typescript -// react/src/components/orders/actions.ts -export async function createProductionOrder( - orderId: string, - data?: CreateProductionOrderData -): Promise - -// CreateProductionOrderData 타입 -interface CreateProductionOrderData { - processType?: 'screen' | 'slat' | 'bending'; - priority?: 'urgent' | 'high' | 'normal' | 'low'; - assigneeId?: number; - teamId?: number; - scheduledDate?: string; - memo?: string; -} -``` - -**평가**: ✅ **정상 구현됨** - ---- - -### 3.2 🔴 공정관리 → 작업지시 연동 (설계 문제 발견 → 해결 방향 결정) - -#### 3.2.0 ✅ 개념 명확화 (2025-01-09 추가 분석) - -**공정 관리 페이지 확인** (`/master-data/process-management`): - -| 공정코드 | 공정명 | 구분 | 담당부서 | 상태 | -|---------|-------|------|---------|------| -| P-001 | 슬랫 | 생산 | 경영본부 | 사용중 | -| P-002 | 스크린 | 생산 | 개발팀 | 사용중 | - -**핵심 발견**: -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 💡 개념 정리 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ WorkOrder.process_type = "공정명" (스크린, 슬랫, 절곡) │ -│ → 공정 관리 테이블(processes)에서 등록된 공정 │ -│ → 하드코딩 ❌ → 공정 테이블 FK로 연결해야 함 ✅ │ -│ │ -│ Process.process_type = "공정 구분" (생산, 검사, 포장, 조립) │ -│ → 공정의 분류/카테고리 │ -│ → common_codes에서 관리해야 함 ✅ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**최종 정리**: - -| 구분 | 필드명 | 실제 의미 | 현재 상태 | 올바른 상태 | -|------|--------|----------|----------|------------| -| **WorkOrder** | `process_type` | 공정명 | 하드코딩 (screen/slat/bending) | **공정 테이블 FK** | -| **Process** | `process_type` | 공정 구분 | 하드코딩 (생산/검사/포장/조립) | common_codes | - ---- - -#### 3.2.1 process_type 불일치 문제 (기존 분석) - -| 구분 | 공정관리 (Process) | 작업지시 (WorkOrder) | -|------|:------------------:|:-------------------:| -| **필드명** | `process_type` | `process_type` | -| **값 (Frontend)** | '생산', '검사', '포장', '조립' | 'screen', 'slat', 'bending' | -| **값 개수** | 4개 (한글) | 3개 (영문) | -| **실제 의미** | 공정 **구분** (카테고리) | 공정 **명** (공정 테이블 데이터) | - -**문제점**: -- 동일한 필드명(`process_type`)을 사용하지만 **완전히 다른 의미** -- WorkOrder는 **공정 테이블을 참조해야 하는데** 하드코딩되어 있음 -- **FK 관계가 없음** - Process 테이블과 WorkOrder 테이블 연결 없음 - -#### 3.2.2 코드 증거 - -**공정관리 타입** (`react/src/types/process.ts`): -```typescript -export type ProcessType = '생산' | '검사' | '포장' | '조립'; -``` - -**작업지시 타입** (`react/src/components/production/WorkOrders/types.ts`): -```typescript -export type ProcessType = 'screen' | 'slat' | 'bending'; - -export const PROCESS_TYPE_LABELS: Record = { - screen: '스크린', - slat: '슬랫', - bending: '절곡', -}; -``` - -**API 모델** (`api/app/Models/Production/WorkOrder.php`): -```php -const PROCESS_SCREEN = 'screen'; -const PROCESS_SLAT = 'slat'; -const PROCESS_BENDING = 'bending'; -``` - -#### 3.2.3 영향도 분석 - -| 기능 | 현재 상태 | 문제점 | -|------|----------|--------| -| 공정 선택 | WorkOrder 생성 시 하드코딩된 3개 옵션만 사용 | Process 테이블 활용 안됨 | -| 분류 규칙 | Process에만 존재 | WorkOrder에서 품목 자동 분류 불가 | -| 작업 단계 | Process와 WorkOrder 각각 별도 정의 | 데이터 중복 | -| 메타데이터 | Process에 풍부한 정보 (인원, 설비, 템플릿) | WorkOrder에서 미활용 | - ---- - -### 3.3 🟡 공정관리 → 수주관리 연동 (연결 없음) - -**현재 상태**: -- Process와 Order 간 직접적인 연결 관계 없음 -- 이는 **의도된 설계**로 보임 (공정은 생산 단계에서 적용) - ---- - -## 4. 문제점 요약 - -### 4.1 핵심 문제: process_type 이중 정의 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🔴 핵심 문제 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ 공정관리(Process)와 작업지시(WorkOrder)가 │ -│ 동일한 필드명(process_type)을 사용하지만 │ -│ 완전히 다른 값 체계와 목적을 가지고 있음 │ -│ │ -│ ┌─────────────┐ ┌─────────────┐ │ -│ │ Process │ ❌ │ WorkOrder │ │ -│ │ (생산/검사) │ ─────── │ (screen/slat) │ │ -│ └─────────────┘ 연결없음 └─────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 4.2 문제 유형 분류 - -| # | 문제 | 심각도 | 영향 | -|---|------|:------:|------| -| 1 | process_type 값 체계 불일치 | 🔴 높음 | 데이터 일관성, 확장성 | -| 2 | Process ↔ WorkOrder FK 부재 | 🔴 높음 | 메타데이터 활용 불가 | -| 3 | 공정 정보 중복 정의 | 🟡 중간 | 유지보수 복잡성 | -| 4 | 새 공정 추가 시 코드 수정 필요 | 🟡 중간 | 확장성 제한 | - ---- - -## 5. 해결 방안 (검토 필요) - -### 5.1 Option A: 현행 유지 (의도된 분리) - -**전제**: 공정관리와 작업지시가 **서로 다른 도메인**임을 인정 - -``` -공정관리 (Process) 작업지시 (WorkOrder) -───────────────── ───────────────── -목적: 품목 분류 자동화 목적: 실제 생산 작업 관리 -대상: 모든 품목 유형 대상: 특화 제조품 (스크린/슬랫/절곡) -사용자: 품질/물류팀 사용자: 생산팀 -``` - -**장점**: -- 현재 코드 변경 불필요 -- 각 도메인의 독립성 유지 - -**단점**: -- `process_type` 필드명 혼란 지속 -- 공정 메타데이터 재활용 불가 - -**권장 조치**: -- WorkOrder의 `process_type`을 `manufacturing_type` 또는 `product_line`으로 **리네이밍** -- 문서에 두 개념의 차이 명확히 기술 - ---- - -### 5.2 Option B: 통합 연결 (FK 추가) - -**전제**: 공정관리가 작업지시의 **상위 템플릿** 역할을 해야 함 - -``` -Process (공정 템플릿) - │ - │ process_id (FK) - ▼ -WorkOrder (작업지시) -``` - -**필요 변경**: -1. `work_orders` 테이블에 `process_id` FK 추가 -2. Process 모델에 제조 공정 유형 추가 (screen, slat, bending) -3. WorkOrder 생성 시 Process 선택 UI 추가 -4. 공정별 메타데이터 (작업단계, 인원, 설비) 자동 적용 - -**장점**: -- 데이터 일관성 확보 -- 공정 메타데이터 재활용 -- 새 공정 추가 시 코드 수정 불필요 - -**단점**: -- DB 마이그레이션 필요 -- 기존 데이터 마이그레이션 필요 -- API 및 프론트엔드 수정 필요 - ---- - -### 5.3 Option C: 하이브리드 (권장) - -**전제**: 점진적 통합으로 위험 최소화 - -**Phase 1**: 명명 정리 (즉시) -- WorkOrder의 `process_type` → `manufacturing_type` 리네이밍 -- 문서 정리 및 팀 공유 - -**Phase 2**: 연결 준비 (중기) -- Process 모델에 `is_manufacturing` 플래그 추가 -- 제조 전용 공정 구분 (screen, slat, bending) - -**Phase 3**: 통합 (장기) -- WorkOrder에 `process_id` FK 추가 (optional) -- 메타데이터 연동 구현 - ---- - -## 6. 컨펌 결과 (✅ 결정 완료 → 재결정) - -| # | 항목 | ~~이전 결정~~ | **최종 결정** | 결정일 | -|---|------|-------------|--------------|--------| -| 1 | **설계 방향** | ~~Option C (하이브리드)~~ | **Option B** (FK 추가) | 2025-01-09 | -| 2 | **필드 변경** | ~~리네이밍만~~ | **FK로 변경** | 2025-01-09 | -| 3 | **FK 추가 여부** | ~~❌ 불필요~~ | **✅ 필요** - 공정 테이블 FK | 2025-01-09 | -| 4 | **도메인 연결** | ~~독립 도메인~~ | **Process → WorkOrder 연결** | 2025-01-09 | - -### 6.0 재결정 사유 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 💡 핵심 발견 (공정 관리 페이지 확인) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ WorkOrder.process_type 값 (screen, slat, bending)이 │ -│ 실제로는 공정 관리 페이지에서 등록된 "공정명"임을 확인 │ -│ │ -│ /master-data/process-management 등록 현황: │ -│ - P-001: 슬랫 (slat) │ -│ - P-002: 스크린 (screen) │ -│ │ -│ ∴ 하드코딩된 값이 아닌 공정 테이블 FK로 연결해야 함 │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 6.1 다음 작업 (FK 추가 구현) - -``` -WorkOrder `process_type` (varchar) → `process_id` (FK) 변경 작업 범위: - -1. DB 마이그레이션 - - work_orders.process_type (varchar) 제거 - - work_orders.process_id (FK) 추가 → processes.id 참조 - - 기존 데이터 마이그레이션 (screen→P-002, slat→P-001, bending→신규등록) - -2. API 수정 - - api/app/Models/Production/WorkOrder.php - - PROCESS_* 상수 제거 - - process_type 필드 → process_id FK 필드 - - process() BelongsTo 관계 추가 - - api/app/Services/OrderService.php (생산지시 생성 로직) - - api/app/Services/WorkOrderService.php (비즈니스 로직) - - 관련 FormRequest, Resource 클래스 - -3. Frontend 수정 - - react/src/components/production/WorkOrders/types.ts - - ProcessType enum 제거 - - process_id: number 필드 추가 - - process 관계 데이터 타입 추가 - - 관련 컴포넌트 (actions.ts, components) - - 공정 선택 드롭다운 → API에서 공정 목록 조회 -``` - ---- - -## 7. 참고 문서 - -- **공정관리 계획**: `docs/plans/process-management-plan.md` -- **수주관리 계획**: `docs/plans/order-management-plan.md` -- **작업지시 계획**: `docs/plans/work-order-plan.md` -- **시스템 아키텍처**: `docs/architecture/system-overview.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - ---- - -## 8. 분석 파일 참조 - -### 8.1 API 레이어 -| 파일 | 역할 | -|------|------| -| `api/app/Http/Controllers/Api/V1/QuoteController.php` | 견적 CRUD | -| `api/app/Http/Controllers/Api/V1/OrderController.php` | 수주 CRUD + 생산지시 생성 | -| `api/app/Http/Controllers/V1/ProcessController.php` | 공정 CRUD | -| `api/app/Http/Controllers/Api/V1/WorkOrderController.php` | 작업지시 CRUD | -| `api/app/Services/QuoteService.php` | 견적 비즈니스 로직 | -| `api/app/Services/OrderService.php` | 견적→수주 변환, 수주→작업지시 연동 | -| `api/app/Services/WorkOrderService.php` | 작업지시 비즈니스 로직 | - -### 8.2 모델 레이어 -| 파일 | 핵심 필드 | -|------|----------| -| `api/app/Models/Quote/Quote.php` | `status` (draft/sent/approved/finalized/converted), `product_category` | -| `api/app/Models/Order.php` | `status_code`, `quote_id` (FK) | -| `api/app/Models/Process.php` | `process_type` (생산/검사/포장/조립) | -| `api/app/Models/Production/WorkOrder.php` | `process_type` (screen/slat/bending), `sales_order_id` (FK) | - -### 8.3 프론트엔드 레이어 -| 파일 | 역할 | -|------|------| -| `react/src/components/quotes/types.ts` | Quote 타입 정의 | -| `react/src/components/quotes/QuotationSelectDialog.tsx` | 견적 선택 UI | -| `react/src/types/process.ts` | Process 타입 정의 | -| `react/src/components/production/WorkOrders/types.ts` | WorkOrder 타입 정의 | -| `react/src/components/orders/actions.ts` | Order API 호출 + 생산지시 생성 + 견적변환 | -| `react/src/components/process-management/actions.ts` | Process API 호출 | -| `react/src/components/production/WorkOrders/actions.ts` | WorkOrder API 호출 | - ---- - -## 9. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-09 | 문서 생성 | MES 통합 흐름 분석 완료 | - | - | -| 2025-01-09 | 견적 분석 추가 | Quote → Order 연동 분석 (섹션 3.0) | - | - | -| 2025-01-09 | 결정 반영 | Option C 선택, 리네이밍 진행, FK 미추가 결정 | - | ✅ | -| 2025-01-09 | **재결정** | 공정 관리 페이지 확인 후 **Option B (FK 추가)로 변경** | - | ✅ | - -### 9.1 재결정 상세 - -**재결정 배경**: -- 공정 관리 페이지(`/master-data/process-management`) 실제 확인 -- `screen`, `slat`, `bending` 값이 공정명(Process Name)임을 확인 -- P-001: 슬랫, P-002: 스크린 등록 확인 - -**이전 결정 → 최종 결정**: -| 항목 | 이전 | 최종 | -|------|------|------| -| 설계 방향 | Option C (하이브리드) | **Option B (FK 추가)** | -| 필드 처리 | 리네이밍만 | **FK로 변경** | -| FK 추가 | 불필요 | **필요** | -| 도메인 관계 | 독립 | **연결** | - ---- - -*이 문서는 /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 deleted file mode 100644 index 54261a4..0000000 --- a/plans/archive/mng-item-formula-integration-plan.md +++ /dev/null @@ -1,837 +0,0 @@ -# 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 deleted file mode 100644 index 172f216..0000000 --- a/plans/archive/mng-item-management-plan.md +++ /dev/null @@ -1,1447 +0,0 @@ -# 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/archive/mng-quote-formula-development-plan.md b/plans/archive/mng-quote-formula-development-plan.md deleted file mode 100644 index a632902..0000000 --- a/plans/archive/mng-quote-formula-development-plan.md +++ /dev/null @@ -1,553 +0,0 @@ -# MNG 견적수식 관리 개발 계획 - -> **작성일**: 2025-12-22 -> **상태**: ✅ 완료 -> **대상**: mng.sam.kr/quote-formulas - ---- - -## 1. 현황 분석 - -### 1.1 MNG 프로젝트 현재 상태 - -#### 구현된 기능 (mng) - -| 기능 | 상태 | 설명 | -|-----|------|-----| -| 수식 목록 | ✅ 완료 | 페이지네이션, 필터링, HTMX 테이블 | -| 수식 생성 | ✅ 완료 | 카테고리, 유형, 변수명, 수식 입력 | -| 수식 수정 | ✅ 완료 | 편집 폼, API 연동 | -| 수식 삭제 | ✅ 완료 | Soft Delete, 복원, 영구삭제 | -| 수식 복제 | ✅ 완료 | 수식 복사 기능 | -| 활성/비활성 | ✅ 완료 | 토글 기능 | -| 카테고리 관리 | ✅ 완료 | CRUD 구현 | -| 시뮬레이터 | ✅ 완료 | 입력값 → 계산 결과 미리보기 | -| 변수 참조 | ✅ 완료 | 사용 가능한 변수 목록 표시 | -| 수식 검증 | ✅ 완료 | 문법 검증 API | -| 범위(Range) 관리 UI | ✅ 완료 | 범위별 결과 설정 화면 (Phase 1) | -| 매핑(Mapping) 관리 UI | ✅ 완료 | 매핑 규칙 설정 화면 (Phase 2) | -| 품목(Item) 관리 UI | ✅ 완료 | 출력 품목 설정 화면 (Phase 3) | - -### 1.2 API 프로젝트 현재 상태 - -#### 모델 구조 (api) - -``` -QuoteFormulaCategory (카테고리) -└── QuoteFormula (수식) - ├── QuoteFormulaRange (범위 조건) - ├── QuoteFormulaMapping (매핑 규칙) - └── QuoteFormulaItem (출력 품목) -``` - -#### 시더 데이터 (api) - -| 시더 | 데이터 수 | 설명 | -|-----|---------|-----| -| QuoteFormulaCategorySeeder | 11개 | 카테고리 (오픈사이즈~단가수식) | -| QuoteFormulaSeeder | 30개 수식, 18개 범위 | 스크린 계산 수식 | -| QuoteFormulaItemSeeder | 25개 | 품목 마스터 | - -#### 서비스 (api) - -| 서비스 | 역할 | -|-------|-----| -| QuoteCalculationService | 자동산출 실행 엔진 | -| FormulaEvaluatorService | 수식 평가, 범위/매핑 처리 | -| QuoteService | 견적 CRUD, 상태 관리 | -| QuoteNumberService | 견적번호 생성 | -| QuoteDocumentService | PDF/이메일/카카오 발송 (TODO) | - ---- - -## 2. MNG vs API 비교 분석 - -### 2.1 데이터 구조 비교 - -| 항목 | MNG | API | 일치 | -|-----|-----|-----|-----| -| quote_formula_categories | ✅ | ✅ | ✅ | -| quote_formulas | ✅ | ✅ | ✅ | -| quote_formula_ranges | ✅ | ✅ | ✅ | -| quote_formula_mappings | ✅ | ✅ | ✅ | -| quote_formula_items | ✅ | ✅ | ✅ | - -**결론**: 모델 구조는 동일함 (같은 DB 사용) - -### 2.2 기능 비교 - -| 기능 | MNG | API | 비고 | -|-----|-----|-----|-----| -| 수식 CRUD | ✅ | ✅ | 동일 | -| 카테고리 CRUD | ✅ | ✅ | 동일 | -| 범위 관리 UI | ✅ | ✅ (시더) | Phase 1 완료 | -| 매핑 관리 UI | ✅ | ✅ (시더) | Phase 2 완료 | -| 품목 관리 UI | ✅ | ✅ (시더) | Phase 3 완료 | -| 시뮬레이터 | ✅ | ✅ | 동일 | -| 자동산출 API | - | ✅ | API 전용 | - ---- - -## 3. 개발 계획 (완료) - -### 3.1 목표 - -MNG에서 **범위(Range), 매핑(Mapping), 품목(Item)** 관리 UI를 추가하여: -1. 시더 없이도 관리자가 직접 수식 규칙 설정 가능 -2. SAM 자체 품목 마스터로 가격 설정 -3. 실시간 시뮬레이션으로 설정 검증 가능 - -### 3.2 개발 범위 (완료) - -#### Phase 1: 범위(Range) 관리 UI ✅ - -**우선순위**: 높음 -**이유**: 모터, 가이드레일, 케이스 자동 선택에 필수 - -**기능 목록**: -1. 수식 상세 페이지에 범위 관리 탭 추가 -2. 범위 목록 표시 (min ~ max → 결과) -3. 범위 추가/수정/삭제 -4. 드래그앤드롭 순서 변경 -5. item_code 연결 (품목 선택) - -**화면 설계**: -``` -[수식 수정] 페이지 -├── [기본 정보] 탭 (기존) -├── [범위 설정] 탭 ← 추가 -│ ├── 조건 변수: [K (중량)] ▼ -│ ├── 범위 목록 -│ │ ┌─────────────────────────────────────────────────┐ -│ │ │ # │ 최소값 │ 최대값 │ 결과값 │ 품목코드 │ -│ │ ├─────────────────────────────────────────────────┤ -│ │ │ 1 │ 0 │ 150 │ 150K │ PT-MOTOR-150│ -│ │ │ 2 │ 150 │ 300 │ 300K │ PT-MOTOR-300│ -│ │ │ 3 │ 300 │ 400 │ 400K │ PT-MOTOR-400│ -│ │ └─────────────────────────────────────────────────┘ -│ └── [+ 범위 추가] -├── [매핑 설정] 탭 -└── [품목 설정] 탭 -``` - -**API 엔드포인트 (MNG 내부)**: -``` -GET /api/admin/quote-formulas/formulas/{id}/ranges -POST /api/admin/quote-formulas/formulas/{id}/ranges -PUT /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId} -DELETE /api/admin/quote-formulas/formulas/{id}/ranges/{rangeId} -POST /api/admin/quote-formulas/formulas/{id}/ranges/reorder -``` - -#### Phase 2: 매핑(Mapping) 관리 UI ✅ - -**우선순위**: 중간 -**이유**: 제어기 유형 등 코드 매핑에 사용 - -**기능 목록**: -1. 수식 상세 페이지에 매핑 관리 탭 추가 -2. 매핑 목록 표시 (소스값 → 결과값) -3. 매핑 추가/수정/삭제 - -**화면 설계**: -``` -[매핑 설정] 탭 -├── 소스 변수: [CONTROL_TYPE] ▼ -├── 매핑 목록 -│ ┌──────────────────────────────────────────────────┐ -│ │ # │ 소스값 │ 결과값 │ 품목코드 │ -│ ├──────────────────────────────────────────────────┤ -│ │ 1 │ EMB │ 매립형 │ PT-CTRL-EMB │ -│ │ 2 │ EXP │ 노출형 │ PT-CTRL-EXP │ -│ │ 3 │ BOX_1P │ 콘트롤박스 │ PT-CTRL-BOX-1P │ -│ └──────────────────────────────────────────────────┘ -└── [+ 매핑 추가] -``` - -#### Phase 3: 품목(Item) 관리 UI ✅ - -**우선순위**: 중간 -**이유**: 수식 결과로 생성되는 품목 정의 - -**기능 목록**: -1. 수식 상세 페이지에 품목 관리 탭 추가 -2. 품목 목록 표시 -3. 품목 추가/수정/삭제 -4. 수량/단가 수식 입력 -5. SAM 품목 마스터에서 가격 참조 - -**화면 설계**: -``` -[품목 설정] 탭 -├── 품목 목록 -│ ┌───────────────────────────────────────────────────────────┐ -│ │ 품목코드 │ 품목명 │ 규격 │ 수량식 │ 단가식│ -│ ├───────────────────────────────────────────────────────────┤ -│ │ PT-MOTOR-150 │ 개폐전동기 150kg│ 150K(S) │ 1 │ 285000│ -│ │ PT-GR-3000 │ 가이드레일 3000 │ 3000mm │ 2 │ 42000 │ -│ └───────────────────────────────────────────────────────────┘ -└── [+ 품목 추가] -``` - -### 3.3 파일 구조 (구현 완료) - -#### Controllers -``` -app/Http/Controllers/ -├── QuoteFormulaController.php (수정: 탭 추가) -└── Api/Admin/Quote/ - ├── QuoteFormulaController.php - ├── QuoteFormulaRangeController.php ✅ - ├── QuoteFormulaMappingController.php ✅ - ├── QuoteFormulaItemController.php ✅ - └── QuoteFormulaCategoryController.php -``` - -#### Services -``` -app/Services/Quote/ -├── QuoteFormulaService.php -├── QuoteFormulaRangeService.php ✅ -├── QuoteFormulaMappingService.php ✅ -├── QuoteFormulaItemService.php ✅ -└── QuoteFormulaCategoryService.php -``` - -#### Views -``` -resources/views/quote-formulas/ -├── index.blade.php -├── create.blade.php -├── edit.blade.php (수정: 탭 구조) -├── simulator.blade.php -└── partials/ - ├── basic-info-tab.blade.php ✅ - ├── ranges-tab.blade.php ✅ - ├── mappings-tab.blade.php ✅ - └── items-tab.blade.php ✅ -``` - ---- - -## 4. 기술 스택 - -### 4.1 Frontend (MNG) -- **Framework**: Laravel Blade + Alpine.js -- **Styling**: Tailwind CSS + DaisyUI -- **AJAX**: HTMX (hx-get, hx-post, hx-delete) -- **Modal**: DaisyUI modal 컴포넌트 - -### 4.2 Backend (MNG) -- **Framework**: Laravel 12 -- **ORM**: Eloquent -- **DB**: MySQL (samdb) -- **Auth**: Session 기반 - -### 4.3 API 연동 -- MNG 내부 API (`/api/admin/quote-formulas/*`) - ---- - -## 5. 검증 계획 - -### 5.1 시뮬레이터 테스트 -``` -입력: W0=3000, H0=2500 -예상 결과: - - CASE: PT-CASE-3600 (S=3270) - - GR: PT-GR-3000 (H1=2770) - - MOTOR: PT-MOTOR-150 (K=41.21kg) -``` - -### 5.2 CRUD 테스트 -- 범위 추가/수정/삭제 후 시뮬레이터 결과 확인 -- 품목 가격 변경 후 합계 확인 - ---- - -## 6. 참고 자료 - -### 6.1 파일 위치 (MNG) -``` -mng/ -├── app/Http/Controllers/ -│ ├── QuoteFormulaController.php -│ └── Api/Admin/Quote/ -│ ├── QuoteFormulaController.php -│ ├── QuoteFormulaRangeController.php -│ ├── QuoteFormulaMappingController.php -│ ├── QuoteFormulaItemController.php -│ └── QuoteFormulaCategoryController.php -├── app/Services/Quote/ -│ ├── QuoteFormulaService.php -│ ├── QuoteFormulaRangeService.php -│ ├── QuoteFormulaMappingService.php -│ ├── QuoteFormulaItemService.php -│ └── QuoteFormulaCategoryService.php -├── app/Models/Quote/ -│ ├── QuoteFormula.php -│ ├── QuoteFormulaCategory.php -│ ├── QuoteFormulaRange.php -│ ├── QuoteFormulaMapping.php -│ └── QuoteFormulaItem.php -└── resources/views/quote-formulas/ - ├── index.blade.php - ├── create.blade.php - ├── edit.blade.php - ├── simulator.blade.php - └── partials/ - ├── basic-info-tab.blade.php - ├── ranges-tab.blade.php - ├── mappings-tab.blade.php - └── items-tab.blade.php -``` - -### 6.2 API 시더 위치 -``` -api/database/seeders/ -├── QuoteFormulaCategorySeeder.php -├── QuoteFormulaSeeder.php -└── QuoteFormulaItemSeeder.php -``` - ---- - -## 7. 코딩 컨벤션 및 예시 코드 - -### 7.1 API Controller 패턴 (MNG) - -```php -rangeService->getRangesByFormula($formulaId); - - return response()->json([ - 'success' => true, - 'data' => $ranges, - ]); - } - - /** - * 범위 생성 - */ - public function store(Request $request, int $formulaId): JsonResponse - { - $validated = $request->validate([ - 'min_value' => 'nullable|numeric', - 'max_value' => 'nullable|numeric', - 'condition_variable' => 'required|string|max:50', - 'result_value' => 'required|string', - 'result_type' => 'in:fixed,formula', - 'sort_order' => 'nullable|integer', - ]); - - $range = $this->rangeService->createRange($formulaId, $validated); - - return response()->json([ - 'success' => true, - 'message' => '범위가 추가되었습니다.', - 'data' => $range, - ]); - } - - /** - * 범위 수정 - */ - public function update(Request $request, int $formulaId, int $rangeId): JsonResponse - { - $validated = $request->validate([ - 'min_value' => 'nullable|numeric', - 'max_value' => 'nullable|numeric', - 'result_value' => 'required|string', - 'result_type' => 'in:fixed,formula', - ]); - - $this->rangeService->updateRange($rangeId, $validated); - - return response()->json([ - 'success' => true, - 'message' => '범위가 수정되었습니다.', - ]); - } - - /** - * 범위 삭제 - */ - public function destroy(int $formulaId, int $rangeId): JsonResponse - { - $this->rangeService->deleteRange($rangeId); - - return response()->json([ - 'success' => true, - 'message' => '범위가 삭제되었습니다.', - ]); - } - - /** - * 순서 변경 - */ - public function reorder(Request $request, int $formulaId): JsonResponse - { - $validated = $request->validate([ - 'range_ids' => 'required|array', - 'range_ids.*' => 'integer', - ]); - - $this->rangeService->reorder($validated['range_ids']); - - return response()->json([ - 'success' => true, - 'message' => '순서가 변경되었습니다.', - ]); - } -} -``` - -### 7.2 Service 패턴 (MNG) - -```php -orderBy('sort_order') - ->get(); - } - - /** - * 범위 생성 - */ - public function createRange(int $formulaId, array $data): QuoteFormulaRange - { - $data['formula_id'] = $formulaId; - - // 순서 자동 설정 - if (!isset($data['sort_order'])) { - $maxOrder = QuoteFormulaRange::where('formula_id', $formulaId)->max('sort_order') ?? 0; - $data['sort_order'] = $maxOrder + 1; - } - - return QuoteFormulaRange::create($data); - } - - /** - * 범위 수정 - */ - public function updateRange(int $rangeId, array $data): QuoteFormulaRange - { - $range = QuoteFormulaRange::findOrFail($rangeId); - $range->update($data); - - return $range->fresh(); - } - - /** - * 범위 삭제 - */ - public function deleteRange(int $rangeId): void - { - QuoteFormulaRange::destroy($rangeId); - } - - /** - * 순서 변경 - */ - public function reorder(array $rangeIds): void - { - foreach ($rangeIds as $order => $id) { - QuoteFormulaRange::where('id', $id)->update(['sort_order' => $order + 1]); - } - } -} -``` - -### 7.3 API 응답 형식 - -```json -// 성공 응답 -{ - "success": true, - "message": "범위가 추가되었습니다.", - "data": { ... } -} - -// 실패 응답 -{ - "success": false, - "message": "이미 사용 중인 변수명입니다." -} - -// 목록 응답 -{ - "success": true, - "data": [ - { - "id": 1, - "formula_id": 5, - "min_value": "0.0000", - "max_value": "150.0000", - "condition_variable": "K", - "result_value": "{\"value\":\"150K\",\"item_code\":\"PT-MOTOR-150\"}", - "result_type": "fixed", - "sort_order": 1 - } - ] -} -``` - ---- - -## 8. 체크리스트 (완료) - -### 개발 완료 확인 - -- [x] mng 프로젝트 디렉토리: `/Users/hskwon/Works/@KD_SAM/SAM/mng` -- [x] `QuoteFormulaRangeController.php` 생성 -- [x] `QuoteFormulaRangeService.php` 생성 -- [x] `QuoteFormulaMappingController.php` 생성 -- [x] `QuoteFormulaMappingService.php` 생성 -- [x] `QuoteFormulaItemController.php` 생성 -- [x] `QuoteFormulaItemService.php` 생성 -- [x] `routes/api.php`에 라우트 추가 -- [x] `edit.blade.php` 탭 구조로 수정 -- [x] `partials/ranges-tab.blade.php` 생성 -- [x] `partials/mappings-tab.blade.php` 생성 -- [x] `partials/items-tab.blade.php` 생성 - ---- - -*문서 버전*: 2.0 -*작성자*: Claude Code -*검토자*: - -*최종 업데이트*: 2025-12-22 (Phase 1-3 완료, 5130 연동 제거) \ No newline at end of file diff --git a/plans/archive/notification-sound-system-plan.md b/plans/archive/notification-sound-system-plan.md deleted file mode 100644 index f2e7e66..0000000 --- a/plans/archive/notification-sound-system-plan.md +++ /dev/null @@ -1,424 +0,0 @@ -# 알림음 시스템 구현 계획 - -> **작성일**: 2025-01-07 -> **목적**: FCM 푸시 알림 타입별 커스텀 알림음 구현 -> **영향 범위**: app (Capacitor), api (Laravel), mng (Laravel) -> **상태**: ✅ 핵심 기능 완료 (4.3 알림 설정 테이블은 후순위) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 5 - 테스트 및 검증 완료 ✅ | -| **다음 작업** | 완료 (4.3 알림 설정 테이블은 후순위) | -| **진행률** | 10/11 (91%) - 핵심 기능 완료 | -| **마지막 업데이트** | 2025-01-07 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 SAM 앱은 FCM 푸시 알림 시 2개 채널(`push_default`, `push_urgent`)만 지원합니다. -비즈니스 요구사항에 따라 알림 타입별로 다른 알림음이 필요합니다: - -- 결제 알림 → 결제 전용 알림음 -- 수주 알림 → 수주 전용 알림음 -- 발주 알림 → 발주 전용 알림음 -- 계약 알림 → 계약 전용 알림음 -- 일반 알림 → 기본 알림음 -- 신규업체 등록 → 긴급 알림음 - -### 1.2 목표 구조 - -| 타입 | 채널 ID | 알림음 파일 | 설명 | -|------|---------|------------|------| -| 결제 | `push_payment` | `push_payment.wav` | 결제 관련 알림 | -| 수주 | `push_sales_order` | `push_sales_order.wav` | 수주 관련 알림 | -| 발주 | `push_purchase_order` | `push_purchase_order.wav` | 발주 관련 알림 | -| 계약 | `push_contract` | `push_contract.wav` | 계약 관련 알림 | -| 일반 | `push_default` | `push_default.wav` | 일반 알림 (기존) | -| 신규업체 등록 | `push_urgent` | `push_urgent.wav` | 신규업체 등록 (기존) | - -### 1.3 현재 상태 분석 - -#### App (Capacitor Android) -- **파일**: `app/android/app/src/main/java/com/codebridgex/webapp/MainActivity.java` -- **현재**: 2개 채널 (`push_default`, `push_urgent`) -- **알림음**: `res/raw/push_default.wav`, `res/raw/push_urgent.wav` - -#### API (Laravel) -- **파일**: `api/app/Services/Fcm/FcmSender.php` -- **현재**: `channel_id` 파라미터 지원, 사운드는 `'default'` 하드코딩 -- **문제**: 커스텀 사운드 미지원 - -#### MNG (Laravel) -- **파일**: `mng/app/Http/Controllers/FcmController.php` -- **현재**: `sound_key` 파라미터 존재하나 실제 활용 안됨 - -### 1.4 시스템 흐름 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ FCM 알림음 시스템 흐름 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ MNG (발송 UI) │ -│ ┌─────────────────┐ │ -│ │ 타입 선택 │ ← 결제/수주/발주/계약/일반/신규업체 │ -│ │ channel_id 설정 │ │ -│ └────────┬────────┘ │ -│ │ │ -│ ▼ │ -│ API (FCM 발송) │ -│ ┌─────────────────┐ │ -│ │ FcmSender │ │ -│ │ channel_id → │ │ -│ │ android.channel │ │ -│ └────────┬────────┘ │ -│ │ │ -│ ▼ │ -│ Firebase Cloud Messaging │ -│ ┌─────────────────┐ │ -│ │ FCM Server │ │ -│ └────────┬────────┘ │ -│ │ │ -│ ▼ │ -│ App (Capacitor) │ -│ ┌─────────────────┐ │ -│ │ NotificationChannel │ ← channel_id로 매칭 │ -│ │ 채널별 사운드 재생 │ ← push_payment.wav 등 │ -│ └─────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: App - 채널 및 알림음 추가 - -| # | 작업 항목 | 상태 | 파일 | -|---|----------|:----:|------| -| 1.1 | 알림음 파일 준비 (4개) | ✅ | `res/raw/*.wav` | -| 1.2 | MainActivity.java 채널 추가 (4개) | ✅ | `MainActivity.java` | - -### 2.2 Phase 2: API - FcmSender 수정 - -| # | 작업 항목 | 상태 | 파일 | -|---|----------|:----:|------| -| 2.1 | buildMessage() 사운드 동적 처리 | ✅ | `FcmSender.php` | -| 2.2 | 채널-사운드 매핑 (FcmSender 내부 통합) | ✅ | `FcmSender.php` | - -### 2.3 Phase 3: MNG - 발송 UI 수정 - -| # | 작업 항목 | 상태 | 파일 | -|---|----------|:----:|------| -| 3.1 | 타입 선택 드롭다운 추가 | ✅ | `fcm/send.blade.php` | -| 3.2 | 타입-채널 매핑 로직 | ✅ | `FcmController.php` | - -### 2.4 Phase 4: 이벤트 기반 자동 푸시 - -| # | 작업 항목 | 상태 | 파일 | -|---|----------|:----:|------| -| 4.1 | PushNotificationService 생성 | ✅ | `api/app/Services/PushNotificationService.php` | -| 4.2 | 신규 거래처 등록 시 푸시 | ✅ | `api/app/Services/ClientService.php` | -| 4.3 | 알림 설정 테이블 (추후) | ⏭️ | 후순위 | - -### 2.5 Phase 5: 테스트 및 검증 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 5.1 | 각 타입별 푸시 발송 테스트 | ✅ | 6개 타입 | -| 5.2 | 알림음 재생 확인 | ✅ | Android 실기기 | - ---- - -## 3. 상세 작업 내용 - -### 3.1 Phase 1: App - 채널 및 알림음 추가 - -#### 1.1 알림음 파일 준비 - -**위치**: `app/android/app/src/main/res/raw/` - -| 파일명 | 상태 | 비고 | -|--------|------|------| -| `push_default.wav` | ✅ | 일반 알림 | -| `push_urgent.wav` | ✅ | 신규업체 등록 | -| `push_payment.wav` | ✅ | 결제 알림 | -| `push_sales_order.wav` | ✅ | 수주 알림 | -| `push_purchase_order.wav` | ✅ | 발주 알림 | -| `push_contract.wav` | ✅ | 계약 알림 | - -> **완료**: 6개 알림음 파일 모두 준비됨 (2025-01-07) - -#### 1.2 MainActivity.java 수정 - -**현재 코드** (2개 채널): -```java -public static final String CHANNEL_DEFAULT = "push_default"; -public static final String CHANNEL_URGENT = "push_urgent"; -``` - -**목표 코드** (6개 채널): -```java -public static final String CHANNEL_DEFAULT = "push_default"; -public static final String CHANNEL_URGENT = "push_urgent"; -public static final String CHANNEL_PAYMENT = "push_payment"; -public static final String CHANNEL_SALES_ORDER = "push_sales_order"; -public static final String CHANNEL_PURCHASE_ORDER = "push_purchase_order"; -public static final String CHANNEL_CONTRACT = "push_contract"; -``` - -### 3.2 Phase 2: API - FcmSender 수정 - -#### 2.1 buildMessage() 수정 - -**현재** (`FcmSender.php:112`): -```php -'android' => [ - 'notification' => [ - 'channel_id' => $channelId, - 'sound' => 'default', // 하드코딩 - ], -], -``` - -**목표**: -```php -'android' => [ - 'notification' => [ - 'channel_id' => $channelId, - 'sound' => $this->getSoundForChannel($channelId), - ], -], -``` - -#### 2.2 채널-사운드 매핑 - -```php -// config/fcm.php 또는 FcmSender 내부 -private function getSoundForChannel(string $channelId): string -{ - return match($channelId) { - 'push_payment' => 'push_payment', - 'push_sales_order' => 'push_sales_order', - 'push_purchase_order' => 'push_purchase_order', - 'push_contract' => 'push_contract', - 'push_urgent' => 'push_urgent', - default => 'push_default', - }; -} -``` - -### 3.3 Phase 3: MNG - 발송 UI 수정 - -#### 3.1 타입 선택 UI - -```html - -``` - -#### 3.2 타입 → 채널 매핑 - -```php -$channelMap = [ - 'general' => 'push_default', - 'payment' => 'push_payment', - 'sales_order' => 'push_sales_order', - 'purchase_order' => 'push_purchase_order', - 'contract' => 'push_contract', - 'new_company' => 'push_urgent', -]; -``` - -### 3.4 Phase 4: 이벤트 기반 자동 푸시 - -#### 4.1 PushNotificationService 생성 - -**파일**: `api/app/Services/PushNotificationService.php` - -```php -getChannelForEvent($event); - - // 해당 테넌트의 활성 토큰 조회 - $tokens = PushDeviceToken::where('tenant_id', $tenantId) - ->where('is_active', true) - ->pluck('token') - ->toArray(); - - if (empty($tokens)) { - return; - } - - $this->fcmSender->sendToMany( - $tokens, - $title, - $body, - $channelId, - $data - ); - } - - /** - * 이벤트 → 채널 매핑 - */ - private function getChannelForEvent(string $event): string - { - return match($event) { - 'payment' => 'push_payment', - 'sales_order' => 'push_sales_order', - 'purchase_order' => 'push_purchase_order', - 'contract' => 'push_contract', - 'new_client' => 'push_urgent', - default => 'push_default', - }; - } -} -``` - -#### 4.2 ClientService에서 푸시 호출 - -**파일**: `api/app/Services/ClientService.php` (store 메서드) - -```php -/** 생성 */ -public function store(array $data) -{ - $tenantId = $this->tenantId(); - - $data['client_code'] = $this->generateClientCode($tenantId); - $data['tenant_id'] = $tenantId; - $data['is_active'] = $data['is_active'] ?? true; - - $client = Client::create($data); - - // 신규 거래처 등록 푸시 발송 - app(PushNotificationService::class) - ->setTenantId($tenantId) - ->sendByEvent( - 'new_client', - $tenantId, - '신규 거래처 등록', - "새로운 거래처 '{$client->name}'이(가) 등록되었습니다.", - ['client_id' => $client->id] - ); - - return $client; -} -``` - -#### 4.3 이벤트 타입 정의 - -| 이벤트 | 채널 | 발생 시점 | -|--------|------|----------| -| `new_client` | `push_urgent` | 거래처 신규 등록 | -| `payment` | `push_payment` | 결제 완료/요청 | -| `sales_order` | `push_sales_order` | 수주 등록/변경 | -| `purchase_order` | `push_purchase_order` | 발주 등록/변경 | -| `contract` | `push_contract` | 계약 등록/만료 | - ---- - -## 4. 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 알림음 파일 추가, 채널 추가 | 불필요 | -| ⚠️ 컨펌 필요 | FcmSender 로직 변경, UI 수정 | **필수** | -| 🔴 금지 | FCM 구조 변경, 기존 채널 삭제 | 별도 협의 | - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 알림음 파일 | 6개 wav 파일 준비 | app | ✅ 완료 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-07 | - | 계획 문서 초안 작성 | - | - | -| 2025-01-07 | 1.2 | MainActivity.java 6개 채널 추가 | `MainActivity.java` | ✅ | -| 2025-01-07 | 2.1/2.2 | FcmSender 사운드 동적 처리 + getSoundForChannel 추가 | `FcmSender.php` | ✅ | -| 2025-01-07 | 3.1 | MNG 알림 타입 드롭다운 추가 (6개 타입) | `fcm/send.blade.php` | ✅ | -| 2025-01-07 | 3.2 | FcmController channel_id 검증 + sound_key 제거 | `FcmController.php` | ✅ | -| 2025-01-07 | 4.1 | PushNotificationService 생성 (이벤트 기반 푸시) | `PushNotificationService.php` | ✅ | -| 2025-01-07 | 4.2 | ClientService.store()에 푸시 알림 연동 | `ClientService.php` | ✅ | -| 2025-01-07 | 5.1/5.2 | 테스트 및 검증 완료 | 서버 배포 후 실기기 테스트 | ✅ | - ---- - -## 7. 참고 문서 - -- **FCM 푸시 계획**: `docs/plans/react-fcm-push-notification-plan.md` -- **API 규칙**: `docs/standards/api-rules.md` - ---- - -## 8. 알림음 파일 준비 가이드 - -### 요구사항 -- **포맷**: WAV (권장) 또는 MP3 -- **길이**: 1-3초 권장 -- **샘플레이트**: 44.1kHz -- **비트레이트**: 16bit - -### 임시 방안 -알림음 파일이 준비되지 않은 경우, 기존 파일을 복사하여 사용: - -```bash -cd app/android/app/src/main/res/raw/ -cp push_default.wav push_payment.wav -cp push_default.wav push_sales_order.wav -cp push_default.wav push_purchase_order.wav -cp push_default.wav push_contract.wav -``` - -### 무료 알림음 리소스 -- [Pixabay Sound Effects](https://pixabay.com/sound-effects/) -- [Freesound](https://freesound.org/) -- [Zapsplat](https://www.zapsplat.com/) - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/order-location-management-plan.md b/plans/archive/order-location-management-plan.md deleted file mode 100644 index cac3da9..0000000 --- a/plans/archive/order-location-management-plan.md +++ /dev/null @@ -1,831 +0,0 @@ -# 수주 하위 구조 관리 시스템 구축 계획 - -> **작성일**: 2026-02-06 -> **목적**: 수주(Order) 하위에 범용 N-depth 트리 구조를 구축하여 개소/구역/공정 등 다양한 하위 단위를 자유롭게 관리 -> **기준 문서**: `docs/features/quotes/README.md`, `docs/specs/database-schema.md`, `docs/standards/api-rules.md` -> **상태**: 🔄 진행중 -> **설계 결정**: 하이브리드 (고정 코어 컬럼 + options JSON) — 통계 쿼리 성능과 유연성 균형 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4.2 - 프론트엔드 노드별 그룹 UI | -| **다음 작업** | 완료 (테스트 검증 필요) | -| **진행률** | 13/13 (100%) | -| **마지막 업데이트** | 2026-02-06 | - ---- - -## 1. 개요 - -### 1.1 배경 - -**즉시 문제**: 견적→수주 전환(`QuoteService::convertToOrder`)에서 개소 정보가 매핑되지 않아 `order_items.floor_code`, `symbol_code`가 null로 저장됨. 반면 `OrderService::syncFromQuote`에는 이미 파싱 로직이 있어 정상 동작. - -**구조적 문제**: 현재 수주 하위 구조는 `order_items` 플랫 테이블뿐이며, 개소/구역/공정 등 다양한 그루핑 단위를 관리할 수 없음. 5130(경동)은 개소별 관리가 필요하지만, 향후 다른 테넌트에서는 구역별, 층별, 공정별 등 다양한 트리 구조가 필요. - -**현재 데이터 흐름 문제**: -``` -견적 저장: - quotes.calculation_inputs.items[] → 개소별 데이터 ✅ - quote_items.note → "4F FSS-01" ✅ - -수주 전환 (convertToOrder): - order_items.floor_code → null ❌ ← $productMapping이 빈 배열 - order_items.symbol_code → null ❌ - -수주 동기화 (syncFromQuote): - order_items.floor_code → "4F" ✅ ← note 파싱 로직 있음 - order_items.symbol_code → "FSS-01" ✅ -``` - -### 1.2 목표 - -1. 견적→수주 전환 시 개소 정보가 정확히 매핑되도록 즉시 수정 (Quick Fix) -2. `order_nodes` 테이블을 신규 생성하여 **범용 N-depth 트리 구조** 제공 -3. 노드별 독립 상태 추적 (대기/진행중/완료/취소) -4. 프론트엔드에서 노드별 그룹 UI 제공 (경동은 개소별 표시) - -### 1.3 아키텍처 결정 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 설계 결정: 하이브리드 (고정 코어 + options JSON) │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ ❌ 순수 EAV → 통계 쿼리 시 JOIN 폭발, 성능 문제 │ -│ ❌ 고정 컬럼 전용 → 경동 개소에만 맞고 범용성 없음 │ -│ ✅ 하이브리드 → 통계용 고정 컬럼 + 유형별 상세는 options JSON │ -│ │ -│ 근거: │ -│ - SAM 프로젝트에서 이미 options JSON 패턴 사용 중 │ -│ (work_order_items.options, quotes.calculation_inputs) │ -│ - MySQL 8 JSON path 쿼리 지원 (options->>'$.floor' 등) │ -│ - 통계 집계는 고정 컬럼(code, name, status, quantity, price)으로 │ -│ - sam_stat 일간/월간 집계에도 고정 컬럼 기반으로 수월 │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.4 핵심 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 1. 범용 트리: N-depth 자기참조(parent_id)로 어떤 구조든 표현 가능 │ -│ 2. 통계 친화: code, name, status, quantity, price는 고정 컬럼 │ -│ 3. 유형 자유: node_type으로 구분, 유형별 상세는 options JSON │ -│ 4. 역호환성: 기존 수주(order_nodes 없는)도 정상 동작 │ -│ 5. SAM 패턴 준수: BelongsToTenant, Auditable, SoftDeletes │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.5 적용 예시 - -**경동 (1-depth: 개소)**: -``` -Order: ORD-260206-001 -├── Node (type:location, code:"1F-FSS-01", name:"1F FSS-01") -│ ├── options: { floor:"1F", symbol:"FSS-01", product_code:"KSS01", -│ │ open_width:5000, open_height:3000, guide_rail:"wall" } -│ └── OrderItems (자재 N개) -│ -└── Node (type:location, code:"2F-SD-02", name:"2F SD-02") - ├── options: { floor:"2F", symbol:"SD-02", product_code:"KWE01", - │ open_width:2800, open_height:2400 } - └── OrderItems (자재 N개) -``` - -**다른 테넌트 (3-depth: 동→층→실)**: -``` -Order: ORD-260206-005 -├── Node (type:zone, code:"A", name:"A동") -│ ├── Node (type:floor, code:"1F", name:"1층") -│ │ ├── Node (type:room, code:"101", name:"회의실") -│ │ │ └── OrderItems -│ │ └── Node (type:room, code:"102", name:"사무실") -│ │ └── OrderItems -│ └── Node (type:floor, code:"2F", name:"2층") -│ └── ... -└── Node (type:zone, code:"B", name:"B동") - └── ... -``` - -### 1.6 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | convertToOrder 로직 수정, 모델 관계 추가 | 불필요 | -| ⚠️ 컨펌 필요 | 마이그레이션 생성, 신규 테이블, API 엔드포인트 추가 | **필수** | -| 🔴 금지 | 기존 order_items.floor_code/symbol_code 삭제, 기존 API 스키마 변경 | 별도 협의 | - -### 1.7 준수 규칙 - -- `docs/standards/api-rules.md` - Service-First, FormRequest, i18n -- `docs/specs/database-schema.md` - 공통 컬럼 패턴 (tenant_id, audit, softDeletes) -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `react/CLAUDE.md` - 'use client' 필수, Server Actions - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: convertToOrder 개소 파싱 (Quick Fix) - -| # | 작업 항목 | 파일 | 상태 | 비고 | -|---|----------|------|:----:|------| -| 1.1 | convertToOrder에 개소 파싱 로직 추가 | `api/app/Services/Quote/QuoteService.php` | ✅ | syncFromQuote 로직 재사용 | -| 1.2 | 개소 파싱 공통 메소드 추출 | `api/app/Services/Quote/QuoteService.php` | ✅ | 중복 코드 제거 | - -### 2.2 Phase 2: order_nodes 테이블 (DB 스키마) - -| # | 작업 항목 | 파일 | 상태 | 비고 | -|---|----------|------|:----:|------| -| 2.1 | order_nodes 마이그레이션 생성 | `api/database/migrations/XXXX_create_order_nodes_table.php` | ✅ | 신규 테이블 | -| 2.2 | order_items에 order_node_id 추가 | `api/database/migrations/XXXX_add_order_node_id_to_order_items.php` | ✅ | nullable FK | -| 2.3 | OrderNode 모델 생성 | `api/app/Models/Orders/OrderNode.php` | ✅ | BelongsToTenant, SoftDeletes, 자기참조 | -| 2.4 | Order 모델에 nodes() 관계 추가 | `api/app/Models/Orders/Order.php` | ✅ | HasMany | -| 2.5 | OrderItem 모델에 node() 관계 추가 | `api/app/Models/Orders/OrderItem.php` | ✅ | BelongsTo, fillable 추가 | - -### 2.3 Phase 3: 전환 로직 연동 (Service) - -| # | 작업 항목 | 파일 | 상태 | 비고 | -|---|----------|------|:----:|------| -| 3.1 | convertToOrder에 OrderNode 생성 로직 추가 | `api/app/Services/Quote/QuoteService.php` | ✅ | Phase 1.1 위에 구축 | -| 3.2 | syncFromQuote에 OrderNode 동기화 추가 | `api/app/Services/OrderService.php` | ✅ | 기존 items 삭제→재생성 패턴 동일 | -| 3.3 | 수주 상세 조회에 nodes eager loading | `api/app/Services/OrderService.php` | ✅ | show() 메소드 수정 | - -### 2.4 Phase 4: 프론트엔드 노드별 UI - -| # | 작업 항목 | 파일 | 상태 | 비고 | -|---|----------|------|:----:|------| -| 4.1 | OrderNode 타입 + 서버 액션 추가 | `react/src/components/orders/actions.ts` | ✅ | 타입 정의, API 호출 | -| 4.2 | 수주 상세 뷰 노드별 그룹 UI | `react/src/components/orders/OrderSalesDetailView.tsx` | ✅ | 트리/아코디언 형식 | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Phase 1: Quick Fix (convertToOrder 개소 파싱) -├── 1.1 syncFromQuote의 개소 파싱 로직을 공통 메소드로 추출 -├── 1.2 convertToOrder에서 공통 메소드 호출하여 $productMapping 전달 -└── 검증: 견적→수주 전환 후 order_items.floor_code/symbol_code 값 확인 - -Phase 2: DB 스키마 (order_nodes 테이블) -├── 2.1 order_nodes 마이그레이션 작성 -│ ├── 트리 구조: parent_id 자기참조 (nullable = 루트) -│ ├── 고정 코어: node_type, code, name, status_code, quantity, unit_price, total_price -│ └── 유연 확장: options JSON -├── 2.2 order_items에 order_node_id 컬럼 마이그레이션 작성 -├── 2.3 OrderNode 모델 생성 (BelongsToTenant, Auditable, SoftDeletes) -│ ├── 자기참조 관계: parent(), children() -│ └── items() HasMany -├── 2.4 Order 모델에 nodes() HasMany 관계 추가 -├── 2.5 OrderItem 모델에 node() BelongsTo 관계 추가 -└── 검증: php artisan migrate 성공, 트리 관계 정상 동작 - -Phase 3: 전환 로직 연동 -├── 3.1 convertToOrder에 OrderNode 생성 로직 삽입 -│ ├── calculation_inputs.items[] 순회하여 OrderNode (type:location) 생성 -│ ├── bomResults[]에서 금액 정보 매핑 -│ └── OrderItem 생성 시 order_node_id 연결 -├── 3.2 syncFromQuote에 OrderNode 동기화 추가 -│ ├── 기존 nodes 소프트삭제 → 신규 생성 -│ └── OrderItem 재생성 시 node 연결 -├── 3.3 수주 상세 조회에 nodes eager loading 추가 -└── 검증: API 호출로 노드 데이터 정상 반환 확인 - -Phase 4: 프론트엔드 UI -├── 4.1 타입 + 서버 액션 -│ ├── OrderNode 인터페이스 정의 -│ └── 수주 상세 조회 응답에 nodes 포함 -├── 4.2 수주 상세 뷰 노드별 그룹 UI -│ ├── 노드별 카드/아코디언 레이아웃 -│ ├── 노드 헤더 (유형/코드/이름/상태/금액) -│ ├── 노드 내 자재 테이블 -│ ├── 하위 노드 중첩 표시 (재귀 컴포넌트) -│ └── 역호환: nodes 없는 수주는 기존 플랫 테이블 유지 -└── 검증: 수주 상세 화면에서 노드별 그룹 표시 확인 -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1: Quick Fix (변경 없음) - -#### 1.1 convertToOrder 개소 파싱 로직 추가 - -**현재 코드** (`QuoteService.php` Line 600-607): -```php -$serialIndex = 1; -foreach ($quote->items as $quoteItem) { - $orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex); - $orderItem->created_by = $userId; - $orderItem->save(); - $serialIndex++; -} -``` - -**수정 코드**: -```php -$calculationInputs = $quote->calculation_inputs ?? []; -$productItems = $calculationInputs['items'] ?? []; - -$serialIndex = 1; -foreach ($quote->items as $quoteItem) { - $productMapping = $this->resolveLocationMapping($quoteItem, $productItems); - $orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex, $productMapping); - $orderItem->created_by = $userId; - $orderItem->save(); - $serialIndex++; -} -``` - -#### 1.2 공통 메소드 추출 - -```php -/** - * 견적 품목에서 개소(층/부호) 정보 추출 - */ -private function resolveLocationMapping(QuoteItem $quoteItem, array $productItems): array -{ - $floorCode = null; - $symbolCode = null; - - // 1순위: note에서 파싱 ("4F FSS-01") - $note = trim($quoteItem->note ?? ''); - if ($note !== '') { - $parts = preg_split('/\s+/', $note, 2); - $floorCode = $parts[0] ?? null; - $symbolCode = $parts[1] ?? null; - } - - // 2순위: formula_source → calculation_inputs - if (empty($floorCode) && empty($symbolCode)) { - $productIndex = 0; - $formulaSource = $quoteItem->formula_source ?? ''; - if (preg_match('/product_(\d+)/', $formulaSource, $matches)) { - $productIndex = (int) $matches[1]; - } - if (isset($productItems[$productIndex])) { - $floorCode = $productItems[$productIndex]['floor'] ?? null; - $symbolCode = $productItems[$productIndex]['code'] ?? null; - } elseif (count($productItems) === 1) { - $floorCode = $productItems[0]['floor'] ?? null; - $symbolCode = $productItems[0]['code'] ?? null; - } - } - - return ['floor_code' => $floorCode, 'symbol_code' => $symbolCode]; -} -``` - ---- - -### 4.2 Phase 2: DB 스키마 - -#### 2.1 order_nodes 마이그레이션 - -```php -Schema::create('order_nodes', function (Blueprint $table) { - $table->id()->comment('ID'); - $table->foreignId('tenant_id')->comment('테넌트 ID'); - $table->foreignId('order_id')->comment('수주 ID'); - - // ---- 트리 구조 ---- - $table->foreignId('parent_id')->nullable()->comment('상위 노드 ID (NULL=루트)'); - - // ---- 고정 코어 (통계/집계용) ---- - $table->string('node_type', 50)->comment('노드 유형 (location, zone, floor, room, process...)'); - $table->string('code', 100)->comment('식별 코드'); - $table->string('name', 200)->comment('표시명'); - $table->string('status_code', 30)->default('PENDING') - ->comment('상태 (PENDING/CONFIRMED/IN_PRODUCTION/PRODUCED/SHIPPED/COMPLETED/CANCELLED)'); - $table->integer('quantity')->default(1)->comment('수량'); - $table->decimal('unit_price', 15, 2)->default(0)->comment('단가'); - $table->decimal('total_price', 15, 2)->default(0)->comment('합계'); - - // ---- 유연 확장 (유형별 상세) ---- - $table->json('options')->nullable()->comment('유형별 동적 속성 JSON'); - - // ---- 정렬 ---- - $table->integer('depth')->default(0)->comment('트리 깊이 (0=루트)'); - $table->integer('sort_order')->default(0)->comment('정렬 순서'); - - // ---- 감사 ---- - $table->foreignId('created_by')->nullable()->comment('생성자 ID'); - $table->foreignId('updated_by')->nullable()->comment('수정자 ID'); - $table->foreignId('deleted_by')->nullable()->comment('삭제자 ID'); - $table->timestamps(); - $table->softDeletes(); - - // ---- 인덱스 ---- - $table->index('tenant_id'); - $table->index('parent_id'); - $table->index(['order_id', 'depth', 'sort_order']); - $table->index(['order_id', 'node_type']); - $table->index(['tenant_id', 'node_type', 'status_code']); // 통계용 -}); -``` - -**통계 쿼리 예시**: -```sql --- 1. 노드 유형별 수주 현황 (고정 컬럼만으로 가능) -SELECT node_type, status_code, COUNT(*), SUM(total_price) -FROM order_nodes WHERE tenant_id = 287 -GROUP BY node_type, status_code; - --- 2. 경동 개소별 상세 (필요 시 JSON path) -SELECT code, name, total_price, - options->>'$.floor' AS floor, - options->>'$.symbol' AS symbol -FROM order_nodes -WHERE order_id = 123 AND node_type = 'location'; -``` - -#### 2.2 order_items에 order_node_id 추가 - -```php -Schema::table('order_items', function (Blueprint $table) { - $table->foreignId('order_node_id') - ->nullable() - ->after('order_id') - ->comment('수주 노드 ID (order_nodes)'); - $table->index('order_node_id'); -}); -``` - -#### 2.3 OrderNode 모델 - -```php -namespace App\Models\Orders; - -class OrderNode extends Model -{ - use Auditable, BelongsToTenant, SoftDeletes; - - protected $table = 'order_nodes'; - - // 상태 코드 (Order와 동일 체계) - public const STATUS_PENDING = 'PENDING'; - public const STATUS_CONFIRMED = 'CONFIRMED'; - public const STATUS_IN_PRODUCTION = 'IN_PRODUCTION'; - public const STATUS_PRODUCED = 'PRODUCED'; - public const STATUS_SHIPPED = 'SHIPPED'; - public const STATUS_COMPLETED = 'COMPLETED'; - public const STATUS_CANCELLED = 'CANCELLED'; - - protected $fillable = [ - 'tenant_id', 'order_id', 'parent_id', - 'node_type', 'code', 'name', - 'status_code', 'quantity', 'unit_price', 'total_price', - 'options', 'depth', 'sort_order', - 'created_by', 'updated_by', 'deleted_by', - ]; - - protected $casts = [ - 'quantity' => 'integer', - 'unit_price' => 'decimal:2', - 'total_price' => 'decimal:2', - 'options' => 'array', - 'depth' => 'integer', - ]; - - // ---- 트리 관계 ---- - public function parent(): BelongsTo - { - return $this->belongsTo(self::class, 'parent_id'); - } - - public function children(): HasMany - { - return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order'); - } - - // ---- 비즈니스 관계 ---- - public function order(): BelongsTo - { - return $this->belongsTo(Order::class); - } - - public function items(): HasMany - { - return $this->hasMany(OrderItem::class, 'order_node_id'); - } - - // ---- 트리 헬퍼 ---- - public function isRoot(): bool - { - return $this->parent_id === null; - } - - public function isLeaf(): bool - { - return $this->children()->count() === 0; - } - - /** - * 하위 노드 포함 전체 트리 재귀 로드 - */ - public function scopeWithRecursiveChildren($query) - { - return $query->with(['children' => function ($q) { - $q->orderBy('sort_order')->with('children', 'items'); - }, 'items']); - } -} -``` - -#### 2.4-2.5 기존 모델 수정 - -**Order 모델**: -```php -public function nodes(): HasMany -{ - return $this->hasMany(OrderNode::class)->orderBy('depth')->orderBy('sort_order'); -} - -public function rootNodes(): HasMany -{ - return $this->hasMany(OrderNode::class)->whereNull('parent_id')->orderBy('sort_order'); -} -``` - -**OrderItem 모델** - fillable + 관계: -```php -// fillable에 추가 -'order_node_id', - -// 관계 -public function node(): BelongsTo -{ - return $this->belongsTo(OrderNode::class, 'order_node_id'); -} -``` - ---- - -### 4.3 Phase 3: 전환 로직 연동 - -#### 3.1 convertToOrder OrderNode 생성 - -**수정 위치**: `QuoteService::convertToOrder()` (Line 590~623) - -```php -return DB::transaction(function () use ($quote, $userId, $tenantId) { - $orderNo = $this->generateOrderNumber($tenantId); - $order = Order::createFromQuote($quote, $orderNo); - $order->created_by = $userId; - $order->save(); - - // ---- OrderNode 생성 (개소별) ---- - $calculationInputs = $quote->calculation_inputs ?? []; - $productItems = $calculationInputs['items'] ?? []; - $bomResults = $calculationInputs['bomResults'] ?? []; - - $nodeMap = []; // productIndex → OrderNode - foreach ($productItems as $idx => $locItem) { - $bomResult = $bomResults[$idx] ?? null; - $grandTotal = $bomResult['grand_total'] ?? 0; - $qty = (int) ($locItem['quantity'] ?? 1); - $floor = $locItem['floor'] ?? ''; - $symbol = $locItem['code'] ?? ''; - - $node = OrderNode::create([ - 'tenant_id' => $tenantId, - 'order_id' => $order->id, - 'parent_id' => null, // 루트 노드 (경동은 1-depth) - 'node_type' => 'location', - 'code' => trim("{$floor}-{$symbol}", '-') ?: "LOC-{$idx}", - 'name' => trim("{$floor} {$symbol}") ?: "개소 ".($idx + 1), - 'status_code' => OrderNode::STATUS_PENDING, - 'quantity' => $qty, - 'unit_price' => $grandTotal, - 'total_price' => $grandTotal * $qty, - 'options' => [ - 'floor' => $floor, - 'symbol' => $symbol, - 'product_code' => $locItem['productCode'] ?? null, - 'product_name' => $locItem['productName'] ?? null, - 'open_width' => $locItem['openWidth'] ?? null, - 'open_height' => $locItem['openHeight'] ?? null, - 'guide_rail_type' => $locItem['guideRailType'] ?? null, - 'motor_power' => $locItem['motorPower'] ?? null, - 'controller' => $locItem['controller'] ?? null, - 'wing_size' => $locItem['wingSize'] ?? null, - 'inspection_fee' => $locItem['inspectionFee'] ?? null, - 'bom_result' => $bomResult, - ], - 'depth' => 0, - 'sort_order' => $idx, - 'created_by' => $userId, - ]); - $nodeMap[$idx] = $node; - } - - // ---- OrderItem 생성 (노드 연결) ---- - $serialIndex = 1; - foreach ($quote->items as $quoteItem) { - $mapping = $this->resolveLocationMapping($quoteItem, $productItems); - $locIdx = $this->resolveLocationIndex($quoteItem, $productItems); - - $productMapping = array_merge($mapping, [ - 'order_node_id' => isset($nodeMap[$locIdx]) ? $nodeMap[$locIdx]->id : null, - ]); - - $orderItem = OrderItem::createFromQuoteItem($quoteItem, $order->id, $serialIndex, $productMapping); - $orderItem->created_by = $userId; - $orderItem->save(); - $serialIndex++; - } - - // 합계 재계산 + 견적 상태 변경 (기존 로직 유지) - $order->load('items'); - $order->recalculateTotals(); - $order->save(); - - $quote->update([ - 'status' => Quote::STATUS_CONVERTED, - 'order_id' => $order->id, - 'updated_by' => $userId, - ]); - - return $quote->refresh()->load(['items', 'client', 'order']); -}); -``` - -**resolveLocationIndex 헬퍼**: -```php -private function resolveLocationIndex(QuoteItem $quoteItem, array $productItems): int -{ - $formulaSource = $quoteItem->formula_source ?? ''; - if (preg_match('/product_(\d+)/', $formulaSource, $matches)) { - return (int) $matches[1]; - } - - $note = trim($quoteItem->note ?? ''); - if ($note !== '') { - $parts = preg_split('/\s+/', $note, 2); - $floor = $parts[0] ?? ''; - $code = $parts[1] ?? ''; - foreach ($productItems as $idx => $item) { - if (($item['floor'] ?? '') === $floor && ($item['code'] ?? '') === $code) { - return $idx; - } - } - } - - return 0; -} -``` - -#### 3.2 syncFromQuote OrderNode 동기화 - -**수정 위치**: `OrderService::syncFromQuote()` (Line 559~659) - -기존 `$order->items()->delete()` 다음에: -```php -// 기존 노드 삭제 후 재생성 -$order->nodes()->delete(); - -// OrderNode 생성 (convertToOrder와 동일 로직) -$nodeMap = []; -foreach ($productItems as $idx => $locItem) { - // ... (convertToOrder와 동일) - $nodeMap[$idx] = $node; -} - -// OrderItem 생성 시 order_node_id 연결 -foreach ($quote->items as $index => $quoteItem) { - $locIdx = $this->resolveLocationIndex($quoteItem, $productItems); - $order->items()->create([ - // ... 기존 필드 ... - 'order_node_id' => $nodeMap[$locIdx]->id ?? null, - ]); -} -``` - -#### 3.3 수주 상세 조회 nodes eager loading - -```php -$order = Order::where('tenant_id', $tenantId) - ->with([ - 'items', - 'rootNodes' => function ($q) { - $q->withRecursiveChildren(); // 재귀 트리 로드 - }, - 'client', - 'quote', - ]) - ->find($id); -``` - ---- - -### 4.4 Phase 4: 프론트엔드 노드별 UI - -#### 4.1 타입 + 서버 액션 - -**OrderNode 타입** (`react/src/components/orders/actions.ts`): -```typescript -export interface OrderNode { - id: number; - parentId: number | null; - nodeType: string; // 'location', 'zone', 'floor', 'room', 'process'... - code: string; - name: string; - statusCode: string; - quantity: number; - unitPrice: number; - totalPrice: number; - options: Record | null; // 유형별 동적 속성 - depth: number; - sortOrder: number; - children: OrderNode[]; // 하위 노드 (재귀) - items: OrderItem[]; // 해당 노드의 자재 -} - -export interface OrderDetail extends Order { - nodes: OrderNode[]; // 루트 노드 배열 (children 재귀 포함) -} -``` - -#### 4.2 수주 상세 뷰 노드별 그룹 UI - -**레이아웃 (경동 1-depth 예시)**: -``` -┌─ 수주 기본 정보 ────────────────────────────────────────┐ -│ 수주번호: ORD-260206-001 | 현장명: 삼성 빌딩 신축 │ -│ 거래처: 삼성물산 | 총금액: 15,000,000원 │ -└─────────────────────────────────────────────────────────┘ - -┌─ 구조 (3개 노드) ──────────────────────────────────────┐ -│ │ -│ ┌─ [location] 1F FSS-01 ──────────────────────────┐ │ -│ │ KSS01(고정스크린) | 5000×3000 | 수량:1 │ │ -│ │ 상태: [PENDING ▾] | 금액: 1,250,000원 │ │ -│ ├──────────────────────────────────────────────────┤ │ -│ │ # | 품목코드 | 품명 | 수량 | 단가 | 금액 │ │ -│ │ 1 | MT-SUS-01 | 슬랫 | 15.5 | 12,000 | 186K │ │ -│ │ 소계: 1,250,000원 │ │ -│ └──────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ [location] 2F SD-02 ──────────────────────────┐ │ -│ │ ... │ │ -│ └─────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────┘ -``` - -**재귀 컴포넌트 (N-depth)**: -```typescript -function OrderNodeCard({ node, depth = 0 }: { node: OrderNode; depth?: number }) { - return ( -
- {/* 노드 헤더 */} - - - {/* 해당 노드의 자재 테이블 */} - {node.items.length > 0 && } - - {/* 하위 노드 재귀 렌더링 */} - {node.children.map(child => ( - - ))} -
- ); -} -``` - -**역호환**: -```typescript -{order.nodes && order.nodes.length > 0 ? ( - order.nodes.map(node => ) -) : ( - -)} -``` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | order_nodes 테이블 생성 | N-depth 자기참조 트리 + 하이브리드 JSON | DB 스키마 | ⚠️ 컨펌 필요 | -| 2 | order_items에 order_node_id 추가 | nullable FK 컬럼 | DB 스키마 | ⚠️ 컨펌 필요 | -| 3 | 노드 상태 코드 체계 | Order와 동일 체계 사용 | 비즈니스 로직 | ⚠️ 컨펌 필요 | -| 4 | 경동 node_type 값 | "location" 사용 | 비즈니스 로직 | ⚠️ 컨펌 필요 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-06 | - | 문서 초안 작성 (order_locations 전용 설계) | - | - | -| 2026-02-06 | 아키텍처 변경 | order_locations → order_nodes (N-depth 트리 + 하이브리드) | - | ✅ 사용자 승인 | - ---- - -## 7. 참고 문서 - -- **견적 시스템 분석**: `docs/features/quotes/README.md` -- **DB 스키마 규칙**: `docs/specs/database-schema.md` -- **API 개발 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - -### 핵심 소스 파일 - -| 파일 | 역할 | 핵심 라인 | -|------|------|----------| -| `api/app/Services/Quote/QuoteService.php` | 견적→수주 전환 | L574-623 (`convertToOrder`) | -| `api/app/Services/OrderService.php` | 수주 동기화 | L559-659 (`syncFromQuote`) | -| `api/app/Models/Orders/Order.php` | 수주 모델 | L23-59 (상태 코드) | -| `api/app/Models/Orders/OrderItem.php` | 수주 품목 모델 | L162-190 (`createFromQuoteItem`) | -| `react/src/components/orders/actions.ts` | 수주 프론트 타입 | L281-300 (OrderItem) | -| `react/src/components/orders/OrderSalesDetailView.tsx` | 수주 상세 뷰 | L386-424 (테이블) | -| `react/src/components/quotes/types.ts` | 견적 타입 | L661-684 (LocationItem) | - ---- - -## 8. 세션 및 메모리 관리 정책 - -### 8.1 세션 시작 시 - -``` -1. read_memory("order-nodes-state") → 진행 상태 파악 -2. 이 문서의 "📍 현재 진행 상태" 섹션 확인 -3. 마지막 완료 작업 확인 후 다음 작업 착수 -``` - -### 8.2 Serena 메모리 구조 - -- `order-nodes-state`: `{ phase, progress, next_step, last_decision }` -- `order-nodes-snapshot`: 현재까지의 코드 변경점 요약 -- `order-nodes-active-symbols`: 수정 중인 파일/함수 목록 - ---- - -## 9. 검증 결과 - -### 9.1 테스트 케이스 - -| # | 시나리오 | 예상 결과 | 실제 결과 | 상태 | -|---|---------|----------|----------|------| -| 1 | 견적 3개소 → 수주 전환 | order_nodes 3행(type:location) 생성, 각 order_items에 node_id 연결 | - | ⏳ | -| 2 | 수주 상세 조회 | rootNodes + children 재귀 + items eager loading 정상 | - | ⏳ | -| 3 | 견적 수정 → 수주 동기화 | 기존 nodes 삭제 후 재생성, items 재연결 | - | ⏳ | -| 4 | 기존 수주 (nodes 없음) 조회 | 기존 플랫 테이블 정상 표시, 에러 없음 | - | ⏳ | -| 5 | 프론트 노드별 그룹 표시 | 노드 카드 내 자재 테이블, 역호환 플랫 뷰 | - | ⏳ | -| 6 | 통계 쿼리 성능 | 고정 컬럼(node_type, status, total_price) 기반 GROUP BY 정상 | - | ⏳ | - -### 9.2 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 전환 시 order_nodes 생성됨 | ⏳ | Phase 2+3 | -| N-depth 트리 구조 지원 | ⏳ | Phase 2 (parent_id 자기참조) | -| order_items에 order_node_id 연결됨 | ⏳ | Phase 3 | -| 프론트 노드별 그룹 표시 | ⏳ | Phase 4 | -| 기존 수주 역호환 정상 | ⏳ | Phase 4 | -| 통계 쿼리가 고정 컬럼으로 가능 | ⏳ | Phase 2 (인덱스) | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 범용 N-depth 트리 + 통계 친화 하이브리드 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 (6개 기준) | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 (13개 작업 항목) | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 1→2→3→4 순서 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 7 (라인번호 포함) | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 3+4 (코드 포함) | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 (6개 테스트 케이스) | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드/파일/라인 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 1.2 목표 | -| Q2. 왜 하이브리드 구조를 선택했는가? | ✅ | 1.3 아키텍처 결정 | -| Q3. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 | -| Q4. 어떤 파일을 수정해야 하는가? | ✅ | 2. 대상 범위 | -| Q5. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /plan 스킬 + Sequential Thinking MCP로 생성되었습니다.* -*아키텍처: N-depth 트리(order_nodes) + 하이브리드(고정 코어 + options JSON)* \ No newline at end of file diff --git a/plans/archive/order-management-plan.md b/plans/archive/order-management-plan.md deleted file mode 100644 index ecb5f87..0000000 --- a/plans/archive/order-management-plan.md +++ /dev/null @@ -1,335 +0,0 @@ -# 수주관리 (Order Management) API 연동 계획 - -> **작성일**: 2025-01-08 -> **목적**: 수주관리 페이지 Mock 데이터 → API 연동 -> **상태**: ✅ Phase 3 완료 (100% 완료) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 버그 수정 - 목록 페이지 서버 에러 해결 (3건) | -| **다음 작업** | 완료 | -| **진행률** | 3/3 Phase (100%) + 버그 수정 완료 | -| **마지막 업데이트** | 2025-01-09 | -| **커밋** | 버그 수정 커밋 완료 | - ---- - -## 1. 개요 - -### 1.1 배경 -수주관리 페이지는 프론트엔드 UI가 구현되어 있으나, **하드코딩된 Mock 데이터(SAMPLE_ORDERS)**를 사용 중입니다. -실제 비즈니스 운영을 위해 API 연동이 필요합니다. - -### 1.2 현재 구현 상태 분석 - -#### API (Laravel) - ✅ Phase 1 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|------| -| Model | `api/app/Models/Orders/Order.php` | ✅ 존재 | -| Model | `api/app/Models/Orders/OrderItem.php` | ✅ 존재 | -| Model | `api/app/Models/Orders/OrderHistory.php` | ✅ 존재 | -| Model | `api/app/Models/Orders/OrderVersion.php` | ✅ 존재 | -| Model | `api/app/Models/Orders/OrderItemComponent.php` | ✅ 존재 | -| Controller | `api/app/Http/Controllers/Api/V1/OrderController.php` | ✅ **완료** | -| Service | `api/app/Services/OrderService.php` | ✅ **완료** | -| FormRequest | `api/app/Http/Requests/Order/*.php` | ✅ **완료** (3개) | -| Route | `/api/v1/orders` | ✅ **완료** (7개 엔드포인트) | -| Swagger | `api/app/Swagger/v1/OrderApi.php` | ✅ **완료** | - -#### Frontend (React/Next.js) - ✅ Phase 2 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|------| -| 목록 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/page.tsx` | ✅ API 연동 | -| 등록 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx` | ✅ API 연동 | -| 상세 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx` | ✅ API 연동 | -| 수정 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` | ✅ API 연동 | -| 생산지시 페이지 | `react/src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx` | ✅ 완료 | -| 등록 컴포넌트 | `react/src/components/orders/OrderRegistration.tsx` | ✅ 완료 | -| 견적선택 다이얼로그 | `react/src/components/orders/QuotationSelectDialog.tsx` | ✅ 완료 | -| 품목추가 다이얼로그 | `react/src/components/orders/ItemAddDialog.tsx` | ✅ 완료 | -| **actions.ts** | `react/src/components/orders/actions.ts` | ✅ **완료** | - -### 1.3 연관관계 -``` -┌─────────────────┐ ┌─────────────────┐ -│ Quote │────── quote_id ────▶│ Order │ -│ (견적서) │ │ (수주) │ -└─────────────────┘ └─────────────────┘ - │ - │ sales_order_id - ▼ - ┌─────────────────┐ - │ WorkOrder │ - │ (작업지시) │ - └─────────────────┘ -``` - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 필드 추가/변경, API 엔드포인트 추가 | 불필요 | -| ⚠️ 컨펌 필요 | 테이블 구조 변경, 기존 API 수정 | **필수** | -| 🔴 금지 | 기존 Order 모델 구조 변경 | 별도 협의 | - ---- - -## 2. 대상 범위 - -### Phase 1: API 개발 (✅ 완료) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | OrderController 생성 | ✅ | CRUD + 상태관리 (7개 메서드) | -| 1.2 | OrderService 생성 | ✅ | 비즈니스 로직 (index, stats, show, store, update, destroy, updateStatus) | -| 1.3 | FormRequest 생성 | ✅ | Store, Update, UpdateStatus (3개) | -| 1.4 | API 라우트 등록 | ✅ | routes/api.php (7개 엔드포인트) | -| 1.5 | Swagger 문서 작성 | ✅ | OrderApi.php (스키마 8개) | - -### Phase 2: Frontend 연동 (✅ 완료) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | actions.ts 생성 | ✅ | API 호출 함수 + 타입 정의 + 변환 함수 | -| 2.2 | 목록 페이지 연동 | ✅ | getOrders(), getOrderStats() 연동 | -| 2.3 | 상세 페이지 연동 | ✅ | getOrderById() 연동 + 타입 오류 수정 | -| 2.4 | 등록 페이지 연동 | ✅ | createOrder() 연동 | -| 2.5 | 수정 페이지 연동 | ✅ | updateOrder() 연동 + 타입 오류 수정 | - -### Phase 3: 고급 기능 (✅ 완료) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 견적서 → 수주 변환 | ✅ | QuotationSelectDialog + createOrderFromQuote() | -| 3.2 | 생산지시 생성 연동 | ✅ | createProductionOrder() + production-order 페이지 | -| 3.3 | 상태 흐름 관리 | ✅ | 수주확정 다이얼로그 + updateOrderStatus() | - ---- - -## 3. API 엔드포인트 설계 - -### 3.1 REST API - -| Method | Endpoint | 설명 | 우선순위 | -|--------|----------|------|:--------:| -| GET | `/api/v1/orders` | 수주 목록 조회 (페이징/필터) | 🔴 | -| GET | `/api/v1/orders/stats` | 수주 통계 | 🔴 | -| GET | `/api/v1/orders/{id}` | 수주 상세 조회 | 🔴 | -| POST | `/api/v1/orders` | 수주 생성 | 🔴 | -| PUT | `/api/v1/orders/{id}` | 수주 수정 | 🟡 | -| DELETE | `/api/v1/orders/{id}` | 수주 삭제 | 🟡 | -| PATCH | `/api/v1/orders/{id}/status` | 상태 변경 | 🟡 | -| POST | `/api/v1/orders/{id}/production-order` | 생산지시 생성 | 🟢 | -| POST | `/api/v1/orders/from-quote/{quoteId}` | 견적→수주 변환 | 🟢 | - -### 3.2 데이터 스키마 - -#### Order (수주) - 기존 모델 기반 -```typescript -interface Order { - id: number; - tenantId: number; - quoteId?: number; // 원본 견적 - orderNo: string; // 수주번호 (KD-TS-YYMMDD-NN) - orderTypeCode: 'ORDER' | 'PURCHASE'; - statusCode: 'DRAFT' | 'CONFIRMED' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'; - clientId?: number; - clientName?: string; - siteName?: string; // 현장명 - quantity: number; - supplyAmount: number; - taxAmount: number; - totalAmount: number; - deliveryDate?: Date; - deliveryMethodCode?: string; - memo?: string; - createdBy?: number; - updatedBy?: number; - createdAt: Date; - updatedAt: Date; - // Relations - items?: OrderItem[]; - client?: Client; -} -``` - -#### OrderItem (수주 품목) -```typescript -interface OrderItem { - id: number; - orderId: number; - itemId?: number; - itemName: string; - specification?: string; - quantity: number; - unit?: string; - unitPrice: number; - supplyAmount: number; - taxAmount: number; - totalAmount: number; - sortOrder: number; -} -``` - ---- - -## 4. 작업 절차 - -### Step 1: API 개발 (Backend) - -``` -1. OrderService 생성 - ├── index(): 목록 조회 (페이징, 필터링) - ├── show(): 상세 조회 - ├── store(): 생성 - ├── update(): 수정 - ├── destroy(): 삭제 - ├── updateStatus(): 상태 변경 - ├── stats(): 통계 조회 - └── createFromQuote(): 견적→수주 변환 - -2. OrderController 생성 - ├── FormRequest DI - └── ApiResponse::handle() 사용 - -3. FormRequest 생성 - ├── StoreOrderRequest - └── UpdateOrderRequest - -4. 라우트 등록 - └── Route::prefix('orders')->group(...) - -5. Swagger 문서 작성 - └── app/Swagger/v1/OrderApi.php -``` - -### Step 2: Frontend 연동 - -``` -1. actions.ts 생성 - ├── getOrders(): 목록 조회 - ├── getOrderById(): 상세 조회 - ├── createOrder(): 생성 - ├── updateOrder(): 수정 - ├── deleteOrder(): 삭제 - ├── updateOrderStatus(): 상태 변경 - └── getOrderStats(): 통계 조회 - -2. 페이지별 연동 - ├── page.tsx: SAMPLE_ORDERS → getOrders() - ├── [id]/page.tsx: Mock → getOrderById() - ├── new/page.tsx: Mock → createOrder() - └── [id]/edit/page.tsx: Mock → updateOrder() -``` - ---- - -## 5. 의존성 - -### 5.1 필수 선행 작업 -- **없음** - Order 모델 이미 존재, 바로 작업 가능 - -### 5.2 연관 기능 (선택적) -- **견적관리 (Quote)**: 견적→수주 변환 시 필요 -- **거래처관리 (Client)**: 거래처 연동 -- **품목관리 (Item)**: 품목 마스터 연동 - -### 5.3 후속 연동 -- **작업지시 (WorkOrder)**: 생산지시 생성 시 `sales_order_id` 연결 -- **출하관리**: 수주 완료 후 출하 처리 - ---- - -## 6. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **DB 스키마**: `docs/specs/database-schema.md` -- **Swagger 가이드**: `docs/guides/swagger-guide.md` - -### 참고 코드 -- **작업지시 API (참고용)**: `api/app/Http/Controllers/Api/V1/WorkOrderController.php` -- **공정관리 actions.ts (참고용)**: `react/src/components/process-management/actions.ts` - ---- - -## 7. 검증 방법 - -### 7.1 API 테스트 -```bash -# 목록 조회 -curl -X GET "http://api.sam.kr/api/v1/orders" -H "X-Api-Key: ..." - -# 상세 조회 -curl -X GET "http://api.sam.kr/api/v1/orders/1" -H "X-Api-Key: ..." - -# 통계 조회 -curl -X GET "http://api.sam.kr/api/v1/orders/stats" -H "X-Api-Key: ..." -``` - -### 7.2 성공 기준 -| 기준 | 측정 방법 | -|------|----------| -| API CRUD 동작 | Swagger UI 테스트 통과 | -| 목록 페이지 | 실제 데이터 표시 | -| 상세 페이지 | 수주 정보 정상 표시 | -| 등록/수정 | 데이터 저장 및 조회 | -| 상태 변경 | DRAFT → CONFIRMED 전환 | - ---- - -## 8. 자기완결성 점검 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | Mock → API 연동 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 7 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3 단계별 정의 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 선행 작업 없음 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 모든 경로 검증됨 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | Step 1-2 상세 정의 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | curl 테스트 + 기준 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 파일/엔드포인트 명시 | - ---- - -## 9. 버그 수정 이력 - -### 2025-01-09: 목록 페이지 서버 에러 수정 - -| # | 파일 | 문제 | 수정 내용 | -|---|------|------|----------| -| 1 | `react/.../page.tsx:120` | API 응답 데이터 구조 불일치 | `ordersResult.data` → `ordersResult.data.items` | -| 2 | `api/.../OrderService.php:113` | Quote 필드명 오류 | `quote:id,quote_no,site_name` → `quote:id,quote_number,site_name` | -| 3 | `react/.../actions.ts:384` | Quote 필드명 오류 | `apiData.quote?.quote_no` → `apiData.quote?.quote_number` | - -**원인 분석:** -- `getOrders()` 함수는 `{ items: Order[], total, page, totalPages }` 구조를 반환하나, 페이지에서 `ordersResult.data`를 직접 사용하여 타입 불일치 발생 -- Quote 모델의 필드명이 `quote_number`인데 `quote_no`로 잘못 참조 - -**영향 범위:** -- 수주 목록 페이지 접근 시 서버 에러 발생 -- 견적 연동 수주의 견적번호 표시 오류 - -### 2025-01-09: 수주 등록 페이지 거래처 API 연동 - -| # | 파일 | 변경 내용 | -|---|------|----------| -| 1 | `react/.../OrderRegistration.tsx` | `SAMPLE_CLIENTS` 하드코딩 제거 | -| 2 | `react/.../OrderRegistration.tsx` | `useClientList` 훅으로 실제 API 연동 | -| 3 | `react/.../OrderRegistration.tsx` | 로딩 상태 처리 ("불러오는 중...") | -| 4 | `react/.../OrderRegistration.tsx` | 견적 선택 시 발주처 필드 비활성화 | - -**개선 내용:** -- 발주처(거래처) 드롭다운이 `/api/proxy/clients` API에서 실제 데이터 조회 -- 견적 선택 시 발주처가 자동 설정되고 필드 비활성화 -- 로딩 중 "불러오는 중..." 플레이스홀더 표시 - ---- - -*이 문서는 독립 세션에서 바로 작업 시작 가능하도록 설계되었습니다.* \ No newline at end of file diff --git a/plans/archive/order-workorder-shipment-integration-plan.md b/plans/archive/order-workorder-shipment-integration-plan.md deleted file mode 100644 index 105c5c3..0000000 --- a/plans/archive/order-workorder-shipment-integration-plan.md +++ /dev/null @@ -1,659 +0,0 @@ -# 수주-작업지시-출하 하이브리드 연동 구조 구현 계획 - -> **작성일**: 2025-01-19 -> **목적**: Order → WorkOrder → Shipment 간 FK 연결 강화 및 상태 동기화 로직 구현 -> **기준 문서**: `api/app/Models/Orders/Order.php`, `api/app/Models/Production/WorkOrder.php`, `api/app/Models/Tenants/Shipment.php` -> **상태**: 📋 계획 수립 완료 (Serena ID: order-integration-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4: 작업완료 시 자동 출하 생성 기능 구현 | -| **다음 작업** | ✅ 모든 Phase 완료 | -| **진행률** | 4/4 Phase (100%) | -| **마지막 업데이트** | 2025-01-19 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 SAM 시스템은 수주(Order), 작업지시(WorkOrder), 출하(Shipment)가 독립적으로 운영되고 있습니다. - -**현재 문제점:** -- `shipments` 테이블에 `work_order_id` FK가 없음 -- 작업 완료 시 출하로 자동 연결되지 않음 -- Order의 전체 진행 상태를 추적할 수 없음 -- 데이터 정합성 보장이 어려움 - -**목표:** -- 하이브리드 마스터-디테일 구조로 전환 -- `orders.status_code`로 전체 진행 상태 추적 -- 각 단계별 상태 변경 시 연관 테이블 자동 동기화 - -### 1.2 목표 구조 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 목표 구조 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ orders (마스터) │ -│ ├─ status_code: 전체 진행상태 추적 │ -│ │ DRAFT → CONFIRMED → IN_PRODUCTION → PRODUCED │ -│ │ → SHIPPING → SHIPPED → COMPLETED │ -│ │ │ -│ ├──(1:N)──▶ work_orders (생산 상세) │ -│ │ ├─ sales_order_id FK ✅ (기존) │ -│ │ └─ status: 생산 프로세스 상태 │ -│ │ │ -│ └──(1:N)──▶ shipments (출하 상세) │ -│ ├─ order_id FK ✅ (기존) │ -│ ├─ work_order_id FK 🆕 (신규 추가) │ -│ └─ status: 출하 프로세스 상태 │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. orders.status_code = 전체 프로세스의 Single Source of Truth │ -│ 2. 하위 테이블(work_orders, shipments)은 상세 정보만 관리 │ -│ 3. 상태 변경 시 상위 테이블 자동 동기화 │ -│ 4. 기존 데이터 호환성 유지 (work_order_id는 nullable) │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 모델 관계 추가, 상수 추가, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 마이그레이션, 서비스 로직 변경, 상태 동기화 | **필수** | -| 🔴 금지 | 기존 테이블 구조 파괴적 변경, 기존 API 삭제 | 별도 협의 | - -### 1.5 준수 규칙 - -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `CLAUDE.md` - SAM API Development Rules - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: DB 스키마 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | `shipments` 테이블에 `work_order_id` FK 추가 마이그레이션 | ⏳ | nullable, index 포함 | - -### 2.2 Phase 2: 모델 관계 추가 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | Order 모델에 `shipments()` HasMany 관계 추가 | ⏳ | | -| 2.2 | WorkOrder 모델에 `shipments()` HasMany 관계 추가 | ⏳ | | -| 2.3 | Shipment 모델에 `workOrder()` BelongsTo 관계 추가 | ⏳ | | -| 2.4 | Shipment 모델에 `work_order_id` fillable 추가 | ⏳ | | - -### 2.3 Phase 3: Order 상태 확장 및 동기화 로직 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | Order 모델에 생산/출하 관련 상태 상수 추가 | ⏳ | IN_PRODUCTION, PRODUCED, SHIPPING, SHIPPED | -| 3.2 | WorkOrderService에 Order 상태 동기화 로직 추가 | ⏳ | 상태 변경 시 Order.status_code 업데이트 | -| 3.3 | ShipmentService에 Order 상태 동기화 로직 추가 | ⏳ | 상태 변경 시 Order.status_code 업데이트 | - -### 2.4 Phase 4: 연동 기능 (선택) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | ShipmentService.store()에 work_order_id 연결 로직 추가 | ⏳ | 출하 생성 시 WorkOrder 선택 가능 | -| 4.2 | WorkOrder 완료 시 Shipment 자동 생성 옵션 | ⏳ | 선택적 기능 | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Phase 1: DB 스키마 수정 -└── 1.1 마이그레이션 생성 및 실행 - ├── add_work_order_id_to_shipments_table.php - ├── work_order_id FK (nullable) - └── index 추가 - -Phase 2: 모델 관계 추가 -├── 2.1 Order.php - shipments() HasMany -├── 2.2 WorkOrder.php - shipments() HasMany -├── 2.3 Shipment.php - workOrder() BelongsTo -└── 2.4 Shipment.php - fillable에 work_order_id 추가 - -Phase 3: 상태 동기화 -├── 3.1 Order.php - 상태 상수 확장 -│ ├── STATUS_IN_PRODUCTION = 'IN_PRODUCTION' -│ ├── STATUS_PRODUCED = 'PRODUCED' -│ ├── STATUS_SHIPPING = 'SHIPPING' -│ └── STATUS_SHIPPED = 'SHIPPED' -├── 3.2 WorkOrderService.php - syncOrderStatus() 메서드 추가 -│ ├── in_progress → Order: IN_PRODUCTION -│ ├── completed → Order: PRODUCED -│ └── shipped → Order: (Shipment 생성 시) -└── 3.3 ShipmentService.php - syncOrderStatus() 메서드 추가 - ├── scheduled/ready → Order: SHIPPING (첫 출하 생성 시) - └── completed → Order: SHIPPED (모든 출하 완료 시) - -Phase 4: 연동 기능 (선택) -├── 4.1 ShipmentService.store() - work_order_id 파라미터 추가 -└── 4.2 WorkOrderService.updateStatus() - 자동 Shipment 생성 옵션 -``` - -### 3.2 상태 흐름도 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 전체 상태 흐름 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ [Order] │ -│ DRAFT ──▶ CONFIRMED ──▶ IN_PRODUCTION ──▶ PRODUCED │ -│ │ │ │ │ -│ ▼ ▼ ▼ │ -│ WorkOrder WorkOrder WorkOrder │ -│ 생성 in_progress completed │ -│ │ │ -│ ▼ │ -│ ──────────────────────▶ SHIPPING ──▶ SHIPPED ──▶ COMPLETED │ -│ │ │ │ -│ ▼ ▼ │ -│ Shipment Shipment │ -│ 생성 completed │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1: DB 스키마 수정 - -#### 1.1 마이그레이션: shipments 테이블에 work_order_id 추가 - -**파일**: `api/database/migrations/2025_01_19_XXXXXX_add_work_order_id_to_shipments_table.php` - -```php -foreignId('work_order_id') - ->nullable() - ->after('order_id') - ->comment('작업지시 ID'); - - $table->index(['tenant_id', 'work_order_id']); - }); - } - - public function down(): void - { - Schema::table('shipments', function (Blueprint $table) { - $table->dropIndex(['tenant_id', 'work_order_id']); - $table->dropColumn('work_order_id'); - }); - } -}; -``` - ---- - -### 4.2 Phase 2: 모델 관계 추가 - -#### 2.1 Order 모델 - shipments() 관계 - -**파일**: `api/app/Models/Orders/Order.php` - -```php -use App\Models\Tenants\Shipment; - -/** - * 출하 목록 - */ -public function shipments(): HasMany -{ - return $this->hasMany(Shipment::class, 'order_id'); -} -``` - -#### 2.2 WorkOrder 모델 - shipments() 관계 - -**파일**: `api/app/Models/Production/WorkOrder.php` - -```php -use App\Models\Tenants\Shipment; - -/** - * 출하 목록 - */ -public function shipments(): HasMany -{ - return $this->hasMany(Shipment::class); -} -``` - -#### 2.3-2.4 Shipment 모델 수정 - -**파일**: `api/app/Models/Tenants/Shipment.php` - -```php -use App\Models\Production\WorkOrder; - -// fillable에 추가 -protected $fillable = [ - // ... 기존 필드들 - 'work_order_id', // 추가 -]; - -// casts에 추가 -protected $casts = [ - // ... 기존 캐스트들 - 'work_order_id' => 'integer', // 추가 -]; - -/** - * 작업지시 관계 - */ -public function workOrder(): BelongsTo -{ - return $this->belongsTo(WorkOrder::class); -} -``` - ---- - -### 4.3 Phase 3: Order 상태 확장 및 동기화 로직 - -#### 3.1 Order 모델 - 상태 상수 확장 - -**파일**: `api/app/Models/Orders/Order.php` - -```php -// 기존 상태 -public const STATUS_DRAFT = 'DRAFT'; -public const STATUS_CONFIRMED = 'CONFIRMED'; -public const STATUS_IN_PROGRESS = 'IN_PROGRESS'; -public const STATUS_COMPLETED = 'COMPLETED'; -public const STATUS_CANCELLED = 'CANCELLED'; - -// 신규 상태 추가 -public const STATUS_IN_PRODUCTION = 'IN_PRODUCTION'; // 생산중 -public const STATUS_PRODUCED = 'PRODUCED'; // 생산완료 -public const STATUS_SHIPPING = 'SHIPPING'; // 출하중 -public const STATUS_SHIPPED = 'SHIPPED'; // 출하완료 - -/** - * 전체 상태 목록 - */ -public const STATUSES = [ - self::STATUS_DRAFT, - self::STATUS_CONFIRMED, - self::STATUS_IN_PRODUCTION, - self::STATUS_PRODUCED, - self::STATUS_SHIPPING, - self::STATUS_SHIPPED, - self::STATUS_COMPLETED, - self::STATUS_CANCELLED, -]; - -/** - * 상태 라벨 - */ -public const STATUS_LABELS = [ - self::STATUS_DRAFT => '임시저장', - self::STATUS_CONFIRMED => '확정', - self::STATUS_IN_PRODUCTION => '생산중', - self::STATUS_PRODUCED => '생산완료', - self::STATUS_SHIPPING => '출하중', - self::STATUS_SHIPPED => '출하완료', - self::STATUS_COMPLETED => '완료', - self::STATUS_CANCELLED => '취소', -]; -``` - -#### 3.2 WorkOrderService - Order 상태 동기화 - -**파일**: `api/app/Services/WorkOrderService.php` - -```php -use App\Models\Orders\Order; - -/** - * Order 상태 동기화 - * WorkOrder 상태 변경 시 Order.status_code 업데이트 - */ -private function syncOrderStatus(WorkOrder $workOrder): void -{ - if (!$workOrder->sales_order_id) { - return; - } - - $order = Order::find($workOrder->sales_order_id); - if (!$order) { - return; - } - - $newStatus = null; - - switch ($workOrder->status) { - case WorkOrder::STATUS_IN_PROGRESS: - case WorkOrder::STATUS_WAITING: - case WorkOrder::STATUS_PENDING: - // 하나라도 진행중이면 생산중 - $newStatus = Order::STATUS_IN_PRODUCTION; - break; - - case WorkOrder::STATUS_COMPLETED: - // 모든 작업지시가 완료되었는지 확인 - $allCompleted = WorkOrder::where('sales_order_id', $order->id) - ->whereNotIn('status', [WorkOrder::STATUS_COMPLETED, WorkOrder::STATUS_SHIPPED]) - ->doesntExist(); - - if ($allCompleted) { - $newStatus = Order::STATUS_PRODUCED; - } - break; - } - - if ($newStatus && $order->status_code !== $newStatus) { - $order->update(['status_code' => $newStatus]); - - $this->auditLogger->log( - $order->tenant_id, - 'order', - $order->id, - 'status_synced_from_work_order', - ['status_code' => $order->getOriginal('status_code')], - ['status_code' => $newStatus, 'work_order_id' => $workOrder->id] - ); - } -} -``` - -**updateStatus() 메서드에 호출 추가:** - -```php -public function updateStatus(int $id, string $status, ?array $resultData = null) -{ - // ... 기존 로직 ... - - return DB::transaction(function () use ($workOrder, $status, $resultData, $tenantId, $userId) { - // ... 기존 상태 변경 로직 ... - - $workOrder->save(); - - // Order 상태 동기화 추가 - $this->syncOrderStatus($workOrder); - - // ... 나머지 로직 ... - }); -} -``` - -#### 3.3 ShipmentService - Order 상태 동기화 - -**파일**: `api/app/Services/ShipmentService.php` - -```php -use App\Models\Orders\Order; - -/** - * Order 상태 동기화 - * Shipment 상태 변경 시 Order.status_code 업데이트 - */ -private function syncOrderStatus(Shipment $shipment): void -{ - if (!$shipment->order_id) { - return; - } - - $order = Order::find($shipment->order_id); - if (!$order) { - return; - } - - $newStatus = null; - - switch ($shipment->status) { - case 'scheduled': - case 'ready': - case 'shipping': - // 출하 프로세스 시작 - if (!in_array($order->status_code, [Order::STATUS_SHIPPING, Order::STATUS_SHIPPED, Order::STATUS_COMPLETED])) { - $newStatus = Order::STATUS_SHIPPING; - } - break; - - case 'completed': - // 모든 출하가 완료되었는지 확인 - $allCompleted = Shipment::where('order_id', $order->id) - ->where('status', '!=', 'completed') - ->doesntExist(); - - if ($allCompleted) { - $newStatus = Order::STATUS_SHIPPED; - } - break; - } - - if ($newStatus && $order->status_code !== $newStatus) { - $order->update(['status_code' => $newStatus]); - } -} -``` - -**store() 및 updateStatus() 메서드에 호출 추가:** - -```php -public function store(array $data): Shipment -{ - // ... 기존 로직 ... - - return DB::transaction(function () use ($data, $tenantId, $userId) { - // ... 기존 생성 로직 ... - - // Order 상태 동기화 추가 - $this->syncOrderStatus($shipment); - - return $shipment->load('items'); - }); -} - -public function updateStatus(int $id, string $status, ?array $additionalData = null): Shipment -{ - // ... 기존 로직 ... - - $shipment->update($updateData); - - // Order 상태 동기화 추가 - $this->syncOrderStatus($shipment); - - return $shipment->load('items'); -} -``` - ---- - -### 4.4 Phase 4: 연동 기능 (선택) - -#### 4.1 ShipmentService.store() - work_order_id 연결 - -**파일**: `api/app/Services/ShipmentService.php` - -```php -public function store(array $data): Shipment -{ - return DB::transaction(function () use ($data, $tenantId, $userId) { - $shipment = Shipment::create([ - // ... 기존 필드들 ... - 'work_order_id' => $data['work_order_id'] ?? null, // 추가 - ]); - - // WorkOrder가 있으면 상태를 shipped로 변경 - if ($shipment->work_order_id) { - $workOrder = WorkOrder::find($shipment->work_order_id); - if ($workOrder && $workOrder->status === WorkOrder::STATUS_COMPLETED) { - $workOrder->update([ - 'status' => WorkOrder::STATUS_SHIPPED, - 'shipped_at' => now(), - ]); - } - } - - // ... 나머지 로직 ... - }); -} -``` - -#### 4.2 ShipmentStoreRequest - work_order_id 검증 - -**파일**: `api/app/Http/Requests/Shipment/ShipmentStoreRequest.php` - -```php -public function rules(): array -{ - return [ - // ... 기존 규칙들 ... - 'work_order_id' => ['nullable', 'integer', 'exists:work_orders,id'], - ]; -} -``` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 마이그레이션 | shipments에 work_order_id FK 추가 | DB | ⏳ 컨펌 필요 | -| 2 | Order 상태 확장 | 4개 상태 추가 (IN_PRODUCTION, PRODUCED, SHIPPING, SHIPPED) | Order 모델, API | ⏳ 컨펌 필요 | -| 3 | 상태 동기화 로직 | WorkOrder/Shipment 상태 변경 시 Order 자동 업데이트 | 서비스 로직 | ⏳ 컨펌 필요 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-19 | - | 계획 문서 초안 작성 | - | - | - ---- - -## 7. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **SAM API 규칙**: `CLAUDE.md` -- **DB 스키마**: `docs/specs/database-schema.md` - -### 분석된 기존 파일 - -| 파일 | 역할 | -|------|------| -| `api/app/Models/Orders/Order.php` | 수주 마스터 모델 | -| `api/app/Models/Production/WorkOrder.php` | 작업지시 모델 | -| `api/app/Models/Tenants/Shipment.php` | 출하 모델 | -| `api/app/Services/WorkOrderService.php` | 작업지시 비즈니스 로직 | -| `api/app/Services/ShipmentService.php` | 출하 비즈니스 로직 | -| `api/database/migrations/2025_12_26_100000_create_work_orders_table.php` | 작업지시 테이블 | -| `api/database/migrations/2025_12_26_150604_create_shipments_table.php` | 출하 테이블 | - ---- - -## 8. 세션 및 메모리 관리 정책 - -### 8.1 세션 시작 시 -```javascript -read_memory("order-integration-state") // 상태 파악 -read_memory("order-integration-snapshot") // 사고 흐름 복구 -``` - -### 8.2 Serena 메모리 구조 -- `order-integration-state`: { phase, progress, next_step, last_decision } -- `order-integration-snapshot`: 현재까지의 논의 및 코드 변경점 요약 -- `order-integration-rules`: 해당 작업에서 결정된 규칙들 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 테스트 케이스 - -| 시나리오 | 예상 결과 | 실제 결과 | 상태 | -|----------|----------|----------|------| -| WorkOrder 생성 (in_progress) | Order.status = IN_PRODUCTION | - | ⏳ | -| WorkOrder 완료 (completed) | Order.status = PRODUCED | - | ⏳ | -| Shipment 생성 | Order.status = SHIPPING | - | ⏳ | -| Shipment 완료 | Order.status = SHIPPED | - | ⏳ | -| 모든 프로세스 완료 | Order.status = COMPLETED | - | ⏳ | - -### 9.2 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| shipments.work_order_id FK 추가 완료 | ⏳ | | -| 모델 관계 정상 동작 | ⏳ | | -| Order 상태 자동 동기화 | ⏳ | | -| 기존 데이터 호환성 유지 | ⏳ | | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 2 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase별 순서 정의 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 7 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 3, 4 상세 코드 포함 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드 예시 포함 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 현재 진행 상태, 3.1 절차 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 섹션 4 상세 작업 내용 | -| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/process-management-plan.md b/plans/archive/process-management-plan.md deleted file mode 100644 index 5c8d7d3..0000000 --- a/plans/archive/process-management-plan.md +++ /dev/null @@ -1,397 +0,0 @@ -# 공정관리 (Process Management) API 연동 계획 - -> **작성일**: 2025-01-08 -> **목적**: 공정관리 기능 검증 및 테스트 -> **상태**: ✅ 검증 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 3: 개별 품목 연결 기능 (process_items) | -| **다음 작업** | 완료 (Phase 2는 선택사항) | -| **진행률** | 5/5 (100%) - Phase 1 + Phase 3 완료 | -| **마지막 업데이트** | 2026-01-08 | - ---- - -## 1. 개요 - -### 1.1 기능 설명 -공정관리는 MES 시스템의 기초 데이터로, 생산 공정을 정의하고 관리하는 기능입니다. -작업지시 생성 시 공정 유형(process_type)으로 연결되며, 자동 분류 규칙을 통해 품목별 공정 배정을 자동화합니다. - -### 1.2 현재 구현 상태 분석 - -#### API (Laravel) - ✅ 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|:----:| -| Model | `api/app/Models/Process.php` | ✅ | -| Model | `api/app/Models/ProcessClassificationRule.php` | ✅ | -| Model | `api/app/Models/ProcessItem.php` | ✅ (Phase 3) | -| Migration | `api/database/migrations/2026_01_08_180607_create_process_items_table.php` | ✅ | -| Service | `api/app/Services/ProcessService.php` | ✅ | -| Controller | `api/app/Http/Controllers/V1/ProcessController.php` | ✅ | -| FormRequest | `api/app/Http/Requests/V1/Process/StoreProcessRequest.php` | ✅ | -| FormRequest | `api/app/Http/Requests/V1/Process/UpdateProcessRequest.php` | ✅ | -| Swagger | `api/app/Swagger/v1/ProcessApi.php` | ✅ | -| Route | `/api/v1/processes` | ✅ | - -#### Frontend (React/Next.js) - ✅ API 연동 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|:----:| -| 목록 페이지 | `react/src/app/[locale]/(protected)/master-data/process-management/page.tsx` | ✅ | -| 등록 페이지 | `react/src/app/[locale]/(protected)/master-data/process-management/new/page.tsx` | ✅ | -| 상세 페이지 | `react/src/app/[locale]/(protected)/master-data/process-management/[id]/page.tsx` | ✅ | -| 수정 페이지 | `react/src/app/[locale]/(protected)/master-data/process-management/[id]/edit/page.tsx` | ✅ | -| 목록 컴포넌트 | `react/src/components/process-management/ProcessListClient.tsx` | ✅ | -| 폼 컴포넌트 | `react/src/components/process-management/ProcessForm.tsx` | ✅ | -| 상세 컴포넌트 | `react/src/components/process-management/ProcessDetail.tsx` | ✅ | -| 규칙 모달 | `react/src/components/process-management/RuleModal.tsx` | ✅ | -| **actions.ts** | `react/src/components/process-management/actions.ts` | ✅ | - -### 1.3 관련 URL -| 화면 | URL | 설명 | -|------|-----|------| -| 공정목록 | `/master-data/process-management` | 토글 기능 포함 | -| 공정등록 | `/master-data/process-management/new` | 모달 - 규칙추가 | -| 공정상세 | `/master-data/process-management/{id}` | 상세 정보 | -| 공정수정 | `/master-data/process-management/{id}/edit` | 수정 폼 | - -### 1.4 연관관계 -``` -┌─────────────────┐ process_type ┌─────────────────┐ -│ Process │ ───────────────────────│ WorkOrder │ -│ (공정관리) │ screen/slat/bending │ (작업지시) │ -└─────────────────┘ └─────────────────┘ - │ - ├── classificationRules (패턴 규칙) - │ ▼ - │ ┌─────────────────────────┐ - │ │ ProcessClassificationRule│ - │ │ (자동 분류 규칙) │ - │ └─────────────────────────┘ - │ - └── processItems (개별 품목) ← Phase 3 - ▼ - ┌─────────────────────────┐ ┌─────────────────┐ - │ ProcessItem │────────│ Item │ - │ (공정-품목 연결) │ │ (품목) │ - └─────────────────────────┘ └─────────────────┘ -``` - ---- - -## 2. API 엔드포인트 - -### 2.1 REST API (구현 완료) -| Method | Endpoint | 설명 | 상태 | -|--------|----------|------|:----:| -| GET | `/api/v1/processes` | 공정 목록 조회 (검색/페이징) | ✅ | -| GET | `/api/v1/processes/{id}` | 공정 상세 조회 | ✅ | -| POST | `/api/v1/processes` | 공정 생성 | ✅ | -| PUT | `/api/v1/processes/{id}` | 공정 수정 | ✅ | -| DELETE | `/api/v1/processes/{id}` | 공정 삭제 | ✅ | -| DELETE | `/api/v1/processes` | 공정 일괄 삭제 | ✅ | -| PATCH | `/api/v1/processes/{id}/toggle` | 공정 상태 토글 | ✅ | -| GET | `/api/v1/processes/options` | 드롭다운용 옵션 목록 | ✅ | -| GET | `/api/v1/processes/stats` | 공정 통계 | ✅ | - -### 2.2 actions.ts 구현 함수 (완료) -```typescript -// 목록/조회 -getProcessList(params) // 목록 조회 -getProcessById(id) // 상세 조회 -getProcessOptions() // 드롭다운 옵션 -getProcessStats() // 통계 조회 - -// CRUD -createProcess(data) // 생성 -updateProcess(id, data) // 수정 -deleteProcess(id) // 삭제 -deleteProcesses(ids) // 일괄 삭제 -toggleProcessActive(id) // 상태 토글 - -// 보조 -getDepartmentOptions() // 부서 옵션 (분류 규칙용) -getItemList(params) // 품목 목록 (분류 규칙용) -``` - ---- - -## 3. 데이터 스키마 - -### 3.1 Process (공정) -```typescript -interface Process { - id: string; - processCode: string; // P-001, P-002 - processName: string; // 공정명 - description?: string; // 공정 설명 - processType: '생산' | '검사' | '포장' | '조립'; - department: string; // 담당 부서 - workLogTemplate?: string; // 작업일지 양식 - classificationRules: ClassificationRule[]; - requiredWorkers: number; // 필요 작업자 수 - equipmentInfo?: string; // 설비 정보 - workSteps: string[]; // 작업 단계 - note?: string; - status: '사용중' | '미사용'; - createdAt: string; - updatedAt: string; -} -``` - -### 3.2 ClassificationRule (자동 분류 규칙) -```typescript -interface ClassificationRule { - id: string; - registrationType: 'pattern' | 'individual'; // 패턴 규칙 vs 개별 품목 - ruleType: '품목코드' | '품목명' | '품목구분'; - matchingType: 'startsWith' | 'endsWith' | 'contains' | 'equals'; - conditionValue: string; - priority: number; - description?: string; - isActive: boolean; - createdAt: string; -} -``` - -### 3.3 ProcessItem (공정-품목 연결) - Phase 3 추가 -```typescript -// API 응답 스키마 -interface ApiProcessItem { - id: number; - process_id: number; - item_id: number; - priority: number; - is_active: boolean; - item?: { - id: number; - code: string; - name: string; - }; -} - -// DB 테이블: process_items -// - id (PK) -// - process_id (FK → processes) -// - item_id (FK → items) -// - priority (정렬 순서) -// - is_active (사용 여부) -// - created_at, updated_at -``` - -### 3.4 API 요청/응답 변환 - -#### 요청 (Frontend → API) -```typescript -// 패턴 규칙과 개별 품목 분리 -{ - classification_rules: [ // 패턴 규칙만 - { rule_type, matching_type, condition_value, ... } - ], - item_ids: [123, 456, 789] // 개별 품목 ID 배열 -} -``` - -#### 응답 (API → Frontend) -```typescript -// process_items를 individual 규칙으로 변환 -{ - classification_rules: [...], // 패턴 규칙 - process_items: [ // 개별 품목 연결 - { id, process_id, item_id, priority, is_active, item: {...} } - ] -} -``` - ---- - -## 4. 작업 범위 - -### Phase 1: 검증 및 테스트 (완료 - 2026-01-08) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | 목록 조회 테스트 | ✅ | 검색, 탭 필터 정상 | -| 1.2 | 등록 기능 테스트 | ✅ | 정상 (담당부서는 DB 데이터 의존) | -| 1.3 | 수정 기능 테스트 | ✅ | 필요인원 변경/저장 정상 | -| 1.4 | 삭제 기능 테스트 | ⏭️ | 데이터 보존으로 생략 | -| 1.5 | 토글 기능 테스트 | ✅ | 사용중↔미사용 전환 정상 | - -### 📋 참고사항 - -- **담당부서 드롭다운**: departments 테이블 데이터에 의존. 데이터 없으면 빈 드롭다운 (정상 동작) - -### Phase 2: 개선 사항 (선택) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | 공정 순서 드래그앤드롭 | ⏭️ | 후순위 | -| 2.2 | 작업 지침서 PDF 업로드 | ⏭️ | 후순위 | -| 2.3 | 공정 흐름도 시각화 | ⏭️ | 후순위 | - -### Phase 3: 개별 품목 연결 기능 (완료 - 2026-01-08) - -#### 배경 -- 기존 분류 규칙에서 400개 이상의 품목 코드를 `,` 구분자로 저장 시도 -- `condition_value` VARCHAR(255) 필드 초과 → API 422 에러 발생 -- 해결: 개별 품목은 별도 테이블(`process_items`)로 관계형 저장 - -#### 완료 작업 - -| # | 작업 항목 | 상태 | 파일/위치 | -|---|----------|:----:|----------| -| 3.1 | ProcessItem 모델 생성 | ✅ | `api/app/Models/ProcessItem.php` | -| 3.2 | process_items 마이그레이션 | ✅ | `api/database/migrations/2026_01_08_180607_*` | -| 3.3 | Process 모델 관계 추가 | ✅ | `processItems()` HasMany | -| 3.4 | ProcessService 수정 | ✅ | `syncProcessItems()` 메서드 추가 | -| 3.5 | Validation 업데이트 | ✅ | `item_ids` 배열 검증 추가 | -| 3.6 | Swagger 문서 업데이트 | ✅ | `ProcessItem` 스키마 추가 | -| 3.7 | Frontend actions.ts 수정 | ✅ | 요청/응답 변환 로직 | - -#### 핵심 변경 사항 - -**API 측 (Laravel)** -```php -// ProcessService.php -private function syncProcessItems(Process $process, array $itemIds): void -{ - $process->processItems()->delete(); - foreach ($itemIds as $index => $itemId) { - ProcessItem::create([ - 'process_id' => $process->id, - 'item_id' => $itemId, - 'priority' => $index, - 'is_active' => true, - ]); - } -} -``` - -**Frontend 측 (Next.js)** -```typescript -// actions.ts -// 패턴 규칙과 개별 품목 분리 -const patternRules = data.classificationRules.filter( - (rule) => rule.registrationType === 'pattern' -); -const individualRules = data.classificationRules.filter( - (rule) => rule.registrationType === 'individual' -); -// item_ids 추출 -const itemIds = individualRules.flatMap((rule) => - rule.conditionValue.split(',').map((id) => parseInt(id.trim(), 10)) -); -``` - ---- - -## 5. 주요 기능 상세 - -### 5.1 토글 기능 -- 목록에서 각 공정의 사용/미사용 상태를 토글 -- `PATCH /api/v1/processes/{id}/toggle` 호출 -- 미사용 공정은 작업지시 생성 시 선택 불가 - -### 5.2 규칙 추가 (모달) -- 자동 분류 규칙을 통해 품목별 공정 자동 배정 -- 우선순위(priority)에 따라 규칙 적용 순서 결정 -- include/exclude로 포함/제외 규칙 설정 - -### 5.3 양식 보기 (모달) -- 작업일지 템플릿 미리보기 -- HTML/마크다운 형식 지원 - ---- - -## 6. 의존성 - -### 6.1 필수 선행 작업 -- **없음** (기초 데이터) - -### 6.2 후속 연동 -- **작업지시 (WorkOrder)**: 공정 유형 선택 (process_type: screen/slat/bending) -- **품목관리 (Item)**: 자동 분류 규칙 적용 - ---- - -## 7. 검증 방법 - -### 7.1 테스트 체크리스트 - -| 기능 | 테스트 항목 | 예상 결과 | -|------|-----------|----------| -| 목록 조회 | 페이지 로드 | 공정 목록 표시 | -| 검색 | "생산" 검색 | 필터링된 결과 | -| 탭 필터 | "사용중" 탭 클릭 | 사용중 공정만 표시 | -| 등록 | 새 공정 등록 | 목록에 추가됨 | -| 수정 | 공정명 변경 | 변경 반영됨 | -| 삭제 | 공정 삭제 | 목록에서 제거됨 | -| 토글 | 상태 토글 | 사용중↔미사용 전환 | -| 규칙 추가 | 분류 규칙 추가 | 규칙 저장됨 | - -### 7.2 API 테스트 -```bash -# 목록 조회 -curl -X GET "http://api.sam.kr/api/v1/processes" -H "X-Api-Key: ..." - -# 상세 조회 -curl -X GET "http://api.sam.kr/api/v1/processes/1" -H "X-Api-Key: ..." - -# 통계 조회 -curl -X GET "http://api.sam.kr/api/v1/processes/stats" -H "X-Api-Key: ..." - -# 토글 -curl -X PATCH "http://api.sam.kr/api/v1/processes/1/toggle" -H "X-Api-Key: ..." -``` - ---- - -## 8. 참고 사항 - -### 8.1 공정 유형 (process_type) -현재 작업지시에서 사용하는 공정 유형: -- `screen`: 스크린 공정 -- `slat`: 슬랫 공정 -- `bending`: 절곡 공정 - -### 8.2 Process vs WorkOrder.process_type -- `Process` 모델: 공정의 메타데이터 (이름, 설명, 규칙 등) -- `WorkOrder.process_type`: 실제 작업지시에 적용된 공정 유형 -- 향후 FK 연결로 확장성 확보 가능 - ---- - -## 9. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - -### 참고 코드 -- **Controller**: `api/app/Http/Controllers/V1/ProcessController.php` -- **Service**: `api/app/Services/ProcessService.php` -- **actions.ts**: `react/src/components/process-management/actions.ts` - ---- - -## 10. 자기완결성 점검 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 검증 및 테스트 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 7 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1 테스트 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 선행 작업 없음 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 모든 경로 검증됨 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 테스트 체크리스트 제공 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | curl + 체크리스트 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 경로 명시 | - ---- - -*이 문서는 독립 세션에서 바로 작업 시작 가능하도록 설계되었습니다.* \ No newline at end of file diff --git a/plans/archive/quote-auto-calculation-development-plan.md b/plans/archive/quote-auto-calculation-development-plan.md deleted file mode 100644 index 2034c20..0000000 --- a/plans/archive/quote-auto-calculation-development-plan.md +++ /dev/null @@ -1,743 +0,0 @@ -# 견적 자동산출 개발 계획 - -> **작성일**: 2025-12-22 -> **상태**: ✅ 구현 완료 -> **목표**: MNG 견적수식 데이터 셋팅 + React 견적관리 자동산출 기능 구현 -> **완료일**: 2025-12-22 -> **실제 소요 시간**: 약 2시간 - ---- - -## 0. 빠른 시작 가이드 - -### 폴더 구조 이해 (중요!) - -| 폴더 | 포트 | 역할 | 비고 | -|------|------|------|------| -| `design/` | localhost:3002 | 디자인 프로토타입 | UI 참고용 | -| `react/` | localhost:3000 | **실제 프론트엔드** | 구현 대상 ✅ | -| `mng/` | mng.sam.kr | 관리자 패널 | 수식 데이터 관리 | -| `api/` | api.sam.kr | REST API | 견적 산출 엔진 | - -### 이 문서만으로 작업을 시작하려면: - -```bash -# 1. Docker 서비스 시작 -cd /Users/hskwon/Works/@KD_SAM/SAM -docker-compose up -d - -# 2. MNG 시더 실행 (Phase 1 완료 후) -cd mng -php artisan quote:seed-formulas --tenant=1 - -# 3. React 개발 서버 (실제 구현 대상) -cd react -npm run dev -# http://localhost:3000 접속 -``` - -### 핵심 파일 위치 - -| 구분 | 파일 경로 | 역할 | -|------|----------|------| -| **MNG 시더** | `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` | 🆕 생성 필요 | -| **React 자동산출** | `react/src/components/quotes/QuoteRegistration.tsx` | ⚡ 수정 필요 (line 332) | -| **API 클라이언트** | `react/src/lib/api/client.ts` | 참조 | -| **API 엔드포인트** | `api/app/Http/Controllers/Api/V1/QuoteController.php` | ✅ 구현됨 | -| **수식 엔진** | `api/app/Services/Quote/QuoteCalculationService.php` | ✅ 구현됨 | - ---- - -## 1. 현황 분석 - -### 1.1 시스템 구조 - -``` -┌───────────────────────────────────────────────────────────────────────────────┐ -│ SAM 시스템 │ -├───────────────────────────────────────────────────────────────────────────────┤ -│ MNG (mng.sam.kr) │ React (react/ 폴더) │ Design │ -│ ├── 기준정보관리 │ ├── 판매관리 │ (참고용) │ -│ │ └── 견적수식관리 ✅ │ │ └── 견적관리 │ │ -│ │ - 카테고리 CRUD │ │ └── 자동견적산출 │ design/ │ -│ │ - 수식 CRUD │ │ UI 있음 ✅ │ :3002 │ -│ │ - 범위/매핑/품목 탭 │ │ API 연동 ❌ │ │ -│ │ │ │ │ │ -│ └── DB: quote_formulas 테이블 │ └── API 호출: │ │ -│ (데이터 없음! ❌) │ POST /v1/quotes/calculate │ │ -└───────────────────────────────────────────────────────────────────────────────┘ - -※ design/ 폴더 (localhost:3002)는 UI 프로토타입용이며, 실제 구현은 react/ 폴더에서 진행 -``` - -### 1.2 React 견적등록 컴포넌트 현황 - -**파일**: `react/src/components/quotes/QuoteRegistration.tsx` - -```typescript -// 현재 상태 (line 332-335) -const handleAutoCalculate = () => { - toast.info(`자동 견적 산출 (${formData.items.length}개 항목) - API 연동 필요`); -}; - -// 입력 필드 (이미 구현됨): -interface QuoteItem { - openWidth: string; // W0 (오픈사이즈 가로) - openHeight: string; // H0 (오픈사이즈 세로) - productCategory: string; // screen | steel - quantity: number; - // ... 기타 필드 -} -``` - -### 1.3 API 엔드포인트 현황 - -**파일**: `api/app/Http/Controllers/Api/V1/QuoteController.php` - -```php -// 이미 구현됨 (line 135-145) -public function calculate(QuoteCalculateRequest $request) -{ - return ApiResponse::handle(function () use ($request) { - $validated = $request->validated(); - return $this->calculationService->calculate( - $validated['inputs'] ?? $validated, - $validated['product_category'] ?? null - ); - }, __('message.quote.calculated')); -} -``` - -### 1.4 수식 시더 데이터 (API) - -**파일**: `api/database/seeders/QuoteFormulaSeeder.php` - -| 카테고리 | 수식 수 | 설명 | -|---------|--------|------| -| OPEN_SIZE | 2 | W0, H0 입력값 | -| MAKE_SIZE | 4 | 제작사이즈 계산 | -| AREA | 1 | 면적 = W1 * H1 / 1000000 | -| WEIGHT | 2 | 중량 계산 (스크린/철재) | -| GUIDE_RAIL | 5 | 가이드레일 자동 선택 | -| CASE | 3 | 케이스 자동 선택 | -| MOTOR | 1 | 모터 자동 선택 (범위 9개) | -| CONTROLLER | 2 | 제어기 매핑 | -| EDGE_WING | 1 | 마구리 수량 | -| INSPECTION | 1 | 검사비 | -| PRICE_FORMULA | 8 | 단가 수식 | -| **합계** | **30개** | + 범위 18개 | - ---- - -## 2. 개발 상세 계획 - -### Phase 1: MNG 시더 데이터 생성 (1일) - -#### 2.1 Artisan 명령어 생성 - -**생성할 파일**: `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` - -```php -option('tenant'); - $only = $this->option('only'); - $fresh = $this->option('fresh'); - - if ($fresh) { - $this->warn('기존 데이터를 삭제합니다...'); - $this->truncateTables($tenantId); - } - - if (!$only || $only === 'categories') { - $this->seedCategories($tenantId); - } - - if (!$only || $only === 'formulas') { - $this->seedFormulas($tenantId); - } - - if (!$only || $only === 'ranges') { - $this->seedRanges($tenantId); - } - - $this->info('✅ 견적수식 시드 완료!'); - return Command::SUCCESS; - } - - private function seedCategories(int $tenantId): void - { - $categories = [ - ['code' => 'OPEN_SIZE', 'name' => '오픈사이즈', 'sort_order' => 1], - ['code' => 'MAKE_SIZE', 'name' => '제작사이즈', 'sort_order' => 2], - ['code' => 'AREA', 'name' => '면적', 'sort_order' => 3], - ['code' => 'WEIGHT', 'name' => '중량', 'sort_order' => 4], - ['code' => 'GUIDE_RAIL', 'name' => '가이드레일', 'sort_order' => 5], - ['code' => 'CASE', 'name' => '케이스', 'sort_order' => 6], - ['code' => 'MOTOR', 'name' => '모터', 'sort_order' => 7], - ['code' => 'CONTROLLER', 'name' => '제어기', 'sort_order' => 8], - ['code' => 'EDGE_WING', 'name' => '마구리', 'sort_order' => 9], - ['code' => 'INSPECTION', 'name' => '검사', 'sort_order' => 10], - ['code' => 'PRICE_FORMULA', 'name' => '단가수식', 'sort_order' => 11], - ]; - - foreach ($categories as $cat) { - DB::table('quote_formula_categories')->updateOrInsert( - ['tenant_id' => $tenantId, 'code' => $cat['code']], - array_merge($cat, [ - 'tenant_id' => $tenantId, - 'is_active' => true, - 'created_at' => now(), - 'updated_at' => now(), - ]) - ); - } - - $this->info("카테고리 " . count($categories) . "개 생성됨"); - } - - private function seedFormulas(int $tenantId): void - { - // API 시더와 동일한 데이터 (api/database/seeders/QuoteFormulaSeeder.php 참조) - $formulas = $this->getFormulaData(); - - $categoryMap = DB::table('quote_formula_categories') - ->where('tenant_id', $tenantId) - ->pluck('id', 'code') - ->toArray(); - - $count = 0; - foreach ($formulas as $formula) { - $categoryId = $categoryMap[$formula['category_code']] ?? null; - if (!$categoryId) continue; - - DB::table('quote_formulas')->updateOrInsert( - ['tenant_id' => $tenantId, 'variable' => $formula['variable']], - [ - 'tenant_id' => $tenantId, - 'category_id' => $categoryId, - 'variable' => $formula['variable'], - 'name' => $formula['name'], - 'type' => $formula['type'], - 'formula' => $formula['formula'] ?? null, - 'output_type' => 'variable', - 'description' => $formula['description'] ?? null, - 'sort_order' => $formula['sort_order'] ?? 0, - 'is_active' => $formula['is_active'] ?? true, - 'created_at' => now(), - 'updated_at' => now(), - ] - ); - $count++; - } - - $this->info("수식 {$count}개 생성됨"); - } - - private function getFormulaData(): array - { - return [ - // 오픈사이즈 - ['category_code' => 'OPEN_SIZE', 'variable' => 'W0', 'name' => '오픈사이즈 W0 (가로)', 'type' => 'input', 'formula' => null, 'sort_order' => 1], - ['category_code' => 'OPEN_SIZE', 'variable' => 'H0', 'name' => '오픈사이즈 H0 (세로)', 'type' => 'input', 'formula' => null, 'sort_order' => 2], - - // 제작사이즈 - ['category_code' => 'MAKE_SIZE', 'variable' => 'W1_SCREEN', 'name' => '제작사이즈 W1 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 140', 'sort_order' => 1], - ['category_code' => 'MAKE_SIZE', 'variable' => 'H1_SCREEN', 'name' => '제작사이즈 H1 (스크린)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 2], - ['category_code' => 'MAKE_SIZE', 'variable' => 'W1_STEEL', 'name' => '제작사이즈 W1 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 110', 'sort_order' => 3], - ['category_code' => 'MAKE_SIZE', 'variable' => 'H1_STEEL', 'name' => '제작사이즈 H1 (철재)', 'type' => 'calculation', 'formula' => 'H0 + 350', 'sort_order' => 4], - - // 면적 - ['category_code' => 'AREA', 'variable' => 'M', 'name' => '면적 계산', 'type' => 'calculation', 'formula' => 'W1 * H1 / 1000000', 'sort_order' => 1], - - // 중량 - ['category_code' => 'WEIGHT', 'variable' => 'K_SCREEN', 'name' => '중량 계산 (스크린)', 'type' => 'calculation', 'formula' => 'M * 2 + W0 / 1000 * 14.17', 'sort_order' => 1], - ['category_code' => 'WEIGHT', 'variable' => 'K_STEEL', 'name' => '중량 계산 (철재)', 'type' => 'calculation', 'formula' => 'M * 25', 'sort_order' => 2], - - // 가이드레일 - ['category_code' => 'GUIDE_RAIL', 'variable' => 'G', 'name' => '가이드레일 제작길이', 'type' => 'calculation', 'formula' => 'H0 + 250', 'sort_order' => 1], - ['category_code' => 'GUIDE_RAIL', 'variable' => 'GR_AUTO_SELECT', 'name' => '가이드레일 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 2], - - // 케이스 - ['category_code' => 'CASE', 'variable' => 'S_SCREEN', 'name' => '케이스 사이즈 (스크린)', 'type' => 'calculation', 'formula' => 'W0 + 220', 'sort_order' => 1], - ['category_code' => 'CASE', 'variable' => 'S_STEEL', 'name' => '케이스 사이즈 (철재)', 'type' => 'calculation', 'formula' => 'W0 + 240', 'sort_order' => 2], - ['category_code' => 'CASE', 'variable' => 'CASE_AUTO_SELECT', 'name' => '케이스 자재 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 3], - - // 모터 - ['category_code' => 'MOTOR', 'variable' => 'MOTOR_AUTO_SELECT', 'name' => '모터 자동 선택', 'type' => 'range', 'formula' => null, 'sort_order' => 1], - - // 제어기 - ['category_code' => 'CONTROLLER', 'variable' => 'CONTROLLER_TYPE', 'name' => '제어기 유형', 'type' => 'input', 'formula' => null, 'sort_order' => 0], - ['category_code' => 'CONTROLLER', 'variable' => 'CTRL_AUTO_SELECT', 'name' => '제어기 자동 선택', 'type' => 'mapping', 'formula' => null, 'sort_order' => 1], - - // 검사 - ['category_code' => 'INSPECTION', 'variable' => 'INSP_FEE', 'name' => '검사비', 'type' => 'calculation', 'formula' => '1', 'sort_order' => 1], - ]; - } - - // ... 나머지 메서드 (seedRanges, truncateTables 등) -} -``` - -#### 2.2 작업 순서 - -```bash -# 1. 명령어 파일 생성 -# mng/app/Console/Commands/SeedQuoteFormulasCommand.php - -# 2. 실행 -cd mng -php artisan quote:seed-formulas --tenant=1 - -# 3. 확인 -php artisan tinker ->>> \App\Models\Quote\QuoteFormula::count() -# 예상: 30 - -# 4. 시뮬레이터 테스트 -# mng.sam.kr/quote-formulas/simulator -# 입력: W0=3000, H0=2500 -``` - ---- - -### Phase 2: React 자동산출 기능 구현 (2-3일) - -#### 2.1 API 클라이언트 추가 - -**수정할 파일**: `react/src/lib/api/quote.ts` (신규) - -```typescript -// react/src/lib/api/quote.ts -import { ApiClient } from './client'; -import { AUTH_CONFIG } from './auth/auth-config'; - -// API 응답 타입 -interface CalculationResult { - inputs: Record; - outputs: Record; - items: Array<{ - item_code: string; - item_name: string; - specification?: string; - unit?: string; - quantity: number; - unit_price: number; - total_price: number; - formula_variable: string; - }>; - costs: { - material_cost: number; - labor_cost: number; - install_cost: number; - subtotal: number; - }; - errors: string[]; -} - -interface CalculateRequest { - inputs: { - W0: number; - H0: number; - QTY?: number; - INSTALL_TYPE?: string; - CONTROL_TYPE?: string; - }; - product_category: 'screen' | 'steel'; -} - -// Quote API 클라이언트 -class QuoteApiClient extends ApiClient { - constructor() { - super({ - mode: 'bearer', - apiKey: AUTH_CONFIG.apiKey, - getToken: () => { - if (typeof window !== 'undefined') { - return localStorage.getItem('auth_token'); - } - return null; - }, - }); - } - - /** - * 자동 견적 산출 - */ - async calculate(request: CalculateRequest): Promise<{ success: boolean; data: CalculationResult; message: string }> { - return this.post('/api/v1/quotes/calculate', request); - } - - /** - * 입력 스키마 조회 - */ - async getCalculationSchema(productCategory?: string): Promise<{ success: boolean; data: Record }> { - const query = productCategory ? `?product_category=${productCategory}` : ''; - return this.get(`/api/v1/quotes/calculation-schema${query}`); - } -} - -export const quoteApi = new QuoteApiClient(); -``` - -#### 2.2 QuoteRegistration.tsx 수정 - -**수정할 파일**: `react/src/components/quotes/QuoteRegistration.tsx` - -```typescript -// 추가할 import -import { quoteApi } from '@/lib/api/quote'; -import { useState } from 'react'; - -// 상태 추가 (컴포넌트 내부) -const [calculationResult, setCalculationResult] = useState(null); -const [isCalculating, setIsCalculating] = useState(false); - -// handleAutoCalculate 수정 (line 332-335) -const handleAutoCalculate = async () => { - const item = formData.items[activeItemIndex]; - - if (!item.openWidth || !item.openHeight) { - toast.error('오픈사이즈(W0, H0)를 입력해주세요.'); - return; - } - - setIsCalculating(true); - try { - const response = await quoteApi.calculate({ - inputs: { - W0: parseFloat(item.openWidth), - H0: parseFloat(item.openHeight), - QTY: item.quantity, - INSTALL_TYPE: item.guideRailType, - CONTROL_TYPE: item.controller, - }, - product_category: item.productCategory as 'screen' | 'steel' || 'screen', - }); - - if (response.success) { - setCalculationResult(response.data); - toast.success('자동 산출이 완료되었습니다.'); - } else { - toast.error(response.message || '산출 중 오류가 발생했습니다.'); - } - } catch (error) { - console.error('자동 산출 오류:', error); - toast.error('서버 연결에 실패했습니다.'); - } finally { - setIsCalculating(false); - } -}; - -// 산출 결과 반영 함수 추가 -const handleApplyCalculation = () => { - if (!calculationResult) return; - - // 산출된 품목을 견적 항목에 반영 - const newItems = calculationResult.items.map((item, index) => ({ - id: `calc-${Date.now()}-${index}`, - floor: formData.items[activeItemIndex].floor, - code: item.item_code, - productCategory: formData.items[activeItemIndex].productCategory, - productName: item.item_name, - openWidth: formData.items[activeItemIndex].openWidth, - openHeight: formData.items[activeItemIndex].openHeight, - guideRailType: formData.items[activeItemIndex].guideRailType, - motorPower: formData.items[activeItemIndex].motorPower, - controller: formData.items[activeItemIndex].controller, - quantity: item.quantity, - wingSize: formData.items[activeItemIndex].wingSize, - inspectionFee: item.unit_price, - unitPrice: item.unit_price, - totalAmount: item.total_price, - })); - - setFormData({ - ...formData, - items: [...formData.items.slice(0, activeItemIndex), ...newItems, ...formData.items.slice(activeItemIndex + 1)], - }); - - setCalculationResult(null); - toast.success(`${newItems.length}개 품목이 반영되었습니다.`); -}; -``` - -#### 2.3 산출 결과 표시 UI 추가 - -```tsx -{/* 자동 견적 산출 버튼 아래에 추가 */} -{calculationResult && ( - - - - - 산출 결과 - - - - {/* 계산 변수 */} -
- {Object.entries(calculationResult.outputs).map(([key, val]) => ( -
-
{val.name}
-
{typeof val.value === 'number' ? val.value.toFixed(2) : val.value}
-
- ))} -
- - {/* 산출 품목 */} - - - - - - - - - - - - {calculationResult.items.map((item, i) => ( - - - - - - - - ))} - - - - - - - -
품목코드품목명수량단가금액
{item.item_code}{item.item_name}{item.quantity}{item.unit_price.toLocaleString()}{item.total_price.toLocaleString()}
합계{calculationResult.costs.subtotal.toLocaleString()}원
- - {/* 반영 버튼 */} - -
-
-)} -``` - ---- - -### Phase 3: 통합 테스트 (1일) - -#### 3.1 테스트 시나리오 - -| 번호 | 테스트 케이스 | 입력값 | 예상 결과 | -|-----|-------------|-------|----------| -| 1 | 기본 스크린 산출 | W0=3000, H0=2500 | 가이드레일 PT-GR-3000, 모터 PT-MOTOR-150 | -| 2 | 대형 스크린 산출 | W0=5000, H0=4000 | 모터 규격 상향 (300K 이상) | -| 3 | 철재 산출 | W0=2000, H0=2000, steel | 중량 M*25 적용 | -| 4 | 품목 반영 | 산출 후 반영 클릭 | 견적 항목에 추가됨 | -| 5 | 에러 처리 | W0/H0 미입력 | "오픈사이즈를 입력해주세요" | - -#### 3.2 검증 체크리스트 - -``` -□ MNG 시뮬레이터에서 수식 계산 정확도 확인 -□ React 자동산출 버튼 클릭 → API 호출 확인 -□ 산출 결과 테이블 정상 표시 -□ "품목에 반영하기" 클릭 → 견적 항목 추가 확인 -□ 견적 저장 시 calculation_inputs 필드 저장 확인 -□ 에러 시 적절한 메시지 표시 -``` - ---- - -## 3. SAM 개발 규칙 요약 - -### 3.1 API 개발 규칙 (CLAUDE.md 참조) - -```php -// Controller: FormRequest + ApiResponse 패턴 -public function calculate(QuoteCalculateRequest $request) -{ - return ApiResponse::handle(function () use ($request) { - return $this->calculationService->calculate($request->validated()); - }, __('message.quote.calculated')); -} - -// Service: 비즈니스 로직 분리 -class QuoteCalculationService extends Service -{ - public function calculate(array $inputs, ?string $productCategory = null): array - { - $tenantId = $this->tenantId(); // 필수 - // ... - } -} - -// 응답 형식 -{ - "success": true, - "message": "견적이 산출되었습니다.", - "data": { ... } -} -``` - -### 3.2 React 개발 패턴 - -```typescript -// API 클라이언트 패턴 (react/src/lib/api/client.ts) -class ApiClient { - async post(endpoint: string, data?: unknown): Promise - async get(endpoint: string): Promise -} - -// 컴포넌트 패턴 -// - shadcn/ui 컴포넌트 사용 -// - toast (sonner) 알림 -// - FormField, Card, Button 등 -``` - -### 3.3 MNG 개발 패턴 - -```php -// Artisan 명령어 패턴 -protected $signature = 'quote:seed-formulas {--tenant=1}'; - -// 모델 사용 -use App\Models\Quote\QuoteFormula; -use App\Models\Quote\QuoteFormulaCategory; - -// 서비스 패턴 -class QuoteFormulaService { - public function __construct( - private FormulaEvaluatorService $evaluator - ) {} -} -``` - ---- - -## 4. 파일 구조 - -``` -SAM/ -├── mng/ -│ ├── app/Console/Commands/ -│ │ └── SeedQuoteFormulasCommand.php # 🆕 Phase 1 -│ ├── app/Models/Quote/ -│ │ ├── QuoteFormula.php # ✅ 있음 -│ │ ├── QuoteFormulaCategory.php # ✅ 있음 -│ │ └── QuoteFormulaRange.php # ✅ 있음 -│ └── app/Services/Quote/ -│ └── FormulaEvaluatorService.php # ✅ 있음 -│ -├── api/ -│ ├── app/Http/Controllers/Api/V1/ -│ │ └── QuoteController.php # ✅ calculate() 있음 -│ ├── app/Services/Quote/ -│ │ ├── QuoteCalculationService.php # ✅ 있음 -│ │ └── FormulaEvaluatorService.php # ✅ 있음 -│ └── database/seeders/ -│ └── QuoteFormulaSeeder.php # 참조용 데이터 -│ -├── react/ -│ ├── src/lib/api/ -│ │ ├── client.ts # ✅ ApiClient 클래스 -│ │ └── quote.ts # 🆕 Phase 2 -│ └── src/components/quotes/ -│ └── QuoteRegistration.tsx # ⚡ Phase 2 수정 -│ -└── docs/plans/ - └── quote-auto-calculation-development-plan.md # 이 문서 -``` - ---- - -## 5. 수식 계산 예시 - -``` -입력: W0=3000mm, H0=2500mm, product_category=screen - -계산 순서: -1. W1 = W0 + 140 = 3140mm (스크린 제작 가로) -2. H1 = H0 + 350 = 2850mm (스크린 제작 세로) -3. M = W1 * H1 / 1000000 = 8.949㎡ (면적) -4. K = M * 2 + W0 / 1000 * 14.17 = 60.41kg (중량) -5. G = H0 + 250 = 2750mm (가이드레일 길이) -6. S = W0 + 220 = 3220mm (케이스 사이즈) - -범위 자동 선택: -- 가이드레일: G=2750 → 2438 < G ≤ 3000 → PT-GR-3000 × 2개 -- 케이스: S=3220 → 3000 < S ≤ 3600 → PT-CASE-3600 × 1개 -- 모터: K=60.41 → 0 < K ≤ 150 → PT-MOTOR-150 × 1개 -``` - ---- - -## 6. 일정 요약 - -| Phase | 작업 | 예상 기간 | 상태 | -|-------|------|----------|------| -| 1 | MNG 시더 명령어 생성 | 1일 | ✅ 완료 | -| 2 | React Quote API 클라이언트 생성 | 0.5일 | ✅ 완료 | -| 3 | React handleAutoCalculate API 연동 | 0.5일 | ✅ 완료 | -| 4 | 산출 결과 UI 추가 | 0.5일 | ✅ 완료 | -| 5 | 문서 업데이트 | 0.5시간 | ✅ 완료 | -| **합계** | | **약 2시간** | ✅ | - ---- - -## 7. 완료된 구현 내역 - -### 생성된 파일 -| 파일 경로 | 역할 | -|----------|------| -| `mng/app/Console/Commands/SeedQuoteFormulasCommand.php` | MNG 견적수식 시더 명령어 | -| `react/src/lib/api/quote.ts` | React Quote API 클라이언트 | - -### 수정된 파일 -| 파일 경로 | 변경 내용 | -|----------|----------| -| `react/src/components/quotes/QuoteRegistration.tsx` | handleAutoCalculate API 연동, 산출 결과 UI 추가 | - -### MNG 시더 실행 결과 -``` -✅ 견적수식 시드 완료! -카테고리: 11개 -수식: 18개 -범위: 18개 -``` - -### React 기능 구현 -- `handleAutoCalculate`: API 호출 및 로딩 상태 관리 -- `handleApplyCalculation`: 산출 결과를 견적 항목에 반영 -- 산출 결과 UI: 변수, 품목 테이블, 비용 합계 표시 -- 에러 처리: 입력값 검증, API 에러 토스트 - ---- - -*문서 버전*: 3.0 (구현 완료) -*작성자*: Claude Code -*최종 업데이트*: 2025-12-22 \ No newline at end of file diff --git a/plans/archive/quote-v2-auto-calculation-fix-plan.md b/plans/archive/quote-v2-auto-calculation-fix-plan.md deleted file mode 100644 index 2b372ec..0000000 --- a/plans/archive/quote-v2-auto-calculation-fix-plan.md +++ /dev/null @@ -1,262 +0,0 @@ -# 견적 V2 자동 견적 산출 오류 수정 계획 - -> **작성일**: 2026-01-26 -> **목적**: 자동 견적 산출 기능의 4가지 오류 분석 및 수정 -> **기준 문서**: `QuoteRegistrationV2.tsx`, `LocationDetailPanel.tsx`, `QuoteSummaryPanel.tsx`, `actions.ts` -> **상태**: ✅ 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 테스트 및 검증 완료 | -| **다음 작업** | - | -| **진행률** | 4/4 (100%) ✅ | -| **마지막 업데이트** | 2026-01-26 | - ---- - -## 1. 개요 - -### 1.1 배경 -견적 V2 페이지(`/sales/quote-management/test-new`)에서 자동 견적 산출 버튼 클릭 후 다음 4가지 문제 발생: -1. 오른쪽 패널에 제품 리스트가 표시되지 않음 -2. 개소별 합계(상세소계)가 표시되지 않음 -3. 상세별 합계(그룹)가 표시되지 않음 -4. 예상 견적금액이 0원으로 표시됨 - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - API 응답 구조와 프론트엔드 기대 구조의 일치 확보 │ -│ - Mock 데이터 fallback 로직은 디버깅/테스트용으로만 유지 │ -│ - 실제 BOM 계산 결과가 UI에 정확히 반영되도록 수정 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 타입 캐스팅 수정, 데이터 매핑 로직 수정 | 불필요 | -| ⚠️ 컨펌 필요 | API 응답 구조 변경, 새 인터페이스 정의 | **필수** | -| 🔴 금지 | API 엔드포인트 변경, DB 스키마 변경 | 별도 협의 | - ---- - -## 2. 근본 원인 분석 - -### 2.1 API 응답 구조 불일치 (핵심 원인) - -**API 실제 응답** (`actions.ts:962-965`): -```typescript -return { - success: true, - data: result.data || [], // 배열을 직접 반환 -}; -``` - -**API 서버 응답** (`QuoteCalculationService.php:168-178`): -```php -return [ - 'success' => $failCount === 0, - 'summary' => [ - 'total_count' => count($inputItems), - 'success_count' => $successCount, - 'fail_count' => $failCount, - 'grand_total' => round($grandTotal, 2), - ], - 'items' => $results, // items 배열 안에 결과가 있음 -]; -``` - -**컴포넌트 기대 구조** (`QuoteRegistrationV2.tsx:459-462`): -```typescript -const apiData = result.data as { - summary?: { grand_total: number }; - items?: Array<{ index: number; result: BomCalculationResult }>; -}; -const bomItems = apiData.items || []; // ❌ result.data가 배열이면 items가 없음! -``` - -### 2.2 문제 발생 흐름 - -``` -사용자 → "자동 견적 산출" 클릭 - ↓ -calculateBomBulk(bomItems) 호출 - ↓ -API 서버: { success, summary, items: [...] } 반환 - ↓ -actions.ts: result.data = 전체 응답 객체 (또는 배열로 잘못 파싱) - ↓ -QuoteRegistrationV2.tsx: result.data.items 접근 시도 - ↓ -❌ items가 undefined → bomItems = [] - ↓ -locations에 bomResult 저장 안됨 - ↓ -LocationDetailPanel: bomResult?.items 없음 → Mock 데이터 표시 -QuoteSummaryPanel: bomResult?.subtotals 없음 → Mock 데이터 표시 - ↓ -💥 모든 UI 영역에 데이터 없음 -``` - -### 2.3 영향 받는 컴포넌트 - -| 컴포넌트 | 파일 | 영향 | -|----------|------|------| -| QuoteRegistrationV2 | `QuoteRegistrationV2.tsx:457-481` | bomResult 저장 안됨 | -| LocationDetailPanel | `LocationDetailPanel.tsx:152-184` | Mock 데이터로 fallback | -| QuoteSummaryPanel | `QuoteSummaryPanel.tsx:136-164` | Mock 데이터로 fallback | - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: API 응답 처리 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | `actions.ts` 응답 구조 확인 | ✅ | API 서버 응답과 비교 | -| 1.2 | `actions.ts` BomBulkResponse 타입 추가 | ✅ | 정확한 API 응답 구조 정의 | -| 1.3 | `QuoteRegistrationV2.tsx` handleCalculate 수정 | ✅ | 응답 매핑 로직 및 디버그 로그 추가 | - -### 3.2 Phase 2: 데이터 바인딩 수정 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | `FormulaEvaluatorService.php` items에 process_group 추가 | ✅ | addProcessGroupToItems 메서드 추가 | -| 2.2 | `LocationDetailPanel.tsx` bomItemsByTab 수정 | ✅ | process_group_key 기반 매핑 | -| 2.3 | `QuoteSummaryPanel.tsx` detailTotals 수정 | ✅ | grouped_items에서 items 가져오기 | -| 2.4 | `actions.ts` BomCalculationResult 타입 확장 | ✅ | process_group, grouped_items 필드 추가 | - ---- - -## 4. 상세 작업 내용 - -### 4.1 Phase 1.2: handleCalculate 함수 수정 - -**현재 코드** (`QuoteRegistrationV2.tsx:457-479`): -```typescript -if (result.success && result.data) { - // ❌ 잘못된 타입 캐스팅 - result.data 자체가 API 응답 객체임 - const apiData = result.data as { - summary?: { grand_total: number }; - items?: Array<{ index: number; result: BomCalculationResult }>; - }; - const bomItems = apiData.items || []; // ❌ undefined - // ... -} -``` - -**수정 방안**: -`actions.ts`의 응답 처리를 확인하여 두 가지 접근법 중 선택: - -#### 방안 A: actions.ts 수정 (권장) -```typescript -// actions.ts에서 API 응답 구조 유지 -return { - success: true, - data: { - summary: result.data.summary, - items: result.data.items, - }, -}; -``` - -#### 방안 B: QuoteRegistrationV2.tsx 수정 -```typescript -if (result.success && result.data) { - // result.data가 { summary, items } 구조인지 확인 - const apiData = result.data as unknown as { - summary?: { grand_total: number }; - items?: Array<{ index: number; result: BomCalculationResult }>; - }; - // ... -} -``` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | API 응답 구조 | actions.ts의 result.data 반환 방식 검토 | react/actions.ts | 대기 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-26 | 분석 | 문서 초안 작성 및 근본 원인 분석 완료 | - | - | -| 2026-01-26 | 수정 | actions.ts BomBulkResponse 타입 추가 | react/src/components/quotes/actions.ts | ✅ | -| 2026-01-26 | 수정 | QuoteRegistrationV2.tsx handleCalculate 수정 | react/src/components/quotes/QuoteRegistrationV2.tsx | ✅ | -| 2026-01-26 | 수정 | FormulaEvaluatorService.php process_group 추가 | api/app/Services/Quote/FormulaEvaluatorService.php | ✅ | -| 2026-01-26 | 수정 | LocationDetailPanel.tsx bomItemsByTab 수정 | react/src/components/quotes/LocationDetailPanel.tsx | ✅ | -| 2026-01-26 | 수정 | QuoteSummaryPanel.tsx detailTotals 수정 | react/src/components/quotes/QuoteSummaryPanel.tsx | ✅ | -| 2026-01-26 | 수정 | ItemService.php has_bom 필드 추가 | api/app/Services/ItemService.php | ✅ | -| 2026-01-26 | 수정 | actions.ts FinishedGoods에 has_bom, bom 필드 추가 | react/src/components/quotes/actions.ts | ✅ | -| 2026-01-26 | 수정 | QuoteRegistrationV2.tsx DevFill BOM 필터링 | react/src/components/quotes/QuoteRegistrationV2.tsx | ✅ | -| 2026-01-26 | 검증 | 브라우저 테스트 완료 - 4가지 문제 모두 해결 확인 | - | ✅ | - ---- - -## 7. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **API 규칙**: `docs/standards/api-rules.md` - ---- - -## 8. 검증 결과 - -> 브라우저 자동화 테스트 완료 (2026-01-26) - -### 8.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| DevFill 후 자동 견적 산출 | 제품 리스트 표시 | 볼트 M10×40, 너트 M10, 볼트 M8×30 등 6개 품목 표시 | ✅ | -| 개소 선택 | 개소별 합계 표시 | 1F / SS-01 상세소계: 3,119,555.94원 | ✅ | -| 그룹별 합계 | 상세별 합계 표시 | 절곡 공정: 735,891.24원, 철재 공정: 2,383,364.7원 | ✅ | -| 전체 금액 | 예상 견적금액 > 0 | 예상 견적금액: 3,119,555.94원 | ✅ | - -### 8.2 테스트 환경 - -- **URL**: `http://dev.sam.kr/sales/quote-management/test-new` -- **테스트 방법**: Claude-in-Chrome 브라우저 자동화 -- **데이터**: DevFill로 생성된 테스트 데이터 - -### 8.3 추가 발견 및 해결 사항 - -테스트 중 DevFill이 BOM 없는 제품을 선택하여 계산 결과가 0으로 나오는 문제 발견: - -| 문제 | 원인 | 해결 | -|------|------|------| -| DevFill 후 bomItemsCount: 0 | BOM 없는 제품 선택 | DevFill에서 BOM 있는 제품만 필터링 | -| has_bom 필드 없음 | API 응답에 미포함 | ItemService.php에서 계산 필드 추가 | -| getFinishedGoods에서 필드 누락 | 매핑 시 has_bom, bom 미포함 | FinishedGoods 인터페이스 및 매핑 수정 | - -### 8.4 최종 검증 결과 - -``` -[DevFill] BOM 있는 제품: 15개 / 전체: 2017개 -[BOM 계산 결과] -- bomItemsCount: 6 -- bomGrandTotal: 3,119,555.94 -- 공정별 그룹: 절곡, 철재 -``` - -**모든 4가지 UI 문제 해결 확인 완료** ✅ - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/react-fcm-push-notification-plan.md b/plans/archive/react-fcm-push-notification-plan.md deleted file mode 100644 index 7583ba8..0000000 --- a/plans/archive/react-fcm-push-notification-plan.md +++ /dev/null @@ -1,543 +0,0 @@ -# React FCM 푸시 알림 연동 계획 - -> **작성일**: 2025-12-30 -> **목적**: Capacitor 앱 웹뷰가 dev.sam.kr (Next.js)을 로드할 때 FCM 푸시 알림 지원 -> **기준 문서**: mng/public/js/fcm.js (포팅 대상), api/app/Swagger/v1/PushApi.php -> **상태**: ✅ 구현 완료 (Serena ID: react-fcm-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4: 통합 완료 | -| **다음 작업** | 테스트 (Capacitor 앱에서 확인) | -| **진행률** | 4/4 (100%) ✅ | -| **마지막 업데이트** | 2025-12-30 | - ---- - -## 1. 개요 - -### 1.1 현재 구조 - -``` -Capacitor 앱 (웹뷰) - │ - ▼ - mng (현재) - │ - ├── fcm.js 로드 - │ ├── Capacitor PushNotifications 사용 - │ ├── 토큰 발급 - │ └── api에 토큰 등록 - │ - ▼ - api - │ - └── /push/register-token -``` - -### 1.2 목표 구조 - -``` -Capacitor 앱 (웹뷰) - │ - ▼ - dev.sam.kr (react) ← 변경 - │ - ├── FCM 훅/유틸리티 (포팅) - │ ├── Capacitor PushNotifications 사용 (동일) - │ ├── 토큰 발급 (동일) - │ └── api에 토큰 등록 (동일) - │ - ▼ - api (변경 없음) - │ - └── /push/register-token -``` - -### 1.3 핵심 포인트 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심: mng/public/js/fcm.js를 react에 포팅 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. Capacitor PushNotifications 플러그인 사용 (동일) │ -│ 2. 토큰 발급 → api 등록 로직 (동일) │ -│ 3. 포그라운드 알림 → sonner 토스트로 변경 │ -│ 4. 백엔드 API 변경 없음 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | Capacitor 플러그인 설치, 훅 생성, 유틸리티 추가 | 불필요 | -| ⚠️ 컨펌 필요 | 포그라운드 알림 UX (토스트 디자인) | **필수** | -| 🔴 금지 | API 엔드포인트 변경, DB 스키마 변경 | 별도 협의 | - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: Capacitor 플러그인 설치 ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | @capacitor/push-notifications 설치 | ✅ | ^8.0.0 | -| 1.2 | @capacitor/app 설치 | ✅ | ^8.0.0 | -| 1.3 | @capacitor/core 설치 | ✅ | ^8.0.0 | - -### 2.2 Phase 2: FCM 유틸리티 포팅 ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | lib/capacitor/fcm.ts 생성 | ✅ | 9.1KB | -| 2.2 | useFCM 훅 생성 | ✅ | 3.3KB | -| 2.3 | FCM Provider 생성 | ✅ | contexts/FCMProvider.tsx | - -### 2.3 Phase 3: 포그라운드 알림 UI ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | sonner 토스트 연동 | ✅ | useFCM에서 처리 | -| 3.2 | 알림 사운드 재생 | ✅ | public/sounds/ | -| 3.3 | 클릭 시 URL 이동 | ✅ | window.location.href | - -### 2.4 Phase 4: 통합 ✅ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | layout.tsx에 FCMProvider 추가 | ✅ | (protected)/layout.tsx | -| 4.2 | 로그아웃 시 토큰 해제 | ✅ | logout.ts 수정 | -| 4.3 | 토큰 등록 테스트 | ⏳ | Capacitor 앱에서 확인 필요 | -| 4.4 | 포그라운드/백그라운드 알림 테스트 | ⏳ | Capacitor 앱에서 확인 필요 | - ---- - -## 3. 기술 상세 - -### 3.1 기존 mng/public/js/fcm.js 분석 - -```javascript -// 핵심 기능 요약 -1. Capacitor 네이티브 환경 체크 (ios/android) -2. PushNotifications.requestPermissions() - 권한 요청 -3. PushNotifications.register() - 토큰 발급 -4. registration 이벤트 → api에 토큰 등록 -5. pushNotificationReceived → 포그라운드 알림 (토스트 + 사운드) -6. pushNotificationActionPerformed → 알림 클릭 시 URL 이동 -``` - -### 3.2 FCM 유틸리티 (포팅) - -```typescript -// src/lib/capacitor/fcm.ts -import { Capacitor } from '@capacitor/core'; -import { PushNotifications } from '@capacitor/push-notifications'; -import { App } from '@capacitor/app'; - -const CONFIG = { - apiBaseUrl: process.env.NEXT_PUBLIC_API_URL || 'https://api.codebridge-x.com', - fcmTokenKey: 'fcm_token', - soundBasePath: '/sounds/', - defaultSound: 'default', -}; - -let isAppForeground = true; - -/** - * FCM 초기화 (Capacitor 네이티브 환경에서만 동작) - */ -export async function initializeFCM( - accessToken: string, - onForegroundNotification?: (notification: PushNotification) => void -): Promise { - // 네이티브 환경 체크 - const platform = Capacitor.getPlatform(); - if (platform !== 'ios' && platform !== 'android') { - console.log('[FCM] Not running in native app'); - return false; - } - - if (!Capacitor.isPluginAvailable('PushNotifications')) { - console.log('[FCM] PushNotifications plugin not available'); - return false; - } - - try { - // 앱 상태 리스너 - App.addListener('appStateChange', ({ isActive }) => { - isAppForeground = isActive; - console.log('[FCM] App state:', isActive ? 'foreground' : 'background'); - }); - - // 기존 리스너 제거 - await PushNotifications.removeAllListeners(); - - // 리스너 등록 - PushNotifications.addListener('registration', async (token) => { - console.log('[FCM] Token received:', token.value?.substring(0, 20) + '...'); - await handleTokenRegistration(token.value, accessToken); - }); - - PushNotifications.addListener('registrationError', (err) => { - console.error('[FCM] Registration error:', err); - }); - - PushNotifications.addListener('pushNotificationReceived', (notification) => { - console.log('[FCM] Push received (foreground):', notification); - if (onForegroundNotification) { - onForegroundNotification(notification); - } - handleForegroundSound(notification); - }); - - PushNotifications.addListener('pushNotificationActionPerformed', (action) => { - console.log('[FCM] Push action performed:', action); - const url = action.notification?.data?.url; - if (url) { - window.location.href = url; - } - }); - - // 권한 요청 - const perm = await PushNotifications.requestPermissions(); - console.log('[FCM] Push permission:', perm.receive); - - if (perm.receive !== 'granted') { - console.log('[FCM] Push permission not granted'); - return false; - } - - // 토큰 발급 요청 - await PushNotifications.register(); - return true; - - } catch (error) { - console.error('[FCM] Initialization error:', error); - return false; - } -} - -/** - * 토큰 등록 처리 - */ -async function handleTokenRegistration(newToken: string, accessToken: string): Promise { - const oldToken = sessionStorage.getItem(CONFIG.fcmTokenKey); - - if (oldToken === newToken) { - console.log('[FCM] Token unchanged, skip'); - return; - } - - const success = await registerTokenToServer(newToken, accessToken); - - if (success) { - sessionStorage.setItem(CONFIG.fcmTokenKey, newToken); - console.log('[FCM] Token saved to sessionStorage'); - } -} - -/** - * 서버에 토큰 등록 - */ -async function registerTokenToServer(token: string, accessToken: string): Promise { - try { - const response = await fetch(`${CONFIG.apiBaseUrl}/api/v1/push/register-token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - token, - platform: Capacitor.getPlatform(), - device_name: navigator.userAgent?.substring(0, 100) || null, - app_version: process.env.NEXT_PUBLIC_APP_VERSION || null, - }), - }); - - if (response.ok) { - console.log('[FCM] Token registered successfully'); - return true; - } - - console.error('[FCM] Token registration failed:', response.status); - return false; - - } catch (error) { - console.error('[FCM] Failed to send token:', error); - return false; - } -} - -/** - * 토큰 해제 (로그아웃 시) - */ -export async function unregisterFCMToken(accessToken?: string): Promise { - const token = sessionStorage.getItem(CONFIG.fcmTokenKey); - if (!token) return true; - - try { - if (accessToken) { - await fetch(`${CONFIG.apiBaseUrl}/api/v1/push/unregister-token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - body: JSON.stringify({ token }), - }); - } - } catch (e) { - console.warn('[FCM] Unregister failed'); - } - - sessionStorage.removeItem(CONFIG.fcmTokenKey); - return true; -} - -/** - * 포그라운드 사운드 재생 - */ -function handleForegroundSound(notification: any): void { - if (!isAppForeground) return; - - const soundKey = notification.data?.sound_key; - if (!soundKey) return; - - try { - const audio = new Audio(`${CONFIG.soundBasePath}${soundKey}.wav`); - audio.volume = 0.5; - audio.play().catch(() => { - // 기본 사운드 시도 - const defaultAudio = new Audio(`${CONFIG.soundBasePath}${CONFIG.defaultSound}.wav`); - defaultAudio.volume = 0.5; - defaultAudio.play().catch(() => {}); - }); - } catch (err) { - console.warn('[FCM] Sound error:', err); - } -} - -/** - * Capacitor 네이티브 환경인지 확인 - */ -export function isCapacitorNative(): boolean { - const platform = Capacitor.getPlatform(); - return platform === 'ios' || platform === 'android'; -} - -// 타입 정의 -export interface PushNotification { - title?: string; - body?: string; - data?: { - type?: string; - url?: string; - sound_key?: string; - }; -} -``` - -### 3.3 useFCM 훅 - -```typescript -// src/hooks/useFCM.ts -'use client'; - -import { useEffect, useRef } from 'react'; -import { useSession } from 'next-auth/react'; -import { toast } from 'sonner'; -import { - initializeFCM, - unregisterFCMToken, - isCapacitorNative, - PushNotification, -} from '@/lib/capacitor/fcm'; - -export function useFCM() { - const { data: session } = useSession(); - const initialized = useRef(false); - - useEffect(() => { - // 네이티브 환경이 아니면 무시 - if (!isCapacitorNative()) return; - - // 로그인 안 됐으면 무시 - if (!session?.accessToken) return; - - // 이미 초기화됐으면 무시 - if (initialized.current) return; - - initialized.current = true; - - // FCM 초기화 - initializeFCM(session.accessToken, handleForegroundNotification); - - // 클린업 (로그아웃 시) - return () => { - // 로그아웃 시 토큰 해제는 별도 처리 - }; - }, [session?.accessToken]); - - // 포그라운드 알림 핸들러 - function handleForegroundNotification(notification: PushNotification) { - const { title, body, data } = notification; - const type = data?.type || 'default'; - const url = data?.url; - - // 타입별 토스트 스타일 - const toastFn = getToastFunction(type); - - toastFn(title || '알림', { - description: body, - action: url ? { - label: '보기', - onClick: () => { - window.location.href = url; - }, - } : undefined, - duration: 5000, - }); - } - - // 타입별 토스트 함수 - function getToastFunction(type: string) { - const errorTypes = ['invoice_failed', 'payment_failed', 'order_cancelled']; - const warningTypes = ['approval_required', 'stock_low']; - const successTypes = ['order_completed', 'payment_completed', 'approval_approved']; - - if (errorTypes.includes(type)) return toast.error; - if (warningTypes.includes(type)) return toast.warning; - if (successTypes.includes(type)) return toast.success; - return toast.info; - } - - // 로그아웃 시 호출 - async function cleanup(accessToken?: string) { - await unregisterFCMToken(accessToken); - initialized.current = false; - } - - return { cleanup }; -} -``` - -### 3.4 FCM Provider - -```typescript -// src/providers/FCMProvider.tsx -'use client'; - -import { useFCM } from '@/hooks/useFCM'; - -export function FCMProvider({ children }: { children: React.ReactNode }) { - // FCM 훅 실행 (초기화) - useFCM(); - - return <>{children}; -} -``` - -### 3.5 레이아웃에 Provider 추가 - -```typescript -// src/app/layout.tsx (또는 적절한 위치) -import { FCMProvider } from '@/providers/FCMProvider'; - -export default function RootLayout({ children }) { - return ( - - - - - {children} - - - - - ); -} -``` - ---- - -## 4. 파일 구조 - -``` -react/ -├── public/ -│ └── sounds/ ← 알림 사운드 (mng에서 복사) -│ ├── default.wav -│ └── *.wav -├── src/ -│ ├── lib/ -│ │ └── capacitor/ -│ │ └── fcm.ts ← 🆕 FCM 핵심 로직 (포팅) -│ ├── hooks/ -│ │ └── useFCM.ts ← 🆕 FCM 훅 -│ └── providers/ -│ └── FCMProvider.tsx ← 🆕 FCM Provider -├── capacitor.config.ts ← 확인/수정 필요 -└── package.json ← Capacitor 플러그인 추가 -``` - ---- - -## 5. 의존성 - -| 패키지 | 버전 | 용도 | -|--------|------|------| -| @capacitor/core | (기존) | Capacitor 코어 | -| @capacitor/push-notifications | ^6.0.0 | 푸시 알림 플러그인 | -| @capacitor/app | ^6.0.0 | 앱 상태 감지 | -| sonner | (기존) | 포그라운드 토스트 | - ---- - -## 6. mng vs react 비교 - -| 항목 | mng (기존) | react (포팅) | -|------|-----------|--------------| -| **FCM 플러그인** | Capacitor PushNotifications | 동일 | -| **토큰 저장** | sessionStorage | 동일 | -| **API 호출** | fetch | 동일 | -| **포그라운드 알림** | showToast (커스텀) | sonner 토스트 | -| **사운드 재생** | Audio API | 동일 | -| **URL 이동** | window.location.href | 동일 (또는 router.push) | - ---- - -## 7. 참고 문서 - -| 문서 | 용도 | -|------|------| -| `mng/public/js/fcm.js` | 포팅 원본 | -| `api/app/Swagger/v1/PushApi.php` | 백엔드 API 스펙 | -| [Capacitor Push Notifications](https://capacitorjs.com/docs/apis/push-notifications) | 공식 문서 | - ---- - -## 8. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 포그라운드 알림 UX | sonner 토스트 디자인/위치 | UX | ⏳ | - ---- - -## 9. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-12-30 | 계획 수립 | 계획 문서 작성 | - | - | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/react-server-component-audit-plan.md b/plans/archive/react-server-component-audit-plan.md deleted file mode 100644 index ae0ce56..0000000 --- a/plans/archive/react-server-component-audit-plan.md +++ /dev/null @@ -1,147 +0,0 @@ -# React 서버 컴포넌트 점검 계획 - -> **작성일**: 2025-01-09 -> **목적**: push하지 않은 작업분 중 서버 컴포넌트를 클라이언트 컴포넌트로 변경 -> **상태**: ✅ 점검 완료 - 수정 불필요 - ---- - -## 📍 점검 결과 요약 - -| 항목 | 내용 | -|------|------| -| **점검 대상** | push하지 않은 커밋 (origin/master..HEAD) | -| **커밋 수** | 20개 | -| **점검 파일 수** | 31개 (tsx/ts 파일) | -| **서버 컴포넌트 발견** | 0개 | -| **수정 필요** | ❌ 없음 | - ---- - -## 1. 점검 배경 - -### 1.1 정책 -- 프론트엔드 정책: **서버 컴포넌트 사용 금지** -- 모든 컴포넌트는 **클라이언트 컴포넌트**로 작성해야 함 -- `'use client'` 지시어 필수 - -### 1.2 점검 범위 -- **대상**: react 폴더의 push하지 않은 작업분 -- **제외**: 이미 push된 커밋 (프론트엔드에서 수정 중) - ---- - -## 2. 점검 대상 파일 - -### 2.1 변경된 TSX 파일 (16개) - -| # | 파일 | 'use client' | 상태 | -|---|------|:------------:|:----:| -| 1 | `src/app/[locale]/(protected)/sales/order-management-sales/[id]/edit/page.tsx` | ✅ | 정상 | -| 2 | `src/app/[locale]/(protected)/sales/order-management-sales/[id]/page.tsx` | ✅ | 정상 | -| 3 | `src/app/[locale]/(protected)/sales/order-management-sales/[id]/production-order/page.tsx` | ✅ | 정상 | -| 4 | `src/app/[locale]/(protected)/sales/order-management-sales/new/page.tsx` | ✅ | 정상 | -| 5 | `src/app/[locale]/(protected)/sales/order-management-sales/page.tsx` | ✅ | 정상 | -| 6 | `src/components/approval/DocumentCreate/ApprovalLineSection.tsx` | ✅ | 정상 | -| 7 | `src/components/approval/DocumentCreate/ReferenceSection.tsx` | ✅ | 정상 | -| 8 | `src/components/hr/EmployeeManagement/EmployeeForm.tsx` | ✅ | 정상 | -| 9 | `src/components/orders/OrderRegistration.tsx` | ✅ | 정상 | -| 10 | `src/components/orders/QuotationSelectDialog.tsx` | ✅ | 정상 | -| 11 | `src/components/process-management/ProcessDetail.tsx` | ✅ | 정상 | -| 12 | `src/components/process-management/RuleModal.tsx` | ✅ | 정상 | -| 13 | `src/components/production/WorkOrders/SalesOrderSelectModal.tsx` | ✅ | 정상 | -| 14 | `src/components/production/WorkOrders/WorkOrderCreate.tsx` | ✅ | 정상 | -| 15 | `src/components/production/WorkOrders/WorkOrderDetail.tsx` | ✅ | 정상 | -| 16 | `src/components/production/WorkOrders/WorkOrderList.tsx` | ✅ | 정상 | - -### 2.2 변경된 TS 파일 (15개) - 검토 불필요 - -TS 파일은 컴포넌트가 아닌 유틸리티/타입/액션 파일로 서버 컴포넌트 대상 아님: - -- `src/components/business/construction/*/actions.ts` (6개) -- `src/components/orders/actions.ts` -- `src/components/orders/index.ts` -- `src/components/process-management/actions.ts` -- `src/components/production/WorkOrders/actions.ts` -- `src/components/production/WorkOrders/types.ts` -- `src/lib/api/common-codes.ts` -- `src/lib/api/index.ts` -- `src/types/process.ts` -- `src/components/business/construction/site-management/types.ts` - ---- - -## 3. Push하지 않은 커밋 목록 - -``` -311ddd9 docs: Phase D~K 마이그레이션 완료 상태 반영 (95%) -6615f39 feat(order-management): Mock → API 연동 및 common-codes 유틸리티 추가 -d472b77 fix(approval): 결재선/참조 Select 값 변경 불가 버그 수정 -5fa20c8 feat(item-management): Mock → API 연동 완료 -749f0ce feat: 거래처관리 API 연동 (Phase 2.2) -273d570 feat(시공사): 2.1 현장관리 - Frontend API 연동 -78e193c refactor(work-orders): process_type을 process_id FK로 변환 -9d30555 feat(시공사): 1.2 인수인계보고서 - Frontend API 연동 -d15a203 feat(work-orders): 다중 담당자 UI 구현 -8172226 Merge remote-tracking branch 'origin/master' -668cde3 Merge remote-tracking branch 'origin/master' -c651e7b feat(WEB): 수주관리 Phase 3 완료 - 고급 기능 구현 -2d7809b feat: [시공관리] 계약관리 Frontend API 연동 -12b4259 refactor(work-orders): 코드 리뷰 기반 프론트엔드 개선 -fde8726 feat(WEB): 수주관리 Phase 2 타입 정의 확장 및 공정관리 개별 품목 표시 수정 -ba36c0e feat: 공정 관리 Frontend actions 업데이트 -d797868 fix(WEB): 공정관리 개별 품목 저장 안되는 버그 수정 -3d2dea6 feat: 수주 관리 Phase 3 - Frontend API 연동 -6632943 Merge remote-tracking branch 'origin/master' -288871c feat(WEB): 직원 관리 폼 직급/부서/직책 Select 드롭다운 연동 -572ffe8 feat(orders): Phase 2 - Frontend API 연동 완료 -``` - ---- - -## 4. 점검 결론 - -### 4.1 결과 -**✅ 모든 TSX 파일에 'use client' 지시어가 있음** - -push하지 않은 작업분에서 서버 컴포넌트가 발견되지 않았습니다. -모든 컴포넌트가 클라이언트 컴포넌트 정책을 준수하고 있습니다. - -### 4.2 수정 필요 항목 -**없음** - ---- - -## 5. 향후 권장사항 - -### 5.1 새 파일 생성 시 체크리스트 -``` -□ TSX 파일 첫 줄에 'use client' 지시어 추가 -□ page.tsx 파일도 예외 없이 'use client' 필수 -□ layout.tsx 파일도 필요시 'use client' 추가 -``` - -### 5.2 코드 리뷰 시 확인 -- PR 리뷰 시 새 TSX 파일의 'use client' 지시어 확인 -- async 컴포넌트 패턴 지양 (useEffect, React Query 등 사용) - -### 5.3 린트 규칙 고려 -향후 ESLint 커스텀 룰 추가 검토: -```javascript -// .eslintrc.js 예시 -rules: { - 'react/enforce-use-client': 'error' // 커스텀 룰 -} -``` - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | -|------|------|----------| -| 2025-01-09 | 문서 생성 | 서버 컴포넌트 점검 완료, 수정 불필요 확인 | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/sam-stat-database-design-plan.md b/plans/archive/sam-stat-database-design-plan.md deleted file mode 100644 index f63455e..0000000 --- a/plans/archive/sam-stat-database-design-plan.md +++ /dev/null @@ -1,1294 +0,0 @@ -# SAM 통계 시스템 (sam_stat DB) 설계 계획 - -> **작성일**: 2026-01-29 -> **목적**: SAM ERP의 확장 가능한 통계 전용 데이터베이스(sam_stat) 설계 -> **기준 문서**: `docs/specs/database-schema.md`, `docs/architecture/system-overview.md` -> **상태**: ✅ 구현 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 6: 문서화 및 마무리 완료 (Swagger, DB 스키마 문서, 계획 문서 완료 처리) | -| **다음 작업** | ✅ 전체 완료 | -| **진행률** | 6/6 Phase (100%) | -| **마지막 업데이트** | 2026-01-30 | - ---- - -## 0. 프로젝트 컨텍스트 (새 세션용) - -> **이 섹션은 새 세션에서 이 문서만으로 작업을 시작할 수 있도록 필요한 모든 컨텍스트를 포함한다.** - -### 0.1 프로젝트 구조 - -``` -/Users/kent/Works/@KD_SAM/SAM/ -├── api/ ← 작업 대상 (Laravel 12 REST API, PHP 8.4+) -│ ├── app/ -│ │ ├── Console/Commands/ # Artisan 커맨드 (19개 존재) -│ │ ├── Http/Controllers/Api/V1/ # API 컨트롤러 -│ │ ├── Models/ # Eloquent 모델 (167개) -│ │ │ ├── Stats/ # ← 새로 생성할 통계 모델 디렉토리 -│ │ │ ├── Tenants/ # 테넌트 스코프 모델 (가장 많음) -│ │ │ ├── Orders/ # 수주 관련 -│ │ │ ├── Production/ # 생산 관련 -│ │ │ └── ... -│ │ └── Services/ # 비즈니스 로직 (Service-First 아키텍처) -│ │ ├── Stats/ # ← 새로 생성할 통계 서비스 디렉토리 -│ │ ├── DashboardService.php # 기존 대시보드 (355줄, 원본 DB 실시간 집계) -│ │ ├── ReportService.php # 기존 보고서 (일일일보, 지출예상) -│ │ ├── DailyReportService.php # 일일 보고서 (어음, 계좌, 요약) -│ │ ├── AiReportService.php # AI 보고서 -│ │ └── ... -│ ├── config/ -│ │ └── database.php # DB 연결 설정 (mysql, chandj 존재) -│ ├── database/ -│ │ └── migrations/ # 279개 마이그레이션 파일 -│ ├── routes/ -│ │ ├── console.php # 스케줄러 정의 (Laravel 12 방식) -│ │ └── api/v1/ -│ │ ├── common.php # dashboard, reports 라우트 -│ │ ├── finance.php # daily-report 라우트 -│ │ └── ... # 14개 라우트 파일 -│ └── .env # 환경변수 -├── mng/ # 관리자 패널 (Plain Laravel + Blade/Tailwind) -├── react/ # Next.js 15 프론트엔드 -├── docker/ -│ └── docker-compose.yml # Docker 설정 -└── docs/ # 기술 문서 - ├── specs/database-schema.md # DB 스키마 문서 - ├── architecture/system-overview.md - └── plans/ # 이 문서의 위치 -``` - -### 0.2 현재 DB 환경 - -``` -# .env (api/) -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 # Docker 내부: sam-mysql-1 -DB_PORT=3306 -DB_DATABASE=samdb # ← 원본 DB (219개 테이블) -DB_USERNAME=samuser -DB_PASSWORD=sampass - -# sam_stat 연결은 아직 없음 → Phase 1에서 추가 -``` - -**config/database.php 현재 연결:** -- `mysql` - 기본 samdb (원본) -- `chandj` - 5130 레거시 DB (사용하지 않음) -- `sam_stat` - **아직 없음** (이 작업에서 추가) - -### 0.3 기존 대시보드/보고서 시스템 (변경 대상) - -| 파일 | 경로 | 역할 | 통계 전환 시 영향 | -|------|------|------|------------------| -| DashboardController | `api/app/Http/Controllers/Api/V1/DashboardController.php` | summary, charts, approvals | Phase 4.5에서 sam_stat 조회로 전환 | -| ReportController | `api/app/Http/Controllers/Api/V1/ReportController.php` | daily, expense-estimate, export | Phase 4.5에서 sam_stat 조회로 전환 | -| DailyReportController | `api/app/Http/Controllers/Api/V1/DailyReportController.php` | note-receivables, accounts, summary | Phase 4.5에서 sam_stat 조회로 전환 | -| DashboardService | `api/app/Services/DashboardService.php` (355줄) | 원본 DB에서 실시간 집계 (Attendance, Approval, Deposit, Sale 등) | **핵심 전환 대상** | -| ReportService | `api/app/Services/ReportService.php` | 일일일보, 지출예상 (Excel 내보내기 포함) | 부분 전환 | -| DailyReportService | `api/app/Services/DailyReportService.php` | 어음/외상채권, 계좌현황 | 부분 전환 | -| AiReportService | `api/app/Services/AiReportService.php` | AI 보고서 생성/조회 | 변경 없음 | - -**현재 API 라우트 (변경 없음, 내부 데이터소스만 전환):** -``` -# common.php -GET /api/v1/dashboard/summary → DashboardController@summary -GET /api/v1/dashboard/charts → DashboardController@charts -GET /api/v1/dashboard/approvals → DashboardController@approvals -GET /api/v1/reports/daily → ReportController@daily -GET /api/v1/reports/daily/export → ReportController@dailyExport -GET /api/v1/reports/expense-estimate → ReportController@expenseEstimate - -# finance.php -GET /api/v1/daily-report/note-receivables → DailyReportController@noteReceivables -GET /api/v1/daily-report/daily-accounts → DailyReportController@dailyAccounts -GET /api/v1/daily-report/summary → DailyReportController@summary -``` - -### 0.4 기존 스케줄러 패턴 (따라야 할 패턴) - -```php -// api/routes/console.php (Laravel 12 방식 - Kernel.php 없음) -use Illuminate\Support\Facades\Schedule; - -// 기존 스케줄러: 매일 03:00 API 로그 정리 -Schedule::command('api-log:prune') - ->dailyAt('03:00') - ->appendOutputTo(storage_path('logs/scheduler.log')) - ->onSuccess(function () { Log::info('...'); }) - ->onFailure(function () { Log::error('...'); }); -``` - -### 0.5 기존 Artisan 커맨드 패턴 - -``` -api/app/Console/Commands/ -├── PruneAuditLogs.php # 감사 로그 정리 (참고 패턴) -├── CleanupExpiredLinks.php # 만료 링크 정리 -├── RecordStorageUsage.php # 저장소 사용량 기록 -├── TenantsBootstrap.php # 테넌트 초기화 -└── ... # 총 19개 -``` - -### 0.6 모델 패턴 (따라야 할 패턴) - -```php -// 기존 모델 예시 - 멀티테넌트 + Soft Delete -namespace App\Models\Tenants; - -use App\Models\Scopes\TenantScope; -use Illuminate\Database\Eloquent\SoftDeletes; - -class Deposit extends Model -{ - use SoftDeletes; - - protected $table = 'deposits'; - - protected static function booted(): void - { - static::addGlobalScope(new TenantScope); - } -} - -// 통계 모델은 다른 DB 연결 사용 -// protected $connection = 'sam_stat'; -// TenantScope 대신 tenant_id를 직접 WHERE 조건으로 사용 -``` - -### 0.7 환경별 구성 - -#### 로컬 환경 (Docker) - -```yaml -# docker/docker-compose.yml 내 MySQL 서비스 -# Docker 내부 호스트: sam-mysql-1 -# sam_stat DB는 같은 MySQL 인스턴스에 생성 (별도 서버 불필요) -``` - -```bash -# 로컬 sam_stat DB 생성 -docker compose exec mysql mysql -u root -proot \ - -e "CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" - -# 로컬 마이그레이션 실행 -docker compose exec api php artisan migrate --database=sam_stat - -# 로컬 시딩 -docker compose exec api php artisan db:seed --class=DimDateSeeder -``` - -#### 개발 서버 (non-Docker, codebridge-x.com) - -> **개발 서버는 Docker를 사용하지 않는다.** -> 로컬에서 코드 작업 후 Git push하면 되지만, 개발 서버에서 아래 **1회 세팅이 필요**하다. - -```bash -# 1. sam_stat DB 생성 (개발 서버 MySQL 직접 접속) -mysql -u [user] -p \ - -e "CREATE DATABASE sam_stat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" - -# 2. .env에 STAT_DB_* 환경변수 추가 (개발 서버의 api/.env) -# STAT_DB_HOST=127.0.0.1 -# STAT_DB_PORT=3306 -# STAT_DB_DATABASE=sam_stat -# STAT_DB_USERNAME=[개발서버 DB 유저] -# STAT_DB_PASSWORD=[개발서버 DB 비밀번호] - -# 3. 마이그레이션 실행 -cd /path/to/api && php artisan migrate --database=sam_stat - -# 4. dim_date 시딩 -php artisan db:seed --class=DimDateSeeder - -# 5. 스케줄러 cron 확인 (이미 등록되어 있다면 추가 불필요) -# * * * * * cd /path/to/api && php artisan schedule:run >> /dev/null 2>&1 -``` - -#### 배포 워크플로우 - -``` -로컬 (Docker, *.sam.kr) - ↓ Git push -개발 서버 (non-Docker, codebridge-x.com) - ↓ 수동 배포 - ↓ 최초 1회: DB 생성 + .env + migrate + seed + cron 확인 - ↓ 이후: git pull → php artisan migrate --database=sam_stat -운영 (TBD) -``` - -**코드에 커밋되는 것:** `config/database.php`, 마이그레이션, 모델, 서비스, 커맨드 -**환경별 수동 설정:** `.env` (STAT_DB_*), DB 생성, cron - -### 0.8 핵심 코딩 규칙 (이 작업에 적용) - -1. **Service-First**: 비즈니스 로직 → Service, Controller는 DI + 호출만 -2. **FormRequest**: Controller에서 직접 검증 금지 -3. **BelongsToTenant**: 원본 모델만 적용, 통계 모델은 tenant_id WHERE 직접 사용 -4. **i18n**: 메시지는 `__('message.xxx')` 형태 -5. **ApiResponse**: `use App\Helpers\ApiResponse;` → `ApiResponse::handle()` -6. **Swagger**: 별도 파일 `api/app/Swagger/v1/{Resource}Api.php`에 작성 -7. **커밋**: 사용자 승인 후에만 커밋 (자동 커밋 금지) - -### 0.9 작업 시작 체크리스트 - -``` -새 세션에서 이 문서를 받았을 때: - -□ 1. 이 문서의 "📍 현재 진행 상태" 확인 -□ 2. Phase별 작업 상태 (⏳/🔄/✅) 확인 -□ 3. Docker 실행 확인: docker compose ps (docker/ 디렉토리) -□ 4. DB 접속 확인: docker compose exec mysql mysql -u root -proot samdb -□ 5. sam_stat DB 존재 여부 확인: SHOW DATABASES LIKE 'sam_stat'; -□ 6. 마이그레이션 상태 확인: cd api && php artisan migrate:status -□ 7. 다음 작업 항목의 "비고" 컬럼 참조하여 작업 시작 -``` - ---- - -## 1. 개요 - -### 1.1 배경 - -SAM ERP는 219개 테이블, 17개 비즈니스 도메인을 가진 종합 제조/건설 ERP 시스템이다. -현재 대시보드(DashboardService, ReportService 등)는 **원본 DB(samdb)에서 실시간 집계**하는 방식으로 동작한다. - -**문제점:** -- 원본 DB에 집계 쿼리 부하 (JOIN, GROUP BY, SUM 등) -- 과거 데이터 추세 분석 불가 (스냅샷 없음) -- 도메인별 KPI 누적 관리 불가 -- 대시보드 응답 속도 저하 가능성 -- 통계 요구사항 증가 시 원본 스키마 오염 - -**해결 방안:** -- `sam_stat` 별도 DB에 사전 집계(pre-aggregated) 통계 데이터 저장 -- 배치/스케줄러로 원본(samdb) → 통계(sam_stat) DB 동기화 -- 원본 DB 부하 분리, 빠른 조회, 이력 보존 - -### 1.2 설계 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 원본 DB 무간섭 - sam_stat은 읽기 전용 파생 데이터 │ -│ 2. 멀티테넌트 유지 - 모든 통계 테이블에 tenant_id 필수 │ -│ 3. 시간축 기반 - 일/주/월/분기/년 단위 집계 지원 │ -│ 4. 확장 가능 - 새 도메인 통계 추가 시 테이블만 추가 │ -│ 5. 멱등성 보장 - 같은 기간 재집계 시 동일 결과 (UPSERT) │ -│ 6. 메타데이터 드리븐 - stat_definitions로 동적 통계 정의 가능 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 통계 필드 추가, 집계 주기 변경, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | 새 통계 테이블 생성, 스케줄러 추가, 마이그레이션 | **필수** | -| 🔴 금지 | 원본 DB 스키마 변경, 원본 테이블에 통계 컬럼 추가 | 별도 협의 | - ---- - -## 2. 분석: 필요한 통계 도메인 - -SAM의 17개 비즈니스 도메인을 분석하여 8개 핵심 통계 영역을 도출했다. - -### 2.1 통계 도메인 매핑 - -| # | 통계 도메인 | 원본 테이블 | 핵심 지표 | 우선순위 | -|---|-----------|-----------|----------|---------| -| 1 | **매출/수주** | orders, order_items, sales, clients | 수주액, 매출액, 수주건수, 고객별 매출 | 🔴 P0 | -| 2 | **재무/회계** | deposits, withdrawals, purchases, bills, bank_transactions | 입출금, 미수/미지급, 자금흐름, 어음현황 | 🔴 P0 | -| 3 | **생산/작업** | work_orders, work_order_items, work_results | 생산량, 작업효율, 불량률, 납기준수율 | 🔴 P0 | -| 4 | **재고/자재** | stocks, stock_transactions, material_receipts, shipments | 재고회전율, 입출고량, 안전재고, 로트추적 | 🟡 P1 | -| 5 | **견적/영업** | quotes, quote_items, sales_prospects, biddings | 수주전환율, 견적성공률, 영업파이프라인 | 🟡 P1 | -| 6 | **인사/근태** | attendance, leaves, payrolls, salaries | 출근율, 근태현황, 인건비, 부서별통계 | 🟡 P1 | -| 7 | **건설/프로젝트** | sites, contracts, expected_expenses, labor_distributions | 프로젝트수익률, 공정진행률, 원가분석 | 🟢 P2 | -| 8 | **시스템/감사** | audit_logs, api_request_logs, fcm_send_logs | API사용량, 사용자활동, 알림발송률 | 🟢 P2 | - ---- - -## 3. sam_stat 데이터베이스 설계 - -### 3.1 아키텍처 개요 - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ sam_stat DB │ -├──────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ -│ │ 메타 테이블 (2) │ │ 이벤트/팩트 테이블 (2) │ │ -│ │ │ │ │ │ -│ │ stat_definitions │ │ stat_events │ │ -│ │ stat_job_logs │ │ stat_snapshots │ │ -│ └─────────────────────┘ └─────────────────────────────────┘ │ -│ │ -│ ┌───────────────────────────────────────────────────────────┐ │ -│ │ 도메인별 집계 테이블 (8 도메인) │ │ -│ │ │ │ -│ │ stat_sales_daily stat_inventory_daily │ │ -│ │ stat_finance_daily stat_quote_pipeline_daily │ │ -│ │ stat_production_daily stat_hr_attendance_daily │ │ -│ │ stat_project_monthly stat_system_daily │ │ -│ │ │ │ -│ │ 요약 테이블 (월간/연간) │ │ -│ │ │ │ -│ │ stat_sales_monthly stat_finance_monthly │ │ -│ │ stat_production_monthly stat_kpi_monthly │ │ -│ │ │ │ -│ └───────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ -│ │ 차원 테이블 (Dim) │ │ KPI/알림 테이블 │ │ -│ │ │ │ │ │ -│ │ dim_date │ │ stat_kpi_targets │ │ -│ │ dim_client │ │ stat_alerts │ │ -│ │ dim_product │ │ │ │ -│ └─────────────────────┘ └─────────────────────────────────┘ │ -│ │ -│ 총 테이블: 18개 │ -└──────────────────────────────────────────────────────────────────┘ -``` - -### 3.2 데이터 흐름 - -``` -samdb (원본) sam_stat (통계) -┌──────────┐ ┌──────────────┐ -│ orders │──┐ │ │ -│ sales │──┤ Scheduler │ stat_sales_ │ -│ deposits │──┼──(매일 02:00)──→│ daily │ -│ stocks │──┤ │ │ -│ work_ │──┤ │ stat_finance_│ -│ orders │──┘ │ daily │ -│ │ │ │ -│ │ Scheduler │ stat_*_ │ -│ │──(매월 1일)──────→│ monthly │ -│ │ │ │ -│ │ 실시간 이벤트 │ stat_events │ -│ │──(Observer)─────→│ │ -└──────────┘ └──────────────┘ -``` - ---- - -## 4. 테이블 상세 설계 - -### 4.1 메타 테이블 - -#### `stat_definitions` - 통계 정의 (메타데이터 드리븐) - -```sql -CREATE TABLE stat_definitions ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - code VARCHAR(100) NOT NULL UNIQUE, -- 'sales_daily_revenue' - domain VARCHAR(50) NOT NULL, -- 'sales', 'finance', 'production' - name VARCHAR(200) NOT NULL, -- '일일 매출액' - description TEXT NULL, - source_tables JSON NOT NULL, -- ["orders", "order_items", "sales"] - aggregation VARCHAR(20) NOT NULL DEFAULT 'daily', -- daily, weekly, monthly, quarterly, yearly - query_template TEXT NULL, -- 집계 SQL 템플릿 (선택) - is_active BOOLEAN NOT NULL DEFAULT TRUE, - config JSON NULL, -- 추가 설정 (임계값, 단위 등) - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - INDEX idx_domain (domain), - INDEX idx_aggregation (aggregation), - INDEX idx_active (is_active) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_job_logs` - 집계 작업 이력 - -```sql -CREATE TABLE stat_job_logs ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - job_type VARCHAR(100) NOT NULL, -- 'sales_daily', 'finance_monthly' - target_date DATE NOT NULL, -- 집계 대상 날짜 - status ENUM('pending','running','completed','failed') NOT NULL DEFAULT 'pending', - records_processed INT UNSIGNED DEFAULT 0, - error_message TEXT NULL, - started_at TIMESTAMP NULL, - completed_at TIMESTAMP NULL, - duration_ms INT UNSIGNED NULL, - created_at TIMESTAMP NULL, - - INDEX idx_tenant_job (tenant_id, job_type), - INDEX idx_status (status), - INDEX idx_target_date (target_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.2 차원 테이블 (Dimension) - -#### `dim_date` - 날짜 차원 - -```sql -CREATE TABLE dim_date ( - date_key DATE PRIMARY KEY, -- '2026-01-29' - year SMALLINT NOT NULL, - quarter TINYINT NOT NULL, -- 1~4 - month TINYINT NOT NULL, - week TINYINT NOT NULL, -- ISO week - day_of_week TINYINT NOT NULL, -- 1(월)~7(일) - day_of_month TINYINT NOT NULL, - is_weekend BOOLEAN NOT NULL, - is_holiday BOOLEAN NOT NULL DEFAULT FALSE, - holiday_name VARCHAR(100) NULL, - fiscal_year SMALLINT NULL, -- 회계연도 - fiscal_quarter TINYINT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `dim_client` - 고객 차원 (스냅샷) - -```sql -CREATE TABLE dim_client ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - client_id BIGINT UNSIGNED NOT NULL, -- 원본 clients.id - client_name VARCHAR(200) NOT NULL, - client_group_id BIGINT UNSIGNED NULL, - client_group_name VARCHAR(200) NULL, - client_type VARCHAR(50) NULL, -- 고객/공급업체/양쪽 - region VARCHAR(100) NULL, - valid_from DATE NOT NULL, - valid_to DATE NULL, -- NULL = 현재 유효 - is_current BOOLEAN NOT NULL DEFAULT TRUE, - - INDEX idx_tenant_client (tenant_id, client_id), - INDEX idx_current (is_current) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `dim_product` - 제품 차원 (스냅샷) - -```sql -CREATE TABLE dim_product ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - product_id BIGINT UNSIGNED NOT NULL, -- 원본 products.id - product_code VARCHAR(100) NOT NULL, - product_name VARCHAR(300) NOT NULL, - product_type VARCHAR(50) NULL, -- PRODUCT/PART/SUBASSEMBLY - category_id BIGINT UNSIGNED NULL, - category_name VARCHAR(200) NULL, - valid_from DATE NOT NULL, - valid_to DATE NULL, - is_current BOOLEAN NOT NULL DEFAULT TRUE, - - INDEX idx_tenant_product (tenant_id, product_id), - INDEX idx_current (is_current) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.3 도메인별 집계 테이블 (Fact) - -#### 🔴 P0: `stat_sales_daily` - 매출/수주 일일 통계 - -```sql -CREATE TABLE stat_sales_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 수주 - order_count INT UNSIGNED DEFAULT 0, -- 신규 수주 건수 - order_amount DECIMAL(18,2) DEFAULT 0, -- 수주 금액 - order_item_count INT UNSIGNED DEFAULT 0, -- 수주 품목 수 - - -- 매출 - sales_count INT UNSIGNED DEFAULT 0, -- 매출 건수 - sales_amount DECIMAL(18,2) DEFAULT 0, -- 매출 금액 - sales_tax_amount DECIMAL(18,2) DEFAULT 0, -- 세액 - - -- 고객 - new_client_count INT UNSIGNED DEFAULT 0, -- 신규 고객 수 - active_client_count INT UNSIGNED DEFAULT 0, -- 활성 고객 수 - - -- 수주 상태별 건수 - order_draft_count INT UNSIGNED DEFAULT 0, - order_confirmed_count INT UNSIGNED DEFAULT 0, - order_in_progress_count INT UNSIGNED DEFAULT 0, - order_completed_count INT UNSIGNED DEFAULT 0, - order_cancelled_count INT UNSIGNED DEFAULT 0, - - -- 출하 - shipment_count INT UNSIGNED DEFAULT 0, - shipment_amount DECIMAL(18,2) DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date), - INDEX idx_tenant (tenant_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🔴 P0: `stat_finance_daily` - 재무 일일 통계 - -```sql -CREATE TABLE stat_finance_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 입출금 - deposit_count INT UNSIGNED DEFAULT 0, - deposit_amount DECIMAL(18,2) DEFAULT 0, - withdrawal_count INT UNSIGNED DEFAULT 0, - withdrawal_amount DECIMAL(18,2) DEFAULT 0, - net_cashflow DECIMAL(18,2) DEFAULT 0, -- 입금 - 출금 - - -- 매입 - purchase_count INT UNSIGNED DEFAULT 0, - purchase_amount DECIMAL(18,2) DEFAULT 0, - purchase_tax_amount DECIMAL(18,2) DEFAULT 0, - - -- 미수/미지급 - receivable_balance DECIMAL(18,2) DEFAULT 0, -- 미수금 잔액 - payable_balance DECIMAL(18,2) DEFAULT 0, -- 미지급 잔액 - overdue_receivable DECIMAL(18,2) DEFAULT 0, -- 연체 미수금 - - -- 어음 - bill_issued_count INT UNSIGNED DEFAULT 0, - bill_issued_amount DECIMAL(18,2) DEFAULT 0, - bill_matured_count INT UNSIGNED DEFAULT 0, - bill_matured_amount DECIMAL(18,2) DEFAULT 0, - - -- 카드 - card_transaction_count INT UNSIGNED DEFAULT 0, - card_transaction_amount DECIMAL(18,2) DEFAULT 0, - - -- 은행 - bank_balance_total DECIMAL(18,2) DEFAULT 0, -- 전 계좌 잔액 합계 - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🔴 P0: `stat_production_daily` - 생산 일일 통계 - -```sql -CREATE TABLE stat_production_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 작업지시 - wo_created_count INT UNSIGNED DEFAULT 0, -- 신규 작업지시 - wo_completed_count INT UNSIGNED DEFAULT 0, -- 완료 작업지시 - wo_in_progress_count INT UNSIGNED DEFAULT 0, -- 진행중 - wo_overdue_count INT UNSIGNED DEFAULT 0, -- 납기 초과 - - -- 생산량 - production_qty DECIMAL(18,2) DEFAULT 0, -- 생산 수량 - defect_qty DECIMAL(18,2) DEFAULT 0, -- 불량 수량 - defect_rate DECIMAL(5,2) DEFAULT 0, -- 불량률 (%) - - -- 작업 효율 - planned_hours DECIMAL(10,2) DEFAULT 0, -- 계획 공수 - actual_hours DECIMAL(10,2) DEFAULT 0, -- 실적 공수 - efficiency_rate DECIMAL(5,2) DEFAULT 0, -- 효율 (%) - - -- 작업자 - active_worker_count INT UNSIGNED DEFAULT 0, - issue_count INT UNSIGNED DEFAULT 0, -- 발생 이슈 수 - - -- 납기 - on_time_delivery_count INT UNSIGNED DEFAULT 0, - late_delivery_count INT UNSIGNED DEFAULT 0, - delivery_rate DECIMAL(5,2) DEFAULT 0, -- 납기준수율 (%) - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟡 P1: `stat_inventory_daily` - 재고 일일 통계 - -```sql -CREATE TABLE stat_inventory_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 재고 현황 - total_sku_count INT UNSIGNED DEFAULT 0, -- 총 SKU 수 - total_stock_qty DECIMAL(18,2) DEFAULT 0, -- 총 재고 수량 - total_stock_value DECIMAL(18,2) DEFAULT 0, -- 총 재고 금액 - - -- 입출고 - receipt_count INT UNSIGNED DEFAULT 0, -- 입고 건수 - receipt_qty DECIMAL(18,2) DEFAULT 0, - receipt_amount DECIMAL(18,2) DEFAULT 0, - issue_count INT UNSIGNED DEFAULT 0, -- 출고 건수 - issue_qty DECIMAL(18,2) DEFAULT 0, - issue_amount DECIMAL(18,2) DEFAULT 0, - - -- 안전재고 - below_safety_count INT UNSIGNED DEFAULT 0, -- 안전재고 미달 품목 수 - zero_stock_count INT UNSIGNED DEFAULT 0, -- 재고 0 품목 수 - excess_stock_count INT UNSIGNED DEFAULT 0, -- 과잉 재고 품목 수 - - -- 품질검사 - inspection_count INT UNSIGNED DEFAULT 0, - inspection_pass_count INT UNSIGNED DEFAULT 0, - inspection_fail_count INT UNSIGNED DEFAULT 0, - inspection_pass_rate DECIMAL(5,2) DEFAULT 0, -- 합격률 (%) - - -- 재고회전 - turnover_rate DECIMAL(8,2) DEFAULT 0, -- 재고회전율 - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟡 P1: `stat_quote_pipeline_daily` - 견적/영업 일일 통계 - -```sql -CREATE TABLE stat_quote_pipeline_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 견적 - quote_created_count INT UNSIGNED DEFAULT 0, - quote_amount DECIMAL(18,2) DEFAULT 0, - quote_approved_count INT UNSIGNED DEFAULT 0, - quote_rejected_count INT UNSIGNED DEFAULT 0, - quote_conversion_count INT UNSIGNED DEFAULT 0, -- 수주 전환 건수 - quote_conversion_rate DECIMAL(5,2) DEFAULT 0, -- 전환율 (%) - - -- 영업 기회 - prospect_created_count INT UNSIGNED DEFAULT 0, - prospect_won_count INT UNSIGNED DEFAULT 0, - prospect_lost_count INT UNSIGNED DEFAULT 0, - prospect_amount DECIMAL(18,2) DEFAULT 0, -- 파이프라인 금액 - - -- 입찰 - bidding_count INT UNSIGNED DEFAULT 0, - bidding_won_count INT UNSIGNED DEFAULT 0, - bidding_amount DECIMAL(18,2) DEFAULT 0, - - -- 상담 - consultation_count INT UNSIGNED DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟡 P1: `stat_hr_attendance_daily` - 인사/근태 일일 통계 - -```sql -CREATE TABLE stat_hr_attendance_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- 근태 - total_employees INT UNSIGNED DEFAULT 0, -- 전체 직원 수 - attendance_count INT UNSIGNED DEFAULT 0, -- 출근 인원 - late_count INT UNSIGNED DEFAULT 0, -- 지각 - absent_count INT UNSIGNED DEFAULT 0, -- 결근 - attendance_rate DECIMAL(5,2) DEFAULT 0, -- 출근율 (%) - - -- 휴가 - leave_count INT UNSIGNED DEFAULT 0, -- 휴가 사용 - leave_annual_count INT UNSIGNED DEFAULT 0, -- 연차 - leave_sick_count INT UNSIGNED DEFAULT 0, -- 병가 - leave_other_count INT UNSIGNED DEFAULT 0, -- 기타 - - -- 초과근무 - overtime_hours DECIMAL(10,2) DEFAULT 0, - overtime_employee_count INT UNSIGNED DEFAULT 0, - - -- 인건비 (급여 정산 기준) - total_labor_cost DECIMAL(18,2) DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟢 P2: `stat_project_monthly` - 건설/프로젝트 월간 통계 - -```sql -CREATE TABLE stat_project_monthly ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NOT NULL, - - -- 프로젝트 현황 - active_site_count INT UNSIGNED DEFAULT 0, - completed_site_count INT UNSIGNED DEFAULT 0, - new_contract_count INT UNSIGNED DEFAULT 0, - contract_total_amount DECIMAL(18,2) DEFAULT 0, - - -- 원가 - expected_expense_total DECIMAL(18,2) DEFAULT 0, - actual_expense_total DECIMAL(18,2) DEFAULT 0, - labor_cost_total DECIMAL(18,2) DEFAULT 0, - material_cost_total DECIMAL(18,2) DEFAULT 0, - - -- 수익률 - gross_profit DECIMAL(18,2) DEFAULT 0, - gross_profit_rate DECIMAL(5,2) DEFAULT 0, -- 수익률 (%) - - -- 이슈 - handover_report_count INT UNSIGNED DEFAULT 0, - structure_review_count INT UNSIGNED DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), - INDEX idx_year_month (stat_year, stat_month) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### 🟢 P2: `stat_system_daily` - 시스템 일일 통계 - -```sql -CREATE TABLE stat_system_daily ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_date DATE NOT NULL, - - -- API 사용량 - api_request_count INT UNSIGNED DEFAULT 0, - api_error_count INT UNSIGNED DEFAULT 0, - api_avg_response_ms INT UNSIGNED DEFAULT 0, - - -- 사용자 활동 - active_user_count INT UNSIGNED DEFAULT 0, - login_count INT UNSIGNED DEFAULT 0, - - -- 감사 - audit_create_count INT UNSIGNED DEFAULT 0, - audit_update_count INT UNSIGNED DEFAULT 0, - audit_delete_count INT UNSIGNED DEFAULT 0, - - -- 알림 - fcm_sent_count INT UNSIGNED DEFAULT 0, - fcm_failed_count INT UNSIGNED DEFAULT 0, - - -- 파일 - file_upload_count INT UNSIGNED DEFAULT 0, - file_upload_size_mb DECIMAL(10,2) DEFAULT 0, - - -- 결재 - approval_submitted_count INT UNSIGNED DEFAULT 0, - approval_completed_count INT UNSIGNED DEFAULT 0, - approval_avg_hours DECIMAL(8,2) DEFAULT 0, -- 평균 처리 시간 - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date (tenant_id, stat_date), - INDEX idx_date (stat_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.4 월간 요약 테이블 - -#### `stat_sales_monthly` - 매출 월간 요약 - -```sql -CREATE TABLE stat_sales_monthly ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NOT NULL, - - -- 일일 합산 - order_count INT UNSIGNED DEFAULT 0, - order_amount DECIMAL(18,2) DEFAULT 0, - sales_count INT UNSIGNED DEFAULT 0, - sales_amount DECIMAL(18,2) DEFAULT 0, - shipment_count INT UNSIGNED DEFAULT 0, - shipment_amount DECIMAL(18,2) DEFAULT 0, - - -- 월간 고유 지표 - unique_client_count INT UNSIGNED DEFAULT 0, -- 거래 고객 수 - avg_order_amount DECIMAL(18,2) DEFAULT 0, -- 평균 수주 금액 - top_client_id BIGINT UNSIGNED NULL, -- 최다 거래 고객 - top_client_amount DECIMAL(18,2) DEFAULT 0, - mom_growth_rate DECIMAL(8,2) NULL, -- 전월 대비 성장률 (%) - yoy_growth_rate DECIMAL(8,2) NULL, -- 전년동월 대비 (%) - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), - INDEX idx_year_month (stat_year, stat_month) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_finance_monthly` - 재무 월간 요약 - -```sql -CREATE TABLE stat_finance_monthly ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NOT NULL, - - deposit_total DECIMAL(18,2) DEFAULT 0, - withdrawal_total DECIMAL(18,2) DEFAULT 0, - net_cashflow DECIMAL(18,2) DEFAULT 0, - purchase_total DECIMAL(18,2) DEFAULT 0, - card_total DECIMAL(18,2) DEFAULT 0, - - receivable_end DECIMAL(18,2) DEFAULT 0, -- 월말 미수금 - payable_end DECIMAL(18,2) DEFAULT 0, -- 월말 미지급 - bank_balance_end DECIMAL(18,2) DEFAULT 0, -- 월말 잔액 - - mom_cashflow_change DECIMAL(8,2) NULL, -- 전월 대비 현금흐름 변화 (%) - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), - INDEX idx_year_month (stat_year, stat_month) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_production_monthly` - 생산 월간 요약 - -```sql -CREATE TABLE stat_production_monthly ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NOT NULL, - - wo_total_count INT UNSIGNED DEFAULT 0, - wo_completed_count INT UNSIGNED DEFAULT 0, - production_qty DECIMAL(18,2) DEFAULT 0, - defect_qty DECIMAL(18,2) DEFAULT 0, - avg_defect_rate DECIMAL(5,2) DEFAULT 0, - avg_efficiency_rate DECIMAL(5,2) DEFAULT 0, - avg_delivery_rate DECIMAL(5,2) DEFAULT 0, - total_planned_hours DECIMAL(10,2) DEFAULT 0, - total_actual_hours DECIMAL(10,2) DEFAULT 0, - issue_total_count INT UNSIGNED DEFAULT 0, - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_year_month (tenant_id, stat_year, stat_month), - INDEX idx_year_month (stat_year, stat_month) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.5 KPI/알림 테이블 - -#### `stat_kpi_targets` - KPI 목표 설정 - -```sql -CREATE TABLE stat_kpi_targets ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - stat_year SMALLINT NOT NULL, - stat_month TINYINT NULL, -- NULL = 연간 목표 - - domain VARCHAR(50) NOT NULL, -- 'sales', 'production' - metric_code VARCHAR(100) NOT NULL, -- 'monthly_sales_amount' - target_value DECIMAL(18,2) NOT NULL, - unit VARCHAR(20) NOT NULL DEFAULT 'KRW', -- KRW, %, count, hours - description VARCHAR(300) NULL, - - created_by BIGINT UNSIGNED NULL, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_metric (tenant_id, stat_year, stat_month, metric_code), - INDEX idx_domain (domain) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_alerts` - 통계 기반 알림 - -```sql -CREATE TABLE stat_alerts ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - domain VARCHAR(50) NOT NULL, - alert_type VARCHAR(100) NOT NULL, -- 'below_target', 'anomaly', 'threshold' - severity ENUM('info','warning','critical') NOT NULL DEFAULT 'info', - title VARCHAR(300) NOT NULL, - message TEXT NOT NULL, - metric_code VARCHAR(100) NULL, - current_value DECIMAL(18,2) NULL, - threshold_value DECIMAL(18,2) NULL, - is_read BOOLEAN NOT NULL DEFAULT FALSE, - is_resolved BOOLEAN NOT NULL DEFAULT FALSE, - resolved_at TIMESTAMP NULL, - resolved_by BIGINT UNSIGNED NULL, - created_at TIMESTAMP NULL, - - INDEX idx_tenant_unread (tenant_id, is_read), - INDEX idx_severity (severity), - INDEX idx_domain (domain) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -### 4.6 이벤트/스냅샷 테이블 - -#### `stat_events` - 실시간 이벤트 로그 (확장용) - -```sql -CREATE TABLE stat_events ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - domain VARCHAR(50) NOT NULL, - event_type VARCHAR(100) NOT NULL, -- 'order_created', 'payment_received' - entity_type VARCHAR(100) NOT NULL, -- 'Order', 'Deposit' - entity_id BIGINT UNSIGNED NOT NULL, - payload JSON NULL, -- 이벤트 데이터 - occurred_at TIMESTAMP NOT NULL, - - INDEX idx_tenant_domain (tenant_id, domain), - INDEX idx_occurred (occurred_at), - INDEX idx_entity (entity_type, entity_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - -#### `stat_snapshots` - 상태 스냅샷 (특정 시점 전체 상태) - -```sql -CREATE TABLE stat_snapshots ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - snapshot_date DATE NOT NULL, - domain VARCHAR(50) NOT NULL, - snapshot_type VARCHAR(50) NOT NULL DEFAULT 'daily', -- daily, weekly, monthly - data JSON NOT NULL, -- 전체 스냅샷 데이터 - created_at TIMESTAMP NULL, - - UNIQUE KEY uk_tenant_date_domain (tenant_id, snapshot_date, domain, snapshot_type), - INDEX idx_date (snapshot_date) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -``` - ---- - -## 5. 테이블 요약 - -| # | 테이블명 | 유형 | 도메인 | 집계 주기 | 우선순위 | -|---|---------|------|--------|----------|---------| -| 1 | `stat_definitions` | 메타 | 공통 | - | 🔴 P0 | -| 2 | `stat_job_logs` | 메타 | 공통 | - | 🔴 P0 | -| 3 | `dim_date` | 차원 | 공통 | 1회 생성 | 🔴 P0 | -| 4 | `dim_client` | 차원 | 공통 | SCD Type 2 | 🟡 P1 | -| 5 | `dim_product` | 차원 | 공통 | SCD Type 2 | 🟡 P1 | -| 6 | `stat_sales_daily` | 팩트 | 매출/수주 | 일간 | 🔴 P0 | -| 7 | `stat_finance_daily` | 팩트 | 재무/회계 | 일간 | 🔴 P0 | -| 8 | `stat_production_daily` | 팩트 | 생산/작업 | 일간 | 🔴 P0 | -| 9 | `stat_inventory_daily` | 팩트 | 재고/자재 | 일간 | 🟡 P1 | -| 10 | `stat_quote_pipeline_daily` | 팩트 | 견적/영업 | 일간 | 🟡 P1 | -| 11 | `stat_hr_attendance_daily` | 팩트 | 인사/근태 | 일간 | 🟡 P1 | -| 12 | `stat_project_monthly` | 팩트 | 건설/프로젝트 | 월간 | 🟢 P2 | -| 13 | `stat_system_daily` | 팩트 | 시스템/감사 | 일간 | 🟢 P2 | -| 14 | `stat_sales_monthly` | 요약 | 매출/수주 | 월간 | 🔴 P0 | -| 15 | `stat_finance_monthly` | 요약 | 재무/회계 | 월간 | 🔴 P0 | -| 16 | `stat_production_monthly` | 요약 | 생산/작업 | 월간 | 🔴 P0 | -| 17 | `stat_kpi_targets` | KPI | 공통 | 수동 설정 | 🟡 P1 | -| 18 | `stat_alerts` | 알림 | 공통 | 실시간 | 🟡 P1 | -| 19 | `stat_events` | 이벤트 | 공통 | 실시간 | 🟢 P2 | -| 20 | `stat_snapshots` | 스냅샷 | 공통 | 일/월 | 🟢 P2 | - -**총 20개 테이블** (메타 2 + 차원 3 + 일간팩트 6 + 월간팩트 1 + 월간요약 3 + KPI/알림 2 + 이벤트/스냅샷 2 + 시스템 1) - ---- - -## 6. 구현 계획 (Phase) - -### Phase 1: 인프라 구축 (P0) -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 1.1 | sam_stat DB 생성 및 Laravel 연결 설정 | ✅ | ① Docker MySQL에 `CREATE DATABASE sam_stat` 실행 ② `api/config/database.php`에 `sam_stat` 연결 추가 ③ `api/.env`에 `STAT_DB_*` 환경변수 추가 | -| 1.2 | 메타 테이블 마이그레이션 | ✅ | `stat_definitions`, `stat_job_logs` 마이그레이션 생성 (`--database=sam_stat` 옵션) | -| 1.3 | dim_date 테이블 생성 및 시딩 | ✅ | 2020-01-01~2030-12-31 날짜 데이터 Seeder 작성 (4,018건) | -| 1.4 | 기반 모델 클래스 생성 | ✅ | `BaseStatModel`, `StatDefinition`, `StatJobLog`, `DimDate` 생성 | -| 1.5 | 집계 커맨드 기반 구조 | ✅ | `StatAggregateDailyCommand.php`, `StatAggregateMonthlyCommand.php` 생성 | -| 1.6 | StatAggregatorService 골격 | ✅ | `StatAggregatorService.php` + `StatDomainServiceInterface.php` - 테넌트 순회 + 도메인별 서비스 호출 구조 | - -**Phase 1 검증 방법:** -```bash -# DB 생성 확인 -docker compose exec mysql mysql -u root -proot -e "SHOW DATABASES LIKE 'sam_stat';" - -# 마이그레이션 실행 -cd api && php artisan migrate --database=sam_stat - -# dim_date 시딩 -cd api && php artisan db:seed --class=DimDateSeeder - -# 커맨드 확인 -cd api && php artisan stat:aggregate-daily --help -``` - -### Phase 2: P0 도메인 구축 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 2.1 | 매출 테이블 마이그레이션 | ✅ | `stat_sales_daily` + `stat_sales_monthly` 마이그레이션 | -| 2.2 | 매출 모델 + 서비스 | ✅ | `StatSalesDaily`, `StatSalesMonthly`, `SalesStatService` - orders, sales, clients, shipments 집계 | -| 2.3 | 재무 테이블 마이그레이션 | ✅ | `stat_finance_daily` + `stat_finance_monthly` 마이그레이션 | -| 2.4 | 재무 모델 + 서비스 | ✅ | `StatFinanceDaily`, `StatFinanceMonthly`, `FinanceStatService` - deposits, withdrawals, purchases, bills, bank_transactions 집계 | -| 2.5 | 생산 테이블 마이그레이션 | ✅ | `stat_production_daily` + `stat_production_monthly` 마이그레이션 | -| 2.6 | 생산 모델 + 서비스 | ✅ | `StatProductionDaily`, `StatProductionMonthly`, `ProductionStatService` - work_orders, work_results 집계 | -| 2.7 | 스케줄러 등록 | ✅ | `console.php`에 `stat:aggregate-daily` (02:00), `stat:aggregate-monthly` (매월 1일 03:00) 등록 | - -**Phase 2 검증 방법:** -```bash -# 수동 집계 실행 (특정 날짜) -cd api && php artisan stat:aggregate-daily --date=2026-01-28 - -# 데이터 확인 -docker compose exec mysql mysql -u root -proot sam_stat \ - -e "SELECT * FROM stat_sales_daily WHERE stat_date='2026-01-28';" -``` - -### Phase 3: P1 도메인 확장 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 3.1 | 차원 테이블 | ✅ | `dim_client`, `dim_product` 마이그레이션 + 모델 + `DimensionSyncService` (SCD Type 2). 원본: `clients`→`dim_client`, `items`→`dim_product` (products 테이블 없어 items 사용) | -| 3.2 | 재고 통계 | ✅ | `stat_inventory_daily` 마이그레이션 + 모델 + `InventoryStatService` - 원본: `stocks`, `stock_transactions`, `inspections` | -| 3.3 | 견적/영업 통계 | ✅ | `stat_quote_pipeline_daily` 마이그레이션 + 모델 + `QuoteStatService` - 원본: `quotes`, `sales_prospects`, `biddings`, `sales_prospect_consultations` | -| 3.4 | 인사/근태 통계 | ✅ | `stat_hr_attendance_daily` 마이그레이션 + 모델 + `HrStatService` - 원본: `attendances`, `leaves`, `user_tenants` | -| 3.5 | KPI/알림 | ✅ | `stat_kpi_targets`, `stat_alerts` 마이그레이션 + 모델 + `KpiAlertService` + `StatCheckKpiAlertsCommand` + 스케줄러 09:00 | - -### Phase 4: P2 도메인 + API + 대시보드 전환 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 4.1 | 건설/프로젝트 통계 | ✅ | `stat_project_monthly` 마이그레이션 + 모델 + `ProjectStatService` - 원본: `sites`, `contracts`, `expected_expenses`. 월간 전용 도메인 | -| 4.2 | 시스템 통계 | ✅ | `stat_system_daily` 마이그레이션 + 모델 + `SystemStatService` - 원본: `api_request_logs`, `personal_access_tokens`(user_tenants 조인), `audit_logs`, `fcm_send_logs`, `files`, `approvals` | -| 4.3 | 이벤트/스냅샷 | ✅ | `stat_events`, `stat_snapshots` 마이그레이션 + 모델 + `StatEventService` + `StatEventObserver` (Order, Sale, Deposit, Withdrawal, Purchase, Approval에 등록) | -| 4.4 | 통계 API | ✅ | `StatController` (summary/daily/monthly/alerts) + `StatQueryService` + FormRequest 3개 + `routes/api/v1/stats.php`. Swagger는 Phase 5에서 추가 | -| 4.5 | 대시보드 전환 | ✅ | `DashboardService` getFinanceSummary/getSalesSummary → sam_stat 우선 조회 + 원본 DB 폴백. 응답에 `source` 필드 추가 | - -### Phase 5: 최적화 및 안정화 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 5.1 | 백필 스크립트 | ✅ | `StatBackfillCommand` - `stat:backfill --from= --to= --domain= --tenant= --skip-monthly --skip-dimensions`. CarbonPeriod 일간 순회 + 월간 집계 + 프로그레스바 + 에러 리포트. 테스트: 7도메인 0.2초 | -| 5.2 | 정합성 검증 | ✅ | `StatVerifyCommand` - `stat:verify --date= --tenant= --domain= --fix`. sales(수주건수/매출금액), finance(입금액/출금액), system(API요청수/감사로그수) 교차 검증. --fix 시 자동 재집계. 테스트: 6건 전부 일치 | -| 5.3 | 파티셔닝 준비 | ✅ | `2026_01_29_300001_prepare_partitioning_daily_tables.php` - 7개 일간 테이블 RANGE COLUMNS(stat_date) 파티셔닝. PK에 stat_date 포함, p2024~p2028 + p_future. 기존 파티션 여부 체크 후 스킵 | -| 5.4 | Redis 캐싱 | ✅ | `StatQueryService` - Cache::remember TTL 5분. 키 패턴: `stat:{daily\|monthly\|dashboard}:{tenantId}:...`. `invalidateCache()` 정적 메서드: Redis keys 패턴 매칭 삭제. 집계 완료 시 StatAggregatorService에서 자동 호출 | -| 5.5 | 모니터링 알림 | ✅ | `StatMonitorService` - recordAggregationFailure(critical), recordMissingData(warning), recordMismatch(critical), resolveAlerts(). StatAggregatorService catch 블록에서 자동 호출. stat_alerts 테이블 연동 검증 완료 | - -### Phase 6: 문서화 및 마무리 -| # | 작업 항목 | 상태 | 구체적 작업 내용 | -|---|----------|:----:|-----------------| -| 6.1 | Swagger API 문서 | ✅ | `app/Swagger/v1/StatApi.php` - Stats 태그, 4개 엔드포인트 (summary/daily/monthly/alerts), StatSalesDaily/StatFinanceDaily/StatDashboardSummary/StatAlert 스키마 정의. `l5-swagger:generate` 성공 | -| 6.2 | DB 스키마 문서 | ✅ | `docs/specs/database-schema.md`에 sam_stat 섹션 추가 - 20개 테이블 (메타 2, 차원 3, 일간 7, 월간 4, KPI/알림/이벤트 4) + Artisan 커맨드 5개 + API 엔드포인트 4개 | -| 6.3 | 계획 문서 완료 | ✅ | Phase 6 섹션 추가, 진행률 100%, 상태 완료 | - ---- - -## 7. 기술 설계 요약 - -### 7.1 Laravel 다중 DB 연결 - -```php -// config/database.php -'connections' => [ - 'mysql' => [ /* 기존 samdb */ ], - 'sam_stat' => [ - 'driver' => 'mysql', - 'host' => env('STAT_DB_HOST', '127.0.0.1'), - 'database' => env('STAT_DB_DATABASE', 'sam_stat'), - 'username' => env('STAT_DB_USERNAME', 'root'), - 'password' => env('STAT_DB_PASSWORD', ''), - // ... 나머지 동일 - ], -], -``` - -### 7.2 모델 구조 - -``` -api/app/Models/Stats/ -├── StatDefinition.php // connection = 'sam_stat' -├── StatJobLog.php -├── Dimensions/ -│ ├── DimDate.php -│ ├── DimClient.php -│ └── DimProduct.php -├── Daily/ -│ ├── StatSalesDaily.php -│ ├── StatFinanceDaily.php -│ ├── StatProductionDaily.php -│ ├── StatInventoryDaily.php -│ ├── StatQuotePipelineDaily.php -│ ├── StatHrAttendanceDaily.php -│ └── StatSystemDaily.php -├── Monthly/ -│ ├── StatSalesMonthly.php -│ ├── StatFinanceMonthly.php -│ ├── StatProductionMonthly.php -│ └── StatProjectMonthly.php -├── StatKpiTarget.php -├── StatAlert.php -├── StatEvent.php -└── StatSnapshot.php -``` - -### 7.3 서비스 구조 - -``` -api/app/Services/Stats/ -├── StatAggregatorService.php // 집계 오케스트레이터 -├── SalesStatService.php // 매출/수주 집계 -├── FinanceStatService.php // 재무 집계 -├── ProductionStatService.php // 생산 집계 -├── InventoryStatService.php // 재고 집계 -├── QuoteStatService.php // 견적/영업 집계 -├── HrStatService.php // 인사/근태 집계 -├── ProjectStatService.php // 건설 집계 -├── SystemStatService.php // 시스템 집계 -└── KpiAlertService.php // KPI 목표 대비 알림 -``` - -### 7.4 스케줄러 구조 - -```php -// app/Console/Kernel.php (또는 routes/console.php) - -// 일간 집계 - 매일 02:00 -Schedule::command('stat:aggregate-daily') - ->dailyAt('02:00') - ->withoutOverlapping(); - -// 월간 집계 - 매월 1일 03:00 -Schedule::command('stat:aggregate-monthly') - ->monthlyOn(1, '03:00') - ->withoutOverlapping(); - -// KPI 알림 체크 - 매일 09:00 -Schedule::command('stat:check-kpi-alerts') - ->dailyAt('09:00'); -``` - -### 7.5 집계 패턴 (UPSERT) - -```php -// 멱등성 보장: 같은 날짜 재실행 시 덮어쓰기 -StatSalesDaily::updateOrCreate( - ['tenant_id' => $tenantId, 'stat_date' => $date], - [ - 'order_count' => $orderCount, - 'order_amount' => $orderAmount, - // ... - ] -); -``` - ---- - -## 8. 참고 문서 - -| 문서 | 경로 | 용도 | -|------|------|------| -| DB 스키마 | `docs/specs/database-schema.md` | 원본 219개 테이블 구조 | -| 시스템 아키텍처 | `docs/architecture/system-overview.md` | 전체 시스템 구조, 미들웨어, Docker | -| API 규칙 | `docs/standards/api-rules.md` | Controller/Service 패턴, ApiResponse | -| 품질 체크리스트 | `docs/standards/quality-checklist.md` | 코드 품질 검증 항목 | -| 빠른 시작 | `docs/quickstart/quick-start.md` | 핵심 개발 규칙 3가지 | -| Swagger 가이드 | `docs/guides/swagger-guide.md` | Swagger 작성 규칙 (Phase 4.4 시) | -| Git 규칙 | `docs/standards/git-conventions.md` | 커밋 메시지 형식 | -| 프로젝트 CLAUDE.md | `/SAM/CLAUDE.md` | 프로젝트 전체 규칙 및 맥락 | -| API CLAUDE.md | `/SAM/api/CLAUDE.md` | API 저장소 상세 규칙 | - ---- - -## 9. 자기완결성 점검 결과 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1: sam_stat 별도 DB로 통계 분리 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 20개 테이블, 8 도메인, Phase별 검증 방법 명시 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4: 테이블별 DDL, 섹션 6: Phase별 구체적 작업 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 섹션 2.1: 원본 테이블 매핑, 섹션 0.2: DB 환경 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 0.1, 0.3: 실제 파일 경로 검증됨 (2026-01-29) | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | Phase 1-5 구체적 작업 + bash 검증 커맨드 포함 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | Phase 1, 2에 검증 bash 커맨드 블록 포함 | -| 8 | 모호한 표현이 없는가? | ✅ | 파일 경로, 클래스명, 테이블명 모두 구체적 | - -### 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 0.9 체크리스트 → 6. Phase 1 | -| Q3. 어떤 파일을 수정/생성해야 하는가? | ✅ | 0.1 프로젝트 구조 + 7.1~7.5 기술 설계 | -| Q4. 기존 코드에 어떤 영향이 있는가? | ✅ | 0.3 기존 대시보드/보고서 시스템 | -| Q5. DB 연결은 어떻게 설정하는가? | ✅ | 0.2 현재 DB 환경 + 7.1 Laravel 다중 DB | -| Q6. 코딩 규칙은 무엇인가? | ✅ | 0.8 핵심 코딩 규칙 | -| Q7. 작업 완료 확인 방법은? | ✅ | Phase 1, 2 검증 방법 블록 | -| Q8. 스케줄러는 어떻게 등록하는가? | ✅ | 0.4 기존 스케줄러 패턴 + 7.4 | -| Q9. Docker 환경은 어떻게 구성되어 있는가? | ✅ | 0.7 Docker 환경 | -| Q10. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 (9개 문서 매핑) | - -**결과**: 10/10 통과 → ✅ 자기완결성 확보 - ---- - -## 10. 변경 이력 - -| 날짜 | 항목 | 내용 | -|------|------|------| -| 2026-01-29 | 초안 작성 | 프로젝트 분석 → 8개 도메인 도출 → 20개 테이블 설계 | -| 2026-01-29 | 자기완결성 보완 | 섹션 0 추가 (프로젝트 컨텍스트, DB 환경, 기존 시스템, 코딩 규칙, 체크리스트) | -| 2026-01-29 | 환경별 배포 구분 | 섹션 0.7 확장: 로컬(Docker) vs 개발서버(non-Docker) 구분, 배포 워크플로우 추가 | -| 2026-01-29 | Phase 1 완료 | 인프라 구축: sam_stat DB 생성, 메타/dim_date 마이그레이션, 기반 모델 4개, 커맨드 2개, AggregatorService + Interface | -| 2026-01-29 | Phase 2 완료 | P0 도메인: 매출/재무/생산 일간+월간 테이블 6개, 모델 6개, 서비스 3개, 스케줄러 2개 등록. 실데이터 집계 검증 완료 | -| 2026-01-29 | Phase 3 완료 | P1 도메인: dim_client/dim_product 차원 + 재고/견적/인사 일간 3개 + KPI/알림 2개 = 테이블 7개, 모델 7개, 서비스 4개(Dimension/Inventory/Quote/Hr/KpiAlert), 커맨드 1개, 스케줄러 1개. 실데이터 검증 완료. products→items, client_groups.name→group_name 수정 | -| 2026-01-29 | Phase 4 완료 | P2 도메인 + API + 대시보드: stat_project_monthly/stat_system_daily/stat_events/stat_snapshots 테이블 4개, 모델 4개, 서비스 4개(Project/System/StatEvent/StatQuery), StatController + FormRequest 3개 + routes/stats.php, StatEventObserver(6모델), DashboardService sam_stat 전환(폴백 패턴). 버그: whereHas→DB Builder 제거, User모델경로 수정. sam_stat 총 20테이블 | -| 2026-01-29 | Phase 5 완료 | 최적화 및 안정화: StatBackfillCommand(백필), StatVerifyCommand(정합성 검증+자동 재집계), 파티셔닝 준비 마이그레이션(7테이블 RANGE), StatQueryService Redis 캐싱(TTL 5분+invalidateCache), StatMonitorService(집계 실패/누락/불일치 알림→stat_alerts), StatAggregatorService에 모니터링+캐시 무효화 연동. severity enum 수정(high→critical). 전체 테스트 통과 | -| 2026-01-30 | Phase 6 완료 | 문서화 및 마무리: StatApi.php Swagger 문서(4 엔드포인트, 4 스키마), database-schema.md sam_stat 섹션 추가(20테이블+5커맨드+4API). 전체 6 Phase 100% 완료 | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/simulator-calculation-logic-mapping.md b/plans/archive/simulator-calculation-logic-mapping.md deleted file mode 100644 index 113c198..0000000 --- a/plans/archive/simulator-calculation-logic-mapping.md +++ /dev/null @@ -1,1057 +0,0 @@ -# 견적 시뮬레이터 완전 동기화 계획 - -> **작성일**: 2025-12-23 (업데이트: 2025-12-30) -> **목표**: design.sam.kr 시뮬레이터와 mng 시뮬레이터가 **동일한 결과**를 출력하도록 완전 동기화 - ---- - -## 1. Design 시스템 전체 분석 - -### 1.1 핵심 파일 구조 - -| 파일 | 줄 수 | 역할 | -|------|-------|------| -| `AutoCalculationSimulator.tsx` | 1,068 | 메인 시뮬레이터 UI + 계산 로직 | -| `formulaEvaluator.ts` | 312 | 수식 평가 엔진 | -| `bomCalculatorWithDebug.ts` | 232 | BOM 계산 + 10단계 디버깅 | -| `DataContext.tsx` | 9,859 | 마스터 데이터 타입 + 상태 관리 | -| `sampleQuoteData_Complete.ts` | 600+ | 샘플 품목 데이터 | -| `addProductBoms.ts` | 298 | 완제품 BOM 구성 | - -### 1.2 데이터 구조 (TypeScript 인터페이스) - -#### 품목 마스터 (ItemMaster) -```typescript -interface ItemMaster { - id: string; - itemCode: string; // 품목코드 - itemName: string; // 품목명 - itemType: 'FG' | 'SF' | 'PT' | 'SM' | 'RM' | 'CS'; - productCategory?: 'SCREEN' | 'STEEL'; // 제품 카테고리 - partType?: 'ASSEMBLY' | 'BENDING' | 'PURCHASED'; - unit: string; - salesPrice?: number; // 판매단가 - purchasePrice?: number; // 매입단가 - marginRate?: number; // 마진율 - bom?: BOMLine[]; // 하위 BOM 목록 - // ... 기타 필드 -} -``` - -#### BOM 라인 (BOMLine) -```typescript -interface BOMLine { - childItemCode: string; // 자식 품목 코드 - childItemName: string; // 자식 품목명 - quantity: number; // 기준 수량 - unit: string; // 단위 - quantityFormula?: string; // 수량 수식 (예: "W*H/1000000", "H/1000") - note?: string; // 비고 -} -``` - -#### 단가 관리 (PricingData) -```typescript -interface PricingData { - id: string; - itemId: string; - itemCode: string; - purchasePrice?: number; // 매입단가 - processingCost?: number; // 가공비 - loss?: number; // LOSS(%) - marginRate?: number; // 마진율 - salesPrice?: number; // 판매단가 - effectiveDate: string; // 적용일 - status: 'draft' | 'active' | 'inactive' | 'finalized'; -} -``` - -#### 카테고리 그룹 (CategoryGroup) - MNG에 누락 -```typescript -interface CategoryGroup { - id: string; - name: string; // "면적기반", "중량기반", "수량기반" - categories: string[]; // 소속 카테고리들 - multiplierVariable?: string; // 곱할 변수 (M, K 등) -} -``` - -### 1.3 계산 변수 체계 - -| 변수 | 설명 | 계산식 | -|------|------|--------| -| `W0` | 오픈사이즈 폭 | 사용자 입력 | -| `H0` | 오픈사이즈 높이 | 사용자 입력 | -| `PC` | 제품 카테고리 | "스크린" / "철재" | -| `W1` | 제작폭 | PC=="스크린" ? W0+140 : W0+110 | -| `H1` | 제작높이 | H0 + 350 | -| `W` | 제작폭 (별칭) | = W1 | -| `H` | 제작높이 (별칭) | = H1 | -| `M` | 면적 (㎡) | (W1 × H1) / 1,000,000 | -| `K` | 중량 (kg) | 스크린: M×2 + W0/1000×14.17, 철재: M×25 | -| `GT` | 가이드레일 설치유형 | "벽면형" / "측면형" | -| `MP` | 모터 전원 | "220V" / "380V" | -| `CT` | 연동제어기 | "단독" / "연동" | -| `QTY` | 수량 | 사용자 입력 | - -### 1.4 수식 평가 함수 - -**지원 함수 목록:** -| 함수 | 설명 | 예시 | -|------|------|------| -| `SUM(a, b, ...)` | 합계 | `SUM(W0, H0, 100)` | -| `AVERAGE(a, b, ...)` | 평균 | `AVERAGE(W0, H0)` | -| `MAX(a, b, ...)` | 최대값 | `MAX(W0, 1000)` | -| `MIN(a, b, ...)` | 최소값 | `MIN(H0, 3000)` | -| `ROUND(val, dec)` | 반올림 | `ROUND(M, 2)` | -| `CEIL(val)` | 올림 | `CEIL(H1 / 1000)` | -| `FLOOR(val)` | 내림 | `FLOOR(W1 / 500)` | -| `ABS(val)` | 절대값 | `ABS(W0 - 2000)` | -| `IF(cond, t, f)` | 조건문 | `IF(W0 > 3000, 2, 1)` | -| `SQRT(val)` | 제곱근 | `SQRT(M)` | -| `POWER(base, exp)` | 거듭제곱 | `POWER(W1, 2)` | - -**평가 과정:** -```typescript -// 1. 변수 치환 (긴 변수명부터) -const sortedVars = Object.keys(vars).sort((a, b) => b.length - a.length); -sortedVars.forEach(varName => { - const regex = new RegExp(`\\b${varName}\\b`, 'g'); - formula = formula.replace(regex, String(vars[varName])); -}); - -// 2. 함수 처리 (CEIL, FLOOR, ROUND 등) -formula = processFunctions(formula); - -// 3. 최종 계산 -return new Function(`return (${formula})`)(); -``` - -### 1.5 BOM 계산 10단계 프로세스 - -| 단계 | 항목 | 예시 | -|------|------|------| -| Step 1 | 수량 공식 확인 | `H/1000` | -| Step 2 | 변수 값 확인 | `{W0:2000, H0:2500, W1:2140, H1:2850, M:6.099}` | -| Step 3 | 수량 계산 과정 | `H/1000 = 2850/1000 = 2.85` | -| Step 4 | 계산된 수량 | `2.85` | -| Step 5 | 단가 소스 | `단가관리 (15,000원)` 또는 `품목마스터 (15,000원)` | -| Step 6 | 기본 단가 | `15,000` | -| Step 7 | 카테고리 승수 | `면적단가 (15,000원/㎡ × 6.099㎡)` | -| Step 8 | 최종 단가 | `91,485` | -| Step 9 | 금액 계산 | `2.85 × 91,485 = 260,732` | -| Step 10 | 최종 금액 | `260,732` | - -### 1.6 단가 계산 로직 - -```typescript -// 1. 단가 조회 우선순위 -let unitPrice = 0; -let priceSource = '단가 없음'; - -// 1순위: pricing 테이블에서 조회 -const itemPricing = pricings.find(p => p.itemCode === bomEntry.childItemCode); -if (itemPricing && itemPricing.salesPrice) { - unitPrice = itemPricing.salesPrice; - priceSource = `단가관리 (${unitPrice.toLocaleString()}원)`; -} -// 2순위: 품목마스터에서 조회 -else if (childItem.salesPrice) { - unitPrice = childItem.salesPrice; - priceSource = `품목마스터 (${unitPrice.toLocaleString()}원)`; -} - -// 2. 면적 기반 품목 판단 -const areaBasedCategories = ['원단', '패널', '도장', '표면처리']; -const isAreaBased = areaBasedCategories.some(cat => - itemCategory.includes(cat) || childItem.itemName.includes(cat) -); - -// 3. 최종 단가 계산 -let finalUnitPrice = unitPrice; -if (isAreaBased && calculationVariables.M > 0) { - finalUnitPrice = unitPrice * calculationVariables.M; // 면적 단가 - priceCalculationNote = `면적단가 (${unitPrice}원/㎡ × ${M}㎡)`; -} else { - priceCalculationNote = '수량단가'; -} - -// 4. 최종 금액 -const totalPrice = calculatedQuantity * finalUnitPrice; -``` - ---- - -## 2. Design 샘플 데이터 분석 - -### 2.1 품목 구성 (약 100개) - -| 유형 | 코드 접두사 | 수량 | 설명 | -|------|------------|------|------| -| 원자재 (RM) | RM-* | 20 | 강판, 알루미늄, 원단, 패킹 등 | -| 부자재 (SM) | SM-* | 25 | 볼트, 너트, 전선, 실리콘 등 | -| 스크린 반제품 (SF) | SF-SCR-* | 20 | 원단, 가이드레일, 케이스, 모터 등 | -| 철재 반제품 (SF) | SF-STL-*, SF-BND-* | 20 | 도어, 프레임, 패널, 절곡 부품 등 | -| 스크린 완제품 (FG) | FG-SCR-* | 5 | 소형/중형/대형/특대/맞춤형 | -| 철재 완제품 (FG) | FG-STL-* | 5 | 소형/중형/대형/양개문/특수 | -| 절곡 완제품 (FG) | FG-BND-* | 4 | L형/U형/Z형/ㄷ형 | - -### 2.2 주요 BOM 수식 패턴 - -| 품목 유형 | 수식 | 설명 | -|----------|------|------| -| 스크린 원단 | `W*H/1000000` | 면적 계산 | -| 가이드레일 | `H/1000` | 높이(m) 기준 | -| 엣지윙 | `H/1000` | 높이(m) 기준 | -| 철재 프레임 | `(W+H)*2/1000` | 둘레(m) 기준 | -| 철재 패널 | `W*H/1000000` | 면적 계산 | -| 실링재 | `(W+H)*2/1000` | 둘레(m) 기준 | -| 파우더 도장 | `W*H/1000000` | 면적 계산 | - -### 2.3 완제품 BOM 예시 (FG-SCR-002 중형 스크린) - -```typescript -{ - itemCode: 'FG-SCR-002', - itemName: '방화스크린 중형 (2000x3000)', - bom: [ - { childItemCode: 'SF-SCR-F01', quantity: 6.0, unit: 'M2', quantityFormula: 'W*H/1000000' }, - { childItemCode: 'SF-SCR-F02', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, - { childItemCode: 'SF-SCR-F03', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, - { childItemCode: 'SF-SCR-F04', quantity: 1, unit: 'EA' }, - { childItemCode: 'SF-SCR-F05', quantity: 1, unit: 'EA' }, - { childItemCode: 'SF-SCR-M02', quantity: 1, unit: 'EA', note: '중형용' }, - { childItemCode: 'SF-SCR-C01', quantity: 1, unit: 'EA' }, - { childItemCode: 'SF-SCR-S01', quantity: 1, unit: 'SET' }, - { childItemCode: 'SF-SCR-W01', quantity: 1, unit: 'EA' }, - { childItemCode: 'SF-SCR-B01', quantity: 2, unit: 'SET', note: '중형용 2세트' }, - { childItemCode: 'SF-SCR-E01', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, - { childItemCode: 'SF-SCR-E02', quantity: 3.0, unit: 'M', quantityFormula: 'H/1000' }, - { childItemCode: 'SF-SCR-REM01', quantity: 1, unit: 'EA' }, - { childItemCode: 'SM-B002', quantity: 30, unit: 'EA', note: '조립용' }, - { childItemCode: 'SM-N002', quantity: 30, unit: 'EA' }, - { childItemCode: 'SM-A001', quantity: 8, unit: 'EA', note: '고정용' }, - ] -} -``` - ---- - -## 3. MNG 현재 상태 분석 - -### 3.1 테이블 구조 - -| 테이블 | 현재 상태 | Design 대응 | -|--------|----------|-------------| -| `items` | 364개 (RM:133, SM:217, PT:6, FG:3, CS:5) | ItemMaster | -| `item_details` | 품목 상세 정보 | ItemMaster 확장 필드 | -| `prices` | 3개 (거의 없음) | PricingData | -| `quote_formulas` | 57개 (기본 변수 있음) | FormulaRule, CalculationFormula | -| `quote_formula_ranges` | 범위 규칙 | FormulaRule.ranges | -| `quote_formula_items` | 수식 품목 매핑 | BOM 연동 | -| `common_codes` | 코드 그룹 | CategoryGroup (부분) | -| `category_groups` | ❌ 없음 | CategoryGroup 추가 필요 | - -### 3.2 quote_formulas 현재 데이터 (샘플) - -``` -[PC] 제품카테고리 (input) => variable -[W0] 가로 (W0) (input) => variable -[H0] 세로 (H0) (input) => variable -[W1_SCREEN] 제작사이즈 W1 (스크린): W0 + 140 => variable -[H1_SCREEN] 제작사이즈 H1 (스크린): H0 + 350 => variable -[W1_STEEL] 제작사이즈 W1 (철재): W0 + 110 => variable -[H1_STEEL] 제작사이즈 H1 (철재): H0 + 350 => variable -[M] 면적 계산: W1 * H1 / 1000000 => variable -[K_SCREEN] 중량 계산 (스크린): M * 2 + W0 / 1000 * 14.17 => variable -[K_STEEL] 중량 계산 (철재): M * 25 => variable -``` - -### 3.3 누락 항목 - -| 항목 | 설명 | 우선순위 | -|------|------|---------| -| `items.process_type` | 공정유형 (스크린/절곡/전기) | 높음 | -| `items.item_category` | 품목분류 (원단/패널/도장 등) | 높음 | -| `category_groups` 테이블 | 면적/중량 기반 분류 | 높음 | -| Design 샘플 품목 데이터 | 100개 품목 Seeder | 높음 | -| BOM 구성 데이터 | 제품별 BOM Seeder | 높음 | -| 단가 데이터 | 품목별 단가 Seeder | 중간 | - ---- - -## 4. 완전 동기화 구현 계획 - -### Phase 1: DB 스키마 확장 (1일) - -#### 1.1 items 테이블 필드 추가 -```sql -ALTER TABLE items ADD COLUMN process_type VARCHAR(20) DEFAULT NULL - COMMENT '공정유형: screen(스크린), bending(절곡), electric(전기), steel(철재)'; - -ALTER TABLE items ADD COLUMN item_category VARCHAR(50) DEFAULT NULL - COMMENT '품목분류: 원단, 패널, 도장, 표면처리, 가이드레일, 케이스, 모터, 제어반 등'; - -CREATE INDEX idx_items_process_type ON items(process_type); -CREATE INDEX idx_items_item_category ON items(item_category); -``` - -#### 1.2 category_groups 테이블 생성 -```sql -CREATE TABLE category_groups ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL, - code VARCHAR(50) NOT NULL COMMENT '코드: area_based, weight_based, quantity_based', - name VARCHAR(100) NOT NULL COMMENT '이름: 면적기반, 중량기반, 수량기반', - multiplier_variable VARCHAR(20) COMMENT '곱셈 변수: M, K, null', - categories JSON COMMENT '소속 카테고리 목록', - description TEXT, - sort_order INT DEFAULT 0, - is_active BOOLEAN DEFAULT TRUE, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - - INDEX idx_tenant (tenant_id), - INDEX idx_code (code) -); -``` - -### Phase 2: Seeder 작성 (2일) - -#### 2.1 품목 마스터 Seeder - -**파일**: `database/seeders/DesignItemSeeder.php` - -```php -class DesignItemSeeder extends Seeder -{ - public function run(): void - { - // 원자재 (20개) - $rawMaterials = [ - ['code' => 'RM-S001', 'name' => '강판 1.2T', 'unit' => 'KG', 'price' => 3500, 'category' => '강판'], - ['code' => 'RM-F001', 'name' => '방화원단 A급', 'unit' => 'M2', 'price' => 28000, 'category' => '원단'], - // ... 18개 더 - ]; - - // 부자재 (25개) - $subMaterials = [ - ['code' => 'SM-B001', 'name' => '볼트 M8x30', 'unit' => 'EA', 'price' => 150, 'category' => '볼트'], - // ... 24개 더 - ]; - - // 스크린 반제품 (20개) - $screenSemiProducts = [ - ['code' => 'SF-SCR-F01', 'name' => '스크린 원단', 'unit' => 'M2', 'price' => 35000, 'category' => '원단', 'process' => 'screen'], - ['code' => 'SF-SCR-F02', 'name' => '가이드레일 (좌)', 'unit' => 'M', 'price' => 42000, 'category' => '가이드레일', 'process' => 'screen'], - // ... 18개 더 - ]; - - // 완제품 (14개) - $finishedProducts = [ - ['code' => 'FG-SCR-001', 'name' => '방화스크린 소형', 'category' => 'SCREEN'], - ['code' => 'FG-SCR-002', 'name' => '방화스크린 중형', 'category' => 'SCREEN'], - // ... 12개 더 - ]; - } -} -``` - -#### 2.2 BOM 구성 Seeder - -**파일**: `database/seeders/DesignBomSeeder.php` - -```php -class DesignBomSeeder extends Seeder -{ - public function run(): void - { - $bomData = [ - 'FG-SCR-002' => [ - ['child' => 'SF-SCR-F01', 'qty' => 1, 'formula' => 'W*H/1000000', 'unit' => 'M2'], - ['child' => 'SF-SCR-F02', 'qty' => 1, 'formula' => 'H/1000', 'unit' => 'M'], - ['child' => 'SF-SCR-F03', 'qty' => 1, 'formula' => 'H/1000', 'unit' => 'M'], - ['child' => 'SF-SCR-F04', 'qty' => 1, 'formula' => '', 'unit' => 'EA'], - // ... 더 많은 BOM 라인 - ], - // ... 다른 제품들 - ]; - } -} -``` - -#### 2.3 CategoryGroup Seeder - -```php -class CategoryGroupSeeder extends Seeder -{ - public function run(): void - { - $groups = [ - [ - 'code' => 'area_based', - 'name' => '면적기반', - 'multiplier_variable' => 'M', - 'categories' => json_encode(['원단', '패널', '도장', '표면처리']), - ], - [ - 'code' => 'weight_based', - 'name' => '중량기반', - 'multiplier_variable' => 'K', - 'categories' => json_encode(['강판', '알루미늄']), - ], - [ - 'code' => 'quantity_based', - 'name' => '수량기반', - 'multiplier_variable' => null, - 'categories' => json_encode(['볼트', '너트', '모터', '제어반']), - ], - ]; - } -} -``` - -### Phase 3: 백엔드 로직 확장 (2일) - -#### 3.1 FormulaEvaluatorService 확장 - -**추가할 메서드:** - -```php -/** - * 카테고리 기반 단가 계산 - */ -private function calculateCategoryPrice( - array $item, - float $basePrice, - array $variables -): array { - $categoryGroup = CategoryGroup::query() - ->whereJsonContains('categories', $item['item_category'] ?? '') - ->first(); - - if (!$categoryGroup || !$categoryGroup->multiplier_variable) { - return [ - 'final_price' => $basePrice, - 'calculation_note' => '수량단가', - 'multiplier' => 1, - ]; - } - - $multiplierVar = $categoryGroup->multiplier_variable; - $multiplierValue = $variables[$multiplierVar] ?? 1; - - return [ - 'final_price' => $basePrice * $multiplierValue, - 'calculation_note' => "{$categoryGroup->name} ({$basePrice}원/{$this->getUnit($multiplierVar)} × {$multiplierValue})", - 'multiplier' => $multiplierValue, - ]; -} - -/** - * 공정별 품목 그룹화 - */ -private function groupItemsByProcess(array $items): array -{ - $processOrder = [ - 'screen' => ['label' => '스크린 공정', 'items' => [], 'subtotal' => 0], - 'bending' => ['label' => '절곡 공정', 'items' => [], 'subtotal' => 0], - 'electric' => ['label' => '전기 공정', 'items' => [], 'subtotal' => 0], - 'assembly' => ['label' => '조립 공정', 'items' => [], 'subtotal' => 0], - 'etc' => ['label' => '기타', 'items' => [], 'subtotal' => 0], - ]; - - foreach ($items as $item) { - $process = $item['process_type'] ?? 'etc'; - if (isset($processOrder[$process])) { - $processOrder[$process]['items'][] = $item; - $processOrder[$process]['subtotal'] += $item['total_price'] ?? 0; - } else { - $processOrder['etc']['items'][] = $item; - $processOrder['etc']['subtotal'] += $item['total_price'] ?? 0; - } - } - - return array_filter($processOrder, fn($g) => count($g['items']) > 0); -} - -/** - * 10단계 디버깅 정보 생성 - */ -private function generateDebugInfo( - array $bomLine, - array $variables, - float $calculatedQty, - float $basePrice, - float $finalPrice, - float $totalPrice, - string $priceSource, - string $calcNote -): array { - return [ - 'step1_formula' => $bomLine['quantity_formula'] ?? '수식 없음', - 'step2_variables' => $variables, - 'step3_quantity_calc' => $this->buildQuantityCalcString($bomLine, $variables, $calculatedQty), - 'step4_quantity' => $calculatedQty, - 'step5_price_source' => $priceSource, - 'step6_base_price' => $basePrice, - 'step7_category_multiplier' => $calcNote, - 'step8_final_price' => $finalPrice, - 'step9_total_calc' => sprintf('%.2f × %s = %s', $calculatedQty, number_format($finalPrice), number_format($totalPrice)), - 'step10_total' => $totalPrice, - ]; -} -``` - -#### 3.2 executeAll() 반환 구조 확장 - -```php -public function executeAll(array $inputVariables): array -{ - // 1. 변수 계산 - $calculatedVariables = $this->calculateVariables($inputVariables); - - // 2. 제품 BOM 조회 - $product = Item::where('code', $inputVariables['PRODUCT_ID'])->first(); - $bomTree = $this->getBomTree($product); - - // 3. BOM 항목별 계산 - $bomItems = []; - foreach ($bomTree as $bomLine) { - $result = $this->calculateBomItem($bomLine, $calculatedVariables); - $bomItems[] = $result; - } - - // 4. 공정별 그룹화 - $groupedByProcess = $this->groupItemsByProcess($bomItems); - - // 5. 총합계 - $totalAmount = array_sum(array_column($bomItems, 'total_price')); - - return [ - 'input_variables' => $inputVariables, - 'calculated_variables' => $calculatedVariables, - 'product' => [ - 'code' => $product->code, - 'name' => $product->name, - 'category' => $product->item_details->product_category ?? null, - ], - 'bom_items' => $bomItems, - 'grouped_by_process' => $groupedByProcess, - 'summary' => [ - 'total_items' => count($bomItems), - 'total_amount' => $totalAmount, - ], - ]; -} -``` - -### Phase 4: 프론트엔드 확장 (1일) - -#### 4.1 simulator.blade.php 결과 표시 개선 - -```blade -{{-- 공정별 그룹화 결과 --}} -@if(isset($result['grouped_by_process'])) -
- @foreach($result['grouped_by_process'] as $processCode => $group) -
-
-

{{ $group['label'] }}

- - 소계: {{ number_format($group['subtotal']) }}원 - -
- - - - - - - - - - - - - @foreach($group['items'] as $item) - - - - - - - - - @endforeach - -
품목코드품목명수량단위단가금액
{{ $item['item_code'] }}{{ $item['item_name'] }}{{ number_format($item['calculated_quantity'], 2) }}{{ $item['unit'] }}{{ number_format($item['final_price']) }}{{ number_format($item['total_price']) }}
-
- @endforeach -
- -{{-- 총합계 --}} -
-
- 총 합계 - - {{ number_format($result['summary']['total_amount']) }}원 - -
-
-``` - -### Phase 5: 검증 및 동기화 (1일) - -#### 5.1 테스트 케이스 - -| 입력값 | Design 결과 | MNG 목표 | -|--------|------------|----------| -| W0=2000, H0=2500, PC=스크린 | W1=2140, H1=2850, M=6.099 | 동일 | -| 스크린 원단 (면적단가) | 35,000 × 6.099 = 213,465원 | 동일 | -| 가이드레일 (길이단가) | 42,000 × 2.85 = 119,700원 | 동일 | -| 모터 (고정단가) | 480,000 × 1 = 480,000원 | 동일 | - -#### 5.2 검증 스크립트 - -```php -// php artisan tinker - -// 동일 입력으로 계산 비교 -$input = [ - 'PC' => '스크린', - 'PRODUCT_ID' => 'FG-SCR-002', - 'W0' => 2000, - 'H0' => 2500, - 'GT' => '벽면형', - 'MP' => '220V', - 'CT' => '단독', - 'QTY' => 1, -]; - -$service = app(\App\Services\Quote\FormulaEvaluatorService::class); -$result = $service->executeAll($input); - -// Design 결과와 비교 -dump([ - 'W1' => $result['calculated_variables']['W1'], // 예상: 2140 - 'H1' => $result['calculated_variables']['H1'], // 예상: 2850 - 'M' => $result['calculated_variables']['M'], // 예상: 6.099 - 'total' => $result['summary']['total_amount'], // Design과 동일해야 함 -]); -``` - ---- - -## 5. 핵심 파일 참조 - -### Design (참조용 - 수정 금지) -``` -/SAM/design/src/ -├── components/ -│ ├── AutoCalculationSimulator.tsx # 메인 시뮬레이터 (1068줄) -│ ├── BomCalculationResults.tsx # 결과 표시 컴포넌트 -│ ├── contexts/ -│ │ └── DataContext.tsx # 마스터 데이터 (9859줄) -│ └── utils/ -│ ├── formulaEvaluator.ts # 수식 평가 (312줄) -│ └── bomCalculatorWithDebug.ts # BOM 계산 (232줄) -└── utils/ - ├── sampleQuoteData_Complete.ts # 샘플 품목 데이터 - └── addProductBoms.ts # BOM 구성 데이터 -``` - -### MNG (수정 대상) -``` -/SAM/mng/ -├── app/Services/Quote/ -│ └── FormulaEvaluatorService.php # 핵심 서비스 확장 대상 -├── database/ -│ ├── migrations/ -│ │ └── 20xx_add_simulator_fields.php # 신규 마이그레이션 -│ └── seeders/ -│ ├── DesignItemSeeder.php # 신규 Seeder -│ ├── DesignBomSeeder.php # 신규 Seeder -│ └── CategoryGroupSeeder.php # 신규 Seeder -├── app/Models/ -│ ├── CategoryGroup.php # 신규 모델 -│ ├── Item.php # 필드 추가 -│ └── Price.php # 기존 모델 -└── resources/views/quote-formulas/ - └── simulator.blade.php # UI 확장 -``` - ---- - -## 6. 작업 일정 요약 - -| Phase | 작업 내용 | 예상 일정 | -|-------|----------|----------| -| Phase 1 | DB 스키마 확장 (마이그레이션) | 1일 | -| Phase 2 | Seeder 작성 (품목/BOM/단가/CategoryGroup) | 2일 | -| Phase 3 | FormulaEvaluatorService 확장 | 2일 | -| Phase 4 | simulator.blade.php UI 개선 | 1일 | -| Phase 5 | 검증 및 동기화 테스트 | 1일 | -| **합계** | | **7일** | - ---- - -## 7. 성공 기준 - -1. **계산 결과 동일**: Design과 MNG에서 동일 입력 시 동일한 금액 산출 -2. **10단계 디버깅**: 각 품목별 계산 과정을 10단계로 확인 가능 -3. **공정별 그룹화**: 스크린/절곡/전기 공정별로 품목 분류 -4. **단가 우선순위**: prices 테이블 > items.salesPrice 순서 적용 -5. **면적/중량 기반 단가**: CategoryGroup 설정에 따라 자동 계산 - ---- - -## 8. Serena 컨텍스트 유지 정책 - -> **목적**: 세션 간 컨텍스트 유지를 위해 Serena MCP 메모리에 역할별 분리 저장 - -### 8.1 메모리 구조 - -``` -simulator-rules.md # 패턴, 규칙, 체크리스트 -simulator-mappings.md # 필드 매핑 상세 (Design ↔ MNG) -simulator-progress.md # 진행 상황 -``` - -### 8.2 메모리 내용 - -#### `simulator-rules.md` -- 계산 변수 체계 (W0, H0, W1, H1, M, K 등) -- 수식 평가 함수 목록 (SUM, CEIL, FLOOR, ROUND, IF 등) -- BOM 10단계 계산 프로세스 -- 단가 우선순위 규칙 -- 체크리스트 - -#### `simulator-mappings.md` -- Design TypeScript 인터페이스 ↔ MNG DB 테이블 매핑 -- 품목 타입 매핑 (RM, SM, SF, FG, PT, CS) -- CategoryGroup 매핑 -- 공정 타입 매핑 (screen, bending, electric, assembly) - -#### `simulator-progress.md` -- Phase별 진행 상태 -- 완료된 작업 목록 -- 남은 작업 및 이슈 - -### 8.3 세션 시작/종료 패턴 - -**세션 시작:** -``` -list_memories() → 기존 상태 확인 -read_memory("simulator-progress.md") → 진행 상황 복원 -read_memory("simulator-rules.md") → 규칙 컨텍스트 로드 -``` - -**세션 종료:** -``` -write_memory("simulator-progress.md", 현재 진행 상황) -``` - -### 8.4 초기 메모리 저장 명령 - -```bash -# 세션 시작 시 아래 명령으로 메모리 초기화 -/sc:save simulator-rules # 규칙 저장 -/sc:save simulator-mappings # 매핑 저장 -/sc:save simulator-progress # 진행 상황 저장 -``` - ---- - -## 9. 검증 결과 (Phase 5) - -> **검증일**: 2025-12-24 -> **테스트 환경**: Docker (sam-mng-1) - -### 9.1 테스트 케이스: FG-SCR-001 (W0=2000, H0=2500) - -#### 변수 계산 (Design 마진 적용) -| 변수 | 계산식 | 결과 | 상태 | -|------|--------|------|------| -| W0 | 입력값 | 2000 | ✅ | -| H0 | 입력값 | 2500 | ✅ | -| W1 | W0 + 140 | 2140 | ✅ | -| H1 | H0 + 350 | 2850 | ✅ | -| M | W1 × H1 / 1,000,000 | 6.099 ㎡ | ✅ | - -#### 품목별 계산 결과 -| 품목코드 | 그룹 | 수량 | 단가 | 금액 | 상태 | -|----------|------|------|------|------|------| -| SF-SCR-F01 | area_based | 6.10 | 35,000 | 213,465원 | ✅ | -| SF-SCR-F02 | quantity_based | 2.85 | 42,000 | 119,700원 | ✅ | -| SF-SCR-F03 | quantity_based | 2.85 | 42,000 | 119,700원 | ✅ | -| SF-SCR-F04 | quantity_based | 1.00 | 145,000 | 145,000원 | ✅ | -| SF-SCR-F05 | (미등록) | 1.00 | 55,000 | 55,000원 | ✅ | -| SF-SCR-M01 | quantity_based | 1.00 | 350,000 | 350,000원 | ✅ | -| SF-SCR-C01 | quantity_based | 1.00 | 280,000 | 280,000원 | ✅ | -| SF-SCR-S01 | (미등록) | 1.00 | 180,000 | 180,000원 | ✅ | -| SF-SCR-W01 | (미등록) | 1.00 | 125,000 | 125,000원 | ✅ | -| SF-SCR-B01 | quantity_based | 1.00 | 78,000 | 78,000원 | ✅ | -| SF-SCR-SW01 | quantity_based | 1.00 | 45,000 | 45,000원 | ✅ | -| SM-B002 | quantity_based | 1.00 | 200 | 200원 | ✅ | -| SM-N002 | quantity_based | 1.00 | 100 | 100원 | ✅ | -| SM-W002 | quantity_based | 1.00 | 60 | 60원 | ✅ | -| **합계** | | | | **1,711,225원** | ✅ | - -### 9.2 10단계 디버깅 검증 - -| 단계 | 항목 | 상태 | -|------|------|------| -| Step 1 | 입력값수집 | ✅ | -| Step 2 | 변수계산 | ✅ | -| Step 3 | 완제품선택 | ✅ | -| Step 4 | BOM전개 | ✅ | -| Step 5 | 단가출처 | ✅ | -| Step 6 | 수량계산 | ✅ | -| Step 7 | 금액계산 | ✅ | -| Step 8 | 공정그룹화 | ✅ | -| Step 9 | 소계계산 | ✅ | -| Step 10 | 최종합계 | ✅ | - -### 9.3 공정별 그룹화 검증 - -| 공정 | 품목 수 | 소계 | 상태 | -|------|---------|------|------| -| screen | 11 | 1,710,865원 | ✅ | -| assembly | 3 | 360원 | ✅ | - -### 9.4 단가 우선순위 검증 - -| 품목 | 단가 출처 | 상태 | -|------|----------|------| -| SF-SCR-F01 | items.salesPrice | ✅ | -| SF-SCR-M01 | items.salesPrice | ✅ | -| SM-B002 | items.salesPrice | ✅ | - -> **참고**: ~~prices 테이블에 active 데이터 없음~~ → **2025-12-29 prices 데이터 85개 추가 완료** - -### 9.5 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 계산 결과 동일 | ✅ | Design 마진 (W+140, H+350) 적용 | -| 10단계 디버깅 | ✅ | 모든 단계 정상 출력 | -| 공정별 그룹화 | ✅ | screen, assembly 분류 | -| 단가 우선순위 | ✅ | prices → items.salesPrice 순서 | -| 면적/중량 기반 단가 | ✅ | CategoryGroup 기반 자동 계산 | - -### 9.6 수정 사항 (Phase 5 중) - -1. **면적기반 단가 중복 계산 수정** - - 문제: `total = quantity × (base_price × multiplier)` (중복) - - 수정: 면적/중량기반은 `total = final_price` (이미 multiplier 적용됨) - -2. **마진값 Design 표준 적용** - - 기존: W+100, H+100 - - 수정: W+140, H+350 (스크린 기준) - ---- - -## 10. Phase 6: prices 테이블 데이터 추가 (2025-12-29) - -### 10.1 작업 내용 - -| 항목 | 내용 | -|------|------| -| 작업일 | 2025-12-29 | -| 목적 | prices 테이블에 시뮬레이터용 단가 데이터 추가 | -| Seeder | `DesignPriceSeeder.php` | -| 대상 품목 | 85개 (RM, SM, SF-SCR, SF-STL, SF-BND) | - -### 10.2 생성된 Seeder - -**파일**: `mng/database/seeders/DesignPriceSeeder.php` - -```php -// items.attributes.salesPrice → prices 테이블 이전 -// 단가 우선순위: prices (1순위) → items.attributes (2순위) -``` - -**실행 명령**: -```bash -php artisan db:seed --class=DesignPriceSeeder -``` - -### 10.3 추가된 데이터 - -| 품목 유형 | 코드 패턴 | 수량 | -|----------|----------|------| -| 원자재 | RM-* | 20개 | -| 부자재 | SM-* | 25개 | -| 스크린 반제품 | SF-SCR-* | 20개 | -| 철재 반제품 | SF-STL-* | 16개 | -| 절곡 반제품 | SF-BND-* | 4개 | -| **합계** | | **85개** | - -### 10.4 단가 우선순위 검증 결과 - -``` -=== prices 우선순위 테스트 === -prices 테이블: 99,999원 (테스트용 변경) -items.attributes: 35,000원 (그대로) -getSalesPriceByItemCode(): 99,999원 - -✓ prices 테이블 우선 적용 확인! -``` - -### 10.5 FormulaEvaluatorService 단가 조회 로직 - -```php -// mng/app/Services/Quote/FormulaEvaluatorService.php:379-410 -private function getItemPrice(string $itemCode): float -{ - // 1순위: Price 모델에서 조회 - $price = Price::getSalesPriceByItemCode($tenantId, $itemCode); - if ($price > 0) { - return $price; - } - - // 2순위: Fallback - items.attributes.salesPrice - $item = DB::table('items')->where('code', $itemCode)->first(); - return (float) ($attributes['salesPrice'] ?? 0); -} -``` - ---- - -## 11. Phase 7: 철재 제품 테스트 케이스 (2025-12-30) - -### 11.1 작업 개요 - -| 항목 | 내용 | -|------|------| -| 작업일 | 2025-12-30 | -| 목적 | 철재 제품(FG-STL-*) 마진값/중량 계산 동기화 및 CategoryGroup 적용 | -| 테스트 완제품 | FG-STL-001 (철재 방화문) | -| 입력값 | W0=2000, H0=2500 | - -### 11.2 수정 사항 - -#### 11.2.1 마진값 동적 적용 (SCREEN/STEEL 분기) - -**파일**: `mng/app/Services/Quote/FormulaEvaluatorService.php` - -| 제품 카테고리 | 마진 W | 마진 H | K 계산식 | -|-------------|-------|-------|---------| -| SCREEN (스크린) | W0+140 | H0+350 | M×2 + W0/1000×14.17 | -| STEEL (철재) | W0+110 | H0+350 | M×25 | - -**변경 내용**: -```php -// 제품 카테고리에 따른 마진값 결정 -if (strtoupper($productCategory) === 'STEEL') { - $marginW = 110; // 철재 마진 - $K = $M * 25; // 철재 중량 -} else { - $marginW = 140; // 스크린 기본 마진 - $K = $M * 2 + ($W0 / 1000) * 14.17; // 스크린 중량 -} -``` - -#### 11.2.2 CategoryGroup 데이터 생성 (tenant 287) - -**문제**: CategoryGroup 데이터가 tenant_id=1에만 존재, tenant_id=287 미등록 - -**해결**: tenant 287용 CategoryGroup 3종 생성 - -| 코드 | 이름 | 승수변수 | 포함 카테고리 | -|------|------|---------|-------------| -| area_based | 면적기반 | M | 원단, 패널, 도장, 표면처리, 유리, 도어, 프레임, 창틀 | -| weight_based | 중량기반 | K | 강판, 알루미늄, 스테인리스, 철재 | -| quantity_based | 수량기반 | (없음) | 볼트, 경첩, 도어락, 도어클로저, 실링재, 문턱, 킥플레이트 등 | - -### 11.3 테스트 결과 - -#### 11.3.1 변수 계산 검증 - -| 변수 | 계산값 | 예상값 | 상태 | -|------|-------|-------|------| -| W1 | 2110 | 2110 (W0+110) | ✅ | -| H1 | 2850 | 2850 (H0+350) | ✅ | -| M | 6.0135 ㎡ | 6.0135 | ✅ | -| K | 150.34 kg | 150.34 (M×25) | ✅ | -| PC | STEEL | STEEL | ✅ | - -#### 11.3.2 CategoryGroup 적용 검증 - -| 품목 | 카테고리 | CategoryGroup | 기준단가 | 승수 | 최종단가 | -|------|---------|--------------|---------|------|---------| -| 철재 도어 | 도어 | area_based | 320,000 | M×6.01 | 1,924,320원 | -| 철재 프레임 | 프레임 | area_based | 58,000 | M×6.01 | 348,783원 | -| 철재 패널 | 패널 | area_based | 68,000 | M×6.01 | 408,918원 | -| 경첩 세트 | 경첩 | quantity_based | 42,000 | - | 42,000원 | -| 도어락 | 도어락 | quantity_based | 95,000 | - | 95,000원 | -| 도어클로저 | 도어클로저 | quantity_based | 115,000 | - | 115,000원 | -| 실링재 | 실링재 | quantity_based | 9,500 | - | 9,500원 | -| 문턱 | 문턱 | quantity_based | 58,000 | - | 58,000원 | -| 킥플레이트 | 킥플레이트 | quantity_based | 45,000 | - | 45,000원 | -| 볼트 세트 | 볼트 | quantity_based | 18,000 | - | 18,000원 | - -**최종 합계**: 3,158,111원 ✅ - -### 11.4 수정된 파일 - -| 파일 | 수정 내용 | -|------|----------| -| `FormulaEvaluatorService.php` | 마진값/K계산 동적 분기, `getItemDetails()`에 item_category 추가 | -| `category_groups` (DB) | tenant 287용 3개 그룹 생성 | - -### 11.5 성공 기준 달성 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 철재 마진 적용 | ✅ | W+110 정상 적용 | -| 철재 중량 계산 | ✅ | M×25 정상 적용 | -| CategoryGroup 매칭 | ✅ | area_based, quantity_based 정상 | -| 면적기반 단가 계산 | ✅ | base_price × M 정상 | -| 수량기반 단가 계산 | ✅ | base_price 그대로 적용 | - -### 11.6 절곡 제품 테스트 (FG-BND-001) - -#### 테스트 결과 - -| 변수 | 계산값 | 상태 | -|------|-------|------| -| W1 | 2110 (W0+110) | ✅ 철재 마진 적용 | -| M | 6.0135 ㎡ | ✅ | -| K | 150.34 kg (M×25) | ✅ 철재 중량 | -| PC | STEEL | ✅ | - -#### CategoryGroup 수정 - -**문제**: "절곡" 카테고리가 CategoryGroup 미등록 → 단가 0원 - -**해결**: `area_based`에 "절곡" 카테고리 추가 - -```json -// area_based categories (수정 후) -["원단","패널","도장","표면처리","스크린원단","유리","도어","프레임","창틀","절곡"] -``` - -#### 수정 후 단가 계산 - -| 품목 | CategoryGroup | 기준단가 | 승수 | 최종단가 | -|------|--------------|---------|------|---------| -| 절곡 | area_based | 28,000 | M×6.01 | 168,378원 | -| 프레임 | area_based | 58,000 | M×6.01 | 348,783원 | -| 도장 | area_based | 32,000 | M×6.01 | 192,432원 | -| 볼트 | quantity_based | 18,000 | - | 18,000원 | - -**최종 합계**: 727,893원 ✅ - -### 11.7 전체 제품 유형 검증 완료 - -| 제품 유형 | 코드 | 마진 | K 계산 | 합계 | -|----------|------|------|--------|------| -| 스크린 | FG-SCR-001 | W+140 ✅ | M×2+W0/1000×14.17 ✅ | 1,711,225원 | -| 철재 | FG-STL-001 | W+110 ✅ | M×25 ✅ | 3,158,111원 | -| 절곡 | FG-BND-001 | W+110 ✅ | M×25 ✅ | 727,893원 | - ---- - -*이 문서는 design.sam.kr 완전 분석을 바탕으로 mng 시뮬레이터 완전 동기화 계획을 상세히 기술합니다.* \ No newline at end of file diff --git a/plans/archive/stock-integration-plan.md b/plans/archive/stock-integration-plan.md deleted file mode 100644 index 5926cd5..0000000 --- a/plans/archive/stock-integration-plan.md +++ /dev/null @@ -1,421 +0,0 @@ -# 재고 통합 시스템 개발 계획 - -> **작성일**: 2025-01-26 -> **목적**: 입고/생산/견적 시스템과 재고(Stock)의 실시간 연동 구현 -> **기준 문서**: `docs/specs/database-schema.md`, `docs/standards/api-rules.md` -> **상태**: 🔄 계획 수립 중 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 3 - 견적/출하 → 재고 연동 완료 | -| **다음 작업** | ✅ 모든 Phase 완료 | -| **진행률** | 12/12 (100%) | -| **마지막 업데이트** | 2025-01-26 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 SAM 시스템의 재고 관리는 **조회 전용**으로만 작동합니다: -- 입고(Receiving)가 완료되어도 Stock이 증가하지 않음 -- 생산(WorkOrder)에서 자재를 투입해도 Stock이 감소하지 않음 -- 견적(Order)이 확정되어도 재고 예약이 되지 않음 -- 출하(Shipment)가 완료되어도 Stock이 감소하지 않음 - -**결과**: 재고현황 페이지가 실제 재고를 반영하지 못함 - -### 1.2 목표 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 목표 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 입고 완료 → Stock 자동 증가 + StockLot 생성 │ -│ 2. 자재 투입 → Stock 자동 차감 (FIFO 기반) │ -│ 3. 견적 확정 → reserved_qty 증가 │ -│ 4. 출하 완료 → stock_qty 차감 │ -│ 5. 모든 변경에 대한 감사 로그 기록 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 성공 기준 - -| 기준 | 측정 방법 | -|------|----------| -| 입고 → 재고 연동 | 입고 완료 시 Stock.stock_qty 자동 증가 확인 | -| 생산 → 재고 연동 | 자재 투입 시 Stock.stock_qty 자동 감소 확인 | -| 견적 → 재고 연동 | 견적 확정 시 Stock.reserved_qty 증가 확인 | -| 출하 → 재고 연동 | 출하 완료 시 Stock.stock_qty 감소 확인 | -| 감사 로그 | 모든 재고 변경이 audit_logs에 기록됨 | -| FIFO 적용 | StockLot이 fifo_order 순서대로 차감됨 | - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 메서드 추가, 파라미터 추가, 문서 수정 | 불필요 | -| ⚠️ 컨펌 필요 | Service 로직 변경, 새 이벤트 추가, 마이그레이션 | **필수** | -| 🔴 금지 | 기존 API 응답 구조 변경, Stock 테이블 컬럼 삭제 | 별도 협의 | - -### 1.5 준수 규칙 -- `docs/standards/api-rules.md` - Service-First 패턴 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/specs/database-schema.md` - DB 스키마 규칙 - ---- - -## 2. 현재 시스템 분석 - -### 2.1 데이터 모델 관계 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 현재 상태 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ Item (품목) │ -│ ↓ 1:1 │ -│ Stock (재고현황) ←── 자동 업데이트 없음 ──┐ │ -│ ↓ 1:N │ │ -│ StockLot (LOT별 상세) ←── 자동 생성 없음 ─┤ │ -│ │ │ -│ Receiving (입고) ─── 연결 끊김 ────────────┤ │ -│ WorkOrder (생산) ─── 연결 없음 ────────────┤ │ -│ Order (견적/수주) ─── 연결 없음 ───────────┤ │ -│ Shipment (출하) ─── 연결 없음 ─────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 목표 데이터 흐름 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 목표 상태 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ [입고 완료] ──→ StockLot 생성 ──→ Stock.refreshFromLots() │ -│ │ -│ [자재 투입] ──→ StockLot 차감(FIFO) ──→ Stock.refreshFromLots()│ -│ │ -│ [견적 확정] ──→ Stock.reserved_qty 증가 │ -│ │ -│ [출하 완료] ──→ StockLot 차감 ──→ Stock.refreshFromLots() │ -│ ──→ Stock.reserved_qty 감소 │ -│ │ -│ [모든 변경] ──→ AuditLog 기록 │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 2.3 핵심 파일 위치 - -| 구분 | 경로 | -|------|------| -| **Stock 모델** | `api/app/Models/Tenants/Stock.php` | -| **StockLot 모델** | `api/app/Models/Tenants/StockLot.php` | -| **StockService** | `api/app/Services/StockService.php` | -| **ReceivingService** | `api/app/Services/ReceivingService.php` | -| **WorkOrderService** | `api/app/Services/WorkOrderService.php` | -| **OrderService** | `api/app/Services/OrderService.php` | - ---- - -## 3. 대상 범위 - -### Phase 1: 입고 → 재고 연동 (우선순위 1) ✅ 완료 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | StockService에 이벤트 기반 구조 설계 | ✅ | increaseFromReceiving(), getOrCreateStock() | -| 1.2 | ReceivingService.process() 수정 - Stock 연동 | ✅ | StockService 호출 추가 | -| 1.3 | StockLot 자동 생성 로직 구현 | ✅ | FIFO 순서 자동 계산 | -| 1.4 | 감사 로그 통합 | ✅ | logStockChange() 구현 | -| 1.5 | 단위 테스트 작성 | ⏭️ | 수동 테스트로 대체 | - -### Phase 2: 생산 → 재고 연동 (우선순위 2) ✅ 완료 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | WorkOrderService에 BOM 기반 자재 조회 구현 | ✅ | getMaterials() 실제 재고 연동 | -| 2.2 | 자재 투입 시 Stock 차감 로직 (FIFO) | ✅ | StockService.decreaseFIFO() | -| 2.3 | 작업 완료 시 제품 Stock 증가 로직 | ⏭️ | 추후 구현 (생산품 LOT 생성 시) | -| 2.4 | 단위 테스트 작성 | ⏭️ | 수동 테스트로 대체 | - -### Phase 3: 견적/출하 → 재고 연동 (우선순위 3) ✅ 완료 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | Order 확정 시 reserved_qty 증가 로직 | ✅ | StockService.reserve(), reserveForOrder() | -| 3.2 | Shipment 출하 시 stock_qty 차감 로직 | ✅ | StockService.decreaseForShipment() | -| 3.3 | 예약 취소/변경 처리 로직 | ✅ | StockService.releaseReservation() | - ---- - -## 4. 상세 설계 - -### 4.1 StockService 이벤트 구조 - -```php -// api/app/Services/StockService.php - -class StockService -{ - /** - * 입고 완료 시 재고 증가 - * @param Receiving $receiving - * @return StockLot - */ - public function increaseFromReceiving(Receiving $receiving): StockLot - { - // 1. StockLot 생성 - // 2. Stock.refreshFromLots() 호출 - // 3. 감사 로그 기록 - } - - /** - * 자재 투입 시 재고 차감 (FIFO) - * @param int $itemId - * @param float $qty - * @param string $reason (work_order, shipment 등) - * @param int $referenceId - * @return array 차감된 LOT 정보 - */ - public function decreaseFIFO(int $itemId, float $qty, string $reason, int $referenceId): array - { - // 1. StockLot을 fifo_order 순서로 조회 - // 2. 필요 수량만큼 차감 (여러 LOT에 걸칠 수 있음) - // 3. Stock.refreshFromLots() 호출 - // 4. 감사 로그 기록 - } - - /** - * 재고 예약 - * @param int $itemId - * @param float $qty - * @param int $orderId - */ - public function reserve(int $itemId, float $qty, int $orderId): void - { - // 1. Stock.reserved_qty 증가 - // 2. Stock.available_qty 재계산 - // 3. 감사 로그 기록 - } - - /** - * 예약 해제 - */ - public function releaseReservation(int $itemId, float $qty, int $orderId): void - { - // reserved_qty 감소 - } -} -``` - -### 4.2 ReceivingService 수정 사항 - -```php -// api/app/Services/ReceivingService.php - process() 메서드 수정 - -public function process(Receiving $receiving, array $data): Receiving -{ - return DB::transaction(function () use ($receiving, $data) { - // 기존 로직 유지 - $receiving->update([ - 'receiving_qty' => $data['receiving_qty'], - 'receiving_date' => $data['receiving_date'], - 'lot_no' => $data['lot_no'], - 'status' => 'completed', - ]); - - // 🆕 재고 연동 추가 - app(StockService::class)->increaseFromReceiving($receiving); - - return $receiving->fresh(); - }); -} -``` - -### 4.3 WorkOrderService 수정 사항 - -```php -// api/app/Services/WorkOrderService.php - registerMaterialInput() 수정 - -public function registerMaterialInput(WorkOrder $workOrder, array $data): void -{ - DB::transaction(function () use ($workOrder, $data) { - // 기존 감사 로그 유지 - - // 🆕 재고 차감 추가 - $stockService = app(StockService::class); - - foreach ($data['materials'] as $material) { - $stockService->decreaseFIFO( - itemId: $material['item_id'], - qty: $material['qty'], - reason: 'work_order_input', - referenceId: $workOrder->id - ); - } - }); -} -``` - -### 4.4 감사 로그 구조 - -| 필드 | 값 | -|------|------| -| `auditable_type` | `Stock` | -| `auditable_id` | Stock ID | -| `event` | `stock_increase`, `stock_decrease`, `stock_reserve` | -| `old_values` | 변경 전 수량 | -| `new_values` | 변경 후 수량 + 사유 + 참조 ID | - ---- - -## 5. 작업 절차 - -### Step 1: Phase 1 - 입고 → 재고 연동 - -``` -1.1 StockService 이벤트 메서드 추가 -├── increaseFromReceiving() 구현 -├── 감사 로그 통합 -└── 단위 테스트 - -1.2 ReceivingService.process() 수정 -├── 기존 로직 분석 -├── StockService 호출 추가 -└── 트랜잭션 보장 - -1.3 StockLot 자동 생성 -├── Receiving 정보로 StockLot 생성 -├── fifo_order 자동 계산 -└── Stock.refreshFromLots() 호출 - -1.4 테스트 및 검증 -├── 입고 생성 → 입고처리 → Stock 확인 -└── 감사 로그 확인 -``` - -### Step 2: Phase 2 - 생산 → 재고 연동 - -``` -2.1 BOM 기반 자재 조회 구현 -├── 품목의 BOM 정보 조회 -├── Mock 데이터 제거 -└── 실제 자재 목록 반환 - -2.2 자재 투입 시 Stock 차감 -├── decreaseFIFO() 구현 -├── 여러 LOT 걸쳐 차감 처리 -└── 재고 부족 시 예외 처리 - -2.3 작업 완료 시 제품 Stock 증가 -├── 생산된 제품의 StockLot 생성 -├── Stock.refreshFromLots() 호출 -└── 감사 로그 기록 -``` - -### Step 3: Phase 3 - 견적/출하 → 재고 연동 - -``` -3.1 Order 확정 시 예약 -├── reserve() 호출 -├── available_qty 감소 -└── 오버부킹 방지 검증 - -3.2 Shipment 출하 시 차감 -├── decreaseFIFO() 호출 -├── reserved_qty 동시 감소 -└── 감사 로그 기록 -``` - ---- - -## 6. 컨펌 대기 목록 - -> API 내부 로직 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | ReceivingService.process() | Stock 연동 로직 추가 | 입고 프로세스 | ⏳ 대기 | -| 2 | WorkOrderService.registerMaterialInput() | Stock 차감 로직 추가 | 생산 프로세스 | ⏳ 대기 | -| 3 | ShipmentService (신규) | Stock 차감 로직 추가 | 출하 프로세스 | ⏳ 대기 | - ---- - -## 7. 리스크 및 대응 - -### 7.1 데이터 정합성 리스크 - -| 리스크 | 확률 | 영향 | 대응 | -|--------|------|------|------| -| 트랜잭션 실패 시 Stock만 변경됨 | 중 | 높음 | DB 트랜잭션으로 원자성 보장 | -| 동시 요청 시 재고 충돌 | 중 | 높음 | 비관적 락(FOR UPDATE) 적용 | -| 재고 부족 상태에서 차감 시도 | 높음 | 중 | 사전 검증 + 예외 처리 | - -### 7.2 성능 리스크 - -| 리스크 | 확률 | 영향 | 대응 | -|--------|------|------|------| -| LOT가 많을 경우 FIFO 조회 느림 | 낮음 | 중 | fifo_order 인덱스 확인 | -| refreshFromLots() 빈번 호출 | 중 | 낮음 | 필요 시에만 호출 (이미 구현됨) | - ---- - -## 8. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2025-01-26 | Phase 3 | 견적/출하→재고 연동 구현 완료 | StockService, OrderService, ShipmentService | ✅ | -| 2025-01-26 | Phase 2 | 생산→재고 연동 구현 완료 | StockService, WorkOrderService | ✅ | -| 2025-01-26 | Phase 1 | 입고→재고 연동 구현 완료 | StockService, ReceivingService | ✅ | -| 2025-01-26 | - | 문서 초안 작성 | - | - | - ---- - -## 9. 참고 문서 - -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **DB 스키마**: `docs/specs/database-schema.md` - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경, 1.2 목표 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 1.3 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 3 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 2.3 핵심 파일 위치 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 작업 절차 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 1.3 성공 기준 | -| 8 | 모호한 표현이 없는가? | ✅ | | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 1.2 목표 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 3. 대상 범위 Phase 1 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 2.3 핵심 파일 위치 | -| Q4. 작업 완료 확인 방법은? | ✅ | 1.3 성공 기준 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/welfare-section-plan.md b/plans/archive/welfare-section-plan.md deleted file mode 100644 index 94541ed..0000000 --- a/plans/archive/welfare-section-plan.md +++ /dev/null @@ -1,1021 +0,0 @@ -# 복리후생비 현황 섹션 개발 계획 - -> **작성일**: 2026-01-22 -> **목적**: CEO 대시보드 복리후생비 현황 섹션 완성 (4개 카드 + 모달 API 연동) -> **기준 문서**: `api/app/Swagger/v1/WelfareApi.php` -> **상태**: 🔄 진행중 (Serena ID: welfare-section-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 2 - 프론트엔드 연동 완료 | -| **다음 작업** | 검증 및 테스트 | -| **진행률** | 6/6 (100%) | -| **마지막 업데이트** | 2026-01-22 | - ---- - -## 1. 개요 - -### 1.1 배경 -CEO 대시보드의 복리후생비 현황 섹션은 4개의 카드로 구성됩니다: -1. **당해년도 복리후생비 한도** - 연간 총 한도 -2. **{분기} 복리후생비 총 한도** - 분기별 한도 -3. **{분기} 복리후생비 잔여한도** - 분기별 남은 한도 -4. **{분기} 복리후생비 사용금액** - 분기별 사용 금액 - -현재 상태: -- ✅ 섹션 UI 컴포넌트: 완료 (`WelfareSection.tsx`) -- ✅ 카드 데이터 API: 완료 (`/api/v1/welfare/summary`) -- ✅ 프론트엔드 Hook: 완료 (`useWelfare()`) -- ⚠️ 모달 상세 데이터: Mock 사용 중 (API 연동 필요) - -### 1.2 기준 원칙 -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. API-First: 백엔드 API 완성 후 프론트엔드 연동 │ -│ 2. 기존 패턴 준수: WelfareService 확장 │ -│ 3. Mock 데이터 구조 유지: 기존 모달 설정 형식 호환 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 모달 설정 Mock → API 변환, Transformer 추가 | 불필요 | -| ⚠️ 컨펌 필요 | 새 API 엔드포인트 추가, 서비스 메서드 추가 | **필수** | -| 🔴 금지 | expense_accounts 테이블 구조 변경 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `api/CLAUDE.md` - SAM API Development Rules - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: API 개발 (Backend) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | 모달 상세 데이터 API 개발 | ✅ | `/api/v1/welfare/detail` | -| 1.2 | Swagger 문서 업데이트 | ✅ | WelfareApi.php | - -### 2.2 Phase 2: 프론트엔드 연동 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | 타입 정의 추가 | ✅ | WelfareDetailApiResponse (types.ts) | -| 2.2 | API 함수 추가 | ✅ | useWelfareDetail hook (useCEODashboard.ts) | -| 2.3 | Transformer 추가 | ✅ | transformWelfareDetailResponse (transformers.ts) | -| 2.4 | 모달 설정 동적 생성 | ✅ | CEODashboard.tsx 연동 + fallback | - ---- - -## 3. 작업 절차 - -### 3.1 단계별 절차 - -``` -Step 1: API 개발 (Backend) -├── WelfareService에 getDetail() 메서드 추가 -├── WelfareController에 detail() 액션 추가 -├── routes/api.php에 라우트 등록 -└── Swagger 문서 작성 - -Step 2: 프론트엔드 연동 -├── types.ts에 WelfareDetailApiResponse 추가 -├── useCEODashboard.ts에 fetchWelfareDetail 추가 -├── transformers.ts에 transformWelfareDetailResponse 추가 -└── welfareConfigs.ts를 API 응답 기반으로 수정 -``` - ---- - -## 4. 핵심 참조 코드 (인라인) - -### 4.1 DetailModalConfig 타입 정의 - -**파일**: `react/src/components/business/CEODashboard/types.ts` (라인 414-426) - -```typescript -// 상세 모달 전체 설정 타입 -export interface DetailModalConfig { - title: string; - summaryCards: SummaryCardData[]; - barChart?: BarChartConfig; - pieChart?: PieChartConfig; - horizontalBarChart?: HorizontalBarChartConfig; - comparisonSection?: ComparisonSectionConfig; - referenceTable?: ReferenceTableConfig; - referenceTables?: ReferenceTableConfig[]; - calculationCards?: CalculationCardsConfig; - quarterlyTable?: QuarterlyTableConfig; - table?: TableConfig; -} -``` - -### 4.2 관련 서브 타입 정의 - -```typescript -// 요약 카드 타입 (라인 249-255) -export interface SummaryCardData { - label: string; - value: string | number; - isComparison?: boolean; - isPositive?: boolean; - unit?: string; -} - -// 막대 차트 설정 타입 (라인 265-271) -export interface BarChartConfig { - title: string; - data: BarChartDataItem[]; - dataKey: string; - xAxisKey: string; - color?: string; -} - -// 도넛 차트 설정 타입 (라인 282-285) -export interface PieChartConfig { - title: string; - data: PieChartDataItem[]; -} - -// 도넛 차트 데이터 아이템 (라인 274-279) -export interface PieChartDataItem { - name: string; - value: number; - percentage: number; - color: string; -} - -// 테이블 설정 타입 (라인 332-342) -export interface TableConfig { - title: string; - columns: TableColumnConfig[]; - data: Record[]; - filters?: TableFilterConfig[]; - showTotal?: boolean; - totalLabel?: string; - totalValue?: string | number; - totalColumnKey?: string; - footerSummary?: FooterSummaryItem[]; -} - -// 계산 카드 섹션 설정 타입 (라인 391-395) -export interface CalculationCardsConfig { - title: string; - subtitle?: string; - cards: CalculationCardItem[]; -} - -// 계산 카드 아이템 타입 (라인 383-388) -export interface CalculationCardItem { - label: string; - value: number; - unit?: string; - operator?: '+' | '=' | '-' | '×'; -} - -// 분기별 테이블 설정 타입 (라인 408-411) -export interface QuarterlyTableConfig { - title: string; - rows: QuarterlyTableRow[]; -} - -// 분기별 테이블 행 타입 (라인 398-405) -export interface QuarterlyTableRow { - label: string; - q1?: number | string; - q2?: number | string; - q3?: number | string; - q4?: number | string; - total?: number | string; -} -``` - -### 4.3 현재 Mock 데이터 구조 (welfareConfigs.ts 전체) - -**파일**: `react/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts` - -```typescript -import type { DetailModalConfig } from '../types'; - -export function getWelfareModalConfig(calculationType: 'fixed' | 'ratio'): DetailModalConfig { - // 계산 방식에 따른 조건부 calculationCards 생성 - const calculationCards = calculationType === 'fixed' - ? { - // 직원당 정액 금액/월 방식 - title: '복리후생비 계산', - subtitle: '직원당 정액 금액/월 200,000원', - cards: [ - { label: '직원 수', value: 20, unit: '명' }, - { label: '연간 직원당 월급 금액', value: 2400000, unit: '원', operator: '×' as const }, - { label: '당해년도 복리후생비 총 한도', value: 48000000, unit: '원', operator: '=' as const }, - ], - } - : { - // 연봉 총액 비율 방식 - title: '복리후생비 계산', - subtitle: '연봉 총액 기준 비율 20.5%', - cards: [ - { label: '연봉 총액', value: 1000000000, unit: '원' }, - { label: '비율', value: 20.5, unit: '%', operator: '×' as const }, - { label: '당해년도 복리후생비 총 한도', value: 205000000, unit: '원', operator: '=' as const }, - ], - }; - - return { - title: '복리후생비 상세', - - // 1. 요약 카드 (8개) - summaryCards: [ - // 1행: 당해년도 기준 - { label: '당해년도 복리후생비 계정', value: 3123000, unit: '원' }, - { label: '당해년도 복리후생비 한도', value: 600000, unit: '원' }, - { label: '당해년도 복리후생비 사용', value: 6000000, unit: '원' }, - { label: '당해년도 잔여한도', value: 0, unit: '원' }, - // 2행: 분기 기준 - { label: '1사분기 복리후생비 총 한도', value: 3123000, unit: '원' }, - { label: '1사분기 복리후생비 잔여한도', value: 6000000, unit: '원' }, - { label: '1사분기 복리후생비 사용금액', value: 6000000, unit: '원' }, - { label: '1사분기 복리후생비 초과 금액', value: 6000000, unit: '원' }, - ], - - // 2. 월별 사용 추이 (막대 차트) - barChart: { - title: '월별 복리후생비 사용 추이', - data: [ - { name: '1월', value: 1500000 }, - { name: '2월', value: 1800000 }, - { name: '3월', value: 2200000 }, - { name: '4월', value: 1900000 }, - { name: '5월', value: 2100000 }, - { name: '6월', value: 1700000 }, - ], - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - - // 3. 항목별 사용 비율 (도넛 차트) - pieChart: { - title: '항목별 사용 비율', - data: [ - { name: '식비', value: 55000000, percentage: 55, color: '#FBBF24' }, - { name: '건강검진', value: 25000000, percentage: 5, color: '#60A5FA' }, - { name: '경조사비', value: 10000000, percentage: 10, color: '#F87171' }, - { name: '기타', value: 10000000, percentage: 30, color: '#34D399' }, - ], - }, - - // 4. 일별 사용 내역 (테이블) - table: { - title: '일별 복리후생비 사용 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'cardName', label: '카드명', align: 'left' }, - { key: 'user', label: '사용자', align: 'center' }, - { key: 'date', label: '사용일자', align: 'center', format: 'date' }, - { key: 'store', label: '가맹점명', align: 'left' }, - { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, - { key: 'usageType', label: '사용항목', align: 'center' }, - ], - data: [ - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1000000, usageType: '식비' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1200000, usageType: '건강검진' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1500000, usageType: '경조사비' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 1300000, usageType: '기타' }, - { cardName: '카드명', user: '홍길동', date: '2025-12-12 12:12', store: '가맹점명', amount: 6000000, usageType: '식비' }, - ], - filters: [ - { - key: 'usageType', - options: [ - { value: 'all', label: '전체' }, - { value: '식비', label: '식비' }, - { value: '건강검진', label: '건강검진' }, - { value: '경조사비', label: '경조사비' }, - { value: '기타', label: '기타' }, - ], - defaultValue: 'all', - }, - { - key: 'sortOrder', - options: [ - { value: 'latest', label: '최신순' }, - { value: 'oldest', label: '등록순' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - ], - defaultValue: 'latest', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: 11000000, - totalColumnKey: 'amount', - }, - - // 5. 복리후생비 계산 (조건부 - calculationType에 따라) - calculationCards, - - // 6. 분기별 현황 테이블 - quarterlyTable: { - title: '복리후생비 현황', - rows: [ - { label: '한도금액', q1: 12000000, q2: 12000000, q3: 12000000, q4: 12000000, total: 48000000 }, - { label: '이월금액', q1: 0, q2: '', q3: '', q4: '', total: '' }, - { label: '사용금액', q1: 1000000, q2: '', q3: '', q4: '', total: '' }, - { label: '잔여한도', q1: 11000000, q2: '', q3: '', q4: '', total: '' }, - { label: '초과금액', q1: '', q2: '', q3: '', q4: '', total: '' }, - ], - }, - }; -} -``` - -### 4.4 expense_accounts 테이블 스키마 - -**파일**: `api/database/migrations/2026_01_21_103734_create_expense_accounts_table.php` - -```sql -CREATE TABLE expense_accounts ( - id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '테넌트 ID', - - -- 비용 유형 - account_type VARCHAR(50) NOT NULL COMMENT '계정 유형: welfare, entertainment, etc.', - sub_type VARCHAR(50) NULL COMMENT '세부 유형: meal, gift, etc.', - - -- 비용 정보 - expense_date DATE NOT NULL COMMENT '지출일', - amount DECIMAL(15,2) DEFAULT 0 COMMENT '금액', - description VARCHAR(500) NULL COMMENT '비용 내역', - receipt_no VARCHAR(100) NULL COMMENT '증빙번호', - - -- 거래처 정보 - vendor_id BIGINT UNSIGNED NULL COMMENT '거래처 ID', - vendor_name VARCHAR(200) NULL COMMENT '거래처명 (직접 입력)', - - -- 카드/결제 정보 - payment_method VARCHAR(50) NULL COMMENT '결제수단: card, cash, transfer', - card_no VARCHAR(50) NULL COMMENT '카드 마지막 4자리', - - -- 감사 컬럼 - created_by BIGINT UNSIGNED NULL COMMENT '등록자', - updated_by BIGINT UNSIGNED NULL COMMENT '수정자', - deleted_by BIGINT UNSIGNED NULL COMMENT '삭제자', - - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - deleted_at TIMESTAMP NULL, - - -- 인덱스 - INDEX idx_tenant_type_date (tenant_id, account_type, expense_date), - INDEX idx_tenant_date (tenant_id, expense_date), - - -- 외래키 - FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, - FOREIGN KEY (vendor_id) REFERENCES clients(id) ON DELETE SET NULL -); -``` - -**account_type 값**: -- `welfare` - 복리후생비 -- `entertainment` - 접대비 - -**sub_type 값** (welfare의 경우): -- `meal` - 식비 -- `health_check` - 건강검진 -- `congratulation` - 경조사비 -- `other` - 기타 - ---- - -## 5. API → 모달 설정 변환 매핑 - -### 5.1 API 응답 스키마 (제안) - -```typescript -// 백엔드 API 응답: GET /api/v1/welfare/detail -interface WelfareDetailApiResponse { - // 요약 카드 데이터 - summary: { - annual_account: number; // 당해년도 복리후생비 계정 - annual_limit: number; // 당해년도 복리후생비 한도 - annual_used: number; // 당해년도 복리후생비 사용 - annual_remaining: number; // 당해년도 잔여한도 - quarterly_limit: number; // 분기 복리후생비 총 한도 - quarterly_remaining: number; // 분기 복리후생비 잔여한도 - quarterly_used: number; // 분기 복리후생비 사용금액 - quarterly_exceeded: number; // 분기 복리후생비 초과 금액 - }; - - // 월별 사용 추이 - monthly_usage: { - month: number; // 1-12 - amount: number; - }[]; - - // 항목별 분포 - category_distribution: { - category: string; // meal, health_check, congratulation, other - label: string; // 식비, 건강검진, 경조사비, 기타 - amount: number; - ratio: number; // 백분율 (0-100) - }[]; - - // 일별 사용 내역 - transactions: { - id: number; - card_name: string; - user_name: string; - expense_date: string; // YYYY-MM-DD HH:mm - vendor_name: string; - amount: number; - sub_type: string; - sub_type_label: string; - }[]; - - // 계산 정보 - calculation: { - type: 'fixed' | 'ratio'; - employee_count: number; - monthly_amount?: number; // fixed 방식 - total_salary?: number; // ratio 방식 - ratio?: number; // ratio 방식 (%) - annual_limit: number; - }; - - // 분기별 현황 - quarterly: { - quarter: number; // 1-4 - limit: number; - carryover: number; - used: number; - remaining: number; - exceeded: number; - }[]; -} -``` - -### 5.2 변환 매핑 테이블 - -| API 필드 | DetailModalConfig 필드 | 변환 로직 | -|----------|----------------------|----------| -| `summary.annual_account` | `summaryCards[0].value` | 직접 매핑 | -| `summary.annual_limit` | `summaryCards[1].value` | 직접 매핑 | -| `summary.annual_used` | `summaryCards[2].value` | 직접 매핑 | -| `summary.annual_remaining` | `summaryCards[3].value` | 직접 매핑 | -| `summary.quarterly_limit` | `summaryCards[4].value` | 라벨에 분기 동적 삽입 | -| `summary.quarterly_remaining` | `summaryCards[5].value` | 라벨에 분기 동적 삽입 | -| `summary.quarterly_used` | `summaryCards[6].value` | 라벨에 분기 동적 삽입 | -| `summary.quarterly_exceeded` | `summaryCards[7].value` | 라벨에 분기 동적 삽입 | -| `monthly_usage[]` | `barChart.data[]` | `{ name: '${month}월', value: amount }` | -| `category_distribution[]` | `pieChart.data[]` | 색상 매핑 추가 필요 | -| `transactions[]` | `table.data[]` | 필드명 camelCase 변환 | -| `calculation` | `calculationCards` | type에 따라 분기 | -| `quarterly[]` | `quarterlyTable.rows[]` | 행/열 피벗 변환 | - -### 5.3 색상 매핑 (카테고리별) - -```typescript -const CATEGORY_COLORS: Record = { - meal: '#FBBF24', // 식비 - 노란색 - health_check: '#60A5FA', // 건강검진 - 파란색 - congratulation: '#F87171', // 경조사비 - 빨간색 - other: '#34D399', // 기타 - 초록색 -}; -``` - ---- - -## 6. 상세 작업 내용 - -### 6.1 Phase 1: API 개발 - -#### 1.1 WelfareService 확장 - -**파일**: `api/app/Services/WelfareService.php` - -**추가할 메서드**: -```php -/** - * 복리후생비 상세 정보 조회 (모달용) - */ -public function getDetail( - ?string $calculationType = 'fixed', - ?int $fixedAmountPerMonth = 200000, - ?float $ratio = 0.05, - ?int $year = null, - ?int $quarter = null -): array { - // 1. 요약 데이터 조회 - // 2. 월별 사용 추이 조회 - // 3. 항목별 분포 조회 - // 4. 일별 사용 내역 조회 - // 5. 계산 정보 생성 - // 6. 분기별 현황 조회 -} -``` - -**필요한 쿼리**: -```php -// 월별 사용 추이 -DB::table('expense_accounts') - ->select(DB::raw('MONTH(expense_date) as month'), DB::raw('SUM(amount) as amount')) - ->where('tenant_id', $tenantId) - ->where('account_type', 'welfare') - ->whereYear('expense_date', $year) - ->whereNull('deleted_at') - ->groupBy(DB::raw('MONTH(expense_date)')) - ->orderBy('month') - ->get(); - -// 항목별 분포 -DB::table('expense_accounts') - ->select('sub_type', DB::raw('SUM(amount) as amount')) - ->where('tenant_id', $tenantId) - ->where('account_type', 'welfare') - ->whereBetween('expense_date', [$startDate, $endDate]) - ->whereNull('deleted_at') - ->groupBy('sub_type') - ->get(); - -// 일별 사용 내역 -DB::table('expense_accounts') - ->select('id', 'card_no', 'created_by', 'expense_date', 'vendor_name', 'amount', 'sub_type') - ->where('tenant_id', $tenantId) - ->where('account_type', 'welfare') - ->whereBetween('expense_date', [$startDate, $endDate]) - ->whereNull('deleted_at') - ->orderByDesc('expense_date') - ->get(); -``` - -#### 1.2 WelfareController 확장 - -**파일**: `api/app/Http/Controllers/Api/V1/WelfareController.php` - -**추가할 메서드**: -```php -/** - * 복리후생비 상세 조회 (모달용) - */ -public function detail(Request $request): JsonResponse -{ - $calculationType = $request->query('calculation_type', 'fixed'); - $fixedAmountPerMonth = $request->query('fixed_amount_per_month') - ? (int) $request->query('fixed_amount_per_month') - : 200000; - $ratio = $request->query('ratio') - ? (float) $request->query('ratio') - : 0.05; - $year = $request->query('year') ? (int) $request->query('year') : null; - $quarter = $request->query('quarter') ? (int) $request->query('quarter') : null; - - return ApiResponse::handle(function () use ($calculationType, $fixedAmountPerMonth, $ratio, $year, $quarter) { - return $this->welfareService->getDetail( - $calculationType, - $fixedAmountPerMonth, - $ratio, - $year, - $quarter - ); - }, __('message.fetched')); -} -``` - -#### 1.3 라우트 등록 - -**파일**: `api/routes/api.php` - -```php -Route::prefix('welfare')->group(function () { - Route::get('/summary', [WelfareController::class, 'summary']); - Route::get('/detail', [WelfareController::class, 'detail']); // 추가 -}); -``` - -### 6.2 Phase 2: 프론트엔드 연동 - -#### 2.1 타입 정의 추가 - -**파일**: `react/src/lib/api/dashboard/types.ts` - -```typescript -// Welfare Detail API 응답 타입 -export interface WelfareDetailApiResponse { - summary: { - annual_account: number; - annual_limit: number; - annual_used: number; - annual_remaining: number; - quarterly_limit: number; - quarterly_remaining: number; - quarterly_used: number; - quarterly_exceeded: number; - }; - monthly_usage: { - month: number; - amount: number; - }[]; - category_distribution: { - category: string; - label: string; - amount: number; - ratio: number; - }[]; - transactions: { - id: number; - card_name: string; - user_name: string; - expense_date: string; - vendor_name: string; - amount: number; - sub_type: string; - sub_type_label: string; - }[]; - calculation: { - type: 'fixed' | 'ratio'; - employee_count: number; - monthly_amount?: number; - total_salary?: number; - ratio?: number; - annual_limit: number; - }; - quarterly: { - quarter: number; - limit: number; - carryover: number; - used: number; - remaining: number; - exceeded: number; - }[]; -} -``` - -#### 2.2 API 함수 추가 - -**파일**: `react/src/hooks/useCEODashboard.ts` - -```typescript -export async function fetchWelfareDetail( - options: { - calculationType?: 'fixed' | 'ratio'; - fixedAmountPerMonth?: number; - ratio?: number; - year?: number; - quarter?: number; - } -): Promise { - const params = new URLSearchParams(); - if (options.calculationType) params.append('calculation_type', options.calculationType); - if (options.fixedAmountPerMonth) params.append('fixed_amount_per_month', options.fixedAmountPerMonth.toString()); - if (options.ratio) params.append('ratio', options.ratio.toString()); - if (options.year) params.append('year', options.year.toString()); - if (options.quarter) params.append('quarter', options.quarter.toString()); - - return fetchApi(`welfare/detail?${params.toString()}`); -} -``` - -#### 2.3 Transformer 추가 - -**파일**: `react/src/lib/api/dashboard/transformers.ts` - -```typescript -const CATEGORY_COLORS: Record = { - meal: '#FBBF24', - health_check: '#60A5FA', - congratulation: '#F87171', - other: '#34D399', -}; - -export function transformWelfareDetailToModalConfig( - api: WelfareDetailApiResponse, - quarter: number -): DetailModalConfig { - const quarterLabel = `${quarter}사분기`; - - return { - title: '복리후생비 상세', - - summaryCards: [ - { label: '당해년도 복리후생비 계정', value: api.summary.annual_account, unit: '원' }, - { label: '당해년도 복리후생비 한도', value: api.summary.annual_limit, unit: '원' }, - { label: '당해년도 복리후생비 사용', value: api.summary.annual_used, unit: '원' }, - { label: '당해년도 잔여한도', value: api.summary.annual_remaining, unit: '원' }, - { label: `${quarterLabel} 복리후생비 총 한도`, value: api.summary.quarterly_limit, unit: '원' }, - { label: `${quarterLabel} 복리후생비 잔여한도`, value: api.summary.quarterly_remaining, unit: '원' }, - { label: `${quarterLabel} 복리후생비 사용금액`, value: api.summary.quarterly_used, unit: '원' }, - { label: `${quarterLabel} 복리후생비 초과 금액`, value: api.summary.quarterly_exceeded, unit: '원' }, - ], - - barChart: { - title: '월별 복리후생비 사용 추이', - data: api.monthly_usage.map(m => ({ name: `${m.month}월`, value: m.amount })), - dataKey: 'value', - xAxisKey: 'name', - color: '#60A5FA', - }, - - pieChart: { - title: '항목별 사용 비율', - data: api.category_distribution.map(c => ({ - name: c.label, - value: c.amount, - percentage: c.ratio, - color: CATEGORY_COLORS[c.category] || '#9CA3AF', - })), - }, - - table: { - title: '일별 복리후생비 사용 내역', - columns: [ - { key: 'no', label: 'No.', align: 'center' }, - { key: 'cardName', label: '카드명', align: 'left' }, - { key: 'user', label: '사용자', align: 'center' }, - { key: 'date', label: '사용일자', align: 'center', format: 'date' }, - { key: 'store', label: '가맹점명', align: 'left' }, - { key: 'amount', label: '사용금액', align: 'right', format: 'currency' }, - { key: 'usageType', label: '사용항목', align: 'center' }, - ], - data: api.transactions.map((t, i) => ({ - no: i + 1, - cardName: t.card_name, - user: t.user_name, - date: t.expense_date, - store: t.vendor_name, - amount: t.amount, - usageType: t.sub_type_label, - })), - filters: [ - { - key: 'usageType', - options: [ - { value: 'all', label: '전체' }, - { value: '식비', label: '식비' }, - { value: '건강검진', label: '건강검진' }, - { value: '경조사비', label: '경조사비' }, - { value: '기타', label: '기타' }, - ], - defaultValue: 'all', - }, - { - key: 'sortOrder', - options: [ - { value: 'latest', label: '최신순' }, - { value: 'oldest', label: '등록순' }, - { value: 'amountDesc', label: '금액 높은순' }, - { value: 'amountAsc', label: '금액 낮은순' }, - ], - defaultValue: 'latest', - }, - ], - showTotal: true, - totalLabel: '합계', - totalValue: api.transactions.reduce((sum, t) => sum + t.amount, 0), - totalColumnKey: 'amount', - }, - - calculationCards: api.calculation.type === 'fixed' - ? { - title: '복리후생비 계산', - subtitle: `직원당 정액 금액/월 ${(api.calculation.monthly_amount || 0).toLocaleString()}원`, - cards: [ - { label: '직원 수', value: api.calculation.employee_count, unit: '명' }, - { label: '연간 직원당 월급 금액', value: (api.calculation.monthly_amount || 0) * 12, unit: '원', operator: '×' }, - { label: '당해년도 복리후생비 총 한도', value: api.calculation.annual_limit, unit: '원', operator: '=' }, - ], - } - : { - title: '복리후생비 계산', - subtitle: `연봉 총액 기준 비율 ${api.calculation.ratio}%`, - cards: [ - { label: '연봉 총액', value: api.calculation.total_salary || 0, unit: '원' }, - { label: '비율', value: api.calculation.ratio || 0, unit: '%', operator: '×' }, - { label: '당해년도 복리후생비 총 한도', value: api.calculation.annual_limit, unit: '원', operator: '=' }, - ], - }, - - quarterlyTable: { - title: '복리후생비 현황', - rows: [ - { - label: '한도금액', - q1: api.quarterly.find(q => q.quarter === 1)?.limit || '', - q2: api.quarterly.find(q => q.quarter === 2)?.limit || '', - q3: api.quarterly.find(q => q.quarter === 3)?.limit || '', - q4: api.quarterly.find(q => q.quarter === 4)?.limit || '', - total: api.quarterly.reduce((sum, q) => sum + q.limit, 0), - }, - { - label: '이월금액', - q1: api.quarterly.find(q => q.quarter === 1)?.carryover || '', - q2: api.quarterly.find(q => q.quarter === 2)?.carryover || '', - q3: api.quarterly.find(q => q.quarter === 3)?.carryover || '', - q4: api.quarterly.find(q => q.quarter === 4)?.carryover || '', - total: '', - }, - { - label: '사용금액', - q1: api.quarterly.find(q => q.quarter === 1)?.used || '', - q2: api.quarterly.find(q => q.quarter === 2)?.used || '', - q3: api.quarterly.find(q => q.quarter === 3)?.used || '', - q4: api.quarterly.find(q => q.quarter === 4)?.used || '', - total: api.quarterly.reduce((sum, q) => sum + q.used, 0), - }, - { - label: '잔여한도', - q1: api.quarterly.find(q => q.quarter === 1)?.remaining || '', - q2: api.quarterly.find(q => q.quarter === 2)?.remaining || '', - q3: api.quarterly.find(q => q.quarter === 3)?.remaining || '', - q4: api.quarterly.find(q => q.quarter === 4)?.remaining || '', - total: '', - }, - { - label: '초과금액', - q1: api.quarterly.find(q => q.quarter === 1)?.exceeded || '', - q2: api.quarterly.find(q => q.quarter === 2)?.exceeded || '', - q3: api.quarterly.find(q => q.quarter === 3)?.exceeded || '', - q4: api.quarterly.find(q => q.quarter === 4)?.exceeded || '', - total: '', - }, - ], - }, - }; -} -``` - -#### 2.4 모달 설정 동적 생성 - -**파일**: `react/src/components/business/CEODashboard/modalConfigs/welfareConfigs.ts` - -```typescript -import type { DetailModalConfig } from '../types'; -import { fetchWelfareDetail, transformWelfareDetailToModalConfig } from '@/lib/api/dashboard'; - -// 기존 Mock 함수 (fallback용) -export function getWelfareModalConfigMock(calculationType: 'fixed' | 'ratio'): DetailModalConfig { - // ... 기존 Mock 코드 유지 -} - -// 새로운 API 기반 함수 -export async function getWelfareModalConfigFromApi( - options: { - calculationType: 'fixed' | 'ratio'; - fixedAmountPerMonth?: number; - ratio?: number; - year?: number; - quarter?: number; - } -): Promise { - try { - const apiData = await fetchWelfareDetail(options); - return transformWelfareDetailToModalConfig(apiData, options.quarter || getCurrentQuarter()); - } catch (error) { - console.error('[Welfare] Failed to fetch detail, using mock data:', error); - return getWelfareModalConfigMock(options.calculationType); - } -} - -function getCurrentQuarter(): number { - return Math.ceil((new Date().getMonth() + 1) / 3); -} -``` - ---- - -## 7. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 새 API 엔드포인트 | `GET /api/v1/welfare/detail` 추가 | api | ✅ 완료 | -| 2 | WelfareService 확장 | getDetail() 메서드 추가 | api | ✅ 완료 | - ---- - -## 8. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-22 | - | 문서 초안 작성 | - | - | -| 2026-01-22 | - | 자기완결성 보완 (타입, 스키마, 매핑 추가) | - | - | -| 2026-01-22 | Phase 1.1 | getDetail() 메서드 추가 | WelfareService.php | ✅ | -| 2026-01-22 | Phase 1.1 | detail() 액션 추가 | WelfareController.php | ✅ | -| 2026-01-22 | Phase 1.1 | /welfare/detail 라우트 추가 | routes/api.php | ✅ | -| 2026-01-22 | Phase 1.2 | Swagger 스키마 및 엔드포인트 추가 | WelfareApi.php | ✅ | -| 2026-01-22 | Phase 2.1 | WelfareDetailApiResponse 타입 추가 | types.ts | ✅ | -| 2026-01-22 | Phase 2.2 | useWelfareDetail hook 추가 | useCEODashboard.ts | ✅ | -| 2026-01-22 | Phase 2.3 | transformWelfareDetailResponse 추가 | transformers.ts | ✅ | -| 2026-01-22 | Phase 2.4 | 모달 설정 API 연동 + fallback | CEODashboard.tsx, welfareConfigs.ts | ✅ | - ---- - -## 9. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **API 규칙**: `api/CLAUDE.md` (SAM API Development Rules) -- **Swagger 가이드**: `docs/guides/swagger-guide.md` - ---- - -## 10. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 10.1 세션 시작 시 (Load Strategy) -```javascript -// 순차적 로드 -read_memory("welfare-section-state") // 1. 상태 파악 -read_memory("welfare-section-snapshot") // 2. 사고 흐름 복구 -``` - -### 10.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | 🛠 **Snapshot** | `write_memory("welfare-section-snapshot", "코드변경+논의요약")` | -| **20% 이하** | 🧹 **Context Purge** | `write_memory("welfare-section-active-symbols", "주요 수정 파일/함수")` | -| **10% 이하** | 🛑 **Stop & Save** | 최종 상태 저장 후 세션 교체 권고 | - -### 10.3 Serena 메모리 구조 -- `welfare-section-state`: { phase, progress, next_step, last_decision } -- `welfare-section-snapshot`: 현재까지의 논의 및 코드 변경점 요약 -- `welfare-section-active-symbols`: 현재 수정 중인 파일/심볼 리스트 - ---- - -## 11. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 11.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| 모달 클릭 (fixed) | 정액 방식 계산 표시 | - | ⏳ | -| 모달 클릭 (ratio) | 비율 방식 계산 표시 | - | ⏳ | -| 분기 변경 (Q1→Q2) | 해당 분기 데이터 표시 | - | ⏳ | - -### 11.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 4개 카드 데이터 실시간 반영 | ✅ | API 연동 완료 상태 | -| 모달 상세 데이터 API 연동 | ✅ | Backend API + Frontend hook 완료 | -| Mock 데이터 제거 | ✅ | API 우선, Mock fallback 유지 | - ---- - -## 12. 자기완결성 점검 결과 - -### 12.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 모달 데이터 API 연동 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 11.2 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | 2. 대상 범위 참조 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 1 → Phase 2 순서 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 4. 핵심 참조 코드 인라인 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 6. 상세 작업 내용 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 11.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 코드 스니펫 포함 | - -### 12.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 3.1 단계별 절차 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 6. 상세 작업 내용 | -| Q4. DetailModalConfig 구조는? | ✅ | 4.1, 4.2 타입 정의 | -| Q5. Mock 데이터 구조는? | ✅ | 4.3 현재 Mock 데이터 | -| Q6. DB 테이블 스키마는? | ✅ | 4.4 expense_accounts | -| Q7. API → 모달 변환 방법은? | ✅ | 5. 변환 매핑 | -| Q8. 작업 완료 확인 방법은? | ✅ | 11. 검증 결과 | -| Q9. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 | - -**결과**: 9/9 통과 → ✅ 자기완결성 확보 - -### 12.3 보완 이력 - -| 날짜 | 항목 | 원본 | 보완 내용 | -|------|------|------|----------| -| 2026-01-22 | DetailModalConfig | 없음 | 타입 정의 전체 인라인 | -| 2026-01-22 | Mock 데이터 | 없음 | welfareConfigs.ts 전체 인라인 | -| 2026-01-22 | DB 스키마 | 없음 | expense_accounts 테이블 구조 | -| 2026-01-22 | 변환 매핑 | 없음 | API → 모달 매핑 테이블 및 코드 | - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/archive/work-order-plan.md b/plans/archive/work-order-plan.md deleted file mode 100644 index 56c5c1b..0000000 --- a/plans/archive/work-order-plan.md +++ /dev/null @@ -1,409 +0,0 @@ -# 작업지시 (Work Orders) API 연동 계획 - -> **작성일**: 2025-01-08 -> **목적**: 작업지시 기능 검증 및 테스트 -> **상태**: ✅ 전체 테스트 완료 (2025-01-11) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 전체 기능 테스트 완료 (2025-01-11) | -| **다음 작업** | 운영 준비 | -| **진행률** | 5/5 (100%) | -| **마지막 업데이트** | 2025-01-11 | - ---- - -## 1. 개요 - -### 1.1 기능 설명 -작업지시는 MES 시스템의 핵심 기능으로, 수주를 기반으로 실제 생산 작업을 지시하고 추적합니다. -공정 유형별(스크린/슬랫/절곡)로 작업 단계를 관리하며, 담당자 배정 및 작업 상태를 추적합니다. - -### 1.2 현재 구현 상태 분석 - -#### API (Laravel) - ✅ 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|:----:| -| Model | `api/app/Models/Production/WorkOrder.php` | ✅ | -| Model | `api/app/Models/Production/WorkOrderItem.php` | ✅ | -| Model | `api/app/Models/Production/WorkOrderBendingDetail.php` | ✅ | -| Model | `api/app/Models/Production/WorkOrderIssue.php` | ✅ | -| Service | `api/app/Services/WorkOrderService.php` | ✅ | -| Controller | `api/app/Http/Controllers/Api/V1/WorkOrderController.php` | ✅ | -| FormRequest | `api/app/Http/Requests/WorkOrder/*.php` | ✅ | -| Route | `/api/v1/work-orders` | ✅ | - -#### Frontend (React/Next.js) - ✅ API 연동 완료 -| 구성요소 | 파일 경로 | 상태 | -|---------|----------|:----:| -| 목록 페이지 | `react/src/app/[locale]/(protected)/production/work-orders/page.tsx` | ✅ | -| 등록 페이지 | `react/src/app/[locale]/(protected)/production/work-orders/create/page.tsx` | ✅ | -| 상세 페이지 | `react/src/app/[locale]/(protected)/production/work-orders/[id]/page.tsx` | ✅ | -| 목록 컴포넌트 | `react/src/components/production/WorkOrders/WorkOrderList.tsx` | ✅ | -| 등록 컴포넌트 | `react/src/components/production/WorkOrders/WorkOrderCreate.tsx` | ✅ | -| 상세 컴포넌트 | `react/src/components/production/WorkOrders/WorkOrderDetail.tsx` | ✅ | -| 수주선택 모달 | `react/src/components/production/WorkOrders/SalesOrderSelectModal.tsx` | ✅ | -| 담당자선택 모달 | `react/src/components/production/WorkOrders/AssigneeSelectModal.tsx` | ✅ | -| 타입 정의 | `react/src/components/production/WorkOrders/types.ts` | ✅ | -| **actions.ts** | `react/src/components/production/WorkOrders/actions.ts` | ✅ | - -### 1.3 관련 URL -| 화면 | URL | 설명 | -|------|-----|------| -| 작업지시목록 | `/production/work-orders` | 상태별 필터링, 검색 | -| 작업지시등록 | `/production/work-orders/create` | 모달 - 수주선택 | -| 작업지시상세 | `/production/work-orders/{id}` | 상세 정보 | - -### 1.4 연관관계 -``` -┌─────────────────┐ ┌─────────────────┐ -│ Order │────sales_order_id──▶│ WorkOrder │ -│ (수주) │ │ (작업지시) │ -└─────────────────┘ └─────────────────┘ - │ - ┌───────────────────────────────────────┼───────────────────────────────────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ WorkOrderItem │ │WorkOrderBending │ │ WorkOrderIssue │ -│ (작업품목) │ │ Detail │ │ (이슈) │ -└─────────────────┘ │ (절곡상세) │ └─────────────────┘ - └─────────────────┘ - │ - │ work_order_id - ▼ - ┌─────────────────┐ - │ WorkResult │ - │ (작업실적) │ - └─────────────────┘ -``` - ---- - -## 2. API 엔드포인트 - -### 2.1 REST API (구현 완료) -| Method | Endpoint | 설명 | 상태 | -|--------|----------|------|:----:| -| GET | `/api/v1/work-orders` | 목록 조회 (필터/페이징) | ✅ | -| GET | `/api/v1/work-orders/stats` | 통계 조회 | ✅ | -| GET | `/api/v1/work-orders/{id}` | 상세 조회 | ✅ | -| POST | `/api/v1/work-orders` | 작업지시 생성 | ✅ | -| PUT | `/api/v1/work-orders/{id}` | 작업지시 수정 | ✅ | -| DELETE | `/api/v1/work-orders/{id}` | 작업지시 삭제 | ✅ | -| PATCH | `/api/v1/work-orders/{id}/status` | 상태 변경 | ✅ | -| PATCH | `/api/v1/work-orders/{id}/assign` | 담당자 배정 | ✅ | -| PATCH | `/api/v1/work-orders/{id}/bending/toggle` | 절곡 상세 토글 | ✅ | -| POST | `/api/v1/work-orders/{id}/issues` | 이슈 등록 | ✅ | -| PATCH | `/api/v1/work-orders/{id}/issues/{issueId}/resolve` | 이슈 해결 | ✅ | - -### 2.2 actions.ts 구현 함수 (완료) -```typescript -// 목록/조회 -getWorkOrders(params) // 목록 조회 -getWorkOrderStats() // 통계 조회 -getWorkOrderById(id) // 상세 조회 - -// CRUD -createWorkOrder(data) // 생성 -updateWorkOrder(id, data) // 수정 -deleteWorkOrder(id) // 삭제 - -// 상태/배정 -updateWorkOrderStatus(id, status) // 상태 변경 -assignWorkOrder(id, data) // 담당자 배정 - -// 절곡 공정 -toggleBendingField(id, field, value) // 절곡 상세 토글 - -// 이슈 관리 -addWorkOrderIssue(id, data) // 이슈 등록 -resolveWorkOrderIssue(id, issueId) // 이슈 해결 - -// 연동 -getSalesOrdersForWorkOrder() // 수주 목록 (작업지시용) -getDepartmentsWithUsers() // 부서/사용자 목록 (담당자 배정용) -``` - ---- - -## 3. 데이터 스키마 - -### 3.1 WorkOrder (작업지시) -```typescript -interface WorkOrder { - id: string; - workOrderNo: string; // WO202512260001 - lotNo: string; // 수주번호 참조 - processType: 'screen' | 'slat' | 'bending'; - status: WorkOrderStatus; - // 기본 정보 - client: string; // 발주처 - projectName: string; // 현장명 - dueDate: string; // 납기일 - assignee: string; // 작업자 - // 날짜 - orderDate: string; // 지시일 - shipmentDate: string; // 출고예정일 - // 플래그 - isAssigned: boolean; - isStarted: boolean; - priority: number; // 1~9 - // 품목 - items: WorkOrderItem[]; - // 공정 진행 - currentStep: number; - // 절곡 전용 - bendingDetails?: BendingDetail[]; - // 이슈 - issues?: WorkOrderIssue[]; - note?: string; -} -``` - -### 3.2 WorkOrderStatus (상태) -```typescript -type WorkOrderStatus = - | 'unassigned' // 미배정 - | 'pending' // 승인대기 - | 'waiting' // 작업대기 - | 'in_progress' // 작업중 - | 'completed' // 작업완료 - | 'shipped'; // 출하완료 -``` - -### 3.3 ProcessType (공정 유형) -```typescript -type ProcessType = 'screen' | 'slat' | 'bending'; - -// 공정별 작업 단계 -const SCREEN_STEPS = ['원단절단', '미싱', '앤드락작업', '중간검사', '포장']; -const SLAT_STEPS = ['코일절단', '성형', '미미작업', '검사', '포장']; -const BENDING_STEPS = ['가이드레일 제작', '케이스 제작', '하단마감재 제작', '검사']; -``` - ---- - -## 4. 작업 범위 - -### Phase 1: 검증 및 테스트 ✅ 완료 (2025-01-11) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | 목록 조회 테스트 | ✅ | 필터링/검색/페이징 정상 | -| 1.2 | 등록 기능 테스트 | ✅ | 수주 선택 모달 동작 확인 | -| 1.3 | 상세 조회 테스트 | ✅ | 버그 수정 완료 (site_name 컬럼 수정) | -| 1.4 | 상태 변경 테스트 | ✅ | 전체 상태 전이 검증 완료 | -| 1.5 | 담당자 배정 테스트 | ✅ | 배정 시 상태 자동 전이 확인 | - -**Phase 1 테스트 상세:** -- **버그 수정**: WorkOrderService.php:119 - `project_name` → `site_name` (Order 모델에 맞춤) -- **상태 전이**: pending ⇄ waiting ⇄ in_progress ⇄ completed ⇄ shipped 모두 정상 -- **담당자 배정**: 배정 시 unassigned → pending 자동 전이 확인 - -### Phase 2: 공정별 기능 테스트 ✅ 완료 (2025-01-11) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | 스크린 공정 작업지시 | ✅ | process_id=2 생성 확인 | -| 2.2 | 슬랫 공정 작업지시 | ✅ | process_id=1 생성 확인 | -| 2.3 | 공정별 필터링 | ✅ | forProcess(), forProcessName() 정상 | -| 2.4 | 작업지시 품목 관리 | ✅ | WorkOrderItem CRUD 확인 | - -**Phase 2 테스트 상세:** -- **공정 목록**: 슬랫(P-001), 스크린(P-002) 활성화 확인 -- **공정별 필터**: `forProcess(1)`, `forProcessName('슬랫')` 정상 동작 -- **품목 관리**: 작업지시별 품목 추가/조회 정상 - -### Phase 3: 이슈 및 연동 ✅ 완료 (2025-01-11) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 이슈 등록 기능 | ✅ | 이슈 생성 정상 | -| 3.2 | 이슈 해결 기능 | ✅ | 해결 상태/시간 저장 확인 | -| 3.3 | 수주 연동 확인 | ✅ | salesOrder 관계 정상 | -| 3.4 | 작업실적 연동 | ⏭️ | 후순위 (별도 기능) | - -**Phase 3 테스트 상세:** -- **이슈 관리**: 등록(open) → 해결(resolved) 전체 흐름 정상 -- **open_issues_count**: 미해결 이슈 카운트 속성 정상 -- **수주 연동**: WorkOrder.salesOrder 관계를 통한 수주 정보 조회 정상 - ---- - -## 5. 주요 기능 상세 - -### 5.1 수주 선택 (모달) -``` -작업지시 등록 - │ - ▼ "수주 선택" 버튼 -┌─────────────────────────────────┐ -│ SalesOrderSelectModal │ -│ - 수주 목록 (for_work_order=1) │ -│ - 검색 기능 │ -│ - 선택 시 정보 자동 채움 │ -└─────────────────────────────────┘ -``` - -### 5.2 상태 흐름 -``` -unassigned (미배정) - │ - ▼ 담당자 배정 -pending (승인대기) - │ - ▼ 승인 -waiting (작업대기) - │ - ▼ 작업 시작 -in_progress (작업중) - │ - ▼ 작업 완료 -completed (작업완료) - │ - ▼ 출하 -shipped (출하완료) -``` - -### 5.3 공정별 작업 단계 - -#### 스크린 공정 (screen) -1. 원단절단 (cutting) -2. 미싱 (sewing) -3. 앤드락작업 (endlock) -4. 중간검사 (inspection) -5. 포장 (packing) - -#### 슬랫 공정 (slat) -1. 코일절단 (coil_cutting) -2. 성형 (forming) -3. 미미작업 (finishing) -4. 검사 (inspection) -5. 포장 (packing) - -#### 절곡 공정 (bending) -1. 가이드레일 제작 (guide_rail) -2. 케이스 제작 (case) -3. 하단마감재 제작 (bottom_finish) -4. 검사 (inspection) - -### 5.4 절곡 상세 토글 -- 절곡 공정의 세부 항목 완료 여부 토글 -- `PATCH /api/v1/work-orders/{id}/bending/toggle` -- 필드: shaft_cutting, bearing, shaft_welding, assembly 등 - -### 5.5 이슈 관리 -- 작업 중 발생한 이슈 등록 -- 우선순위: low, medium, high -- 상태: pending → resolved - ---- - -## 6. 의존성 - -### 6.1 필수 선행 작업 -- **공정관리 (Process)**: 공정 유형 정의 - ✅ 완료 -- **사원관리**: 담당자 배정 (assignee_id) -- **부서관리**: 팀 배정 (team_id) - -### 6.2 관련 의존성 -- **수주관리 (Order)**: 수주 데이터 필요 (sales_order_id) - - ✅ Order API 연동 완료 (2025-01-09) - - 수주 → 생산지시 생성 기능 연동됨 - -### 6.3 후속 연동 -- **작업실적 (WorkResult)**: 작업 완료 후 실적 등록 -- **품질검사**: 검사 공정 연동 -- **출하관리**: 출하 처리 - ---- - -## 7. 검증 방법 - -### 7.1 테스트 체크리스트 - -| 기능 | 테스트 항목 | 예상 결과 | -|------|-----------|----------| -| 목록 조회 | 페이지 로드 | 작업지시 목록 표시 | -| 상태 필터 | "작업중" 탭 클릭 | 해당 상태만 표시 | -| 검색 | 작업지시번호 검색 | 필터링된 결과 | -| 등록 | 새 작업지시 등록 | 목록에 추가됨 | -| 상세 조회 | 행 클릭 | 상세 정보 표시 | -| 상태 변경 | 상태 버튼 클릭 | 상태 전환됨 | -| 담당자 배정 | 배정 버튼 클릭 | 담당자 변경됨 | -| 이슈 등록 | 이슈 추가 | 이슈 목록에 표시 | - -### 7.2 API 테스트 -```bash -# 목록 조회 -curl -X GET "http://api.sam.kr/api/v1/work-orders" -H "X-Api-Key: ..." - -# 상세 조회 -curl -X GET "http://api.sam.kr/api/v1/work-orders/1" -H "X-Api-Key: ..." - -# 통계 조회 -curl -X GET "http://api.sam.kr/api/v1/work-orders/stats" -H "X-Api-Key: ..." - -# 상태 변경 -curl -X PATCH "http://api.sam.kr/api/v1/work-orders/1/status" \ - -H "X-Api-Key: ..." \ - -H "Content-Type: application/json" \ - -d '{"status": "in_progress"}' -``` - ---- - -## 8. 참고 사항 - -### 8.1 작업지시번호 형식 -- 형식: `WO{YYYYMMDD}{NNNN}` -- 예: `WO202512260001` -- 자동 생성: `WorkOrderService::generateWorkOrderNo()` - -### 8.2 Worker Screen (작업자 화면) -- 별도 화면: `/production/worker-screen` -- 작업자가 직접 작업 진행/완료 처리 -- 이슈 보고 기능 -- `react/src/components/production/WorkerScreen/` 참고 - -### 8.3 Production Dashboard -- 생산 현황 대시보드: `/production/dashboard` -- 공정별 작업 현황 시각화 -- `react/src/components/production/ProductionDashboard/` 참고 - ---- - -## 9. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` - -### 참고 코드 -- **Controller**: `api/app/Http/Controllers/Api/V1/WorkOrderController.php` -- **Service**: `api/app/Services/WorkOrderService.php` -- **actions.ts**: `react/src/components/production/WorkOrders/actions.ts` - ---- - -## 10. 자기완결성 점검 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 검증 및 테스트 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 7 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3 테스트 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Order API 연동 완료 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 모든 경로 검증됨 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 테스트 체크리스트 제공 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | curl + 체크리스트 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 경로 명시 | - ---- - -*이 문서는 독립 세션에서 바로 작업 시작 가능하도록 설계되었습니다.* \ No newline at end of file diff --git a/plans/bending-preproduction-stock-plan.md b/plans/bending-preproduction-stock-plan.md deleted file mode 100644 index 352ae35..0000000 --- a/plans/bending-preproduction-stock-plan.md +++ /dev/null @@ -1,838 +0,0 @@ -# 절곡품 선생산 → 재고 적재 흐름 통합 개발 계획 - -> **작성일**: 2026-02-21 -> **목적**: 레거시 5130 절곡품(가이드레일/셔터박스/바텀바) 관리를 SAM 기존 재고 시스템에 통합하고, 선생산→재고적재 흐름 구현 -> **기준 문서**: `api/app/Services/StockService.php`, `api/app/Services/WorkOrderService.php`, `docs/plans/bending-info-auto-generation-plan.md` -> **상태**: 🔄 Phase 3 완료 (3.5 마이그레이션 제외) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 3.5 레거시 데이터 마이그레이션 커맨드 작성 완료 | -| **다음 작업** | 마이그레이션 실행 및 검증 | -| **진행률** | 14/14 (100%) | -| **마지막 업데이트** | 2026-02-21 | - ---- - -## 0. 용어 및 비즈니스 배경 - -### 0.1 절곡품이란? -- **절곡(Bending)**: 금속판(철판, SUS, EGI)을 절곡기로 구부려 만드는 부품 -- **주요 절곡품 3종**: - - **가이드레일**: 방화셔터가 상하로 이동하는 레일 (벽면형/측면형, SUS/EGI 마감) - - **셔터박스(케이스)**: 방화셔터가 말려 들어가는 상부 박스 (양면/밑면/후면 점검구) - - **바텀바(하단마감재)**: 방화셔터 하부를 마감하는 부품 (스크린/철재) -- **연기차단재**: 가이드레일/케이스에 부착하는 연기 차단용 부자재 (W50 레일용, W80 케이스용) - -### 0.2 선생산 운영 방식 -- 절곡품은 **수주와 무관하게 미리 대량 생산**하여 재고로 비축 -- 수주 발생 시 비축된 재고에서 **투입(차감)**하여 사용 -- 이유: 절곡 공정은 셋업 시간이 길어 건별 생산보다 일괄 생산이 효율적 - -### 0.3 SAM 프로젝트 구조 -``` -SAM/ -├── api/ # Laravel 12 REST API (백엔드) -├── react/ # Next.js 15 프론트엔드 -├── mng/ # 관리자 패널 (Plain Laravel) -├── 5130/ # 레거시 시스템 소스코드 (참조용) -└── docs/ # 기술 문서 -``` - -### 0.4 SAM 핵심 아키텍처 규칙 -- **Service-First**: 비즈니스 로직은 반드시 Service 레이어 -- **Multi-tenancy**: 모든 모델에 `BelongsToTenant` trait, tenant_id 필수 -- **컬럼 추가 정책**: FK/조인키만 컬럼 추가, 나머지 속성은 `options` JSON 활용 -- **FormRequest**: Controller에서 검증 금지, FormRequest 사용 - ---- - -## 1. 개요 - -### 1.1 배경 - -레거시 5130에서 절곡품(가이드레일, 셔터박스, 바텀바)은 **수주와 무관하게 미리 생산하여 재고로 관리**하는 형태. -수주 발생 시 재고에서 투입(차감)하는 방식으로 운영됨. - -SAM에는 이미 재고 관리 시스템(`stocks` + `stock_lots` + `stock_transactions`)이 구축되어 있으나, -**생산 완료 → 재고 입고** 경로가 없어 절곡품 선생산 흐름을 지원하지 못함. - -### 1.2 레거시 5130 절곡품 관리 구조 - -``` -[5130 시스템] - -┌─────────────────────────────────────────────────────────────┐ -│ 절곡품 마스터 (3종) │ -│ ├── guiderail 테이블 (가이드레일) │ -│ │ ├── 대분류: 스크린/철재 │ -│ │ ├── 인정/비인정, 제품코드(KSS01 등) │ -│ │ ├── 치수: rail_width × rail_length │ -│ │ ├── material_summary (소요자재량 JSON) │ -│ │ └── bending_components (절곡 구성품) │ -│ ├── shutterbox 테이블 (셔터박스) │ -│ │ ├── 점검구 형태: 양면/밑면/후면 │ -│ │ └── 치수: box_width × box_height │ -│ └── bottombar 테이블 (바텀바/하단마감재) │ -│ ├── 대분류: 스크린/철재 │ -│ └── 치수: bar_width × bar_height │ -│ │ -│ 재고 관리 │ -│ ├── lot 테이블 (생산 LOT) │ -│ │ ├── 3코드 식별: prod + spec + slength │ -│ │ ├── lot_number, surang(수량), rawLot(원자재LOT) │ -│ │ └── 재고 = SUM(lot.surang) - SUM(bending_work_log.qty) │ -│ └── bending_work_log 테이블 (사용 이력) │ -│ └── quantity, reg_date, lot_no │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 1.3 SAM 현재 상태 (AS-IS) - -``` -[수주 기반 흐름만 존재] - -Order(수주) ──→ WorkOrder(생산지시) ──→ 자재투입 ──→ 완료 ──→ Shipment(출하) - │ │ │ - │ sales_order_id 필수 │ 재고차감 │ ⚠️ 재고입고 없이 - │ (비즈니스 로직상) │ (기존 OK) │ 바로 출하 - -[구매입고 흐름 (별도)] - -Receiving(입고) ──→ StockService::increaseFromReceiving() (라인 241) - │ Stock + StockLot 생성 - │ StockTransaction(IN, receiving) - └─ FIFO 순서 부여 -``` - -### 1.4 목표 흐름 (TO-BE) - -``` -[선생산 흐름 (신규)] - -선생산 작업지시 ──→ 자재투입 ──→ 생산완료 - │ sales_order_id = NULL │ - │ mode = 'manual' (프론트) │ - ▼ - ⭐ 재고 입고 (신규) - StockService::increaseFromProduction() - Stock + StockLot 생성 - StockTransaction(IN, production_output) - │ - ▼ - [완성품 재고 적재] - LOT 추적, FIFO 관리 - │ - ▼ - [수주 발생 시] - 재고 확인 → reserve() → 부족분만 생산지시 - -[기존 수주 기반 흐름 (변경 없음)] - -Order ──→ WorkOrder ──→ 완료 ──→ Shipment (기존 유지) -``` - -### 1.5 핵심 설계 결정 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 설계 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 기존 재고 시스템(stocks/stock_lots/stock_transactions) 재활용 │ -│ 2. Receiving은 구매입고 전용 유지 → 생산입고는 직접 StockService │ -│ 3. 멀티테넌시 정책: FK만 컬럼, 나머지는 options JSON │ -│ 4. items.options 체계 활용 (production_source, lot_managed 등) │ -│ 5. 절곡품 전용 페이지 불필요 → 기존 재고현황에 필터 추가 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.6 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 상수 추가, 필터 파라미터 추가, options JSON 활용 | 불필요 | -| ⚠️ 컨펌 필요 | 신규 메서드 추가, 비즈니스 로직 분기, 프론트 UI 변경 | **필수** | -| 🔴 금지 | 기존 입출고 로직 변경, stocks 테이블 구조 변경, 기존 API 스펙 변경 | 별도 협의 | - -### 1.7 준수 규칙 -- `CLAUDE.md` - Service-First, FormRequest, BelongsToTenant -- `SAM_QUICK_REFERENCE.md` - API 규칙 -- `docs/plans/bending-info-auto-generation-plan.md` - BendingInfoBuilder 참조 -- `docs/plans/bending-worklog-reimplementation-plan.md` - 프론트 절곡 컴포넌트 참조 - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: 재고 입고 기반 구축 (백엔드) - -| # | 작업 항목 | 상태 | 영향 파일 | -|---|----------|:----:|----------| -| 1.1 | StockTransaction REASON 상수 추가 | ✅ | `api/app/Models/Tenants/StockTransaction.php` (라인 41-57) | -| 1.2 | StockLot에 work_order_id 컬럼 추가 | ✅ | `api/database/migrations/` (신규), `api/app/Models/Tenants/StockLot.php` | -| 1.3 | StockService::increaseFromProduction() 구현 | ✅ | `api/app/Services/StockService.php` (라인 241 참조) | -| 1.4 | WorkOrderService 완료 처리 분기 로직 | ✅ | `api/app/Services/WorkOrderService.php` (라인 563-593) | - -### 2.2 Phase 2: 선생산 작업지시 흐름 (백엔드 + 프론트) - -| # | 작업 항목 | 상태 | 영향 파일 | -|---|----------|:----:|----------| -| 2.1 | 수주 없는 작업지시 API 보완 | ✅ | 이미 지원됨 (sales_order_id nullable, items 직접 전달 가능) | -| 2.2 | items.options 기반 비즈니스 로직 분기 | ✅ | Phase 1에서 shouldStockIn()으로 구현 완료 | -| 2.3 | 작업지시 생성 프론트 UI 보완 (manual 모드) | ✅ | `react/.../WorkOrderCreate.tsx` + `actions.ts` (품목 검색/추가 UI, items 파라미터) | -| 2.4 | 재고현황 item_category 필터 추가 (API) | ✅ | `api/app/Services/StockService.php`, `StockController.php` | -| 2.5 | 재고현황 절곡품 필터 추가 (프론트) | ✅ | `react/.../StockStatusList.tsx` + `actions.ts` (카테고리 필터 드롭다운) | - -### 2.3 Phase 3: 수주 연동 고도화 - -| # | 작업 항목 | 상태 | 영향 파일 | -|---|----------|:----:|----------| -| 3.1 | 수주의 절곡 BOM 품목별 재고 확인 API | ✅ | `api/app/Services/OrderService.php`, `OrderController.php`, `routes/api/v1/sales.php` | -| 3.2 | 가용 재고 자동 예약(reserve) 로직 | ✅ | 기존 `reserveForOrder()` (라인 639-642)에서 이미 처리됨 | -| 3.3 | 부족분 수동 처리 (사용자 결정) | ✅ | 프론트에서 부족 현황 표시 → 사용자가 수동으로 선생산 작업지시 생성 | -| 3.4 | 수주화면 절곡 재고 현황 표시 (프론트) | ✅ | `react/src/components/orders/actions.ts`, `orders/index.ts`, `order-management-sales/[id]/page.tsx` | -| 3.5 | 5130 레거시 데이터 마이그레이션 | ⏳ | `api/database/seeders/` 또는 마이그레이션 스크립트 (별도 진행) | - ---- - -## 3. 작업 절차 - -### 3.1 Phase 1 상세 절차 - -``` -Step 1.1: StockTransaction REASON 상수 추가 -├── 파일: api/app/Models/Tenants/StockTransaction.php -├── 위치: 라인 49 (REASON_ORDER_CANCEL 다음) -├── 추가: const REASON_PRODUCTION_OUTPUT = 'production_output'; -├── REASONS 배열에도 추가 (라인 51-57) -└── 검증: 모델 상수 선언 확인 - -Step 1.2: StockLot에 work_order_id 컬럼 추가 -├── 마이그레이션 파일 생성 -│ └── stock_lots 테이블에 work_order_id (nullable, FK → work_orders.id) 추가 -│ └── 위치: receiving_id (라인 47) 다음 -├── StockLot 모델 수정 (api/app/Models/Tenants/StockLot.php) -│ ├── fillable에 'work_order_id' 추가 (라인 15-34) -│ └── workOrder() 관계 추가: belongsTo(WorkOrder::class) -├── 멀티테넌시 정책: work_order_id는 FK이므로 컬럼 추가 정당 -└── 검증: migrate:status, 모델 관계 확인 - -Step 1.3: StockService::increaseFromProduction() 구현 -├── 파일: api/app/Services/StockService.php -├── 기존 increaseFromReceiving() (라인 241-314) 참고하여 구현 -│ ├── getOrCreateStock() 재사용 (라인 423-466) -│ ├── getNextFifoOrder() 재사용 (라인 474) -│ ├── StockLot 생성 (work_order_id 참조, receiving_id는 null) -│ ├── Stock.refreshFromLots() 호출 (Stock.php 라인 149-164) -│ ├── recordTransaction() 호출 (라인 1232) -│ └── logStockChange() 호출 (라인 1274) -├── 차이점: receiving_id 대신 work_order_id 사용, supplier 관련 필드 null -├── LOT 번호: WorkOrderService::generateLotNo() (라인 845-866) 에서 생성한 것 수신 -└── 검증: 단위 테스트 (입고 후 재고량 증가 확인) - -Step 1.4: WorkOrderService 완료 처리 분기 로직 -├── 파일: api/app/Services/WorkOrderService.php -├── 수정 위치: updateStatus() 라인 591-593 -│ 현재 코드: -│ if ($status === WorkOrder::STATUS_COMPLETED) { -│ $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId); -│ } -│ 변경: -│ if ($status === WorkOrder::STATUS_COMPLETED) { -│ if ($workOrder->sales_order_id) { -│ $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId); -│ } else { -│ $this->stockInFromProduction($workOrder); -│ } -│ } -├── saveItemResults() (라인 805-840)는 양쪽 모두 실행됨 (라인 563-568, 분기 전에 호출) -├── generateLotNo() (라인 845-866) 에서 LOT 번호 자동 생성 (KD-SA-YYMMDD-NN 형식) -└── 검증: 선생산 WO 완료 시 재고 증가 확인, 기존 수주 WO는 변경 없음 -``` - -### 3.2 Phase 2 상세 절차 - -``` -Step 2.1: 수주 없는 작업지시 API 보완 -├── WorkOrderService::store() 메서드 확인 -│ └── sales_order_id 없이도 items 직접 전달 가능 (기존 경로 활용) -├── work_orders.sales_order_id는 DB에서 이미 nullable -├── 프론트: WorkOrderCreate.tsx의 RegistrationMode (라인 52) -│ └── 현재: type RegistrationMode = 'linked' | 'manual' -│ └── 'manual' 선택 시 수주 연동 없이 생성 가능 -│ └── ⚠️ 주의: 'source_type' 필드는 현재 존재하지 않음 → 필요시 신규 추가 -└── 검증: Postman으로 수주 없는 작업지시 생성 테스트 - -Step 2.2: items.options 기반 비즈니스 로직 분기 -├── Item.options 참조 위치 정리 -│ ├── production_source: 'purchased' | 'self_produced' | 'both' -│ ├── lot_managed: boolean -│ └── consumption_method: 'auto' | 'manual' | 'none' -├── 생산완료 시: production_source === 'self_produced' && lot_managed → 재고 입고 -├── 자재투입 시: consumption_method에 따른 차감 방식 분기 -└── 검증: 절곡 품목의 options 값 시더 데이터 확인 - -Step 2.3: 작업지시 생성 프론트 UI 보완 -├── 파일: react/src/components/production/WorkOrders/WorkOrderCreate.tsx -├── 현재 manual 모드 UI (라인 278-305): -│ └── RadioGroup에 'linked' | 'manual' 선택지, Label: "수동 등록 (재고생산)" -├── 보완 필요: -│ ├── 품목 검색/선택 UI (items 마스터에서 BENDING 카테고리 필터) -│ ├── 수량 입력 -│ └── 공정 선택 (절곡 공정 기본 선택) -├── 생산완료 버튼 UI 변경 (선생산 WO: "재고 입고" / 수주 WO: "출하") -└── 검증: 프론트에서 선생산 작업지시 생성 → 완료 → 재고 확인 - -Step 2.4: 재고현황 item_category 필터 추가 (API) -├── 파일: api/app/Services/StockService.php -├── index() 메서드 (라인 45) 파라미터에 item_category 추가 -│ └── whereHas('item', fn($q) => $q->where('item_category', $category)) -├── StockController 파라미터 바인딩 -└── 검증: API 호출로 BENDING 카테고리 필터링 확인 - -Step 2.5: 재고현황 절곡품 필터 추가 (프론트) -├── 파일: react/src/components/material/StockStatus/StockStatusList.tsx -├── 관련 파일: -│ ├── StockStatusDetail.tsx (상세) -│ ├── stockStatusConfig.ts (설정) -│ ├── actions.ts (API 호출) -│ └── types.ts (타입 정의) -├── 카테고리 탭 또는 드롭다운 추가 -│ └── 전체 | 원자재 | 절곡품(BENDING) | 부자재 | 소모품 -├── API 호출 시 item_category 파라미터 전달 -└── 검증: 절곡품 필터 적용하여 재고 목록 확인 -``` - -### 3.3 Phase 3 상세 절차 - -``` -Step 3.1: 수주 확정 시 재고 자동 확인 -├── OrderService::confirmOrder() 또는 createProductionOrder() 수정 -│ ├── BOM에서 절곡 품목 추출 (item_category === 'BENDING') -│ ├── 각 품목의 가용 재고 조회: StockService::getAvailableStock() (라인 796) -│ └── 재고 현황 반환 (충족/부족 품목별) -├── 프론트에 재고 확인 결과 표시 -└── 검증: 수주 확정 시 재고 현황 표시 확인 - -Step 3.2: 가용 재고 자동 예약 -├── 기존 메서드 활용: -│ ├── StockService::reserve() (라인 832) -│ └── StockService::releaseReservation() (라인 948) -├── 예약 시점: 수주 확정 시 자동 예약 (사용자 확인 후) -├── 예약 해제: 수주 취소 시 releaseReservation() -└── 검증: 예약 후 available_qty 감소 확인 - -Step 3.3: 부족분 자동 생산지시 -├── 수주 확정 시 재고 부족 품목에 대해 자동 생산지시 생성 -│ └── createProductionOrder()에 부족 수량만 반영 -├── 또는 수동: 부족 품목 목록을 사용자에게 표시 → 선생산 지시 유도 -└── 검증: 재고 10개, 필요 15개 → 5개만 생산지시 확인 - -Step 3.4: 수주화면 재고 현황 표시 -├── 수주 상세/편집 화면에 절곡 품목별 재고 현황 표시 -│ └── 품목명 | 필요수량 | 가용재고 | 부족수량 -└── 검증: UI 렌더링 확인 - -Step 3.5: 5130 레거시 데이터 마이그레이션 -├── lot 테이블 → stocks + stock_lots 매핑 -│ ├── prod+spec+slength → items.code (BD-* 패턴) 매핑 -│ ├── surang → stock_lots.qty -│ └── rawLot → stock_lots.options (원자재 LOT 추적) -├── bending_work_log → stock_transactions 매핑 -│ └── quantity → stock_transactions (TYPE_OUT) -├── guiderail/shutterbox/bottombar → items 테이블 매핑 -│ └── item_category = 'BENDING', item_type = 'PT' -└── 검증: 마이그레이션 전후 재고량 일치 확인 -``` - ---- - -## 4. 상세 작업 내용 - -### 4.1 현재 DB 스키마 (수정 대상) - -#### stocks 테이블 (`2025_12_26_132806_create_stocks_table.php`) -``` -id, tenant_id, item_id, item_code, item_name, item_type, -specification, unit, stock_qty, safety_stock, -reserved_qty, available_qty, lot_count, oldest_lot_date, -location, status, last_receipt_date, last_issue_date, -created_by, updated_by, timestamps, softDeletes, deleted_by -``` - -#### stock_lots 테이블 (`2025_12_26_132842_create_stock_lots_table.php`) -``` -id, tenant_id, stock_id(FK→stocks), lot_no, fifo_order(default:1), -receipt_date, qty(decimal 15,3), reserved_qty, available_qty, -unit(default:'EA'), supplier, supplier_lot, po_number, -location, status(default:'available'), receiving_id(nullable), -created_by, updated_by, timestamps, softDeletes, deleted_by - -인덱스: tenant_id, stock_id, lot_no, status, (stock_id+fifo_order) 복합 -유니크: (tenant_id, stock_id, lot_no) -``` - -#### stock_transactions 테이블 (`2026_01_29_000001_create_stock_transactions_table.php`) -``` -id, tenant_id, stock_id, stock_lot_id, type(IN/OUT/RESERVE/RELEASE), -qty, balance_qty, reference_type, reference_id, lot_no, -reason, remark, item_code, item_name, created_by, timestamps -``` - -### 4.2 현재 코드 레퍼런스 (라인번호 포함) - -#### StockTransaction 상수 (`api/app/Models/Tenants/StockTransaction.php`) -```php -// 라인 25-31: TYPE 상수 -const TYPE_IN = 'IN'; // 라인 25 -const TYPE_OUT = 'OUT'; // 라인 27 -const TYPE_RESERVE = 'RESERVE'; // 라인 29 -const TYPE_RELEASE = 'RELEASE'; // 라인 31 - -// 라인 41-57: REASON 상수 -const REASON_RECEIVING = 'receiving'; // 라인 41 -const REASON_WORK_ORDER_INPUT = 'work_order_input'; // 라인 43 -const REASON_SHIPMENT = 'shipment'; // 라인 45 -const REASON_ORDER_CONFIRM = 'order_confirm'; // 라인 47 -const REASON_ORDER_CANCEL = 'order_cancel'; // 라인 49 -const REASONS = [ ... ]; // 라인 51-57 -``` - -#### StockService 주요 메서드 (`api/app/Services/StockService.php`) -``` -라인 45: index(array $params): LengthAwarePaginator -라인 109: stats(): array -라인 159: show(int $id): Item -라인 176: findByItemCode(string $itemCode): ?Item -라인 192: statsByItemType(): array -라인 241: increaseFromReceiving(Receiving $receiving): StockLot ← 참조 대상 -라인 325: adjustFromReceiving(Receiving $receiving, float $newQty): void -라인 423: getOrCreateStock(int $itemId, ?Receiving $receiving = null): Stock ← 재사용 -라인 474: getNextFifoOrder(int $stockId): int ← 재사용 -라인 493: decreaseFIFO(int $itemId, float $qty, string $reason, int $referenceId): array -라인 618: decreaseFromLot(int $stockLotId, float $qty, string $reason, int $referenceId): array -라인 710: increaseToLot(int $stockLotId, float $qty, string $reason, int $referenceId): array -라인 796: getAvailableStock(int $itemId): ?array -라인 832: reserve(int $itemId, float $qty, int $orderId): void -라인 948: releaseReservation(int $itemId, float $qty, int $orderId): void -라인 1050: reserveForOrder($orderItems, int $orderId): void -라인 1071: releaseReservationForOrder($orderItems, int $orderId): void -라인 1099: decreaseForShipment(int $itemId, float $qty, int $shipmentId, ?int $stockLotId = null): array -라인 1232: [private] recordTransaction(...) -라인 1274: [private] logStockChange(...) -``` - -#### WorkOrderService 완료 처리 (`api/app/Services/WorkOrderService.php`) -```php -// 라인 563-568: completed 케이스 (saveItemResults 호출) -case WorkOrder::STATUS_COMPLETED: - $workOrder->started_at = $workOrder->started_at ?? now(); - $workOrder->completed_at = now(); - $this->saveItemResults($workOrder, $resultData, $userId); - break; - -// 라인 591-593: 완료 후 출하 자동 생성 (← 여기에 분기 삽입) -if ($status === WorkOrder::STATUS_COMPLETED) { - $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId); -} - -// 라인 606: 출하 생성 메서드 -private function createShipmentFromWorkOrder(WorkOrder $workOrder, int $tenantId, int $userId): ?Shipment - -// 라인 805: 결과 데이터 저장 (LOT 번호 생성 포함) -private function saveItemResults(WorkOrder $workOrder, ?array $resultData, int $userId): void - -// 라인 845-866: LOT 번호 생성 -private function generateLotNo(WorkOrder $workOrder): string -// 패턴: KD-SA-YYMMDD-NN (예: KD-SA-260221-01) -``` - -#### Stock 모델 refreshFromLots (`api/app/Models/Tenants/Stock.php`) -```php -// 라인 149-164 -public function refreshFromLots(): void -{ - $lots = $this->lots()->where('status', '!=', 'used')->get(); - $this->lot_count = $lots->count(); - $this->stock_qty = $lots->sum('qty'); - $this->reserved_qty = $lots->sum('reserved_qty'); - $this->available_qty = $lots->sum('available_qty'); - $oldestLot = $lots->sortBy('receipt_date')->first(); - $this->oldest_lot_date = $oldestLot?->receipt_date; - $this->last_receipt_date = $lots->max('receipt_date'); - $this->status = $this->calculateStatus(); - $this->save(); -} -``` - -### 4.3 increaseFromReceiving() 실제 코드 (참조용) - -신규 `increaseFromProduction()` 구현 시 아래 코드를 기반으로 작성: - -```php -// api/app/Services/StockService.php 라인 241-314 -public function increaseFromReceiving(Receiving $receiving): StockLot -{ - if (! $receiving->item_id) { - throw new \Exception(__('error.stock.item_id_required')); - } - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - return DB::transaction(function () use ($receiving, $tenantId, $userId) { - $stock = $this->getOrCreateStock($receiving->item_id, $receiving); - $fifoOrder = $this->getNextFifoOrder($stock->id); - - $stockLot = new StockLot; - $stockLot->tenant_id = $tenantId; - $stockLot->stock_id = $stock->id; - $stockLot->lot_no = $receiving->lot_no; - $stockLot->fifo_order = $fifoOrder; - $stockLot->receipt_date = $receiving->receiving_date; - $stockLot->qty = $receiving->receiving_qty; - $stockLot->reserved_qty = 0; - $stockLot->available_qty = $receiving->receiving_qty; - $stockLot->unit = $receiving->order_unit ?? 'EA'; - $stockLot->supplier = $receiving->supplier; // ← 생산입고: null - $stockLot->supplier_lot = $receiving->supplier_lot; // ← 생산입고: null - $stockLot->po_number = $receiving->order_no; // ← 생산입고: null - $stockLot->location = $receiving->receiving_location; - $stockLot->status = 'available'; - $stockLot->receiving_id = $receiving->id; // ← 생산입고: null, work_order_id 대신 사용 - $stockLot->created_by = $userId; - $stockLot->updated_by = $userId; - $stockLot->save(); - - $stock->refreshFromLots(); - - $this->recordTransaction( - stock: $stock, - type: StockTransaction::TYPE_IN, - qty: $receiving->receiving_qty, - reason: StockTransaction::REASON_RECEIVING, // ← 생산입고: REASON_PRODUCTION_OUTPUT - referenceType: 'receiving', // ← 생산입고: 'work_order' - referenceId: $receiving->id, // ← 생산입고: $workOrder->id - lotNo: $receiving->lot_no, - stockLotId: $stockLot->id - ); - - $this->logStockChange(...); - return $stockLot; - }); -} -``` - -### 4.4 increaseFromProduction() 구현 설계 - -```php -/** - * 생산 완료 시 완성품 재고 입고 - * increaseFromReceiving()을 기반으로 구현 - * - * @param WorkOrder $workOrder 선생산 작업지시 - * @param WorkOrderItem $woItem 작업지시 품목 - * @param float $goodQty 양품 수량 (saveItemResults에서 기록) - * @param string $lotNo LOT 번호 (generateLotNo에서 생성) - */ -public function increaseFromProduction( - WorkOrder $workOrder, - WorkOrderItem $woItem, - float $goodQty, - string $lotNo -): StockLot { - $tenantId = $this->tenantId(); - $userId = $this->apiUserId(); - - return DB::transaction(function () use ($workOrder, $woItem, $goodQty, $lotNo, $tenantId, $userId) { - // 1. Stock 조회 또는 생성 - // getOrCreateStock()의 두 번째 파라미터(Receiving)는 null - // → specification, unit은 Item에서 가져옴 - $stock = $this->getOrCreateStock($woItem->item_id); - - // 2. FIFO 순서 - $fifoOrder = $this->getNextFifoOrder($stock->id); - - // 3. StockLot 생성 - $stockLot = new StockLot; - $stockLot->tenant_id = $tenantId; - $stockLot->stock_id = $stock->id; - $stockLot->lot_no = $lotNo; - $stockLot->fifo_order = $fifoOrder; - $stockLot->receipt_date = now()->toDateString(); - $stockLot->qty = $goodQty; - $stockLot->reserved_qty = 0; - $stockLot->available_qty = $goodQty; - $stockLot->unit = $woItem->unit ?? 'EA'; - $stockLot->supplier = null; // 구매입고 전용 필드 - $stockLot->supplier_lot = null; - $stockLot->po_number = null; - $stockLot->location = null; - $stockLot->status = 'available'; - $stockLot->receiving_id = null; // 구매입고가 아님 - $stockLot->work_order_id = $workOrder->id; // ★ 생산입고 참조 - $stockLot->created_by = $userId; - $stockLot->updated_by = $userId; - $stockLot->save(); - - // 4. Stock 합계 갱신 - $stock->refreshFromLots(); - - // 5. 거래 이력 기록 - $this->recordTransaction( - stock: $stock, - type: StockTransaction::TYPE_IN, - qty: $goodQty, - reason: StockTransaction::REASON_PRODUCTION_OUTPUT, - referenceType: 'work_order', - referenceId: $workOrder->id, - lotNo: $lotNo, - stockLotId: $stockLot->id - ); - - // 6. 감사 로그 - $this->logStockChange( - stock: $stock, - action: 'production_in', - details: [ - 'work_order_id' => $workOrder->id, - 'work_order_item_id' => $woItem->id, - 'qty' => $goodQty, - 'lot_no' => $lotNo, - ] - ); - - return $stockLot; - }); -} -``` - -### 4.5 WorkOrderService 완료 분기 구현 설계 - -```php -// 라인 591-593 변경: updateStatus() 내부 -if ($status === WorkOrder::STATUS_COMPLETED) { - if ($workOrder->sales_order_id) { - // 기존 로직: 수주 연동 → 출하 자동 생성 - $this->createShipmentFromWorkOrder($workOrder, $tenantId, $userId); - } else { - // 신규 로직: 선생산 → 재고 입고 - $this->stockInFromProduction($workOrder); - } -} - -// 신규 private 메서드 -private function stockInFromProduction(WorkOrder $workOrder): void -{ - foreach ($workOrder->items as $woItem) { - if ($this->shouldStockIn($woItem)) { - $resultData = $woItem->options['result'] ?? []; - $goodQty = $resultData['good_qty'] ?? $woItem->quantity; - $lotNo = $resultData['lot_no'] ?? ''; - - if ($goodQty > 0 && $lotNo) { - $this->stockService->increaseFromProduction( - $workOrder, $woItem, $goodQty, $lotNo - ); - } - } - } -} - -private function shouldStockIn(WorkOrderItem $woItem): bool -{ - $item = $woItem->item; - $options = $item->options ?? []; - - return ($options['production_source'] ?? null) === 'self_produced' - && ($options['lot_managed'] ?? false) === true; -} -``` - -### 4.6 데이터 매핑 (5130 → SAM) - -#### 절곡품 마스터 매핑 - -| 5130 | SAM | 비고 | -|------|-----|------| -| guiderail.model_name | items.code (BD-가이드레일-*) | item_category=BENDING | -| guiderail.rail_width × rail_length | items.options.dimensions | JSON | -| guiderail.material_summary | items.options.material_summary | JSON | -| guiderail.finishing_type | items.options.finishing_type | JSON | -| shutterbox.box_width × box_height | items.code (BD-케이스-*) | 치수 코드화 | -| bottombar.bar_width × bar_height | items.code (BD-하단마감재-*) | 치수 코드화 | - -#### 재고 매핑 - -| 5130 | SAM | 비고 | -|------|-----|------| -| lot.lot_number | stock_lots.lot_no | 1:1 | -| lot.surang | stock_lots.qty | 생산 수량 | -| lot.prod+spec+slength | items.code → stocks.item_id | 3코드→품목코드 변환 | -| lot.rawLot | stock_lots.options.raw_lot | JSON | -| lot.fabric_lot | stock_lots.options.fabric_lot | JSON | -| bending_work_log.quantity | stock_transactions.qty (TYPE_OUT) | 사용 이력 | - -#### 3코드 → 품목코드 변환 규칙 - -| prod | spec | slength | SAM item_code | -|------|------|---------|---------------| -| R(벽면형) | S(SUS) | 53(W50x3000) | BD-가이드레일-벽면형-SUS-W50x3000 | -| R(벽면형) | E(EGI) | 84(W80x4000) | BD-가이드레일-벽면형-EGI-W80x4000 | -| C(케이스) | M(본체) | 30(3000) | BD-케이스-본체-3000 | -| B(하단마감재스크린) | A(스크린용) | 30(3000) | BD-하단마감재-스크린-3000 | - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| C1 | StockLot에 work_order_id 컬럼 추가 | DB 마이그레이션 | stock_lots 테이블 | ⚠️ 컨펌 필요 | -| C2 | WorkOrderService 완료 로직 분기 | 비즈니스 로직 변경 | 생산 완료 프로세스 | ⚠️ 컨펌 필요 | -| C3 | Phase 3 수주→재고 자동 매칭 설계 | 신규 비즈니스 프로세스 | OrderService | ⚠️ Phase 3 착수 전 별도 협의 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-21 | - | 문서 초안 작성 | - | - | -| 2026-02-21 | 보완 | 용어설명, 파일경로 수정, 코드 레퍼런스 추가, DB 스키마 추가 | - | - | -| 2026-02-21 | Phase 1 구현 | 1.1~1.4 전체 완료 | StockTransaction, StockLot, StockService, WorkOrderService | ✅ | - ---- - -## 7. 참고 문서 - -### 직접 관련 문서 -- `docs/plans/bending-info-auto-generation-plan.md` - BendingInfoBuilder 자동 생성 계획 -- `docs/plans/bending-worklog-reimplementation-plan.md` - 절곡 작업일지 프론트 재구현 (완료) -- `docs/projects/legacy-5130/04_PRODUCTION.md` - 레거시 생산 시스템 분석 - -### 핵심 코드 파일 (⚠️ 경로 주의: Models는 Tenants 네임스페이스) - -**백엔드 서비스**: -- `api/app/Services/StockService.php` - 재고 서비스 (increaseFromReceiving 라인 241) -- `api/app/Services/WorkOrderService.php` - 작업지시 서비스 (updateStatus 라인 521, saveItemResults 라인 805) -- `api/app/Services/OrderService.php` - 수주 서비스 (createProductionOrder) -- `api/app/Services/Production/BendingInfoBuilder.php` - 절곡 정보 자동 생성 - -**백엔드 모델** (⚠️ `Models/Tenants/` 경로): -- `api/app/Models/Tenants/Stock.php` - 재고 모델 (refreshFromLots 라인 149) -- `api/app/Models/Tenants/StockLot.php` - 재고 LOT 모델 (fillable 라인 15-34) -- `api/app/Models/Tenants/StockTransaction.php` - 재고 거래 이력 모델 (상수 라인 25-57) - -**DB 마이그레이션**: -- `api/database/migrations/2025_12_26_132806_create_stocks_table.php` -- `api/database/migrations/2025_12_26_132842_create_stock_lots_table.php` -- `api/database/migrations/2026_01_29_000001_create_stock_transactions_table.php` - -### 프론트 코드 파일 -- `react/src/components/production/WorkOrders/WorkOrderCreate.tsx` - 작업지시 생성 (RegistrationMode 라인 52, manual UI 라인 278-305) -- `react/src/components/material/StockStatus/StockStatusList.tsx` - 재고 현황 목록 -- `react/src/components/material/StockStatus/` - 재고 현황 전체 디렉토리 (Detail, Audit, actions, types, config, mockData) -- `react/src/components/production/WorkOrders/documents/bending/` - 절곡 작업일지 컴포넌트 - ---- - -## 8. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 8.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("bending-preproduction-state") // 1. 상태 파악 -read_memory("bending-preproduction-snapshot") // 2. 사고 흐름 복구 -read_memory("bending-preproduction-active-symbols") // 3. 작업 대상 파악 -``` - -### 8.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | Snapshot | `write_memory("bending-preproduction-snapshot", "코드변경+논의요약")` | -| **20% 이하** | Context Purge | `write_memory("bending-preproduction-active-symbols", "수정 파일/함수")` | -| **10% 이하** | Stop & Save | 최종 상태 저장 후 세션 교체 권고 | - -### 8.3 Serena 메모리 구조 -- `bending-preproduction-state`: { phase, progress, next_step, last_decision } -- `bending-preproduction-snapshot`: 현재까지의 논의 및 코드 변경점 요약 -- `bending-preproduction-rules`: 불변 규칙 (Receiving 우회, options JSON 정책 등) -- `bending-preproduction-active-symbols`: 현재 수정 중인 파일/심볼 리스트 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 Phase 1 테스트 케이스 - -| # | 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|---|---------|------|----------|----------|------| -| T1.1 | 선생산 WO 완료 시 재고 입고 | WO(sales_order_id=null) 완료 | Stock/StockLot 생성, qty 증가 | | ⏳ | -| T1.2 | 기존 수주 WO 완료 시 변경 없음 | WO(sales_order_id=43) 완료 | 기존대로 Shipment 생성 | | ⏳ | -| T1.3 | LOT 번호 자동 생성 | 선생산 WO 완료 | KD-SA-YYMMDD-NN 형식 LOT | | ⏳ | -| T1.4 | StockTransaction 기록 | 생산 입고 | TYPE_IN, reason=production_output | | ⏳ | - -### 9.2 Phase 2 테스트 케이스 - -| # | 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|---|---------|------|----------|----------|------| -| T2.1 | 수주 없이 작업지시 생성 | manual 모드 + 절곡 품목 | WO 생성, sales_order_id=null | | ⏳ | -| T2.2 | 재고현황 절곡품 필터 | item_category=BENDING | 절곡품만 표시 | | ⏳ | -| T2.3 | FIFO 출고 | 재고 투입 | 가장 오래된 LOT부터 차감 | | ⏳ | - -### 9.3 Phase 3 테스트 케이스 - -| # | 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|---|---------|------|----------|----------|------| -| T3.1 | 수주 확정 시 재고 확인 | 재고 10, 필요 15 | 부족 5 표시 | | ⏳ | -| T3.2 | 가용 재고 자동 예약 | 재고 10, 필요 5 | reserved_qty=5, available_qty=5 | | ⏳ | -| T3.3 | 부족분 생산지시 | 재고 10, 필요 15 | 5개 생산지시 자동 생성 | | ⏳ | - -### 9.4 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 선생산 WO → 재고 입고 정상 동작 | ⏳ | Phase 1 핵심 | -| 기존 수주 WO 흐름 변경 없음 | ⏳ | 회귀 테스트 | -| 절곡품 재고현황 필터링 가능 | ⏳ | Phase 2 | -| 수주 시 재고 자동 매칭 | ⏳ | Phase 3 | -| 5130 데이터 마이그레이션 완료 | ⏳ | Phase 3 | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 0.2 선생산 운영 방식 + 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.4 성공 기준 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1~3, 14개 작업 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 기존 bending 계획 문서 참조 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 검증 완료 (Models/Tenants/, material/StockStatus/) | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 라인번호 + 실제 코드 바디 포함 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9 테스트 케이스 참조 | -| 8 | 모호한 표현이 없는가? | ✅ | 코드 수준 상세 기술 + 용어 설명 포함 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 절곡품이 뭔가? 왜 선생산하는가? | ✅ | 0.1, 0.2 용어 및 비즈니스 배경 | -| Q2. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q3. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 1 + 3.1 절차 | -| Q4. 어떤 파일을 수정해야 하는가? | ✅ | 2.1~2.3 영향 파일 (정확한 경로) | -| Q5. 기존 코드 구조가 어떻게 되어 있는가? | ✅ | 4.1~4.3 DB 스키마 + 코드 레퍼런스 | -| Q6. 신규 메서드를 어떻게 구현해야 하는가? | ✅ | 4.4~4.5 구현 설계 (전체 코드) | -| Q7. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 | -| Q8. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/clodeCheck/attendance-management_2026-01-14_23-30-00.md b/plans/clodeCheck/attendance-management_2026-01-14_23-30-00.md deleted file mode 100644 index 11e028a..0000000 --- a/plans/clodeCheck/attendance-management_2026-01-14_23-30-00.md +++ /dev/null @@ -1,206 +0,0 @@ -# E2E Test Report: 근태관리 테스트 - -**Test ID**: attendance-management -**Executed**: 2026-01-14 23:30:00 -**Duration**: ~15분 -**Status**: ❌ FAIL (3 bugs found) - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 13 | -| Passed | 10 | -| Failed | 3 | -| Pass Rate | 76.9% | - ---- - -## 필수 검증 결과 - -| # | 검증 항목 | 결과 | 비고 | -|---|----------|------|------| -| 1 | 파일 다운로드 | ❌ FAIL | Network API 호출 없음 | -| 2 | 등록/저장 버튼 | ❌ FAIL | 사유 등록 시 404 에러 | -| 3 | 검색/필터 | ✅ PASS | 데이터 필터링 정상 | -| 4 | 모달 등록 완료 | ❌ FAIL | 근태 등록: 서버 에러, 사유 등록: 404 에러 | - ---- - -## Step Results - -| Step | Name | Status | Notes | -|------|------|--------|-------| -| 1 | 인사관리 메뉴 진입 | ✅ PASS | /hr/attendance-management 이동 완료 | -| 2 | 근태 현황 대시보드 확인 | ✅ PASS | 미출근, 정시출근, 지각, 휴가 카드 표시 | -| 3 | 기간 필터 확인 | ✅ PASS | 당해년도~오늘 버튼, 날짜 입력 필드 확인 | -| 4 | 탭 필터 확인 | ✅ PASS | 전체, 미출근, 정시출근 등 9개 탭 확인 | -| 5 | 근태 테이블 구조 확인 | ✅ PASS | 12개 컬럼 구조 확인 | -| 6 | 근태 등록 모달 열기 | ✅ PASS | 모달 열림, 필드 확인 | -| 7 | 근태 등록 실제 저장 (필수 #4) | ❌ FAIL | "Create failed: 서버 에러" | -| 8 | 근태 등록 모달 닫기 | ✅ PASS | 모달 자동 닫힘 | -| 9 | 사유 등록 모달 열기 | ✅ PASS | 모달 열림, 대상/기준일/유형 필드 확인 | -| 10 | 사유 등록 실제 등록 (필수 #4) | ❌ FAIL | 404 페이지 이동 | -| 11 | 검색 기능 확인 (필수 #3) | ✅ PASS | "홍킬동" 검색 → 6건 필터링 | -| 12 | 엑셀 다운로드 (필수 #1) | ❌ FAIL | Console LOG만 출력, API 호출 없음 | -| 13 | 사유 유형 옵션 확인 | ✅ PASS | 4개 옵션 확인 | - ---- - -## 🐛 Bug Report #1: 엑셀 다운로드 미구현 - -**Report ID**: ATT-BUG-001 -**Priority**: High -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\hr\attendance-management\page.tsx` - -### Issue Summary -엑셀 다운로드 버튼 클릭 시 Console LOG만 출력되고 실제 파일 다운로드가 이루어지지 않음 - -### Steps to Reproduce -1. 근태관리 페이지 접속 -2. "엑셀 다운로드" 버튼 클릭 - -### Expected Result -- 근태 데이터가 엑셀 파일로 다운로드됨 -- Network에 `/api/export/excel` 또는 유사 API 호출 발생 - -### Actual Result -- Console: `[LOG] Excel download`만 출력 -- Network: 다운로드 관련 API 호출 없음 -- 파일 다운로드: 발생하지 않음 - -### Error Details -``` -Console Output: [LOG] Excel download -Network Requests: 다운로드 API 호출 없음 -``` - -### Suggested Fix (Reference Only) -엑셀 다운로드 핸들러에 실제 API 호출 로직 구현 필요 - -**영향 범위**: react / api -**변경 승인 정책**: ⚠️ 컨펌 필요 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` - ---- - -## 🐛 Bug Report #2: 사유 등록 404 에러 - -**Report ID**: ATT-BUG-002 -**Priority**: Critical -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\hr\attendance-management\page.tsx` - -### Issue Summary -사유 등록 모달에서 "등록" 버튼 클릭 시 존재하지 않는 페이지로 이동하여 404 에러 발생 - -### Steps to Reproduce -1. 근태관리 페이지 접속 -2. "사유 등록" 버튼 클릭 -3. 대상 선택 (예: 홍킬동) -4. 유형 선택 (예: 출장신청서) -5. "등록" 버튼 클릭 - -### Expected Result -- 사유가 정상적으로 등록됨 -- 성공 토스트 메시지 표시 -- 근태관리 페이지에 유지 - -### Actual Result -- `/hr/documents/new?type=businessTripRequest` 페이지로 이동 -- "페이지를 찾을 수 없습니다" 에러 페이지 표시 -- Console: `📌 경로 존재 여부: false` - -### Error Details -``` -URL Change: /hr/attendance-management → /hr/documents/new?type=businessTripRequest -Error Message: "요청하신 페이지가 존재하지 않거나 접근 권한이 없습니다." -Console Log: 📌 경로 존재 여부: false -``` - -### Suggested Fix (Reference Only) -1. `/hr/documents/new` 페이지 구현 필요 -2. 또는 사유 등록 로직을 API 호출 방식으로 변경 - -**영향 범위**: react / api / 라우팅 -**변경 승인 정책**: ⚠️ 컨펌 필요 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- 시스템 아키텍처: `C:\Users\codeb\docs\architecture\system-overview.md` - ---- - -## 🐛 Bug Report #3: 근태 등록 서버 에러 - -**Report ID**: ATT-BUG-003 -**Priority**: High -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\hr\attendance-management\page.tsx` - -### Issue Summary -근태 등록 모달에서 "저장" 버튼 클릭 시 서버 에러 발생 - -### Steps to Reproduce -1. 근태관리 페이지 접속 -2. "근태 등록" 버튼 클릭 -3. 대상 선택 (예: 홍킬동) -4. 기준일, 출근/퇴근 시간 확인 -5. "저장" 버튼 클릭 - -### Expected Result -- 근태가 정상적으로 등록됨 -- 성공 토스트 메시지 표시 -- 테이블에 새 데이터 표시 - -### Actual Result -- Console: `[ERROR] Create failed: 서버 에러` -- 모달은 닫히지만 데이터 저장 실패 - -### Error Details -``` -Console Error: [ERROR] Create failed: 서버 에러 -Source: page-0ad2723b9ad2d990.js:0 -``` - -### Suggested Fix (Reference Only) -백엔드 근태 등록 API 엔드포인트 확인 및 에러 원인 분석 필요 - -**영향 범위**: react / api / database -**변경 승인 정책**: ⚠️ 컨펌 필요 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` -- DB 스키마: `C:\Users\codeb\docs\specs\database-schema.md` - ---- - -## Test Environment - -- **URL**: https://dev.codebridge-x.com -- **Test Account**: TestUser5 -- **Browser**: Playwright (Chromium) -- **Date**: 2026-01-14 - ---- - -## Conclusion - -근태관리 페이지의 UI 요소와 기본 기능(대시보드, 필터, 검색)은 정상 동작하지만, **핵심 CRUD 기능에서 3건의 버그가 발견**되었습니다: - -1. **엑셀 다운로드**: 미구현 (Console LOG만 존재) -2. **사유 등록**: 404 에러 (페이지 미존재) -3. **근태 등록**: 서버 에러 (API 문제) - -이 버그들은 실제 업무 사용에 영향을 주므로 우선 수정이 필요합니다. - ---- - -*Generated by E2E Test Framework - 2026-01-14* diff --git a/plans/clodeCheck/bank-transactions_2026-01-15_test-report.md b/plans/clodeCheck/bank-transactions_2026-01-15_test-report.md deleted file mode 100644 index 4c3d7e7..0000000 --- a/plans/clodeCheck/bank-transactions_2026-01-15_test-report.md +++ /dev/null @@ -1,231 +0,0 @@ -# E2E Test Report: 은행거래 (Bank Transactions) - -**Test ID**: bank-transactions -**Executed**: 2026-01-15 -**Status**: ⚠️ PARTIAL (8/10 - 1 Critical Bug) -**Test Environment**: https://dev.codebridge-x.com - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 10 | -| Passed | 8 | -| Failed | 1 | -| Warning | 1 | -| Pass Rate | 80% | - ---- - -## Step Results - -| Step | Test Case | Status | Notes | -|------|-----------|--------|-------| -| 1 | 은행거래 메뉴 진입 | ✅ PASS | /accounting/bank-transactions 접속 확인 | -| 2 | 목록 페이지 구조 검증 | ✅ PASS | 통계 카드 4개, 테이블 컬럼 12개 확인 | -| 3 | 당해년도 버튼 테스트 | ✅ PASS | 2026-01-01 ~ 2026-12-31 변경 확인 | -| 4 | 전전월 버튼 테스트 | ✅ PASS | 2025-11-01 ~ 2025-11-30 변경 확인 | -| 5 | 전월 버튼 테스트 | ✅ PASS | 2025-12-01 ~ 2025-12-31 변경 확인 | -| 6 | 당월 버튼 테스트 | ✅ PASS | 2026-01-01 ~ 2026-01-31 변경 확인 | -| 7 | 어제 버튼 테스트 | ✅ PASS | 2026-01-14 ~ 2026-01-14 변경 확인 | -| 8 | 오늘 버튼 테스트 | ✅ PASS | 2026-01-15 ~ 2026-01-15 변경 확인 | -| 9 | 직접 날짜 입력 테스트 | ✅ PASS | 수동 입력 후 데이터 반영 확인 | -| 10 | 테이블 데이터 표시 | ❌ FAIL | **통계 카드에만 데이터 표시, 테이블은 빈 상태** | - ---- - -## Detailed Test Results - -### 1. 은행거래 메뉴 진입 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | /accounting/bank-transactions | /accounting/bank-transactions | ✅ | -| 페이지 타이틀 | 입출금 계좌조회 | 입출금 계좌조회 | ✅ | -| 인증 상태 | 로그인됨 | 로그인됨 | ✅ | - ---- - -### 2. 목록 페이지 구조 검증 - -#### 통계 카드 (4개) - -| 카드명 | 값 (2025-12) | 결과 | -|--------|-------------|------| -| 입금 | 47,232,008원 | ✅ | -| 출금 | 178,098,104원 | ✅ | -| 입금 유형 미설정 | 3건 | ✅ | -| 출금 유형 미설정 | 4건 | ✅ | - -#### 필터 드롭다운 (3개) - -| # | 필터명 | 옵션 | -|---|--------|------| -| 1 | 계좌 선택 | 전체, KB국민은행\|운영계좌, NH농협은행\|비상금, 신한은행\|급여계좌, 우리은행\|예비계좌, 하나은행\|법인카드 | -| 2 | 구분 | 전체 (입금/출금 구분 추정) | -| 3 | 정렬 | 최신순 | - -#### 테이블 컬럼 (12개) - -| # | 컬럼명 | 결과 | -|---|--------|------| -| 1 | 체크박스 | ✅ | -| 2 | 은행명 | ✅ | -| 3 | 계좌명 | ✅ | -| 4 | 거래일시 | ✅ | -| 5 | 구분 | ✅ | -| 6 | 적요 | ✅ | -| 7 | 거래처 | ✅ | -| 8 | 입금자/수취인 | ✅ | -| 9 | 입금 | ✅ | -| 10 | 출금 | ✅ | -| 11 | 잔액 | ✅ | -| 12 | 입출금 유형 | ✅ | - ---- - -### 3-8. 기간 버튼 클릭 테스트 (6개) - -| 버튼 | 예상 시작일 | 예상 종료일 | 실제 시작일 | 실제 종료일 | 결과 | -|------|-----------|-----------|-----------|-----------|------| -| 당해년도 | 2026-01-01 | 2026-12-31 | 2026-01-01 | 2026-12-31 | ✅ | -| 전전월 | 2025-11-01 | 2025-11-30 | 2025-11-01 | 2025-11-30 | ✅ | -| 전월 | 2025-12-01 | 2025-12-31 | 2025-12-01 | 2025-12-31 | ✅ | -| 당월 | 2026-01-01 | 2026-01-31 | 2026-01-01 | 2026-01-31 | ✅ | -| 어제 | 2026-01-14 | 2026-01-14 | 2026-01-14 | 2026-01-14 | ✅ | -| 오늘 | 2026-01-15 | 2026-01-15 | 2026-01-15 | 2026-01-15 | ✅ | - -**참고**: 모든 기간 버튼이 정확한 날짜 범위로 변경됨 - -#### 기간별 통계 데이터 - -| 기간 | 입금 | 출금 | 입금 유형 미설정 | 출금 유형 미설정 | -|------|------|------|----------------|----------------| -| 당해년도 (2026) | 0원 | 0원 | 0건 | 0건 | -| 전전월 (2025-11) | 68,956,798원 | 12,123,251원 | 4건 | 4건 | -| 전월 (2025-12) | 47,232,008원 | 178,098,104원 | 3건 | 4건 | -| 당월 (2026-01) | 0원 | 0원 | 0건 | 0건 | -| 어제 (2026-01-14) | 0원 | 0원 | 0건 | 0건 | -| 오늘 (2026-01-15) | 0원 | 0원 | 0건 | 0건 | - ---- - -### 9. 직접 날짜 입력 테스트 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 시작일 입력 | 2025-12-01 | 2025-12-01 | ✅ | -| 종료일 입력 | 2025-12-31 | 2025-12-31 | ✅ | -| 통계 카드 업데이트 | 변경됨 | 입금 47,232,008원, 출금 178,098,104원 | ✅ | - ---- - -### 10. 테이블 데이터 표시 ❌ FAIL - -**BUG-BANK-TRANSACTIONS-20260115-001** - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 통계 카드 데이터 | 표시됨 | 입금 47,232,008원, 출금 178,098,104원 | ✅ | -| 테이블 데이터 | 거래 목록 표시 | "검색 결과가 없습니다." | ❌ | -| 테이블 합계 | 입금/출금 합계 | 0 / 0 | ❌ | - ---- - -## 발견된 버그 - -### BUG-BANK-TRANSACTIONS-20260115-001: 통계 카드와 테이블 데이터 불일치 - -**Priority**: Critical -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\accounting\bank-transactions\page.tsx` - -#### Issue Summary -통계 카드에는 입출금 데이터가 정상적으로 표시되지만, 테이블에는 "검색 결과가 없습니다"로 표시되어 실제 거래 내역을 확인할 수 없음. - -#### Steps to Reproduce -1. 회계관리 > 은행거래 접속 -2. 전월 또는 전전월 버튼 클릭 (2025년 데이터 존재) -3. 통계 카드 확인: 입금/출금 금액 표시됨 -4. 테이블 확인: "검색 결과가 없습니다" 표시 - -#### Expected Result -- 통계 카드에 표시된 입금/출금 금액에 해당하는 거래 내역이 테이블에 표시됨 -- 테이블 합계가 통계 카드 금액과 일치 - -#### Actual Result -- 통계 카드: 입금 47,232,008원, 출금 178,098,104원 (정상) -- 테이블: "검색 결과가 없습니다" (오류) -- 테이블 합계: 0 / 0 (오류) - -#### Error Details -``` -통계 API: 정상 동작 (금액 표시됨) -테이블 API: 데이터 반환 안됨 또는 데이터 매핑 오류 - -가능한 원인: -1. 통계 API와 테이블 API가 다른 데이터 소스 참조 -2. 테이블 렌더링 시 데이터 매핑 로직 오류 -3. 페이지네이션 또는 필터링 로직 오류 -4. 프론트엔드에서 API 응답 파싱 오류 -``` - -#### Suggested Fix (Reference Only) -- 통계 API와 테이블 API의 데이터 소스 일치 확인 -- 프론트엔드 테이블 컴포넌트 데이터 바인딩 확인 -- 브라우저 개발자 도구에서 API 응답 확인 필요 - -**영향 범위**: api / react -**변경 승인 정책**: ⚠️ 컨펌 필요 - ---- - -## 필터 드롭다운 옵션 - -### 계좌 선택 드롭다운 - -| # | 옵션 | -|---|------| -| 1 | 전체 | -| 2 | KB국민은행\|운영계좌 | -| 3 | NH농협은행\|비상금 | -| 4 | 신한은행\|급여계좌 | -| 5 | 우리은행\|예비계좌 | -| 6 | 하나은행\|법인카드 | - ---- - -## Conclusion - -10개 테스트 케이스 중 8개 통과 (80%) - -### 검증 완료 항목 -1. ✅ 회계관리 > 은행거래 메뉴 접근 -2. ✅ 목록 페이지 구조 (통계 카드 4개, 테이블 컬럼 12개, 필터 3개) -3. ✅ 당해년도 버튼 클릭 (2026년 전체) -4. ✅ 전전월 버튼 클릭 (2025-11) -5. ✅ 전월 버튼 클릭 (2025-12) -6. ✅ 당월 버튼 클릭 (2026-01) -7. ✅ 어제 버튼 클릭 (2026-01-14) -8. ✅ 오늘 버튼 클릭 (2026-01-15) -9. ✅ 직접 날짜 입력 (시작일/종료일 수동 입력) -10. ❌ 테이블 데이터 표시 (BUG-BANK-TRANSACTIONS-20260115-001) - -### 검증 결과 요약 -- **기간 버튼**: 6개 모두 정상 동작 ✅ -- **직접 날짜 입력**: 정상 동작 ✅ -- **통계 카드**: 데이터 정상 표시 ✅ -- **테이블 데이터**: ❌ 표시 안됨 (Critical Bug) - -### 테스트 제외 항목 -- 검색 기능 -- 페이지네이션 -- 행 클릭 상세 보기 -- 체크박스 선택 및 일괄 처리 -- 정렬 기능 - ---- - -**Report Generated**: 2026-01-15 -**Tester**: Claude E2E Test Agent diff --git a/plans/clodeCheck/card-transactions_2026-01-15_test-report.md b/plans/clodeCheck/card-transactions_2026-01-15_test-report.md deleted file mode 100644 index 9b5f51d..0000000 --- a/plans/clodeCheck/card-transactions_2026-01-15_test-report.md +++ /dev/null @@ -1,351 +0,0 @@ -# E2E Test Report: 카드거래 (Card Transactions) - -**Test ID**: card-transactions -**Executed**: 2026-01-15 -**Status**: ⚠️ PARTIAL (13/15 - 1 Critical Bug) -**Test Environment**: https://dev.codebridge-x.com - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 15 | -| Passed | 13 | -| Failed | 1 | -| Warning | 1 | -| Pass Rate | 86.7% | - ---- - -## Step Results - -| Step | Test Case | Status | Notes | -|------|-----------|--------|-------| -| 1 | 카드거래 메뉴 진입 | ✅ PASS | /accounting/card-transactions 접속 확인 | -| 2 | 목록 페이지 구조 검증 | ✅ PASS | 통계 카드 2개, 테이블 컬럼 8개 확인 | -| 3 | 2년 기간 설정 | ✅ PASS | 2024-01-15 ~ 2026-01-15 설정, 12행 로드 | -| 4 | 테이블 데이터 존재 확인 | ✅ PASS | 12행, 합계 190,119,372원 | -| 5 | 계정과목명 드롭다운 옵션 확인 | ✅ PASS | 16개 옵션 확인 | -| 6 | 체크박스 선택 | ✅ PASS | 첫 번째 행 선택 | -| 7 | 계정과목명 일괄변경 실행 | ❌ FAIL | API 200 OK 추정, 데이터 미반영 | -| 8 | 일괄변경 결과 확인 | ⚠️ WARN | 데이터 미변경 (미설정 유지) | -| 9 | 행 클릭하여 모달창 열기 | ✅ PASS | 모달 "카드 내역 상세" 표시 | -| 10 | 모달창 필드 상태 확인 | ✅ PASS | 읽기전용 5개, 편집가능 2개 | -| 11 | 모달창에서 적요 수정 | ✅ PASS | "테스트 적요 수정" 입력 | -| 12 | 모달창에서 사용유형 수정 | ✅ PASS | "접대비" 선택, 17개 옵션 확인 | -| 13 | 모달창 저장 버튼 클릭 | ✅ PASS | 저장 성공, 테이블 반영 확인 | -| 14 | 수정 데이터 반영 확인 | ✅ PASS | 사용유형 "접대비"로 변경됨 | -| 15 | 모달창 취소 버튼 동작 확인 | ✅ PASS | 모달 닫힘, 데이터 미변경 | - ---- - -## Detailed Test Results - -### 1. 카드거래 메뉴 진입 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | /accounting/card-transactions | /accounting/card-transactions | ✅ | -| 페이지 타이틀 | 카드거래 | 카드 내역 조회 | ⚠️ 명칭 상이 | -| 인증 상태 | 로그인됨 | 로그인됨 | ✅ | - ---- - -### 2. 목록 페이지 구조 검증 - -#### 통계 카드 (2개) - -| 카드명 | 값 | 결과 | -|--------|-----|------| -| 전월 사용액 | 0원 | ✅ | -| 당월 사용액 | 0원 | ✅ | - -**참고**: 시나리오에는 "사용금액", "사용유형 미설정" 카드로 정의되어 있으나 실제로는 "전월 사용액", "당월 사용액"으로 구성 - -#### 테이블 컬럼 (8개) - -| # | 컬럼명 | 시나리오 | 결과 | -|---|--------|----------|------| -| 1 | 체크박스 | 체크박스 | ✅ | -| 2 | 카드 | 카드명 | ⚠️ 명칭 상이 | -| 3 | 카드명 | - | 추가 컬럼 | -| 4 | 사용자 | - | 추가 컬럼 | -| 5 | 사용일시 | 사용일시 | ✅ | -| 6 | 가맹점명 | 가맹점명 | ✅ | -| 7 | 사용금액 | 사용금액 | ✅ | -| 8 | 사용유형 | 사용유형 | ✅ | - -**참고**: 시나리오의 "적요" 컬럼이 목록에 없음, 대신 "카드", "카드명", "사용자" 컬럼 존재 - ---- - -### 3. 2년 기간 설정 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 시작일 | 2024-01-15 | 2024-01-15 | ✅ | -| 종료일 | 2026-01-15 | 2026-01-15 | ✅ | -| 데이터 로드 | 있음 | 12행, 190,119,372원 | ✅ | - ---- - -### 4. 테이블 데이터 존재 확인 - -| 항목 | 값 | -|------|-----| -| 총 행 수 | 12 | -| 합계 금액 | 190,119,372원 | -| 표시 기간 | 2025-01-12 ~ 2025-11-19 | - -**데이터 샘플**: -| 사용일시 | 가맹점명 | 사용금액 | 사용유형 | -|----------|----------|----------|----------| -| 2025-11-19 | GS칼텍스 지급 | 3,293,557원 | 미설정 | -| 2025-10-25 | SK이노베이션 지급 | 1,238,454원 | 미설정 | -| 2025-10-10 | 현대제철 지급 | 30,481,719원 | 미설정 | - ---- - -### 5. 계정과목명 드롭다운 옵션 - -**목록 페이지 옵션 (16개)**: -1. 미설정 -2. 매입대금 -3. 선급금 -4. 가지급금 -5. 임대료 -6. 이자비용 -7. 보증금 지급 -8. 차입금 상환 -9. 배당금 지급 -10. 부가세 납부 -11. 급여 -12. 4대보험 -13. 세금 -14. 공과금 -15. 경비 -16. 기타 - -**참고**: 시나리오 정의와 옵션 목록이 다름 (시나리오: 미설정, 접대비, 복리후생비 등) - ---- - -### 6-8. 계정과목명 일괄변경 테스트 ❌ FAIL - -**BUG-CARD-20260115-001** - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 체크박스 선택 | 1개 항목 선택 | 1개 항목 선택됨 | ✅ | -| 계정과목명 선택 | 경비 | 경비 선택됨 | ✅ | -| 저장 버튼 클릭 | 동작 | 동작 | ✅ | -| 확인 다이얼로그 | 표시 | "1개의 카드 사용 내역을 경비(으)로 모두 변경하시겠습니까?" | ✅ | -| 확인 버튼 클릭 | 동작 | 동작 | ✅ | -| 데이터 변경 | 미설정 → 경비 | **미설정 (변경 없음)** | ❌ | - -**버그 상세**: -- **증상**: 확인 다이얼로그까지 정상 표시되나 실제 데이터 변경 안됨 -- **심각도**: Critical -- **영향**: 목록 페이지에서 일괄변경 기능 미동작 -- **관련 버그**: - - BUG-DEPOSIT-20260115-001 (입금관리 동일 증상) - - BUG-WITHDRAWAL-20260115-001 (출금관리 동일 증상) - - BUG-SALES-20260115-001 (매출관리 동일 증상) - ---- - -### 9-10. 모달창 열기 및 필드 검증 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 모달 타이틀 | 카드거래 상세 | 카드 내역 상세 | ⚠️ 명칭 상이 | -| 설명 | - | 카드 사용 상세 내역을 등록합니다 | ✅ | - -#### 모달 필드 상태 - -| 필드명 | 타입 | 상태 | 값 (테스트 행) | -|--------|------|------|----------------| -| 사용일시 | paragraph | disabled | 2025-11-19 | -| 카드 | paragraph | disabled | - (-) | -| 사용자 | paragraph | disabled | - | -| 사용금액 | paragraph | disabled | 3,293,557원 | -| 가맹점 | paragraph | disabled | GS칼텍스 지급 | -| 적요 | textbox | **enabled** | (빈 값) | -| 사용 유형 | combobox | **enabled** | 미설정 | - -#### 모달 버튼 - -| 버튼 | 존재 여부 | -|------|----------| -| 수정 | ✅ | -| Close | ✅ | - -**참고**: 시나리오의 "저장" 버튼은 실제로 "수정" 버튼, "취소" 버튼은 "Close" 버튼 - ---- - -### 11-14. 모달창 수정 및 저장 ✅ PASS - -#### 수정 내용 - -| 필드 | 변경 전 | 변경 후 | -|------|---------|---------| -| 적요 | (빈 값) | 테스트 적요 수정 | -| 사용 유형 | 미설정 | 접대비 | - -#### 모달 사용 유형 드롭다운 옵션 (17개) - -**⚠️ 중요: 목록 페이지 옵션과 다름!** - -1. 미설정 -2. 복리후생비 -3. 접대비 -4. 여비교통비 -5. 차량유지비 -6. 소모품비 -7. 운반비 -8. 통신비 -9. 도서인쇄비 -10. 교육훈련비 -11. 보험료 -12. 광고선전비 -13. 회비 -14. 지급수수료 -15. 세금과공과 -16. 수선비 -17. 임차료 -18. 잡비 - -#### 저장 결과 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 수정 버튼 동작 | 저장 실행 | 저장 실행 | ✅ | -| 모달 닫힘 | 닫힘 | 닫힘 | ✅ | -| URL 유지 | /accounting/card-transactions | /accounting/card-transactions | ✅ | -| 에러 페이지 | 없음 | 없음 | ✅ | -| 테이블 반영 | 접대비 | 접대비 | ✅ | - ---- - -### 15. 모달창 취소 버튼 동작 확인 ✅ PASS - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 다른 행 클릭 | 모달 열림 | 모달 열림 (SK이노베이션 지급) | ✅ | -| Close 버튼 클릭 | 모달 닫힘 | 모달 닫힘 | ✅ | -| 데이터 변경 | 없음 | 미설정 유지 | ✅ | - ---- - -## 발견된 버그 - -### BUG-CARD-20260115-001: 계정과목명 일괄변경 데이터 미반영 - -**Priority**: Critical -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\accounting\card-transactions\page.tsx` - -#### Issue Summary -목록 페이지에서 체크박스로 항목 선택 후 계정과목명을 변경하고 저장 시, 확인 다이얼로그까지 표시되나 실제 데이터는 변경되지 않음. - -#### Steps to Reproduce -1. 회계관리 > 카드거래 접속 -2. 테이블에서 행 체크박스 선택 -3. 계정과목명 드롭다운에서 옵션 선택 (예: 경비) -4. 저장 버튼 클릭 -5. 확인 다이얼로그에서 확인 클릭 -6. 결과: 데이터 미변경 - -#### Expected Result -- 선택된 항목의 사용유형이 변경됨 -- 테이블에 변경된 값 반영 - -#### Actual Result -- 확인 다이얼로그까지 정상 표시 -- 데이터가 변경되지 않음 (미설정 유지) - -#### Error Details -``` -Dialog Message: "1개의 카드 사용 내역을 경비(으)로 모두 변경하시겠습니까?" -Result: 데이터 미변경 (미설정 → 미설정) - -동일 패턴 버그: -- BUG-DEPOSIT-20260115-001 (입금관리) -- BUG-WITHDRAWAL-20260115-001 (출금관리) -- BUG-SALES-20260115-001 (매출관리) -``` - -#### Suggested Fix (Reference Only) -- 확인 버튼 클릭 후 API 호출 로직 점검 -- 요청 페이로드와 실제 DB 업데이트 로직 확인 -- 프론트엔드에서 올바른 파라미터 전송 여부 확인 - -**영향 범위**: api / react -**변경 승인 정책**: ⚠️ 컨펌 필요 - ---- - -## 시나리오 vs 실제 시스템 차이점 - -| 항목 | 시나리오 정의 | 실제 시스템 | 비고 | -|------|--------------|------------|------| -| 페이지 타이틀 | 카드거래 | 카드 내역 조회 | 명명 규칙 차이 | -| 모달 타이틀 | 카드거래 상세 | 카드 내역 상세 | 명명 규칙 차이 | -| 통계 카드 | 사용금액, 사용유형 미설정 | 전월 사용액, 당월 사용액 | 구조 차이 | -| 테이블 컬럼 | 7개 (체크박스, 카드명, 사용일시, 가맹점명, 사용금액, 적요, 사용유형) | 8개 (체크박스, 카드, 카드명, 사용자, 사용일시, 가맹점명, 사용금액, 사용유형) | 컬럼 차이 | -| 목록 계정과목 옵션 | 9개 | 16개 | 옵션 수 차이 | -| 모달 사용유형 옵션 | 9개 | 17개 | 옵션 수 차이 | -| 저장 버튼 (모달) | 저장 | 수정 | 버튼명 차이 | -| 취소 버튼 (모달) | 취소 | Close | 버튼명 차이 | - ---- - -## 드롭다운 옵션 불일치 ⚠️ 주의 - -**목록 페이지 계정과목명 (16개)**: -미설정, 매입대금, 선급금, 가지급금, 임대료, 이자비용, 보증금 지급, 차입금 상환, 배당금 지급, 부가세 납부, 급여, 4대보험, 세금, 공과금, 경비, 기타 - -**모달 사용 유형 (17개)**: -미설정, 복리후생비, 접대비, 여비교통비, 차량유지비, 소모품비, 운반비, 통신비, 도서인쇄비, 교육훈련비, 보험료, 광고선전비, 회비, 지급수수료, 세금과공과, 수선비, 임차료, 잡비 - -**⚠️ 두 드롭다운의 옵션이 완전히 다름!** 이는 의도된 설계인지 확인 필요. - ---- - -## Conclusion - -15개 테스트 케이스 중 13개 통과 (86.7%) - -### 검증 완료 항목 -1. ✅ 회계관리 > 카드거래 메뉴 접근 -2. ✅ 목록 페이지 구조 (통계 카드 2개, 테이블 컬럼 8개) -3. ✅ 2년 기간 설정 (2024-01-15 ~ 2026-01-15) -4. ✅ 테이블 데이터 표시 (12행, 190,119,372원) -5. ✅ 계정과목명 드롭다운 옵션 (16개) -6. ✅ 체크박스 선택 기능 -7. ❌ 계정과목명 일괄변경 (BUG-CARD-20260115-001) -8. ✅ 행 클릭 → 모달창 열기 -9. ✅ 모달창 필드 상태 (읽기전용 5개, 편집가능 2개) -10. ✅ 모달창 적요 수정 -11. ✅ 모달창 사용유형 수정 (17개 옵션) -12. ✅ 모달창 저장 → 테이블 반영 확인 -13. ✅ 모달창 취소(Close) 버튼 동작 - -### 핵심 발견 사항 -- **일괄변경 버그**: 입금/출금/매출/카드거래 4개 메뉴에서 동일 패턴 버그 발생 -- **모달 수정 기능 정상**: 개별 행 수정은 정상 동작 -- **드롭다운 옵션 불일치**: 목록 페이지와 모달의 옵션 목록이 다름 - -### 테스트 제외 항목 -- 검색 기능 -- 필터 기능 (전체/최신순) -- 페이지네이션 -- 기간 버튼 (당해년도, 전전월 등) -- 새로고침 버튼 - ---- - -**Report Generated**: 2026-01-15 -**Tester**: Claude E2E Test Agent diff --git a/plans/clodeCheck/employee-register_2026-01-14_20-00-00.md b/plans/clodeCheck/employee-register_2026-01-14_20-00-00.md deleted file mode 100644 index 9880be9..0000000 --- a/plans/clodeCheck/employee-register_2026-01-14_20-00-00.md +++ /dev/null @@ -1,179 +0,0 @@ -# E2E Test Report: 직원 등록 테스트 - -**Test ID**: employee-register -**Executed**: 2026-01-14 20:00:00 -**Duration**: ~5분 -**Status**: ❌ FAIL - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 8 | -| Passed | 7 | -| Failed | 1 | - -## Step Results - -| Step | Name | Status | Duration | Notes | -|------|------|--------|----------|-------| -| 1 | 인사관리 메뉴 진입 | ✅ PASS | 2s | 인사관리 > 직원관리 메뉴 이동 성공 | -| 2 | 사원 등록 페이지 이동 | ✅ PASS | 1s | /hr/employee-management/new 이동 성공 | -| 3 | 사원 정보 입력 | ✅ PASS | 3s | 이름, 주민등록번호, 휴대폰, 이메일, 연봉 입력 완료 | -| 4 | 급여계좌 입력 | ✅ PASS | 2s | 은행명, 계좌번호, 예금주 입력 완료 | -| 5 | 사원 상세 입력 | ✅ PASS | 2s | 사원코드, 성별, 주소 입력 완료 | -| 6 | 인사 정보 입력 | ✅ PASS | 3s | 입사일, 고용형태(정규직), 직급(과장) 선택 완료 | -| 7 | 사용자 정보 입력 | ✅ PASS | 2s | 아이디, 비밀번호, 비밀번호 확인 입력 완료 | -| 8 | 등록 완료 | ❌ FAIL | 2s | 서버 에러 발생 | - -## Test Data Used - -| Field | Value | -|-------|-------| -| 이름 | 테스트직원_1768387800 | -| 주민등록번호 | 900101-1234567 | -| 휴대폰 | 010-9876-5432 | -| 이메일 | testemployee_1768387800@codebridge-x.com | -| 연봉 | 50000000 | -| 은행명 | 신한은행 | -| 계좌번호 | 110-123-456789 | -| 예금주 | 테스트직원_1768387800 | -| 사원코드 | EMP2026001 | -| 성별 | 남성 | -| 상세주소 | 123번지 4층 | -| 입사일 | 2026-01-14 | -| 고용형태 | 정규직 | -| 직급 | 과장 | -| 상태 | 재직 | -| 아이디 | testuser_1768387800 | -| 비밀번호 | password123! | -| 권한 | 일반 사용자 | -| 계정상태 | 활성 | - -## Error Details - -### Step 8: 등록 완료 - -**Error Type**: Server Error -**Error Message**: `[EmployeeNewPage] Create failed: 서버 에러` -**Console Log**: -``` -[ERROR] [EmployeeNewPage] Create failed: 서버 에러 -``` - -**Network Request**: -``` -[POST] https://dev.codebridge-x.com/hr/employee-management/new => 서버 에러 -``` - -**Screenshot**: [에러 스크린샷](screenshots/employee-register_error_2026-01-14.png) - -## Assertions - -| Type | Expected | Actual | Result | -|------|----------|--------|--------| -| URL (Step 2) | /hr/employee-management/new | /hr/employee-management/new | ✅ PASS | -| 이름 입력 | 테스트직원_1768387800 | 테스트직원_1768387800 | ✅ PASS | -| 이메일 입력 | testemployee_1768387800@codebridge-x.com | testemployee_1768387800@codebridge-x.com | ✅ PASS | -| 고용형태 선택 | 정규직 | 정규직 | ✅ PASS | -| 직급 선택 | 과장 | 과장 | ✅ PASS | -| 아이디 입력 | testuser_1768387800 | testuser_1768387800 | ✅ PASS | -| 등록 완료 | 목록 페이지 리다이렉트 | 서버 에러 | ❌ FAIL | - -## Test Environment - -- **Browser**: Chromium (Playwright) -- **URL**: https://dev.codebridge-x.com -- **Login User**: TestUser5 / 홍킬동 -- **Test Scenario**: employee-register.json - -## Screenshots - -- [에러 스크린샷](screenshots/employee-register_error_2026-01-14.png) - ---- - -## 🐛 Bug Report for Developer - -**Report ID**: 2026-01-14_20-00-00 -**Priority**: High -**Component**: `C:\Users\codeb\react\app\[locale]\(protected)\hr\employee-management\new\page.tsx` - -### Issue Summary -사원 등록 시 서버 에러 발생 - 모든 필수 필드 입력 완료 후 등록 버튼 클릭 시 "서버 에러" 토스트 메시지 출력 - -### Steps to Reproduce -1. 인사관리 > 직원관리 메뉴 진입 -2. "사원 등록" 버튼 클릭 -3. 모든 필수 필드 입력: - - 이름: 테스트직원_1768387800 - - 이메일: testemployee_1768387800@codebridge-x.com - - 아이디: testuser_1768387800 - - 비밀번호: password123! - - 비밀번호 확인: password123! -4. "등록" 버튼 클릭 - -### Expected Result -- 사원 등록 성공 -- 목록 페이지(/hr/employee-management)로 리다이렉트 -- 성공 토스트 메시지 표시 -- 목록에 신규 등록된 사원 표시 - -### Actual Result -- 서버 에러 발생 -- 토스트 메시지: "서버 에러" -- 페이지 이동 없음 (등록 페이지 유지) - -### Error Details -``` -Console Error: [EmployeeNewPage] Create failed: 서버 에러 -``` - -### Screenshots -- [에러 발생 화면](screenshots/employee-register_error_2026-01-14.png) - -### Suggested Fix (Reference Only) - -**영향 범위**: api / react -**변경 승인 정책**: ⚠️ 컨펌 필요 - -**가능한 원인 분석**: -1. **API 엔드포인트 문제**: 사원 등록 API가 500 에러 반환 -2. **데이터 검증 실패**: 서버측 데이터 검증에서 예상치 못한 에러 -3. **DB 제약 조건**: 중복 키 또는 외래 키 제약 조건 위반 -4. **필수 필드 누락**: 부서/직책 미선택으로 인한 서버 검증 실패 가능성 - -**조사 필요 사항**: -1. API 서버 로그 확인 (500 에러 상세 내용) -2. 사원 등록 API 요청 payload 검증 -3. DB 테이블 스키마 및 제약 조건 확인 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` -- DB 스키마: `C:\Users\codeb\docs\specs\database-schema.md` - ---- - -## Notes - -### 테스트 실패 원인 분석 -1. **서버 에러**: API 엔드포인트에서 500 에러 반환 추정 -2. **부서/직책 미선택**: "부서/직책을 추가해주세요" 메시지가 표시되어 있으나, 필수 필드인지 확인 필요 -3. **출퇴근 위치 미선택**: 출근/퇴근 위치가 선택되지 않았으나, 필수 여부 확인 필요 - -### UI/UX 확인 사항 -- ✅ 폼 입력 필드 정상 동작 -- ✅ 드롭다운 선택 정상 동작 -- ✅ 라디오 버튼 선택 정상 동작 -- ✅ 날짜 입력 정상 동작 -- ❌ 등록 버튼 클릭 시 서버 에러 - -### 직급 드롭다운 참고 -- 테스트 시 "사원" 옵션을 찾으려 했으나 "과장"만 표시됨 -- 직급 옵션이 "과장"만 있는 것은 기준정보 설정에 따라 다를 수 있음 - ---- - -**Test Result**: ❌ **FAILED** (7/8 steps passed) diff --git a/plans/clodeCheck/salary-management_2026-01-15_10-30-00.md b/plans/clodeCheck/salary-management_2026-01-15_10-30-00.md deleted file mode 100644 index 7b52803..0000000 --- a/plans/clodeCheck/salary-management_2026-01-15_10-30-00.md +++ /dev/null @@ -1,175 +0,0 @@ -# E2E Test Report: 급여관리 테스트 - -**Test ID**: salary-management -**Executed**: 2026-01-15 10:30:00 -**Duration**: ~8분 -**Status**: ⚠️ PARTIAL (4/5 PASS, 1 FAIL) - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 13 | -| Passed | 12 | -| Failed | 1 | -| Pass Rate | 92.3% | - ---- - -## 필수 검증 항목 결과 - -| # | 검증 항목 | 결과 | 비고 | -|---|----------|------|------| -| 1 | 파일 다운로드 (엑셀) | ❌ FAIL | 기능 미구현 - toast.info만 출력 | -| 2 | 등록/저장 버튼 | ✅ PASS | 지급완료/지급예정 상태 변경 성공 | -| 3 | 검색/필터 | ✅ PASS | 16건 → 1건 필터링 정상 동작 | -| 4 | 모달 등록 완료 | ✅ PASS | 급여 상세 다이얼로그 저장 성공 | -| 5 | 목업 페이지 감지 | ✅ PASS | 정상 페이지 (목업 아님) | - ---- - -## Step Results - -| Step | Name | Status | Notes | -|------|------|--------|-------| -| 1 | 로그인 | ✅ PASS | TestUser5 / password123! 로그인 성공 | -| 2 | 인사관리 > 급여관리 메뉴 진입 | ✅ PASS | /hr/salary-management 페이지 진입 | -| 3 | 필수 검증 #5: 목업 페이지 감지 | ✅ PASS | 입력 필드 및 동작하는 버튼 존재 | -| 4 | 급여 현황 대시보드 확인 | ✅ PASS | 6개 카드 표시 확인 (총 실지급액, 기본급, 수당, 초과근무, 상여, 공제) | -| 5 | 급여 테이블 구조 확인 | ✅ PASS | 14개 컬럼 존재 확인 | -| 6 | 날짜 필터 확인 | ✅ PASS | 시작일/종료일 필드 존재 | -| 7 | 필수 검증 #3: 검색 기능 | ✅ PASS | "홍" 검색 → 16건에서 1건으로 필터링 | -| 8 | 정렬 옵션 확인 | ✅ PASS | 직급순/이름순/부서순/지급일순/지급액순 옵션 확인 | -| 9 | 필수 검증 #2: 상태 변경 (지급완료) | ✅ PASS | 체크박스 선택 후 지급완료 버튼 동작 | -| 10 | 수정 버튼 - 상세 다이얼로그 열기 | ✅ PASS | 급여 수정 다이얼로그 정상 열림 | -| 11 | 필수 검증 #4: 상세 다이얼로그 저장 | ✅ PASS | 상태 변경 후 저장 성공, 토스트 "급여 정보가 저장되었습니다." | -| 12 | 다이얼로그 닫기 확인 | ✅ PASS | 저장 후 자동으로 모달 닫힘 | -| 13 | 필수 검증 #1: 엑셀 다운로드 | ❌ FAIL | 기능 미구현 | - ---- - -## Errors - -### ❌ 필수 검증 #1: 엑셀 다운로드 FAIL - -**버그 유형**: 기능 미구현 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 버튼 클릭 | 다운로드 시작 | 토스트만 표시 | ❌ | -| Console LOG | export 로그 | 없음 | ❌ | -| Network API 호출 | /api/export, /api/download | 미호출 | ❌ | -| Download Event | 발생 | 미발생 | ❌ | -| 토스트 메시지 | 다운로드 완료 | "엑셀 다운로드 기능은 준비 중입니다." | ❌ | - -**최종 판정**: ❌ FAIL (Console LOG만 존재, API 미호출, 다운로드 미발생) - -**코드 분석**: -```tsx -// c:/Users/codeb/react/src/components/hr/SalaryManagement/index.tsx:441 - -``` - ---- - -## 🐛 Bug Report for Developer - -**Report ID**: BUG-SALARY-001-2026-01-15 -**Priority**: Medium -**Component**: `c:\Users\codeb\react\src\components\hr\SalaryManagement\index.tsx:441` - -### Issue Summary -엑셀 다운로드 버튼 클릭 시 실제 다운로드가 발생하지 않고 "엑셀 다운로드 기능은 준비 중입니다." 토스트만 표시됨 - -### Steps to Reproduce -1. 급여관리 페이지 (/hr/salary-management) 접속 -2. "엑셀 다운로드" 버튼 클릭 -3. 토스트 메시지만 표시되고 파일 다운로드 없음 - -### Expected Result -- 엑셀 파일(.xlsx) 다운로드 시작 -- Network API 호출 (예: POST /api/salary/export) -- 다운로드 완료 토스트 또는 파일 저장 다이얼로그 - -### Actual Result -- toast.info('엑셀 다운로드 기능은 준비 중입니다.') 출력 -- Network API 호출 없음 -- 파일 다운로드 없음 - -### Error Details -- Console 에러: 없음 -- Network 요청: 미발생 -- 상태: 기능 미구현 - -### Suggested Fix (Reference Only) - -**영향 범위**: react / api -**변경 승인 정책**: ⚠️ 컨펌 필요 - -1. **React 컴포넌트 수정** (`SalaryManagement/index.tsx`) - - toast.info 대신 실제 export API 호출 로직 구현 - - API 응답으로 Blob 받아 다운로드 처리 - -2. **API 엔드포인트 구현** (필요시) - - POST /api/salary/export 또는 GET /api/salary/download - - 급여 데이터를 엑셀 형식으로 변환하여 반환 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` - ---- - -## 추가 발견 사항 - -### ⚠️ 지급항목 추가 버튼 미구현 - -급여 상세 다이얼로그 내 "지급항목 추가" 버튼도 동일하게 미구현 상태입니다. - -```tsx -// c:/Users/codeb/react/src/components/hr/SalaryManagement/index.tsx:227-229 -const handleAddPaymentItem = useCallback(() => { - // TODO: 지급항목 추가 다이얼로그 또는 로직 구현 - toast.info('지급항목 추가 기능은 준비 중입니다.'); -}, []); -``` - ---- - -## 테스트 환경 - -| 항목 | 값 | -|------|-----| -| 테스트 URL | https://dev.codebridge-x.com | -| 테스트 계정 | TestUser5 | -| 시나리오 파일 | tests/e2e/scenarios/salary-management.json | -| 브라우저 | Playwright (Chromium) | - ---- - -## Console Warnings - -| 유형 | 메시지 | 심각도 | -|------|--------|--------| -| WARNING | Missing `Description` or `aria-describedby={undefined}` for {DialogContent} | Low | - -**권장 조치**: 접근성 개선을 위해 Dialog에 aria-describedby 속성 추가 필요 - ---- - -## 결론 - -급여관리 페이지는 전반적으로 정상 동작하지만, **엑셀 다운로드 기능**과 **지급항목 추가 기능**이 미구현 상태입니다. -해당 기능들은 버튼만 존재하고 실제 로직이 toast.info()로 대체되어 있으므로 백엔드 API 연동 및 프론트엔드 로직 구현이 필요합니다. - -| 기능 | 상태 | 우선순위 | -|------|------|----------| -| 엑셀 다운로드 | 미구현 | Medium | -| 지급항목 추가 | 미구현 | Low | - diff --git a/plans/clodeCheck/sales-management_2026-01-15_test-report.md b/plans/clodeCheck/sales-management_2026-01-15_test-report.md deleted file mode 100644 index 0a81c92..0000000 --- a/plans/clodeCheck/sales-management_2026-01-15_test-report.md +++ /dev/null @@ -1,226 +0,0 @@ -# E2E Test Report: 매출관리 (Sales Management) - -**Test ID**: sales-management -**Executed**: 2026-01-15 -**Status**: ❌ FAIL (11/12) -**Test Environment**: https://dev.codebridge-x.com - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 12 | -| Passed | 11 | -| Failed | 1 | -| Pass Rate | 91.7% | - ---- - -## Step Results - -| Step | Test Case | Status | Duration | Notes | -|------|-----------|--------|----------|-------| -| 1 | 로그인 및 페이지 진입 | ✅ PASS | - | 이미 로그인 상태, /accounting/sales 접속 확인 | -| 2 | 목업 감지 | ✅ PASS | - | 실제 데이터 81건 표시, API 연동 정상 | -| 3 | 테이블 구조 확인 | ✅ PASS | - | 11개 컬럼 확인 (번호~거래명세서) | -| 4 | 계정과목명 드롭박스 변경 | ✅ PASS | - | 8개 옵션 표시, 선택 정상 동작 | -| 5 | 저장 버튼 동작 | ✅ PASS | - | 확인 다이얼로그 + 성공 토스트 표시 | -| 6 | **계정과목명 변경 데이터 반영** | ❌ FAIL | - | **토스트 성공 표시되나 실제 데이터 미변경** | -| 7 | 매출 등록 페이지 이동 | ✅ PASS | - | /accounting/sales/new 이동 확인 | -| 8 | 기본정보 드롭박스 테스트 | ✅ PASS | - | 거래처명 5개, 매출유형 7개 옵션 확인 | -| 9 | 품목 추가/삭제 및 자동계산 | ✅ PASS | - | 동적 추가/삭제 정상, 공급가액/부가세 자동계산 | -| 10 | Switch 버튼 동작 | ✅ PASS | - | 세금계산서/거래명세서 발행 토글 정상 | -| 11 | 취소 버튼 동작 | ✅ PASS | - | 목록 페이지 복귀 확인 | -| 12 | 등록 API 호출 | ⏭️ SKIP | - | 이전 테스트에서 검증 완료 | - ---- - -## Detailed Test Results - -### 1. 목록 페이지 검증 - -#### 목업 감지 검증 -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 데이터 존재 | 있음 | 81건 | ✅ | -| API 연동 | 정상 | 정상 | ✅ | -| 입력 필드 | 있음 | 있음 | ✅ | -| 버튼 동작 | 정상 | 정상 | ✅ | - -**판정**: 정상 페이지 (목업 아님) - -#### 테이블 구조 -| # | 컬럼명 | 존재 여부 | -|---|--------|----------| -| 1 | 번호 | ✅ | -| 2 | 매출번호 | ✅ | -| 3 | 매출일 | ✅ | -| 4 | 거래처 | ✅ | -| 5 | 공급가액 | ✅ | -| 6 | 부가세 | ✅ | -| 7 | 합계금액 | ✅ | -| 8 | 매출유형 | ✅ | -| 9 | 세금계산서 발행완료 | ✅ | -| 10 | 거래명세서 발행완료 | ✅ | -| 11 | (액션) | ✅ | - ---- - -### 2. 계정과목명 일괄 변경 - -#### 드롭박스 옵션 -- 미설정, 제품 매출, 상품 매출, 부품 매출, 용역 매출, 공사 매출, 임대수익, 기타매출 - -#### 저장 동작 검증 -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 확인 다이얼로그 | 표시 | "1개의 매출유형을 제품 매출(으)로 모두 변경하시겠습니까?" | ✅ | -| 성공 토스트 | 표시 | "계정과목명이 변경되었습니다." | ✅ | -| URL 유지 | /accounting/sales | /accounting/sales | ✅ | -| **데이터 변경** | **제품 매출** | **기타 매출 (변경 안됨)** | ❌ | - ---- - -### 3. 매출 등록 페이지 - -#### 페이지 구조 -- 기본 정보: 매출번호(자동생성), 매출일, 거래처명, 매출유형 -- 품목 정보: 테이블 + 추가 버튼 -- 세금계산서: Switch + 상태 표시 -- 거래명세서: Switch + 조회/발행 버튼 + 상태 표시 -- 취소/등록 버튼 - -#### 거래처명 드롭박스 -- 거래처테스트, 아크더레드, 코브라브릿지, 가우스전자, 아크아크 - -#### 매출유형 드롭박스 -- 외상 매출, 제품 매출, 상품 매출, 부품 매출, 공사 매출, 임대 수익, 기타 매출 - ---- - -### 4. 품목 정보 자동계산 검증 - -#### 테스트 데이터 -| 품목 | 수량 | 단가 | 공급가액 | 부가세 | -|------|------|------|----------|--------| -| 테스트 품목 A | 10 | 50,000 | 500,000 | 50,000 | -| 테스트 품목 B | 5 | 30,000 | 150,000 | 15,000 | -| **합계** | - | - | **650,000** | **65,000** | - -#### 자동계산 검증 -| 항목 | 계산식 | 예상 | 실제 | 결과 | -|------|--------|------|------|------| -| 공급가액 A | 10 × 50,000 | 500,000 | 500,000 | ✅ | -| 부가세 A | 500,000 × 10% | 50,000 | 50,000 | ✅ | -| 공급가액 B | 5 × 30,000 | 150,000 | 150,000 | ✅ | -| 부가세 B | 150,000 × 10% | 15,000 | 15,000 | ✅ | -| 합계 공급가액 | 500,000 + 150,000 | 650,000 | 650,000 | ✅ | -| 합계 부가세 | 50,000 + 15,000 | 65,000 | 65,000 | ✅ | - -#### 품목 삭제 검증 -- 두 번째 품목 삭제 후 합계: 500,000 / 50,000 ✅ - ---- - -### 5. Switch 버튼 동작 - -| Switch | 초기 상태 | 클릭 후 상태 | 결과 | -|--------|----------|-------------|------| -| 세금계산서 발행 | 미발행 | 발행완료 | ✅ | -| 거래명세서 발행 | 미발행 | 발행완료 | ✅ | - ---- - -### 6. 취소 버튼 동작 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 클릭 후 URL | /accounting/sales | /accounting/sales | ✅ | -| 페이지 이동 | 목록 페이지 | 목록 페이지 | ✅ | - ---- - -## 🐛 Bug Report: 계정과목명 변경 데이터 미반영 - -**Report ID**: BUG-SALES-20260115-001 -**Priority**: High -**Component**: `C:\Users\codeb\react\src\components\accounting\SalesManagement\` - -### Issue Summary -계정과목명 일괄 변경 기능에서 성공 토스트가 표시되지만 실제 데이터가 변경되지 않음 - -### Steps to Reproduce -1. 매출관리 목록 페이지 (/accounting/sales) 접속 -2. 테이블에서 첫 번째 행의 체크박스 선택 (SL202601150001, 현재 매출유형: "기타 매출") -3. 상단 계정과목명 드롭박스에서 "제품 매출" 선택 -4. "저장" 버튼 클릭 -5. 확인 다이얼로그에서 "확인" 클릭 - -### Expected Result -- 선택된 행의 매출유형이 "제품 매출"로 변경되어야 함 -- 페이지 새로고침 후에도 변경된 값이 유지되어야 함 - -### Actual Result -- ✅ 확인 다이얼로그: "1개의 매출유형을 제품 매출(으)로 모두 변경하시겠습니까?" 표시 -- ✅ 성공 토스트: "계정과목명이 변경되었습니다." 표시 -- ❌ 테이블의 매출유형 값이 여전히 "기타 매출"로 표시됨 -- ❌ 페이지 새로고침 후에도 "기타 매출" 유지 (데이터 미저장) - -### Error Analysis -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 확인 다이얼로그 | 표시 | 표시됨 | ✅ | -| 성공 토스트 | 표시 | 표시됨 | ✅ | -| 매출유형 변경 | 제품 매출 | 기타 매출 (변경 안됨) | ❌ | -| 데이터 영속성 | 저장됨 | 미저장 | ❌ | - -### Suggested Fix (Reference Only) - -**가능한 원인 분석**: -1. **API 미호출**: 프론트엔드에서 저장 API를 호출하지 않을 수 있음 -2. **API 파라미터 오류**: 선택된 ID 또는 변경할 값이 올바르게 전달되지 않을 수 있음 -3. **API 응답 처리 오류**: API는 성공했으나 프론트엔드에서 상태를 갱신하지 않을 수 있음 -4. **백엔드 버그**: API가 성공 응답을 반환하지만 실제 DB 업데이트가 이루어지지 않을 수 있음 - -**영향 범위**: react / api -**변경 승인 정책**: ⚠️ 컨펌 필요 - -**확인 필요 사항**: -1. `actions.ts`의 `updateSale()` 함수가 일괄 변경 시 올바르게 호출되는지 확인 -2. API 요청 payload에 선택된 ID와 변경할 계정과목 값이 포함되는지 확인 -3. 백엔드 `/api/v1/sales/{id}` PUT 엔드포인트의 실제 동작 확인 -4. 네트워크 탭에서 실제 API 호출 여부 및 응답 확인 - -### Related Documentation -- SAM 정책: `C:\Users\codeb\.claude\skills\sam_policy\SKILL.md` -- 문서 인덱스: `C:\Users\codeb\docs\INDEX.md` -- API 규칙: `C:\Users\codeb\docs\standards\api-rules.md` - ---- - -## Conclusion - -11개 테스트 케이스 중 1개 실패 (91.7% 통과율) - -### 검증 완료 항목 (11/12) -1. ✅ 목록 페이지 - 목업 아닌 실제 동작 확인 (81건 데이터) -2. ✅ 테이블 구조 - 11개 컬럼 정상 표시 -3. ✅ 계정과목명 드롭박스 - 8개 옵션 표시, 저장 버튼 동작 정상 -4. ❌ **계정과목명 변경 데이터 반영 - 토스트 성공 표시되나 실제 데이터 미변경 (버그)** -5. ✅ 매출 등록 페이지 - 페이지 이동 정상 -6. ✅ 거래처명 드롭박스 - 5개 옵션 정상 -7. ✅ 매출유형 드롭박스 - 7개 옵션 정상 -8. ✅ 품목 동적 추가/삭제 - 정상 동작 -9. ✅ 자동계산 로직 - 공급가액(수량×단가), 부가세(10%) 정확 -10. ✅ Switch 버튼 - 세금계산서/거래명세서 토글 정상 -11. ✅ 취소 버튼 - 목록 페이지 복귀 정상 - -### 테스트 제외 항목 (사용자 요청) -- 삭제 기능 - ---- - -**Report Generated**: 2026-01-15 -**Tester**: Claude E2E Test Agent diff --git a/plans/clodeCheck/withdrawal-management_2026-01-15_test-report.md b/plans/clodeCheck/withdrawal-management_2026-01-15_test-report.md deleted file mode 100644 index bf7be19..0000000 --- a/plans/clodeCheck/withdrawal-management_2026-01-15_test-report.md +++ /dev/null @@ -1,299 +0,0 @@ -# E2E Test Report: 출금관리 (Withdrawal Management) - -**Test ID**: withdrawal-management -**Executed**: 2026-01-15 -**Status**: ⚠️ PARTIAL (11/12 - 1 Bug) -**Test Environment**: https://dev.codebridge-x.com - ---- - -## Summary - -| Item | Result | -|------|--------| -| Total Steps | 12 | -| Passed | 11 | -| Failed | 1 | -| Pass Rate | 91.7% | - ---- - -## Step Results - -| Step | Test Case | Status | Notes | -|------|-----------|--------|-------| -| 1 | 회계관리 메뉴 진입 | ✅ PASS | /accounting/withdrawals 접속 확인 | -| 2 | 목록 페이지 구조 검증 | ✅ PASS | 통계 카드 4개, 테이블 컬럼 8개 확인 | -| 3 | 계정과목명 드롭다운 옵션 확인 | ✅ PASS | 16개 옵션 확인 (시나리오 14개와 상이) | -| 4 | 계정과목명 일괄변경 테스트 | ❌ FAIL | API 200 OK, 데이터 미반영 | -| 5 | 상세 페이지 진입 | ✅ PASS | /accounting/withdrawals/58 이동 확인 | -| 6 | 상세 페이지 필드 검증 | ✅ PASS | 기본 정보 섹션 7개 필드 확인 | -| 7 | 수정 모드 전환 | ✅ PASS | ?mode=edit URL 변경, 버튼 변경 확인 | -| 8 | 수정 가능 필드 검증 | ✅ PASS | 적요, 거래처, 출금유형 수정 가능 | -| 9 | 필수값 유효성 검증 | ✅ PASS | "거래처를 선택해주세요" 토스트 확인 | -| 10 | 상세 페이지 수정 저장 | ✅ PASS | 거래처, 출금유형 변경 후 저장 성공 | -| 11 | 수정 데이터 반영 확인 | ✅ PASS | 목록에서 변경된 데이터 확인 | -| 12 | 출금유형 미설정 건수 감소 | ✅ PASS | 60건 → 59건 확인 | - ---- - -## Detailed Test Results - -### 1. 회계관리 메뉴 진입 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | /accounting/withdrawals | /accounting/withdrawals | ✅ | -| 페이지 타이틀 | 출금관리 | 출금관리 | ✅ | -| 인증 상태 | 로그인됨 | 로그인됨 | ✅ | - ---- - -### 2. 목록 페이지 구조 검증 - -#### 통계 카드 (4개) - -| 카드명 | 값 | 결과 | -|--------|-----|------| -| 총 출금 | 1,214,143,687원 | ✅ | -| 당월 출금 | 0원 | ✅ | -| 거래처 미설정 | 0건 | ✅ | -| 출금유형 미설정 | 60건 | ✅ | - -#### 테이블 컬럼 (8개) - -| # | 컬럼명 | 시나리오 | 결과 | -|---|--------|----------|------| -| 1 | 체크박스 | 체크박스 | ✅ | -| 2 | 출금일 | 출금일 | ✅ | -| 3 | 출금계좌 | 출금계좌 | ✅ | -| 4 | 수취인명 | 받는분 | ⚠️ 컬럼명 상이 | -| 5 | 출금금액 | 출금금액 | ✅ | -| 6 | 거래처 | 거래처 | ✅ | -| 7 | 적요 | 적요 | ✅ | -| 8 | 출금유형 | 출금유형 | ✅ | - -**참고**: 시나리오의 "받는분" 컬럼이 실제 시스템에서는 "수취인명"으로 표시됨 - ---- - -### 3. 계정과목명 드롭다운 옵션 - -**실제 옵션 (16개)**: -1. 미설정 -2. 매입대금 -3. 선급금 -4. 가지급금 -5. 임대료 -6. 이자비용 -7. 보증금 지급 -8. 차입금 상환 -9. 배당금 지급 -10. 부가세 납부 -11. 급여 -12. 4대보험 -13. 세금 -14. 공과금 -15. 경비 -16. 기타 - -**참고**: 시나리오에는 14개 옵션으로 정의되어 있으나 실제로는 16개 옵션 존재 - ---- - -### 4. 계정과목명 일괄변경 테스트 ❌ FAIL - -**BUG-WITHDRAWAL-20260115-001** - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 체크박스 선택 | 1개 항목 선택 | 1개 항목 선택됨 | ✅ | -| 계정과목명 선택 | 매입대금 | 매입대금 | ✅ | -| 저장 버튼 클릭 | 동작 | 동작 | ✅ | -| 확인 다이얼로그 | 표시 | "1개의 출금 유형을 매입대금(으)로 모두 변경하시겠습니까?" | ✅ | -| 확인 버튼 클릭 | 동작 | 동작 | ✅ | -| API 호출 | POST /accounting/withdrawals | POST /accounting/withdrawals (200 OK) | ✅ | -| 데이터 변경 | 미설정 → 매입대금 | **미설정 (변경 없음)** | ❌ | -| 출금유형 미설정 건수 | 59건 | **60건 (변경 없음)** | ❌ | - -**버그 상세**: -- **증상**: API 호출은 성공(200 OK)하지만 실제 데이터가 변경되지 않음 -- **심각도**: High -- **영향**: 일괄변경 기능 미동작 -- **버그 유형**: 백엔드 API 로직 오류 또는 프론트엔드-백엔드 데이터 불일치 -- **관련 버그**: - - BUG-DEPOSIT-20260115-001 (입금관리 동일 증상) - - BUG-SALES-20260115-001 (매출관리 동일 증상) - ---- - -### 5-6. 상세 페이지 진입 및 필드 검증 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | /accounting/withdrawals/{id} | /accounting/withdrawals/58 | ✅ | -| 페이지 타이틀 | 출금 상세 | 출금 상세 | ✅ | -| 버튼 | 목록, 삭제, 수정 | 목록, 삭제, 수정 | ✅ | - -#### 기본 정보 필드 - -| 필드명 | 타입 | 상태 | 값 | 결과 | -|--------|------|------|-----|------| -| 출금일 | textbox | disabled | 2025-12-27 | ✅ | -| 출금계좌 | textbox | disabled | 운영계좌 | ✅ | -| 수취인명 | textbox | disabled | 두산에너빌리티 | ✅ | -| 출금금액 | textbox | disabled | 1,513,170 | ✅ | -| 적요 | textbox | disabled | 두산에너빌리티 지급 | ✅ | -| 거래처 * | combobox | disabled | 선택 ▼ | ✅ | -| 출금 유형 * | combobox | disabled | 미설정 | ✅ | - ---- - -### 7-8. 수정 모드 전환 및 필드 활성화 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| URL | ?mode=edit 추가 | /accounting/withdrawals/58?mode=edit | ✅ | -| 페이지 타이틀 | 출금 수정 | 출금 수정 | ✅ | -| 버튼 변경 | 취소, 저장 | 취소, 저장 | ✅ | - -#### 수정 모드 필드 상태 - -| 필드명 | 읽기 모드 | 수정 모드 | 결과 | -|--------|----------|----------|------| -| 출금일 | disabled | disabled | ✅ | -| 출금계좌 | disabled | disabled | ✅ | -| 수취인명 | disabled | disabled | ✅ | -| 출금금액 | disabled | disabled | ✅ | -| 적요 | disabled | **enabled** | ✅ | -| 거래처 | disabled | **enabled** | ✅ | -| 출금 유형 | disabled | **enabled** | ✅ | - ---- - -### 9. 필수값 유효성 검증 - -| 시나리오 | 입력값 | 예상 결과 | 실제 결과 | 결과 | -|----------|--------|----------|----------|------| -| 거래처 미선택 후 저장 | 거래처: 선택 ▼, 출금유형: 매입대금 | 유효성 에러 | "거래처를 선택해주세요." 토스트 | ✅ | - ---- - -### 10-12. 상세 페이지 수정 및 저장 - -#### 수정 내용 - -| 필드 | 변경 전 | 변경 후 | -|------|---------|---------| -| 거래처 | 선택 ▼ (두산에너빌리티) | 거래처테스트 | -| 출금유형 | 미설정 | 매입대금 | - -#### 저장 결과 - -| 항목 | 예상 | 실제 | 결과 | -|------|------|------|------| -| 저장 버튼 동작 | 저장 실행 | 저장 실행 | ✅ | -| 리다이렉트 | /accounting/withdrawals | /accounting/withdrawals | ✅ | -| 거래처 변경 | 거래처테스트 | 거래처테스트 | ✅ | -| 출금유형 변경 | 매입대금 | 매입대금 | ✅ | -| 미설정 건수 | 59건 | 59건 | ✅ | - ---- - -## 발견된 버그 - -### BUG-WITHDRAWAL-20260115-001: 계정과목명 일괄변경 데이터 미반영 - -**Priority**: High -**Component**: `C:\Users\codeb\react\src\app\[locale]\(protected)\accounting\withdrawals\page.tsx` - -#### Issue Summary -목록 페이지에서 체크박스로 항목 선택 후 계정과목명을 변경하고 저장 시, API는 성공 응답(200 OK)을 반환하지만 실제 데이터는 변경되지 않음. - -#### Steps to Reproduce -1. 회계관리 > 출금관리 접속 -2. 테이블에서 행 체크박스 선택 -3. 계정과목명 드롭다운에서 옵션 선택 (예: 매입대금) -4. 저장 버튼 클릭 -5. 확인 다이얼로그에서 확인 클릭 -6. 결과: API 200 OK, 데이터 미변경 - -#### Expected Result -- 선택된 항목의 출금유형이 변경됨 -- 출금유형 미설정 건수가 감소함 - -#### Actual Result -- API 응답은 성공(200 OK) -- 데이터가 변경되지 않음 -- 출금유형 미설정 건수 그대로 유지 - -#### Error Details -``` -Network Request: POST /accounting/withdrawals => 200 OK -Console: No errors -Data: 미설정 → 미설정 (변경 없음) -``` - -#### Related Bugs -- BUG-DEPOSIT-20260115-001: 입금관리 일괄변경 (동일 증상) -- BUG-SALES-20260115-001: 매출관리 일괄변경 (동일 증상) - -#### Suggested Fix (Reference Only) -- 백엔드 API 로직 점검 필요 -- 요청 페이로드와 실제 DB 업데이트 로직 확인 -- 프론트엔드에서 올바른 파라미터 전송 여부 확인 - -**영향 범위**: api / react -**변경 승인 정책**: ⚠️ 컨펌 필요 - ---- - -## 시나리오 vs 실제 시스템 차이점 - -| 항목 | 시나리오 정의 | 실제 시스템 | 비고 | -|------|--------------|------------|------| -| 테이블 컬럼명 | 받는분 | 수취인명 | 명명 규칙 차이 | -| 계정과목 옵션 수 | 14개 | 16개 | 2개 추가 (4대보험, 공과금) | - ---- - -## 거래처 드롭다운 옵션 (상세 페이지) - -| # | 거래처명 | -|---|----------| -| 1 | 거래처테스트 | -| 2 | 아크더레드 | -| 3 | 코브라브릿지 | -| 4 | 가우스전자 | -| 5 | 아크아크 | - ---- - -## Conclusion - -12개 테스트 케이스 중 11개 통과 (91.7%) - -### 검증 완료 항목 -1. ✅ 회계관리 > 출금관리 메뉴 접근 -2. ✅ 목록 페이지 구조 (통계 카드 4개, 테이블 컬럼 8개) -3. ✅ 계정과목명 드롭다운 옵션 (16개) -4. ❌ 계정과목명 일괄변경 (BUG-WITHDRAWAL-20260115-001) -5. ✅ 상세 페이지 진입 및 정보 표시 -6. ✅ 수정 모드 전환 -7. ✅ 필드 활성화 상태 변경 -8. ✅ 필수값 유효성 검증 -9. ✅ 상세 페이지 데이터 수정 및 저장 -10. ✅ 수정 데이터 목록 반영 - -### 테스트 제외 항목 -- 삭제 기능 -- 검색 기능 -- 필터 기능 (전체/전체/최신순) -- 페이지네이션 -- 날짜 필터 버튼 (당해년도, 전전월 등) -- 취소 버튼 동작 - ---- - -**Report Generated**: 2026-01-15 -**Tester**: Claude E2E Test Agent diff --git a/plans/db-trigger-audit-system-plan.md b/plans/db-trigger-audit-system-plan.md deleted file mode 100644 index 62da7d9..0000000 --- a/plans/db-trigger-audit-system-plan.md +++ /dev/null @@ -1,1294 +0,0 @@ -# DB 트리거 기반 데이터 변경 추적 시스템 계획 - -> **작성일**: 2026-02-07 -> **목적**: 모든 경로(앱, 직접SQL, AI, phpMyAdmin 등)의 데이터 변경을 DB 레벨에서 추적하고 복구 가능하게 함 -> **기준 문서**: `docs/specs/database-schema.md`, `api/app/Traits/Auditable.php`, `api/config/audit.php` -> **상태**: 🔄 Phase 1-3 완료, Phase 4 핵심 완료 (4.4~4.6 옵션 잔여) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 4 핵심 (mng 대시보드 + 목록 + 상세 + 이력 + 롤백) | -| **다음 작업** | Phase 4.4 트리거 관리 화면 (옵션) | -| **진행률** | 15/16 (94%) - 핵심 기능 완료, 옵션 3개 잔여 | -| **마지막 업데이트** | 2026-02-07 | - ---- - -## 1. 개요 - -### 1.1 배경 - -SAM 프로젝트에는 이미 Laravel `Auditable` trait 기반 감사 로그가 존재하지만, 이는 **Laravel Eloquent ORM을 통한 변경만 추적**한다. 다음 경로의 변경은 추적 불가: - -- AI(Claude 등)가 직접 실행하는 SQL 쿼리 -- phpMyAdmin, DBeaver 등 DB 클라이언트에서의 직접 수정 -- MySQL CLI에서의 직접 쿼리 -- 다른 애플리케이션/스크립트에서의 DB 접근 -- Laravel `DB::statement()` 등 Eloquent 우회 쿼리 - -**해결책**: MySQL 트리거를 사용하여 DB 엔진 레벨에서 모든 INSERT/UPDATE/DELETE를 포착한다. - -### 1.2 기준 원칙 - -``` -+------------------------------------------------------------------+ -| 계층 분리 (Layered Audit) | -+------------------------------------------------------------------+ -| Layer 1: Laravel Audit (기존 유지) | -| - 비즈니스 액션 (released, cloned, items_replaced 등) | -| - 사용자 컨텍스트 풍부 (IP, UA, 세션 정보) | -| - 실패 시 비즈니스 로직 불영향 (try/catch) | -+------------------------------------------------------------------+ -| Layer 2: MySQL Trigger Audit (신규) | -| - 모든 DML 포착 (직접 쿼리 포함, 누락 불가) | -| - 컬럼 단위 old/new values JSON 저장 | -| - 특정 레코드의 특정 시점으로 복원 가능 | -+------------------------------------------------------------------+ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 트리거 대상 테이블 목록 조정, 제외 컬럼 변경 | 불필요 | -| ⚠️ 컨펌 필요 | 마이그레이션 실행, 트리거 생성/변경, 미들웨어 추가, 새 API 엔드포인트 | **필수** | -| 🔴 금지 | 기존 audit_logs 테이블 구조 변경, 기존 Auditable trait 수정 | 별도 협의 | - -### 1.4 준수 규칙 - -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/specs/database-schema.md` - DB 스키마 -- `docs/standards/api-rules.md` - API 규칙 (Audit Logging 섹션) - ---- - -## 2. 대상 범위 - -### 2.1 Phase 1: DB 기반 구축 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | trigger_audit_logs 테이블 마이그레이션 (파티셔닝 포함) | ✅ | 15개 파티션, 3개 인덱스 | -| 1.2 | 트리거 대상 테이블 선정 및 확정 | ✅ | 제외 11개 외 전체 적용 | -| 1.3 | 트리거 자동 생성 (PHP 기반, SP 불가) | ✅ | MySQL CREATE TRIGGER는 PREPARE 미지원 → PHP 마이그레이션으로 전환 | -| 1.4 | 대상 테이블별 트리거 생성 | ✅ | 789개 트리거 (263 테이블 × 3) | -| 1.5 | 세션 변수 설정 미들웨어 (Laravel) | ✅ | @sam_actor_id, @sam_session_info | - -### 2.2 Phase 2: 복구 메커니즘 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | TriggerAuditLog 모델 | ✅ | casts, scopes, changed_columns accessor | -| 2.2 | AuditRollbackService 구현 | ✅ | rollback SQL 생성 + 실행 + getRecordStateAt | -| 2.3 | Trigger Audit 조회 API | ✅ | 6개 엔드포인트 (index, show, stats, history, rollback-preview, rollback) | -| 2.4 | Rollback API 엔드포인트 | ✅ | POST /api/v1/trigger-audit-logs/{id}/rollback + confirm 필수 | - -### 2.3 Phase 3: 관리 도구 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 통합 조회 뷰 (v_unified_audit) | ✅ | APP 3,108건 + TRIGGER 2,649건 통합, COLLATE 해결 | -| 3.2 | 파티션 자동 관리 (artisan 커맨드) | ✅ | audit:partitions --add-months --retention-months --drop --dry-run | -| 3.3 | 트리거 재생성 artisan 커맨드 | ✅ | audit:triggers --table --drop-only --dry-run | - -### 2.4 Phase 4: 관리자 대시보드 (mng) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | 변경 이력 목록 화면 (index) | ✅ | 통계카드+필터+목록+파티션현황+트리거수, 페이지네이션 | -| 4.2 | 레코드 상세 변경 이력 (show + history) | ✅ | diff 뷰(old/new 비교, 변경 컬럼 하이라이트) + 레코드 타임라인 | -| 4.3 | 복구 기능 UI (rollback-preview) | ✅ | SQL 미리보기, 확인 체크박스+confirm, @disable_audit_trigger | -| 4.4 | 트리거 관리 화면 | ⏭️ | 옵션 - artisan audit:triggers 커맨드로 CLI 관리 가능 | -| 4.5 | 대시보드 통계 | ✅ | index에 통합 (전체/오늘/DML별 통계, 상위 테이블, 파티션, 저장소) | -| 4.6 | 보관 정책 설정 | ⏭️ | 옵션 - artisan audit:partitions 커맨드로 CLI 관리 가능 | - ---- - -## 3. 작업 절차 - -### 3.1 아키텍처 다이어그램 - -``` -[사용자/AI/phpMyAdmin/스크립트] - │ - ▼ - ┌─────────┐ - │ MySQL │ - │ Engine │ - └────┬────┘ - │ DML (INSERT/UPDATE/DELETE) - ▼ - ┌─────────────────────────┐ - │ 대상 테이블 │ - │ (제외 목록 외 전체 │ - │ 약 207개) │ - └────┬────────────────────┘ - │ AFTER 트리거 발동 - ▼ - ┌─────────────────────────┐ - │ trigger_audit_logs │ - │ (파티셔닝, 13개월 보관) │ - │ - table_name │ - │ - row_id │ - │ - dml_type │ - │ - old_values (JSON) │ - │ - new_values (JSON) │ - │ - tenant_id │ - │ - actor_id │ ← @sam_actor_id 세션변수 - │ - session_info │ ← @sam_session_info 세션변수 - │ - db_user │ ← CURRENT_USER() - │ - created_at │ - └─────────────────────────┘ - │ - ▼ - ┌─────────────────────────┐ - │ AuditRollbackService │ - │ (Laravel) │ - │ - 이력 조회 │ - │ - Rollback SQL 생성 │ - │ - 특정 시점 복원 │ - └─────────────────────────┘ -``` - -### 3.2 트리거 대상 테이블 - -#### 적용 방침: 전체 적용 (제외 목록 방식) - -로컬 개발 환경에서 1인 사용이므로, **제외 대상을 제외한 모든 테이블에 트리거를 적용**한다. -운영 환경 전환 시 필요에 따라 대상을 축소할 수 있다. - -SP(`sp_create_audit_triggers`)가 `INFORMATION_SCHEMA.TABLES`에서 samdb의 전체 테이블을 읽고, -제외 목록에 없는 모든 테이블에 자동으로 트리거를 생성한다. - -#### 제외 대상 (트리거 미적용) - -| 테이블 패턴 | 사유 | -|-------------|------| -| `audit_logs` | 감사 로그 자체 (순환 방지) | -| `trigger_audit_logs` | 트리거 감사 자체 (순환 방지) | -| `personal_access_tokens` | Sanctum 토큰 (대량 생성/삭제, 보안 데이터) | -| `sessions` | 세션 데이터 (빈번한 갱신) | -| `cache`, `cache_locks` | 캐시 데이터 | -| `jobs`, `job_batches` | 큐 작업 | -| `failed_jobs` | 실패 큐 | -| `migrations` | 마이그레이션 기록 | -| `password_reset_tokens` | 비밀번호 리셋 토큰 | -| `telescope_*` | 디버그 도구 (있는 경우) | - -> **예상**: samdb 약 219개 테이블 - 제외 약 12개 = **약 207개 테이블 × 3 트리거 = 약 621개 트리거** -> -> SP가 `INFORMATION_SCHEMA`에서 동적으로 테이블을 읽으므로, 테이블이 추가/삭제되면 -> `artisan audit:regenerate-triggers` 명령으로 트리거를 재생성하면 된다. - -### 3.3 trigger_audit_logs 테이블 구조 - -```sql -CREATE TABLE trigger_audit_logs ( - id BIGINT UNSIGNED AUTO_INCREMENT, - table_name VARCHAR(64) NOT NULL COMMENT '변경된 테이블명', - row_id VARCHAR(64) NOT NULL COMMENT '변경된 레코드 PK (문자열 지원)', - dml_type ENUM('INSERT','UPDATE','DELETE') NOT NULL COMMENT 'DML 유형', - old_values JSON DEFAULT NULL COMMENT '변경 전 값 (INSERT시 NULL)', - new_values JSON DEFAULT NULL COMMENT '변경 후 값 (DELETE시 NULL)', - changed_columns JSON DEFAULT NULL COMMENT 'UPDATE시 변경된 컬럼 목록', - tenant_id BIGINT UNSIGNED DEFAULT NULL COMMENT '테넌트 ID', - actor_id BIGINT UNSIGNED DEFAULT NULL COMMENT '사용자 ID (세션변수)', - session_info VARCHAR(500) DEFAULT NULL COMMENT '세션 정보 JSON (IP, UA 등)', - db_user VARCHAR(100) DEFAULT NULL COMMENT 'CURRENT_USER()', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '변경 시각', - PRIMARY KEY (id, created_at) -) ENGINE=InnoDB - DEFAULT CHARSET=utf8mb4 - COLLATE=utf8mb4_unicode_ci - COMMENT='DB 트리거 기반 데이터 변경 추적' - PARTITION BY RANGE (UNIX_TIMESTAMP(created_at)) ( - PARTITION p202601 VALUES LESS THAN (UNIX_TIMESTAMP('2026-02-01')), - PARTITION p202602 VALUES LESS THAN (UNIX_TIMESTAMP('2026-03-01')), - PARTITION p202603 VALUES LESS THAN (UNIX_TIMESTAMP('2026-04-01')), - PARTITION p202604 VALUES LESS THAN (UNIX_TIMESTAMP('2026-05-01')), - PARTITION p202605 VALUES LESS THAN (UNIX_TIMESTAMP('2026-06-01')), - PARTITION p202606 VALUES LESS THAN (UNIX_TIMESTAMP('2026-07-01')), - PARTITION p202607 VALUES LESS THAN (UNIX_TIMESTAMP('2026-08-01')), - PARTITION p202608 VALUES LESS THAN (UNIX_TIMESTAMP('2026-09-01')), - PARTITION p202609 VALUES LESS THAN (UNIX_TIMESTAMP('2026-10-01')), - PARTITION p202610 VALUES LESS THAN (UNIX_TIMESTAMP('2026-11-01')), - PARTITION p202611 VALUES LESS THAN (UNIX_TIMESTAMP('2026-12-01')), - PARTITION p202612 VALUES LESS THAN (UNIX_TIMESTAMP('2027-01-01')), - PARTITION p202701 VALUES LESS THAN (UNIX_TIMESTAMP('2027-02-01')), - PARTITION p202702 VALUES LESS THAN (UNIX_TIMESTAMP('2027-03-01')), - PARTITION p_future VALUES LESS THAN MAXVALUE - ); - --- 조회 성능 인덱스 -CREATE INDEX ix_trig_table_row_created - ON trigger_audit_logs (table_name, row_id, created_at); - -CREATE INDEX ix_trig_tenant_created - ON trigger_audit_logs (tenant_id, created_at); -``` - -### 3.4 트리거 자동 생성 Stored Procedure - -```sql --- 특정 테이블에 대해 AFTER INSERT/UPDATE/DELETE 트리거 3개를 자동 생성 --- INFORMATION_SCHEMA.COLUMNS에서 컬럼 목록을 읽어 JSON_OBJECT 구문 자동 조립 - -CALL sp_create_audit_triggers('products'); --- → trg_products_ai (AFTER INSERT) --- → trg_products_au (AFTER UPDATE) --- → trg_products_ad (AFTER DELETE) -``` - -**SP 핵심 로직:** -1. `INFORMATION_SCHEMA.COLUMNS`에서 대상 테이블의 컬럼 목록 조회 -2. 제외 컬럼 필터링 (`created_at`, `updated_at`, `deleted_at`, `remember_token` 등) -3. `JSON_OBJECT('col1', NEW.col1, 'col2', NEW.col2, ...)` 구문 자동 조립 -4. UPDATE 트리거: 컬럼별 `OLD.col <> NEW.col` 비교 → changed_columns 배열 생성 -5. 비활성화 플래그 체크 (`@disable_audit_trigger`) -6. `PREPARE + EXECUTE`로 트리거 DDL 실행 - -### 3.5 세션 변수 미들웨어 - -```php -// app/Http/Middleware/SetAuditSessionVariables.php - -class SetAuditSessionVariables -{ - public function handle($request, $next) - { - if (auth()->check()) { - DB::statement("SET @sam_actor_id = ?", [auth()->id()]); - DB::statement("SET @sam_session_info = ?", [ - json_encode([ - 'ip' => $request->ip(), - 'ua' => substr($request->userAgent(), 0, 255), - 'route' => $request->route()?->getName(), - ]) - ]); - } - - return $next($request); - } -} -``` - -### 3.6 복구 서비스 - -```php -// app/Services/Audit/AuditRollbackService.php - -class AuditRollbackService -{ - // 특정 audit 레코드에 대한 역방향 SQL 생성 - public function generateRollbackSQL(int $auditId): string; - - // 실제 복구 실행 (트랜잭션 내에서) - public function executeRollback(int $auditId): bool; - - // 특정 레코드의 특정 시점 상태 조회 - public function getRecordStateAt(string $table, string $rowId, Carbon $at): ?array; - - // 특정 레코드의 변경 이력 조회 - public function getRecordHistory(string $table, string $rowId): Collection; -} -``` - -**복구 로직:** - -| 원본 DML | 복구 SQL | -|----------|---------| -| INSERT | `DELETE FROM {table} WHERE id = {row_id}` | -| UPDATE | `UPDATE {table} SET {old_values 각 컬럼} WHERE id = {row_id}` | -| DELETE | `INSERT INTO {table} ({old_values 컬럼}) VALUES ({old_values 값})` | - ---- - -## 4. 상세 작업 내용 - -> 각 Phase 진행 후 이 섹션에 상세 내용 추가 - -### 4.1 Phase 1: DB 기반 구축 -- **상태**: ⏳ 대기 -- **예상 파일**: - - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_trigger_audit_logs_table.php` - - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_audit_trigger_stored_procedures.php` - - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_audit_triggers_for_tables.php` - - `api/app/Http/Middleware/SetAuditSessionVariables.php` - -### 4.2 Phase 2: 복구 메커니즘 -- **상태**: ⏳ 대기 -- **예상 파일**: - - `api/app/Models/Audit/TriggerAuditLog.php` - - `api/app/Services/Audit/AuditRollbackService.php` - - `api/app/Http/Controllers/Api/V1/Audit/TriggerAuditLogController.php` - - `api/app/Http/Requests/Audit/TriggerAuditLogIndexRequest.php` - - `api/app/Http/Requests/Audit/TriggerAuditRollbackRequest.php` - - `api/app/Swagger/v1/TriggerAuditLogApi.php` - -### 4.3 Phase 3: 관리 도구 -- **상태**: ⏳ 대기 -- **예상 파일**: - - `api/database/migrations/YYYY_MM_DD_HHMMSS_create_unified_audit_view.php` - - `api/app/Console/Commands/ManageAuditPartitions.php` - - `api/app/Console/Commands/RegenerateAuditTriggers.php` - -### 4.4 Phase 4: 관리자 대시보드 (mng) -- **상태**: ⏳ 대기 -- **예상 파일**: - - `mng/app/Http/Controllers/Admin/TriggerAuditController.php` - - `mng/resources/views/admin/trigger-audit/index.blade.php` (이력 목록) - - `mng/resources/views/admin/trigger-audit/show.blade.php` (상세 diff 뷰) - - `mng/resources/views/admin/trigger-audit/dashboard.blade.php` (대시보드 통계) - - `mng/resources/views/admin/trigger-audit/triggers.blade.php` (트리거 관리) - - `mng/resources/views/admin/trigger-audit/settings.blade.php` (보관 정책) - - `mng/app/Services/TriggerAuditDashboardService.php` - ---- - -## 5. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | 트리거 대상 | 제외 목록 외 전체 (약 207개) 적용 | database | ✅ 확정 | -| 2 | 성능 영향 | 로컬 1인 사용, 제한 없음 | database | ✅ 확정 | -| 3 | Phase 4 범위 | 풀 관리 대시보드 (조회/복구/트리거관리/통계/정책) | mng | ✅ 확정 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-02-07 | 계획 | 문서 초안 작성 | - | - | -| 2026-02-07 | 수정 | 피드백 반영: 전체 테이블 적용, Phase 4 대시보드 추가 | - | ✅ | -| 2026-02-07 | Phase 1 | DB 기반 구축 완료. SP→PHP 전환, 789 트리거 생성, my.cnf 설정 추가 | api/database/migrations/2026_02_07_*, api/app/Http/Middleware/SetAuditSessionVariables.php, docker/mysql/my.cnf | ✅ | -| 2026-02-07 | Phase 2 | 복구 메커니즘 API 완료. 모델/서비스/컨트롤러/라우트 6개 엔드포인트 | TriggerAuditLog.php, TriggerAuditLogService.php, AuditRollbackService.php, TriggerAuditLogController.php, audit.php(route) | ✅ | -| 2026-02-07 | Phase 3 | 관리 도구 완료. 통합 뷰(collation 해결), 파티션 관리, 트리거 재생성 커맨드 | v_unified_audit VIEW, ManageAuditPartitions.php, RegenerateAuditTriggers.php | ✅ | - ---- - -## 7. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **DB 스키마**: `docs/specs/database-schema.md` -- **API 규칙**: `docs/standards/api-rules.md` (Audit Logging 섹션) -- **기존 Auditable**: `api/app/Traits/Auditable.php` -- **기존 audit 설정**: `api/config/audit.php` -- **기존 audit 마이그레이션**: `api/database/migrations/2025_09_11_000100_create_audit_logs_table.php` - -### 외부 참고자료 - -- [MySQL 8.0 Trigger Syntax](https://dev.mysql.com/doc/refman/8.0/en/trigger-syntax.html) -- [MySQL 8.0 Partitioning](https://dev.mysql.com/doc/refman/8.0/en/partitioning.html) -- [Percona - MySQL Trigger Performance](https://www.percona.com/blog/why-mysql-stored-procedures-functions-triggers-bad-performance/) - ---- - -## 8. 리스크 및 대응 방안 - -| 리스크 | 영향 | 대응 | -|--------|------|------|| 트리거 성능 오버헤드 (INSERT 약 40-50%) | 쓰기 성능 저하 | 로컬 1인 사용 환경이므로 무관. 운영 전환 시 대상 축소 가능. Bulk 작업 시 `@disable_audit_trigger=1` | -| 트리거 실패 시 원본 DML도 롤백 | 비즈니스 중단 | 트리거 로직 최소화, audit 테이블 구조 안정적 유지 | -| 스키마 변경 시 트리거 유지보수 | 누락 위험 | SP 기반 자동 생성 → `artisan audit:regenerate-triggers` | -| 저장 용량 증가 | 디스크 사용량 | 월별 파티셔닝 + 13개월 자동 삭제 | -| 세션 변수 미설정 (CLI, Queue) | actor_id NULL | NULL 허용, db_user로 보완 추적 | - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 테스트 케이스 - -| 입력값 | 예상 결과 | 실제 결과 | 상태 | -|--------|----------|----------|------| -| Laravel에서 Product 생성 | audit_logs + trigger_audit_logs 모두 기록 | | ⏳ | -| 직접 SQL로 Product UPDATE | trigger_audit_logs에만 기록 | | ⏳ | -| phpMyAdmin에서 DELETE | trigger_audit_logs에 기록 (actor_id=NULL, db_user 기록) | | ⏳ | -| Bulk INSERT 10,000건 (트리거 활성) | trigger_audit_logs에 10,000건 기록, 성능 측정 | | ⏳ | -| Bulk INSERT 10,000건 (트리거 비활성) | trigger_audit_logs 기록 없음, 기본 성능 | | ⏳ | -| UPDATE 후 rollback API 호출 | old_values로 복원됨 | | ⏳ | -| DELETE 후 rollback API 호출 | 삭제된 레코드 복원됨 | | ⏳ | -| INSERT 후 rollback API 호출 | 삽입된 레코드 삭제됨 | | ⏳ | -| 13개월 이전 파티션 삭제 | 해당 파티션 DROP, 데이터 제거 | | ⏳ | - -### 9.2 성공 기준 - -| 기준 | 달성 | 비고 | -|------|------|------| -| 직접 SQL 변경이 trigger_audit_logs에 기록됨 | ⏳ | | -| old_values/new_values JSON이 정확히 저장됨 | ⏳ | | -| 특정 레코드의 특정 시점 복원이 가능함 | ⏳ | | -| 파티셔닝이 정상 작동함 | ⏳ | | -| 기존 Laravel audit 시스템에 영향 없음 | ⏳ | | -| 트리거 비활성화 플래그가 정상 동작함 | ⏳ | | -| mng 대시보드에서 이력 조회/필터링 가능 | ⏳ | | -| mng에서 특정 변경 복구(rollback) 가능 | ⏳ | | -| mng에서 테이블별 트리거 ON/OFF 가능 | ⏳ | | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 1.1 배경에 명시 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 9.2 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 2.1~2.4 Phase별 작업 항목 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Phase 순서, 기존 시스템 참조 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 7. 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 3.3~3.6 상세 구현 명세 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 9.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 수치/조건 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 2.1 Phase 1 → 1.1 테이블 생성 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 4.1~4.4 예상 파일 목록 | -| Q4. 작업 완료 확인 방법은? | ✅ | 9.1 테스트 케이스, 9.2 성공 기준 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -## 부록 A: 환경 정보 - -### A.1 프로젝트 구조 -``` -SAM/ ← 프로젝트 루트 -├── api/ ← Laravel 12 REST API (독립 git) -│ ├── app/ -│ │ ├── Http/ -│ │ │ ├── Controllers/Api/V1/ ← API 컨트롤러 -│ │ │ ├── Middleware/ ← 미들웨어 -│ │ │ └── Requests/ ← FormRequest -│ │ ├── Models/Audit/ ← 감사 모델 (AuditLog.php) -│ │ ├── Services/Audit/ ← 감사 서비스 (AuditLogger, AuditLogService) -│ │ ├── Traits/ ← Auditable.php, BelongsToTenant.php -│ │ ├── Console/Commands/ ← Artisan 커맨드 -│ │ └── Swagger/v1/ ← Swagger 문서 -│ ├── config/audit.php ← 감사 설정 -│ ├── database/migrations/ ← 마이그레이션 -│ ├── routes/ -│ │ ├── api.php ← 메인 라우트 (v1 prefix → 도메인별 분리) -│ │ └── api/v1/ ← 도메인별 라우트 파일 -│ └── bootstrap/app.php ← Laravel 12 미들웨어 등록 -├── mng/ ← Laravel 12 관리자 패널 (독립 git) -│ ├── app/Http/Controllers/ ← Blade 컨트롤러 -│ ├── resources/views/ ← Blade 뷰 (Tailwind + Alpine.js + HTMX) -│ │ └── layouts/app.blade.php ← 메인 레이아웃 -│ └── routes/web.php ← 웹 라우트 (auth 미들웨어 그룹) -├── react/ ← Next.js 15 프론트엔드 -└── docs/plans/ ← 이 문서 -``` - -### A.2 DB 접속 정보 -``` -엔진: MySQL 8.0 -Docker 컨테이너: sam-mysql-1 -데이터베이스: samdb (주), sam_stat (통계) -호스트: 127.0.0.1 (로컬) / sam-mysql-1 (Docker 내부) -포트: 3306 -사용자: samuser / sampass (일반), root / root (관리자) -문자셋: utf8mb4 / utf8mb4_unicode_ci -타임존: Asia/Seoul -``` - -### A.3 주요 명령어 -```bash -# Docker -cd /Users/kent/Works/@KD_SAM/SAM -docker compose up -d mysql - -# API 마이그레이션 -cd api && php artisan migrate -cd api && php artisan migrate:status - -# MySQL 직접 접속 -docker exec -it sam-mysql-1 mysql -u root -proot samdb - -# MNG Vite 빌드 -cd mng && npm run dev -``` - ---- - -## 부록 B: 기존 감사 시스템 코드 (수정 금지, 참조용) - -### B.1 Auditable Trait (`api/app/Traits/Auditable.php`) -```php -isFillable('created_by') && ! $model->created_by) { - $model->created_by = $actorId; - } - if ($model->isFillable('updated_by') && ! $model->updated_by) { - $model->updated_by = $actorId; - } - } - }); - - static::updating(function ($model) { - $actorId = static::resolveActorId(); - if ($actorId && $model->isFillable('updated_by')) { - $model->updated_by = $actorId; - } - }); - - static::deleting(function ($model) { - $actorId = static::resolveActorId(); - if ($actorId && $model->isFillable('deleted_by')) { - $model->deleted_by = $actorId; - $model->saveQuietly(); - } - }); - - static::created(function ($model) { - $model->logAuditEvent('created', null, $model->toAuditSnapshot()); - }); - - static::updated(function ($model) { - $dirty = $model->getChanges(); - $excluded = $model->getAuditExcludedFields(); - $changed = array_diff_key($dirty, array_flip($excluded)); - if (empty($changed)) return; - - $before = []; - $after = []; - foreach ($changed as $key => $value) { - $before[$key] = $model->getOriginal($key); - $after[$key] = $value; - } - $model->logAuditEvent('updated', $before, $after); - }); - - static::deleted(function ($model) { - $model->logAuditEvent('deleted', $model->toAuditSnapshot(), null); - }); - } - - public function getAuditExcludedFields(): array - { - $defaults = ['created_at','updated_at','deleted_at','created_by','updated_by','deleted_by']; - $custom = property_exists($this, 'auditExclude') ? $this->auditExclude : []; - return array_merge($defaults, $custom); - } - - public function getAuditTargetType(): string - { - return Str::snake(class_basename(static::class)); - } - - protected function toAuditSnapshot(): array - { - return array_diff_key($this->attributesToArray(), array_flip($this->getAuditExcludedFields())); - } - - protected function logAuditEvent(string $action, ?array $before, ?array $after): void - { - try { - $tenantId = $this->tenant_id ?? null; - if (! $tenantId) return; - $request = request(); - AuditLog::create([ - 'tenant_id' => $tenantId, - 'target_type' => $this->getAuditTargetType(), - 'target_id' => $this->getKey(), - 'action' => $action, - 'before' => $before, - 'after' => $after, - 'actor_id' => static::resolveActorId(), - 'ip' => $request?->ip(), - 'ua' => $request?->userAgent(), - 'created_at' => now(), - ]); - } catch (\Throwable $e) { - // 감사 로그 실패는 업무 흐름을 방해하지 않음 - } - } - - protected static function resolveActorId(): ?int - { - return auth()->id(); - } -} -``` - -### B.2 AuditLog 모델 (`api/app/Models/Audit/AuditLog.php`) -```php - 'array', - 'after' => 'array', - 'created_at' => 'datetime', - ]; -} -``` - -### B.3 AuditLogService (`api/app/Services/Audit/AuditLogService.php`) -```php -tenantId(); - $q = AuditLog::query()->where('tenant_id', $tenantId); - - if (! empty($filters['target_type'])) $q->where('target_type', $filters['target_type']); - if (! empty($filters['target_id'])) $q->where('target_id', (int) $filters['target_id']); - if (! empty($filters['action'])) $q->where('action', $filters['action']); - if (! empty($filters['actor_id'])) $q->where('actor_id', (int) $filters['actor_id']); - if (! empty($filters['from'])) $q->where('created_at', '>=', $filters['from']); - if (! empty($filters['to'])) $q->where('created_at', '<=', $filters['to']); - - $sort = $filters['sort'] ?? 'created_at'; - $order = $filters['order'] ?? 'desc'; - $size = (int) ($filters['size'] ?? 20); - - return $q->orderBy($sort, $order)->paginate($size); - } -} -``` - -### B.4 Audit Config (`api/config/audit.php`) -```php - env('AUDIT_RETENTION_DAYS', 395), // 13개월 - 'log_reads' => env('AUDIT_LOG_READS', false), -]; -``` - -### B.5 API 컨트롤러 패턴 (`api/app/Http/Controllers/Api/V1/Design/AuditLogController.php`) -```php -service->paginate($request->validated()); - }, __('message.fetched')); - } -} -``` - -### B.6 API Kernel (`api/app/Http/Kernel.php`) -```php - [], - 'api' => [], - ]; - protected $routeMiddleware = []; -} -``` - -> **참고**: Laravel 12에서 미들웨어 추가 시 `bootstrap/app.php`의 `->withMiddleware()` 또는 -> `Kernel.php`의 `$middleware` / `$middlewareGroups`에 등록한다. - -### B.7 API 라우트 패턴 (`api/routes/api.php`) -```php -// 도메인별 분리 구조 -Route::prefix('v1')->group(function () { - require __DIR__.'/api/v1/auth.php'; - require __DIR__.'/api/v1/design.php'; - // ... 기타 도메인 -}); - -// design.php 내 감사 로그 라우트 예시 -Route::prefix('design')->group(function () { - Route::prefix('audit-logs')->group(function () { - Route::get('', [DesignAuditLogController::class, 'index']); - Route::get('/{id}', [DesignAuditLogController::class, 'show'])->whereNumber('id'); - }); -}); -``` - -### B.8 Artisan 커맨드 패턴 (예: `TenantsBootstrap.php`) -```php - - - - - - @yield('title', 'Dashboard') - {{ config('app.name') }} - @vite(['resources/css/app.css', 'resources/js/app.js']) - - - - - - @include('components.sidebar.main') -
- @yield('content') -
- @stack('scripts') - - -``` - -### C.3 MNG 컨트롤러 패턴 (기존 `AuditLogController.php` 요약) -```php -orderByDesc('created_at'); - - // 필터링 (target_type, action, tenant_id, from, to, search) - if ($request->filled('target_type')) $query->where('target_type', $request->target_type); - if ($request->filled('action')) $query->where('action', $request->action); - if ($request->filled('from')) $query->where('created_at', '>=', $request->from.' 00:00:00'); - if ($request->filled('to')) $query->where('created_at', '<=', $request->to.' 23:59:59'); - - // 통계 - $stats = [...]; - - // 페이지네이션 - $logs = $query->paginate(50)->withQueryString(); - - return view('audit-logs.index', compact('logs', 'stats')); - } - - public function show(int $id): View - { - $log = AuditLog::findOrFail($id); - return view('audit-logs.show', compact('log')); - } -} -``` - -### C.4 MNG 뷰 패턴 (데이터 목록 화면) -```blade -@extends('layouts.app') -@section('title', '페이지 제목') -@section('content') - -{{-- 1. 헤더 --}} -
-

페이지 제목

-
- -{{-- 2. 통계 카드 --}} -
-
-
전체 기록
-
{{ number_format($stats['total']) }}
-
-
- -{{-- 3. 필터 폼 --}} -
-
- - - -
-
- -{{-- 4. 데이터 테이블 (HTMX 방식 또는 일반 방식) --}} -
- - - - - - - - @foreach($items as $item) - - - - @endforeach - -
컬럼
{{ $item->field }}
-
{{ $items->links() }}
-
- -@endsection -``` - -### C.5 MNG 라우트 패턴 (`mng/routes/web.php`) -```php -// 인증 필수 라우트 그룹 -Route::middleware(['auth', 'hq.member', 'password.changed'])->group(function () { - - // 감사 로그 (기존) - Route::prefix('audit-logs')->group(function () { - Route::get('/', [AuditLogController::class, 'index'])->name('audit-logs.index'); - Route::get('/{id}', [AuditLogController::class, 'show'])->name('audit-logs.show'); - }); - - // 새 트리거 감사는 여기에 추가: - // Route::prefix('trigger-audit')->name('trigger-audit.')->group(function () { ... }); -}); -``` - -### C.6 MNG 미들웨어 목록 -``` -mng/app/Http/Middleware/ -├── EnsureHQMember.php ← 본사 소속 확인 -├── EnsurePasswordChanged.php ← 비밀번호 변경 확인 -├── EnsureSuperAdmin.php ← 슈퍼관리자 확인 -└── AutoLoginViaRemember.php ← Remember Token 자동 재인증 -``` - ---- - -## 부록 D: SP 구현 상세 (Phase 1.3 참조) - -### D.1 sp_create_audit_triggers 전체 구현 방향 - -```sql -DELIMITER // - -DROP PROCEDURE IF EXISTS sp_create_audit_triggers // - -CREATE PROCEDURE sp_create_audit_triggers( - IN p_table_name VARCHAR(64), - IN p_db_name VARCHAR(64) -) -BEGIN - DECLARE v_col_list TEXT DEFAULT ''; - DECLARE v_json_new TEXT DEFAULT ''; - DECLARE v_json_old TEXT DEFAULT ''; - DECLARE v_change_check TEXT DEFAULT ''; - DECLARE v_changed_cols TEXT DEFAULT ''; - DECLARE v_tenant_col VARCHAR(64) DEFAULT NULL; - DECLARE v_pk_col VARCHAR(64) DEFAULT 'id'; - DECLARE v_done INT DEFAULT 0; - DECLARE v_col_name VARCHAR(64); - DECLARE v_sql TEXT; - - -- 제외 컬럼 - DECLARE v_exclude_cols TEXT DEFAULT 'created_at,updated_at,deleted_at,remember_token'; - - -- 커서: 대상 컬럼 목록 - DECLARE col_cursor CURSOR FOR - SELECT COLUMN_NAME - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = p_db_name - AND TABLE_NAME = p_table_name - AND FIND_IN_SET(COLUMN_NAME, v_exclude_cols) = 0 - ORDER BY ORDINAL_POSITION; - - DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1; - - -- tenant_id 컬럼 존재 확인 - SELECT COLUMN_NAME INTO v_tenant_col - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = p_db_name - AND TABLE_NAME = p_table_name - AND COLUMN_NAME = 'tenant_id' - LIMIT 1; - - -- PK 컬럼 확인 - SELECT COLUMN_NAME INTO v_pk_col - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = p_db_name - AND TABLE_NAME = p_table_name - AND COLUMN_KEY = 'PRI' - LIMIT 1; - - -- 컬럼별 JSON_OBJECT 구문 조립 - OPEN col_cursor; - col_loop: LOOP - FETCH col_cursor INTO v_col_name; - IF v_done THEN LEAVE col_loop; END IF; - - -- JSON 조립 - IF v_json_new != '' THEN - SET v_json_new = CONCAT(v_json_new, ','); - SET v_json_old = CONCAT(v_json_old, ','); - END IF; - SET v_json_new = CONCAT(v_json_new, '''', v_col_name, ''', NEW.`', v_col_name, '`'); - SET v_json_old = CONCAT(v_json_old, '''', v_col_name, ''', OLD.`', v_col_name, '`'); - - -- UPDATE 변경 감지 조립 (NULL-safe 비교) - IF v_change_check != '' THEN - SET v_change_check = CONCAT(v_change_check, ' OR '); - SET v_changed_cols = CONCAT(v_changed_cols, ','); - END IF; - SET v_change_check = CONCAT(v_change_check, - 'NOT(OLD.`', v_col_name, '` <=> NEW.`', v_col_name, '`)'); - SET v_changed_cols = CONCAT(v_changed_cols, - 'IF(NOT(OLD.`', v_col_name, '` <=> NEW.`', v_col_name, '`),''', v_col_name, ''',NULL)'); - END LOOP; - CLOSE col_cursor; - - -- tenant_id 참조 - SET @tenant_expr = IF(v_tenant_col IS NOT NULL, - CONCAT('NEW.`', v_tenant_col, '`'), 'NULL'); - SET @tenant_expr_old = IF(v_tenant_col IS NOT NULL, - CONCAT('OLD.`', v_tenant_col, '`'), 'NULL'); - - -- 1. 기존 트리거 삭제 - SET @drop1 = CONCAT('DROP TRIGGER IF EXISTS trg_', p_table_name, '_ai'); - SET @drop2 = CONCAT('DROP TRIGGER IF EXISTS trg_', p_table_name, '_au'); - SET @drop3 = CONCAT('DROP TRIGGER IF EXISTS trg_', p_table_name, '_ad'); - PREPARE s FROM @drop1; EXECUTE s; DEALLOCATE PREPARE s; - PREPARE s FROM @drop2; EXECUTE s; DEALLOCATE PREPARE s; - PREPARE s FROM @drop3; EXECUTE s; DEALLOCATE PREPARE s; - - -- 2. AFTER INSERT 트리거 - SET v_sql = CONCAT( - 'CREATE TRIGGER trg_', p_table_name, '_ai AFTER INSERT ON `', p_table_name, '` ', - 'FOR EACH ROW BEGIN ', - 'IF @disable_audit_trigger IS NULL OR @disable_audit_trigger != 1 THEN ', - 'INSERT INTO trigger_audit_logs(table_name,row_id,dml_type,old_values,new_values,tenant_id,actor_id,session_info,db_user) ', - 'VALUES(''', p_table_name, ''',NEW.`', v_pk_col, '`,''INSERT'',NULL,', - 'JSON_OBJECT(', v_json_new, '),', - @tenant_expr, ',@sam_actor_id,@sam_session_info,CURRENT_USER()); ', - 'END IF; END' - ); - SET @s = v_sql; - PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; - - -- 3. AFTER UPDATE 트리거 (변경 있을 때만) - SET v_sql = CONCAT( - 'CREATE TRIGGER trg_', p_table_name, '_au AFTER UPDATE ON `', p_table_name, '` ', - 'FOR EACH ROW BEGIN ', - 'IF @disable_audit_trigger IS NULL OR @disable_audit_trigger != 1 THEN ', - 'IF ', v_change_check, ' THEN ', - 'INSERT INTO trigger_audit_logs(table_name,row_id,dml_type,old_values,new_values,changed_columns,tenant_id,actor_id,session_info,db_user) ', - 'VALUES(''', p_table_name, ''',NEW.`', v_pk_col, '`,''UPDATE'',', - 'JSON_OBJECT(', v_json_old, '),', - 'JSON_OBJECT(', v_json_new, '),', - 'JSON_REMOVE(JSON_ARRAY(', v_changed_cols, '),', - -- NULL 값 제거 (변경 안 된 컬럼) - '''$[0]''),', -- 간소화: 실제 구현 시 NULL 필터링 로직 보강 필요 - @tenant_expr, ',@sam_actor_id,@sam_session_info,CURRENT_USER()); ', - 'END IF; END IF; END' - ); - SET @s = v_sql; - PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; - - -- 4. AFTER DELETE 트리거 - SET v_sql = CONCAT( - 'CREATE TRIGGER trg_', p_table_name, '_ad AFTER DELETE ON `', p_table_name, '` ', - 'FOR EACH ROW BEGIN ', - 'IF @disable_audit_trigger IS NULL OR @disable_audit_trigger != 1 THEN ', - 'INSERT INTO trigger_audit_logs(table_name,row_id,dml_type,old_values,new_values,tenant_id,actor_id,session_info,db_user) ', - 'VALUES(''', p_table_name, ''',OLD.`', v_pk_col, '`,''DELETE'',', - 'JSON_OBJECT(', v_json_old, '),NULL,', - @tenant_expr_old, ',@sam_actor_id,@sam_session_info,CURRENT_USER()); ', - 'END IF; END' - ); - SET @s = v_sql; - PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -END // - -DELIMITER ; -``` - -> **주의**: 위 코드는 구현 방향을 보여주는 참조 코드이다. -> 실제 구현 시 changed_columns의 NULL 필터링, 복합 PK 처리, 에러 핸들링 등을 보강해야 한다. - -### D.2 전체 테이블 일괄 트리거 생성 프로시저 - -```sql -DELIMITER // - -CREATE PROCEDURE sp_create_all_audit_triggers(IN p_db_name VARCHAR(64)) -BEGIN - DECLARE v_tbl VARCHAR(64); - DECLARE v_done INT DEFAULT 0; - DECLARE v_count INT DEFAULT 0; - - -- 제외 테이블 목록 - DECLARE v_exclude TEXT DEFAULT - 'audit_logs,trigger_audit_logs,personal_access_tokens,sessions,' - 'cache,cache_locks,jobs,job_batches,failed_jobs,migrations,' - 'password_reset_tokens'; - - DECLARE tbl_cursor CURSOR FOR - SELECT TABLE_NAME - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = p_db_name - AND TABLE_TYPE = 'BASE TABLE' - AND TABLE_NAME NOT LIKE 'telescope_%' - AND FIND_IN_SET(TABLE_NAME, v_exclude) = 0 - ORDER BY TABLE_NAME; - - DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1; - - OPEN tbl_cursor; - tbl_loop: LOOP - FETCH tbl_cursor INTO v_tbl; - IF v_done THEN LEAVE tbl_loop; END IF; - - CALL sp_create_audit_triggers(v_tbl, p_db_name); - SET v_count = v_count + 1; - END LOOP; - CLOSE tbl_cursor; - - SELECT CONCAT('Created triggers for ', v_count, ' tables') AS result; -END // - -DELIMITER ; - --- 실행: --- CALL sp_create_all_audit_triggers('samdb'); -``` - ---- - -## 부록 E: 복구 서비스 구현 상세 (Phase 2.2 참조) - -```php -dml_type) { - 'INSERT' => $this->buildDeleteSQL($log), - 'UPDATE' => $this->buildRevertUpdateSQL($log), - 'DELETE' => $this->buildReinsertSQL($log), - }; - } - - /** - * 복구 실행 (트랜잭션) - */ - public function executeRollback(int $auditId): bool - { - $log = TriggerAuditLog::findOrFail($auditId); - - // 트리거 감사 비활성화 (복구 작업 자체는 기록 안 함) - DB::statement('SET @disable_audit_trigger = 1'); - - try { - DB::transaction(function () use ($log) { - $sql = $this->generateRollbackSQL($log->id); - DB::statement($sql); - }); - return true; - } finally { - DB::statement('SET @disable_audit_trigger = NULL'); - } - } - - /** - * 특정 레코드의 특정 시점 상태 조회 - */ - public function getRecordStateAt(string $table, string $rowId, Carbon $at): ?array - { - // 해당 시점 이전의 가장 마지막 상태를 추적 - $log = TriggerAuditLog::where('table_name', $table) - ->where('row_id', $rowId) - ->where('created_at', '<=', $at) - ->orderByDesc('created_at') - ->first(); - - if (! $log) return null; - - return match ($log->dml_type) { - 'INSERT', 'UPDATE' => $log->new_values, - 'DELETE' => null, // 해당 시점에 삭제된 상태 - }; - } - - /** - * 특정 레코드의 변경 이력 - */ - public function getRecordHistory(string $table, string $rowId): Collection - { - return TriggerAuditLog::where('table_name', $table) - ->where('row_id', $rowId) - ->orderByDesc('created_at') - ->get(); - } - - private function buildDeleteSQL(TriggerAuditLog $log): string - { - return "DELETE FROM `{$log->table_name}` WHERE `id` = " . DB::getPdo()->quote($log->row_id); - } - - private function buildRevertUpdateSQL(TriggerAuditLog $log): string - { - $sets = collect($log->old_values) - ->map(fn($val, $col) => "`{$col}` = " . ($val === null ? 'NULL' : DB::getPdo()->quote($val))) - ->implode(', '); - - return "UPDATE `{$log->table_name}` SET {$sets} WHERE `id` = " . DB::getPdo()->quote($log->row_id); - } - - private function buildReinsertSQL(TriggerAuditLog $log): string - { - $cols = collect($log->old_values)->keys()->map(fn($c) => "`{$c}`")->implode(', '); - $vals = collect($log->old_values)->values() - ->map(fn($v) => $v === null ? 'NULL' : DB::getPdo()->quote($v)) - ->implode(', '); - - return "INSERT INTO `{$log->table_name}` ({$cols}) VALUES ({$vals})"; - } -} -``` - ---- - -## 부록 F: 세션 시작 가이드 (새 세션용) - -### 이 문서로 작업을 시작하는 방법 - -``` -1. Serena 메모리 로드 - → read_memory("db-trigger-audit-state") : 진행 상태 확인 - -2. 이 문서의 "📍 현재 진행 상태" 확인 - → 마지막 완료 작업, 다음 작업 확인 - -3. 해당 Phase의 "대상 범위" (섹션 2) 확인 - → 구체적 작업 항목과 상태 확인 - -4. 해당 작업의 구현 코드는 "작업 절차" (섹션 3) + "부록" 참조 - → 부록 B: 기존 코드 패턴 (수정 금지) - → 부록 C: MNG 패턴 (Phase 4용) - → 부록 D: SP 구현 상세 (Phase 1.3용) - → 부록 E: 복구 서비스 상세 (Phase 2.2용) - -5. 작업 완료 후 - → 이 문서의 진행 상태 업데이트 - → Serena 메모리 저장: write_memory("db-trigger-audit-state", ...) -``` - -### 환경 확인 명령어 - -```bash -# Docker MySQL 실행 확인 -docker ps | grep sam-mysql - -# 마이그레이션 상태 -cd /Users/kent/Works/@KD_SAM/SAM/api && php artisan migrate:status - -# 현재 트리거 목록 확인 -docker exec -it sam-mysql-1 mysql -u root -proot samdb -e "SHOW TRIGGERS" - -# trigger_audit_logs 레코드 수 -docker exec -it sam-mysql-1 mysql -u root -proot samdb -e "SELECT COUNT(*) FROM trigger_audit_logs" -``` - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/docs-plans-cleanup-plan.md b/plans/docs-plans-cleanup-plan.md new file mode 100644 index 0000000..5b0a73e --- /dev/null +++ b/plans/docs-plans-cleanup-plan.md @@ -0,0 +1,326 @@ +# docs/plans 폴더 정리 계획 + +> **작성일**: 2026-02-26 +> **목적**: docs/plans 폴더의 문서 분류, 통폐합, 히스토리 보관, 인덱스 재작성 +> **상태**: ⏳ Phase 1 대기 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | Phase 4: 최종 검증 완료 | +| **다음 작업** | 없음 (정리 완료) | +| **진행률** | 4/4 Phase (100%) | +| **마지막 업데이트** | 2026-02-26 | + +--- + +## 1. 개요 + +### 1.1 배경 + +`docs/plans/` 폴더에 문서가 누적되면서 다음 문제 발생: +- 같은 도메인에 신/구 문서가 공존 (방향 전환 등으로 새 문서가 생겼으나 이전 문서 미정리) +- 완료된 문서, 폐기된 문서, 진행중인 문서가 혼재 +- archive에 37개 개별 파일이 산재 (참조 효율 저하) +- sub/, clodeCheck/ 등 부수 폴더의 역할 불명확 + +### 1.2 현재 상태 + +``` +docs/plans/ ← 메인: 44개 md 파일 +├── archive/ ← 완료: 37개 md 파일 +├── sub/ ← 하위계획: 7개 md + archive/ +├── clodeCheck/ ← 코드체크 리포트: 7개 md +├── flow-tests/ ← 플로우 테스트 JSON: 32개 +├── SAM_ERP_Storyboard_D1.0_251218/ ← 스토리보드: 38장 +└── index_plans.md ← 현재 인덱스 +``` + +### 1.3 성공 기준 + +- [ ] 모든 메인 문서(44개)가 5단계 중 하나로 분류됨 +- [ ] SUPERSEDED 문서가 최신 문서에 병합되어 삭제됨 +- [ ] COMPLETED 문서가 archive/HISTORY.md로 요약 통합됨 +- [ ] OBSOLETE 문서가 삭제됨 +- [ ] sub/, clodeCheck/ 각 파일 처리 완료 +- [ ] index_plans.md가 ACTIVE+PLANNED 문서만 반영하여 재작성됨 +- [ ] docs/plans/에 ACTIVE + PLANNED 문서만 존재 + +--- + +## 2. 확정된 정책 + +### 2.1 문서 분류 기준 (5단계) + +| 분류 | 정의 | 처리 | 최종 위치 | +|------|------|------|----------| +| **ACTIVE** | 현재 진행중이거나 곧 착수할 문서 | 유지, 최신화 | `docs/plans/` | +| **PLANNED** | 확정된 예정 작업, 선행조건 대기 | 유지, 최신화 | `docs/plans/` | +| **SUPERSEDED** | 새 문서로 대체된 이전 문서 | 새 문서에 병합 후 **삭제** | 파일 없음 | +| **COMPLETED** | 완료된 작업 | HISTORY.md에 요약 후 **삭제** | `archive/HISTORY.md` | +| **OBSOLETE** | 방향 전환/폐기된 문서 | **삭제** | 파일 없음 | + +### 2.2 SUPERSEDED 판정 기준 + +같은 도메인에 문서 2개 이상일 때: +- **최신 문서(나중 생성)가 기준** → 이전 문서는 SUPERSEDED +- 이전 문서에만 있는 유용한 내용 → 최신 문서에 병합 +- 이전 문서가 최신 문서를 참조하지 않고 독립적 → 내용 비교 후 판단 +- 이전 문서가 최신 문서에 참조됨 → 최신 문서에 해당 내용 통합 + +**통폐합 후보 도메인** (파일명 기반, Phase 1에서 확정): +- 견적: `quote-*` 6개 +- 문서시스템: `document-*` 5개 +- 품목: `item-*`, `bom-*`, `mng-item-*` 등 +- 채번: `tenant-numbering-*`, `mng-numbering-*` + +### 2.3 HISTORY.md 구조 + +```markdown +# 완료 작업 히스토리 + +## 견적/수주 +| 기능 | 완료시기 | 요약 | +|------|---------|------| +| 견적 자동계산 | 2025-12 | 경동 수식 엔진 구현, V2 자동계산 적용 | + +## 품목/BOM +| 기능 | 완료시기 | 요약 | +| ... | ... | ... | + +## 생산/절곡 +... +``` + +- 기능 도메인별 섹션으로 구분 +- 각 항목: 기능명 + 완료시기 + 한줄 요약 (상세 불필요) +- 현재 archive/ 37개 + 이번 정리에서 COMPLETED로 분류된 문서 모두 포함 + +### 2.4 sub/, clodeCheck/ 처리 원칙 + +Phase 1에서 **문서별로 판단** (D 옵션): + +**sub/ 각 파일 → 아래 중 택1:** +- A. 메인 승격: 아직 유효 → `docs/plans/`로 이동 +- B. 상위 문서에 병합: 내용이 상위 계획에 포함 가능 +- C. 삭제: 이미 반영되었거나 폐기 + +**clodeCheck/ 각 파일 → 아래 중 택1:** +- A. 삭제: 일회성 리포트 +- B. HISTORY.md에 요약: 한 줄 이력으로 보관 + +### 2.5 변경하지 않는 대상 + +| 폴더 | 이유 | +|------|------| +| `flow-tests/` | 운영 도구 (JSON 테스트 케이스) | +| `SAM_ERP_Storyboard_D1.0_251218/` | 디자인 참조 (스토리보드) | + +--- + +## 3. 실행 계획 (4 Phase) + +### Phase 1: 분류 (읽기 전용) + +**목표**: 모든 문서를 5단계 중 하나로 분류 + +**작업 절차**: +1. 메인 44개 문서의 내용을 읽고 분류 판정 +2. sub/ 7개 문서의 상위 문서 관계 파악 후 분류 판정 +3. clodeCheck/ 7개 리포트의 보관 가치 판정 +4. 현재 archive/ 37개 문서의 요약 정보 추출 (HISTORY.md용) +5. 분류 결과 테이블 작성 → 사용자 확인 + +**산출물**: 아래 테이블 완성 + +#### 3.1.1 메인 문서 분류 결과 + +| # | 파일명 | 분류 | 비고 | +|---|--------|------|------| +| 1 | 5130-to-mng-migration-plan.md | ACTIVE | 13% 진행중 | +| 2 | api-explorer-development-plan.md | PLANNED | 미착수 | +| 3 | bending-info-auto-generation-plan.md | PLANNED | 설계 확정, 착수 대기 | +| 4 | bending-material-input-mapping-plan.md | PLANNED | GAP 분석 완료 | +| 5 | bending-preproduction-stock-plan.md | COMPLETED | 14/14 완료 | +| 6 | bom-item-mapping-plan.md | ACTIVE | 66% Phase 3 검증 잔여 | +| 7 | card-management-section-plan.md | ACTIVE | 50% 모달 연동 진행중 | +| 8 | dashboard-api-integration-plan.md | ACTIVE | 45% Phase 2 예정 | +| 9 | db-backup-system-plan.md | ACTIVE | 79% 서버 작업 3건 잔여 | +| 10 | db-trigger-audit-system-plan.md | COMPLETED | 94% 옵션만 잔여 | +| 11 | dev-toolbar-plan.md | ACTIVE | 38% Phase 2-4 진행중 | +| 12 | document-management-system-plan.md | SUPERSEDED | → document-system-master.md | +| 13 | document-system-master.md | ACTIVE | Phase 4-5 마스터 문서 | +| 14 | document-system-mid-inspection.md | ACTIVE | 5/6 결재만 남음 | +| 15 | document-system-work-log.md | ACTIVE | 3/4+α React 연동 잔여 | +| 16 | dummy-data-seeding-plan.md | PLANNED | 미착수 | +| 17 | employee-user-linkage-plan.md | PLANNED | 미착수 | +| 18 | erp-api-development-plan.md | ACTIVE | Phase L 진행중 | +| 19 | esign-alimtalk-integration.md | PLANNED | 카카오 채널 개설 후 착수 | +| 20 | fg-code-consolidation-plan.md | ACTIVE | 분석완료, Phase 1 착수 전 | +| 21 | hotfix-20260119-action-plan.md | OBSOLETE | 일회성 핫픽스 이력 | +| 22 | incoming-inspection-document-integration-plan.md | PLANNED | 분석만 완료 | +| 23 | incoming-inspection-templates-plan.md | ACTIVE | 83% 4종 품목 대기 | +| 24 | intermediate-inspection-report-plan.md | PLANNED | 검토 대기 | +| 25 | item-inventory-management-plan.md | PLANNED | 설계 확정, 구현 대기 | +| 26 | item-master-data-alignment-plan.md | ACTIVE | 섀도잉 정리 재수행 | +| 27 | items-migration-kyungdong-plan.md | SUPERSEDED | → kd-items-migration-plan.md (archive) | +| 28 | kd-orders-migration-plan.md | PLANNED | 선행조건 미충족 | +| 29 | kd-quote-logic-plan.md | ACTIVE | 80% Phase 5 직전 | +| 30 | mng-item-field-management-plan.md | PLANNED | 미착수 | +| 31 | mng-menu-system-plan.md | ACTIVE | 구현완료, 테스트 잔여 | +| 32 | mng-numbering-rule-management-plan.md | PLANNED | 미착수 | +| 33 | monthly-expense-integration-plan.md | PLANNED | 미착수 | +| ~~34~~ | ~~product-code-traceability-plan.md~~ | **제외** | 진행중 - 정리 대상 아님 | +| 35 | quote-calculation-api-plan.md | PLANNED | 설계 완료, 미착수 | +| 36 | quote-management-8issues-plan.md | PLANNED | 컨펌 대기 | +| 37 | quote-management-url-migration-plan.md | COMPLETED | 92% 잔여 사소 | +| 38 | quote-order-sync-improvement-plan.md | PLANNED | 승인 대기 | +| 39 | quote-system-development-plan.md | SUPERSEDED | → kd-quote-logic-plan.md | +| 40 | react-api-integration-plan.md | ACTIVE | 기능별 API 연동 진행중 | +| 41 | react-mock-remaining-tasks.md | SUPERSEDED | → react-mock-to-api-migration-plan.md | +| 42 | react-mock-to-api-migration-plan.md | ACTIVE | Mock→API 전환 진행중 | +| 43 | receiving-management-analysis-plan.md | PLANNED | 분석 완료, 개발 대기 | +| 44 | simulator-ui-enhancement-plan.md | ACTIVE | 60% Phase 2 진행중 | +| 45 | tenant-id-compliance-plan.md | PLANNED | 실행 대기 | +| 46 | tenant-numbering-system-plan.md | PLANNED | 미착수 | + +#### 3.1.2 sub/ 문서 분류 결과 + +| # | 파일명 | 처리 | 상위 문서 | 비고 | +|---|--------|:----:|----------|------| +| 1 | categories-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 2 | contract-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 3 | items-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 4 | order-management-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 5 | pricing-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 6 | site-management-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | +| 7 | structure-review-plan.md | C (삭제) | construction-api (archive) | 상위 완료 | + +#### 3.1.3 clodeCheck/ 문서 분류 결과 + +| # | 파일명 | 처리 | 비고 | +|---|--------|:----:|------| +| 1 | attendance-management_2026-01-14_23-30-00.md | A (삭제) | 일회성 E2E 리포트 | +| 2 | bank-transactions_2026-01-15_test-report.md | A (삭제) | 일회성 테스트 리포트 | +| 3 | card-transactions_2026-01-15_test-report.md | A (삭제) | 일회성 테스트 리포트 | +| 4 | employee-register_2026-01-14_20-00-00.md | A (삭제) | 일회성 테스트 리포트 | +| 5 | salary-management_2026-01-15_10-30-00.md | A (삭제) | 일회성 테스트 리포트 | +| 6 | sales-management_2026-01-15_test-report.md | A (삭제) | 일회성 테스트 리포트 | +| 7 | withdrawal-management_2026-01-15_test-report.md | A (삭제) | 일회성 테스트 리포트 | + +**Phase 1 완료 기준**: 위 3개 테이블 완성 + 사용자 승인 + +--- + +### Phase 2: 통폐합 (승인 후) + +**목표**: SUPERSEDED 문서를 최신 문서에 병합 + +**작업 절차**: +1. Phase 1에서 SUPERSEDED로 분류된 문서 목록 확인 +2. 각 SUPERSEDED 문서 → 대응하는 최신 문서 매핑 +3. 이전 문서에만 있는 유용한 내용 추출 +4. 최신 문서에 병합 (필요한 내용만) +5. **건별로 사용자 확인** (또는 일괄 승인 선택) +6. 확인 후 이전 문서 삭제 + +**산출물**: 통폐합 매핑 테이블 + +| SUPERSEDED 문서 | 병합 대상 (최신) | 병합 내용 요약 | 승인 | +|----------------|-----------------|---------------|------| +| (Phase 1 결과) | | | | + +**Phase 2 완료 기준**: 모든 SUPERSEDED 문서 처리 + 사용자 승인 + +--- + +### Phase 3: 정리 + +**목표**: COMPLETED/OBSOLETE 처리, HISTORY.md 작성, 인덱스 재작성 + +**병렬 가능한 작업**: + +**3-A. HISTORY.md 작성** +1. 현재 archive/ 37개 문서에서 기능명 + 완료시기 + 한줄요약 추출 +2. Phase 1에서 COMPLETED로 분류된 메인 문서도 동일 처리 +3. 기능 도메인별로 분류하여 HISTORY.md 작성 +4. archive/ 개별 파일 삭제 + +**3-B. OBSOLETE 삭제** +1. Phase 1에서 OBSOLETE로 분류된 문서 삭제 +2. sub/ 처리 (Phase 1 판정에 따라) +3. clodeCheck/ 처리 (Phase 1 판정에 따라) + +**3-C. index_plans.md 재작성** (3-A, 3-B 완료 후) +1. ACTIVE + PLANNED 문서만 기능 도메인별로 정리 +2. 각 문서의 상태/진행률 반영 +3. HISTORY.md 링크 포함 + +**Phase 3 완료 기준**: 폴더에 ACTIVE+PLANNED만 남음 + index 재작성 완료 + +--- + +### Phase 4: 검증 + +**목표**: 최종 구조 확인 + +**체크리스트**: +- [ ] docs/plans/에 ACTIVE + PLANNED 문서만 존재 +- [ ] archive/에 HISTORY.md만 존재 +- [ ] sub/, clodeCheck/ 정리 완료 +- [ ] index_plans.md가 실제 파일과 일치 +- [ ] 삭제된 문서 중 필요한 내용이 누락되지 않았는지 확인 +- [ ] flow-tests/, Storyboard 폴더 영향 없음 + +--- + +## 4. 작업 시 주의사항 + +### 4.0 정리 제외 대상 + +아래 문서는 정리/분류/통폐합 대상에서 **제외**한다: +- `product-code-traceability-plan.md` — 현재 진행중 +- **이 정리 작업 이후 신규 생성되는 문서** — GUIDE.md 원칙에 따라 생성되므로 정리 불필요 + +### 4.1 삭제 전 확인 원칙 +- 문서 삭제 전 반드시 내용을 읽고 유용한 정보 유무 확인 +- SUPERSEDED 삭제 시 최신 문서에 병합 완료 확인 후 삭제 +- **git에서 복구 가능하므로** 과도한 보수적 판단 불필요 + +### 4.2 판단 기준 우선순위 +- 최신 문서 > 이전 문서 +- 구체적 구현 내용 > 추상적 계획 +- 현재 시스템에 적용된 내용 > 적용 예정이었던 내용 + +### 4.3 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | Phase 1 분류 테이블 작성 | 불필요 (읽기 전용) | +| ⚠️ 컨펌 필요 | 문서 병합, 삭제, HISTORY.md 작성 | **Phase별 사용자 승인** | +| 🔴 금지 | flow-tests/, Storyboard 수정 | 별도 협의 | + +--- + +## 5. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | +|------|------|----------| +| 2026-02-26 | 문서 초안 | 정책 수립 완료, 4 Phase 계획 작성 | +| 2026-02-26 | Phase 1~4 완료 | 분류→통폐합→정리→검증 전 과정 완료 | + +--- + +## 6. 참고 문서 + +- **문서 가이드**: `docs/plans/GUIDE.md` ← 정리 시 준수할 최소 원칙 +- **현재 인덱스**: `docs/plans/index_plans.md` +- **문서 인덱스**: `docs/INDEX.md` +- **프로젝트 구조**: `CLAUDE.md` + +--- + +*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/document-management-system-plan.md b/plans/document-management-system-plan.md deleted file mode 100644 index 7894962..0000000 --- a/plans/document-management-system-plan.md +++ /dev/null @@ -1,1119 +0,0 @@ -# 문서관리 시스템 개발 계획 (Phase 1~4) - -> **작성일**: 2026-01-31 -> **목적**: mng에서 문서양식(템플릿)을 관리하고 문서를 생성하여, SAM(react)에서 JSON으로 소비하는 문서관리 시스템을 구축한다 -> **기준 문서**: `docs/specs/database-schema.md`, `mng/CLAUDE.md` -> **상태**: Phase 1~3 ✅ 완료, Phase 4 🔄 (4.4 미완료) -> -> **📌 이 문서는 Phase 1~4 아카이브입니다.** -> **새 작업은 마스터 문서에서 시작하세요**: [`document-system-master.md`](./document-system-master.md) -> Phase 5 상세는 유형별 개별 문서로 분리되었습니다. - ---- - -## 🚀 새 세션 시작 가이드 - -> **이 섹션은 새 세션에서 이 문서만 보고 작업을 시작할 수 있도록 작성되었습니다.** - -### 프로젝트 정보 - -| 항목 | 내용 | -|------|------| -| **작업 프로젝트** | `mng` (관리자 패널) | -| **절대 경로** | `/Users/kent/Works/@KD_SAM/SAM/mng/` | -| **기술 스택** | Laravel 12 + Plain Blade + DaisyUI + HTMX + Alpine.js | -| **로컬 URL** | `https://mng.sam.kr` (Docker 로컬, `admin.sam.kr`도 동일) | -| **관련 API** | `/Users/kent/Works/@KD_SAM/SAM/api/` (Laravel 12 REST API) | -| **프론트** | `/Users/kent/Works/@KD_SAM/SAM/react/` (Next.js 15, 이 작업에서는 미수정) | -| **5130 레거시** | `/Users/kent/Works/@KD_SAM/SAM/5130/` (참조 전용) | -| **문서 경로** | `/Users/kent/Works/@KD_SAM/SAM/docs/` | - -### mng Git 저장소 - -```bash -cd /Users/kent/Works/@KD_SAM/SAM/mng -git status && git branch -``` - -> **주의**: SAM/ 루트는 Git 저장소가 아님. api/, mng/, react/ 각각 독립 Git 저장소. - -### 세션 시작 체크리스트 - -``` -1. 이 문서를 읽는다 (📍 현재 진행 상태 섹션 확인) -2. mng/CLAUDE.md 를 읽는다 (mng 프로젝트 규칙 확인) -3. 마지막 완료 작업 확인 → 다음 작업 결정 -4. 해당 Phase의 상세 절차(섹션 11)를 읽는다 -5. 작업 시작 전 사용자에게 "Phase X.X 시작할까요?" 확인 -``` - -### 핵심 파일 (작업 빈도순) - -| 파일 | 설명 | 크기 | -|------|------|------| -| `mng/resources/views/document-templates/edit.blade.php` | 양식 편집 UI (메인 작업 대상) | 44.5KB | -| `mng/app/Http/Controllers/DocumentTemplateController.php` | 양식 CRUD 컨트롤러 | | -| `mng/app/Http/Controllers/DocumentController.php` | 문서 CRUD 컨트롤러 | | -| `mng/app/Models/DocumentTemplate.php` | 양식 모델 (관계 정의) | | -| `mng/app/Models/Documents/Document.php` | 문서 모델 (상태 워크플로우) | | -| `mng/routes/web.php` (340-353줄) | 양식/문서 라우트 | | - -### 모델 관계 구조 (코드 참조) - -```php -// DocumentTemplate.php 주요 관계 -class DocumentTemplate extends Model { - use BelongsToTenant, SoftDeletes; - - // 결재라인: template->approval_lines (작성/검토/승인) - public function approvalLines() { return $this->hasMany(DocumentTemplateApprovalLine::class, 'template_id')->orderBy('sort_order'); } - - // 기본필드: template->basic_fields (품명, LOT NO 등) - public function basicFields() { return $this->hasMany(DocumentTemplateBasicField::class, 'template_id')->orderBy('sort_order'); } - - // 섹션: template->sections->items (검사기준서 섹션 + 검사항목) - public function sections() { return $this->hasMany(DocumentTemplateSection::class, 'template_id')->orderBy('sort_order'); } - - // 컬럼: template->columns (데이터 테이블 컬럼 정의) - public function columns() { return $this->hasMany(DocumentTemplateColumn::class, 'template_id')->orderBy('sort_order'); } -} - -// Document.php 주요 관계 -class Document extends Model { - use BelongsToTenant, SoftDeletes; - - // 상태: DRAFT -> PENDING -> APPROVED/REJECTED/CANCELLED - protected $casts = ['status' => DocumentStatus::class]; - - public function template() { return $this->belongsTo(DocumentTemplate::class); } - public function approvals() { return $this->hasMany(DocumentApproval::class); } - public function data() { return $this->hasMany(DocumentData::class); } // EAV 패턴 - public function attachments() { return $this->hasMany(DocumentAttachment::class); } - public function linkable() { return $this->morphTo(); } // 다형성 연결 (수주, 작업지시 등) -} -``` - -### mng 라우트 구조 - -```php -// mng/routes/web.php (340-353줄) -Route::resource('document-templates', DocumentTemplateController::class); // /document-templates -Route::resource('documents', DocumentController::class); // /documents -``` - -> **URL 확인**: `https://mng.sam.kr/document-templates` (양식 관리), `https://mng.sam.kr/documents` (문서 관리) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| - -| **마지막 완료 작업** | Phase 4.3 - mng 문서 데이터 입력/저장 연동 검증 완료 (기존 구현 확인) | -| **다음 작업** | Phase 4.4 - 프론트엔드 담당자 협의 후 react 전환 결정 | -| **진행률** | 16/20 (80%) - Phase 1 ✅, Phase 2 ✅, Phase 3 ✅, Phase 4.1-4.3 ✅ | -| **마지막 업데이트** | 2026-01-31 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 SAM(react)에는 검사 성적서(수입검사, 중간검사), 작업일지 등이 하드코딩된 모달 컴포넌트로 존재한다. 5130 레거시 시스템에도 동일 문서들이 PHP 파일 단위로 구현되어 있다. 이들을 **mng에서 동적으로 양식을 관리**하고, **API를 통해 JSON으로 제공**하여 SAM에서 렌더링하는 구조로 전환한다. - -**핵심 문제:** -- 현재 검사 문서가 React 컴포넌트에 하드코딩되어, 새 양식 추가 시 코드 수정이 필요 -- 5130의 수입검사만 약 40종의 자재별 페이지가 개별 PHP 파일로 존재 -- 검사 기준, 항목, 판정 로직이 코드와 혼재되어 비개발자가 관리 불가 -- 중간검사(절곡/스크린/슬랫/조인트바)도 각각 별도 컴포넌트로 분산 - -**해결 방향:** -- mng에서 문서양식(템플릿)을 동적으로 정의 → 검사 항목/기준/판정 로직 포함 -- 양식 기반으로 실제 문서 인스턴스를 생성 → 데이터 입력/결재/출력 -- SAM에서 API로 양식+데이터를 JSON 수신 → 범용 렌더러로 표시 - -### 1.2 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ 1. 양식 정의는 mng에서만 관리 (비개발자도 양식 수정 가능하도록) │ -│ 2. SAM(react)은 JSON을 받아 렌더링만 담당 (문서 로직 없음) │ -│ 3. 기존 DB 구조(document_templates 계열) 최대한 활용 │ -│ 4. 5130 레거시의 검사 기준/항목을 데이터로 이관 │ -│ 5. 결재 워크플로우(DRAFT->PENDING->APPROVED) 유지 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| 즉시 가능 | 양식 필드 추가/변경, 검사항목 추가, 기준값 수정, 뷰(Blade) 수정 | 불필요 | -| 컨펌 필요 | 새 DB 테이블 추가, 기존 테이블 컬럼 변경, API 엔드포인트 추가, 마이그레이션 | **필수** | -| 금지 | 기존 document_templates 테이블 구조 파괴적 변경, 기존 API 삭제 | 별도 협의 | - -### 1.4 준수 규칙 -- `docs/specs/database-schema.md` - DB 스키마 참조 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `mng/CLAUDE.md` - MNG 프로젝트 규칙 - ---- - -## 2. 현황 분석 - -### 2.1 기존 DB 구조 (이미 생성됨) - -``` -document_templates # 양식 마스터 -├── document_template_approval_lines # 결재라인 (작성/검토/승인) -├── document_template_basic_fields # 기본필드 (품명, LOT NO 등) -├── document_template_sections # 섹션 (검사기준서 섹션) -│ └── document_template_section_items # 섹션 항목 (검사항목) -└── document_template_columns # 데이터 테이블 컬럼 - -documents # 문서 인스턴스 -├── document_approvals # 결재 이력 -├── document_data # 필드 데이터 (EAV 패턴) -└── document_attachments # 첨부 파일 -``` - -**주요 테이블 컬럼:** - -| 테이블 | 핵심 컬럼 | -|--------|----------| -| `document_templates` | tenant_id, name, category, title, company_name, footer_remark_label, footer_judgement_label, footer_judgement_options(json) | -| `document_template_approval_lines` | template_id, name, dept, role, sort_order | -| `document_template_basic_fields` | template_id, label, field_type(text/date), default_value, sort_order | -| `document_template_sections` | template_id, title, image_path, sort_order | -| `document_template_section_items` | section_id, category, item, standard, method, frequency, regulation, sort_order | -| `document_template_columns` | template_id, label, column_type(text/check/measurement/select/complex), group_name, sub_labels(json), width, sort_order | -| `documents` | tenant_id, template_id, document_no, title, status(DRAFT/PENDING/APPROVED/REJECTED/CANCELLED), linkable_type, linkable_id | -| `document_data` | document_id, section_id, column_id, row_index, field_key, field_value | -| `document_approvals` | document_id, user_id, step, role, status(PENDING/APPROVED/REJECTED), comment, acted_at | - -### 2.2 기존 MNG 코드 현황 - -| 항목 | 경로 | 상태 | -|------|------|------| -| DocumentTemplate 모델 | `mng/app/Models/DocumentTemplate.php` | 존재 | -| Document 모델 | `mng/app/Models/Documents/Document.php` | 존재 | -| 관련 하위 모델 6개 | `mng/app/Models/Documents/`, `mng/app/Models/DocumentTemplate*.php` | 존재 | -| DocumentTemplateController | `mng/app/Http/Controllers/DocumentTemplateController.php` | 존재 | -| DocumentController | `mng/app/Http/Controllers/DocumentController.php` | 존재 | -| 라우트 (templates, documents) | `mng/routes/web.php` 340-353줄 | 존재 | -| 양식 편집 Blade | `mng/resources/views/document-templates/edit.blade.php` (44.5KB) | 존재 | -| 문서 Blade (index/edit/show) | `mng/resources/views/documents/` | 존재 | - -### 2.3 5130 레거시 검사 문서 현황 - -#### 수입검사 (instock) - -| 항목 | 내용 | -|------|------| -| 위치 | `5130/instock/` | -| 자재별 검사 페이지 | 40+ PHP 파일 (`i_EGI155.php`, `i_SUSplate.php`, `i_wire.php`, `i_motor.php` 등) | -| 메인 로더 | `fetch_inspection.php` (21.8KB) - 자재코드별 동적 로딩 | -| 검사 필드 | 로트번호, 검사일, 납품업체, 품명, 규격, 단위, 품목코드, 입고량, 자재번호, 제조사 | -| 판정 방식 | 항목별 합격/불합격 -> 종합판정 자동계산 | -| LOT 관리 | `lotnum.txt` 파일 기반, YYMMDD-## 형식 | -| PDF 출력 | html2pdf.js 사용 | - -#### 중간검사 (output) - -| 검사 종류 | 파일 | DB 필드 | -|----------|------|---------| -| 절곡품 중간검사 | `viewMidInspectBending.php` (60.7KB) | `recordbendingMid` (JSON) | -| 스크린 중간검사 | `viewMidInspectScreen.php` (33.6KB) | `recordscreenMid` (JSON) | -| 슬랫 중간검사 | `viewMidInspectSlat.php` | `recordslatMid` (JSON) | -| 조인트바 검사 | `viewinspectionJointbar.php` (34.1KB) | `recordjointbar` (JSON) | - -#### 검사 공통 구조 -- 결재: 작성(판매/Order) -> 검토(생산) -> 승인(품질/QC) -- 검사 기준 이미지: `5130/img/inspection/` (20+ 이미지) -- 데이터: JSON으로 DB 저장 (approval chain + measurements) -- QC 관리자 권한 제어 (이세희, 함신옥, 이경호, 노완호) - -### 2.4 SAM(react) 현재 검사 컴포넌트 - -| 컴포넌트 | 경로 | 용도 | -|---------|------|------| -| ImportInspectionDocument | `react/src/.../quality/qms/components/documents/` | 수입검사 성적서 | -| ScreenInspectionDocument | 동일 경로 | 스크린 중간검사 성적서 | -| SlatInspectionDocument | 동일 경로 | 슬랫 중간검사 성적서 | -| BendingInspectionDocument | 동일 경로 | 절곡품 중간검사 성적서 | -| JointbarInspectionDocument | 동일 경로 | 조인트바 중간검사 성적서 | -| ProductInspectionDocument | 동일 경로 | 제품검사 성적서 | -| WorkLogContent | `react/src/components/production/WorkerScreen/` | 작업일지 | -| InspectionReportModal | `react/src/components/production/WorkOrders/documents/` | 중간검사 모달 | -| DocumentViewer | `react/src/components/document-system/viewer/` | 범용 문서 뷰어 | - -**공통 컴포넌트 (document-system):** -- `DocumentHeader.tsx` - 문서 헤더 (로고, 결재라인) -- `QualityApprovalTable.tsx` - 품질 결재표 -- `InfoTable.tsx` - 정보 테이블 -- `DocumentViewer.tsx` - 문서 뷰어 (zoom, drag, print, download) - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: mng 양식 관리 기능 완성 (수입검사) - -수입검사 양식 20여종을 mng에서 동적으로 관리할 수 있도록 기존 코드를 보강한다. - -| # | 작업 항목 | 상태 | 완료 기준 | 비고 | -|---|----------|:----:|----------|------| -| 1.1 | 기존 document-templates 편집 UI 점검 및 보완 | ✅ | `mng.sam.kr/document-templates/{id}/edit`에서 결재라인/기본필드/섹션/항목/컬럼 모두 CRUD 가능. 저장 후 DB에 정상 반영 확인 | 5개 탭 전체 CRUD 완료 확인 | -| 1.2 | 5130 수입검사 데이터 분석 및 양식 구조 설계 | ✅ | 라우팅 구조 + 대표 자재 2종(EGI, SUS) 상세 분석 완료. 나머지 21종은 Phase 1.3에서 개별 분석 병행 | viewJS.php 라우팅 + 공통패턴 추출 | -| 1.3 | 수입검사 양식 시드 데이터 생성 | ✅ | EGI(ID:7), SUS(ID:8) 2종 생성 완료. 각각 결재2+기본필드10+섹션1+검사항목7~8+컬럼7. 나머지 자재는 개별 분석 후 시더에 추가 | `IncomingInspectionTemplateSeeder.php` | -| 1.4 | 양식 미리보기 기능 | ✅ | edit.blade.php에 모달 미리보기 구현 완료. 결재란+기본정보+검사이미지+검사테이블(complex 지원)+Footer(비고+판정) 모두 렌더링 | 기존 구현 확인 완료 | -| 1.5 | 양식 복제 기능 | ✅ | API `POST /{id}/duplicate` + 목록 복제 버튼. 이름 입력 prompt → 전체 관계(결재/필드/섹션/항목/컬럼) 복제. 비활성 상태로 생성 | API+UI 구현 완료 | - -### 3.2 Phase 2: mng 문서 생성/관리 기능 - -양식을 기반으로 실제 검사 문서를 생성하고 데이터를 입력/결재하는 기능. - -| # | 작업 항목 | 상태 | 완료 기준 | 비고 | -|---|----------|:----:|----------|------| -| 2.1 | 문서 생성 (양식 선택 -> 빈 문서 생성) | ✅ | 양식 선택 후 빈 문서(DRAFT)가 documents 테이블에 생성됨. 문서번호 자동 채번 | 카테고리별 prefix (IQC/PRD/SLS/PUR), 결재라인 초기화, 기본필드 뷰 수정 완료 | -| 2.2 | 문서 데이터 입력 UI | ✅ | 양식의 columns/sections 기반 동적 테이블 렌더링. complex/select/check/measurement/text 컬럼 타입 지원. EAV 저장 (section_id, column_id, row_index) | field_key 패턴: s{섹션}_r{행}_c{컬럼}_sub{인덱스} | -| 2.3 | 결재 워크플로우 (제출/승인/반려) | ✅ | DRAFT→PENDING→APPROVED/REJECTED 전체 동작. 단계별 승인, 반려 사유 필수, 재제출 시 결재라인 초기화 | submit/approve/reject API + 승인·반려 UI | -| 2.4 | 문서 목록/검색/필터 | ✅ | 상태별(DRAFT/PENDING/APPROVED), 양식별, 날짜별 필터 동작. 페이징 포함 | 날짜 범위 필터(date_from/date_to) + DRAFT 문서 삭제 기능 추가 | -| 2.5 | 문서 PDF 출력 | ⏭️ | **추후 고려** - react에 이미 html2pdf.js 구현됨 (6.2 결정사항 #1 참고) | | - -### 3.3 Phase 3: 중간검사 양식 추가 - -| # | 작업 항목 | 상태 | 완료 기준 | 비고 | -|---|----------|:----:|----------|------| -| 3.1 | 중간검사 양식 구조 설계 | ✅ | 절곡/스크린/슬랫/조인트바 4종의 검사항목/기준/판정방식 문서화 완료 | 섹션 5.2에 상세 설계. 절곡품 최고 복잡도(★5), 조인트바 최저(★1) | -| 3.2 | 5130 중간검사 데이터 이관 설계 | ✅ | recordbendingMid 등 JSON→양식 매핑 테이블 완성 | 섹션 5.3에 상세 설계. 6단계 이관 프로세스, 변환 규칙, 주의사항 문서화 | -| 3.3 | 중간검사 양식 시드 데이터 | ✅ | 4종 양식 seeder 생성, `mng.sam.kr/document-templates`에서 확인 가능 | MidInspectionTemplateSeeder: 조인트바(ID:10), 슬랫(ID:11), 스크린(ID:12), 절곡품(ID:13) | -| 3.4 | 검사 기준 이미지 관리 | ✅ | `5130/img/inspection/` 이미지 → `mng/public/img/inspection/`로 이관. 양식에서 참조 가능 | 27개 이미지. URL: `/img/inspection/{filename}.jpg` | - -### 3.4 Phase 4: API 연동 및 mng JSON 화면 구현 - -| # | 작업 항목 | 상태 | 완료 기준 | 비고 | -|---|----------|:----:|----------|------| -| 4.1 | API 엔드포인트 설계 (양식 조회, 문서 CRUD) | ✅ | DocumentTemplate 읽기 전용 API(모델6+서비스+컨트롤러+FormRequest+라우트+Swagger). Document 결재 워크플로우 4개 엔드포인트 활성화(submit/approve/reject/cancel) | api 저장소 | -| 4.2 | mng에서 JSON 기반 문서 화면 구현 | ✅ | show.blade.php에 섹션 테이블 읽기전용 렌더링 구현(5가지 컬럼 타입). 종합판정+비고 Footer. 기존 버그 3건 수정(field_key/field_type/section.title) | mng 저장소 | -| 4.3 | mng에서 문서 데이터 입력/저장 연동 | ✅ | Phase 2.2~2.3에서 이미 완전 구현 확인. edit.blade.php JS→DocumentApiController.saveDocumentData()→document_data EAV 저장. 판정(적합/부적합) select+종합판정 Footer 저장 정상. 6.2 결정사항 #2(프론트 입력, 결과만 저장) 적용됨 | 추가 코드 작업 없음 | -| 4.4 | 프론트엔드 담당자 협의 후 react 전환 결정 | ⏳ | mng 완성 후 프론트 담당자와 미팅. react 기존 컴포넌트는 미수정 (6.2 결정사항 #4) | 협의 결과 문서화 | - -### 3.5 Phase 5: 문서 유형 확장 - -> **상세 계획은 개별 문서로 분리됨** → [`document-system-master.md`](./document-system-master.md) - -| # | 작업 항목 | 상태 | 상세 문서 | -|---|----------|:----:|----------| -| 5.1 | 중간검사(PQC) 폼 구현 | ⏳ | [`document-system-mid-inspection.md`](./document-system-mid-inspection.md) | -| 5.2 | 제품검사(FQC) 폼 구현 | ⏳ | [`document-system-product-inspection.md`](./document-system-product-inspection.md) | -| 5.3 | 작업일지 폼 구현 | ⏳ | [`document-system-work-log.md`](./document-system-work-log.md) | -| 5.4 | 기타문서 (견적서/거래명세서/발주서 등) | ⏭️ | 추후 정의 | - ---- - -## 4. 아키텍처 설계 - -### 4.1 시스템 흐름 - -``` -[mng - 양식 관리] [api - REST API] [SAM - react 프론트] - -DocumentTemplate CRUD ----------> GET /document-templates 양식 목록/상세 - - 결재라인 설정 GET /document-templates/{id} - - 기본필드 설정 - - 섹션/항목 설정 POST /documents 문서 생성 - - 컬럼 설정 PUT /documents/{id} 데이터 입력 - GET /documents/{id} 문서 조회 -Document 생성/관리 ------------> POST /documents/{id}/submit 결재 제출 - - 데이터 입력 POST /documents/{id}/approve 결재 승인 - - 결재 처리 POST /documents/{id}/reject 결재 반려 - - PDF 출력 GET /documents/{id}/pdf PDF 다운로드 -``` - -### 4.2 JSON 응답 구조 (양식 상세) - -```json -{ - "template": { - "id": 1, - "name": "EGI 1.55T 수입검사", - "category": "incoming_inspection", - "title": "수 입 검 사 성 적 서", - "companyName": "케이디산업", - "approvalLines": [ - { "name": "작성", "dept": "판매/Order", "role": "담당자", "sortOrder": 1 }, - { "name": "검토", "dept": "생산", "role": "담당자", "sortOrder": 2 }, - { "name": "승인", "dept": "품질", "role": "QC", "sortOrder": 3 } - ], - "basicFields": [ - { "label": "품명", "fieldType": "text", "sortOrder": 1 }, - { "label": "규격", "fieldType": "text", "sortOrder": 2 }, - { "label": "LOT NO", "fieldType": "text", "sortOrder": 3 }, - { "label": "검사일자", "fieldType": "date", "sortOrder": 4 }, - { "label": "납품업체", "fieldType": "text", "sortOrder": 5 }, - { "label": "검사자", "fieldType": "text", "sortOrder": 6 } - ], - "sections": [ - { - "title": "가이드레일", - "imagePath": "/storage/inspection/guiderail.jpg", - "items": [ - { - "category": "겉모양", - "item": "사용상 결함이 될 흠이 없을 것", - "standard": "KS D 3506", - "method": "육안검사", - "frequency": "체크검사", - "regulation": "KS D 3506" - }, - { - "category": "치수", - "item": "두께", - "standard": "1.55 +/- 0.15", - "method": "계측", - "frequency": "입고시", - "regulation": "KS D 3506" - } - ] - } - ], - "columns": [ - { "label": "NO", "columnType": "text", "width": "50px", "sortOrder": 1 }, - { "label": "검사항목", "columnType": "text", "width": "120px", "sortOrder": 2 }, - { "label": "검사기준", "columnType": "text", "width": "150px", "sortOrder": 3 }, - { - "label": "검사 DATA", - "columnType": "complex", - "groupName": "검사 DATA", - "subLabels": ["1", "2", "3", "4", "5"], - "width": "300px", - "sortOrder": 4 - }, - { "label": "판정", "columnType": "select", "width": "80px", "sortOrder": 5 } - ], - "footerRemarkLabel": "부적합 내용", - "footerJudgementLabel": "종합판정", - "footerJudgementOptions": ["합격", "불합격"] - } -} -``` - -### 4.3 JSON 응답 구조 (문서 상세) - -```json -{ - "document": { - "id": 1, - "templateId": 1, - "documentNo": "IQC-260131-01", - "title": "EGI 1.55T 수입검사 성적서", - "status": "APPROVED", - "template": { "...위 구조와 동일..." }, - "basicData": { - "품명": "전기 아연도금 강판", - "규격": "EGI 1.55T", - "LOT NO": "260131-01", - "검사일자": "2026-01-31", - "납품업체": "포스코", - "검사자": "이세희" - }, - "tableData": [ - { - "sectionId": 1, - "rows": [ - { - "rowIndex": 0, - "values": { - "NO": "1", - "검사항목": "겉모양", - "검사기준": "사용상 결함 없을 것", - "검사 DATA": { "1": "양호", "2": "양호", "3": "양호", "4": "-", "5": "-" }, - "판정": "적합" - } - } - ] - } - ], - "footerData": { - "remark": "", - "judgement": "합격" - }, - "approvals": [ - { "step": 1, "role": "작성", "userName": "홍길동", "status": "APPROVED", "actedAt": "2026-01-31" }, - { "step": 2, "role": "검토", "userName": "김철수", "status": "APPROVED", "actedAt": "2026-01-31" }, - { "step": 3, "role": "승인", "userName": "이세희", "status": "APPROVED", "actedAt": "2026-01-31" } - ] - } -} -``` - ---- - -## 5. 5130 데이터 이관 계획 - -### 5.1 수입검사 자재 목록 (주요) - -5130의 `instock/fetch_inspection.php`에서 자재코드별로 로딩하는 개별 페이지를 분석하여, 각 자재별 검사항목을 양식 시드 데이터로 변환한다. - -| # | 자재 | 5130 파일 | 검사 항목 수 | 우선순위 | -|---|------|----------|:----------:|:-------:| -| 1 | EGI 1.55T (전기아연도금강판) | `i_EGI155.php` | ~8 | 높음 | -| 2 | SUS Plate (스테인리스강판) | `i_SUSplate.php` | ~6 | 높음 | -| 3 | GI Plate (아연도금강판) | `i_GIplate.php` | ~6 | 높음 | -| 4 | Wire (와이어) | `i_wire.php` | ~4 | 중간 | -| 5 | Motor (모터) | `i_motor.php` | ~5 | 중간 | -| 6 | Angle (앵글) | `i_angle.php` | ~4 | 중간 | -| 7-20+ | 기타 자재 | 개별 PHP 파일 | 다양 | 낮음 | - -### 5.2 중간검사 양식 구조 설계 (Phase 3.1) - -> **5130 레거시 분석 결과** - 4종 중간검사의 검사항목/기준/판정방식을 문서화 - -#### 5.2.0 공통 구조 - -**결재라인 (4종 공통)** - -| step | name | dept | role | -|------|------|------|------| -| 1 | 작성 | 판매/Order | 담당자 | -| 2 | 검토 | 생산 | 담당자 | -| 3 | 승인 | 품질 | QC | - -**기본필드 (4종 공통)** - -| # | label | field_type | 비고 | -|---|-------|-----------|------| -| 1 | 품명 | text | 절곡품/스크린/철재스라트/조인트바 | -| 2 | 규격 | text | 제품 규격 | -| 3 | 로트크기 | text | 개소 수 | -| 4 | 발주처 | text | 고객사명 | -| 5 | 현장명 | text | 설치 현장 | -| 6 | 검사일자 | date | - | -| 7 | 검사자 | text | - | - -**Footer (4종 공통)** -- `footer_remark_label`: "부적합 내용" -- `footer_judgement_label`: "종합판정" -- `footer_judgement_options`: ["합격", "불합격"] -- 종합판정 로직: 모든 행 "적" → 합격, 하나라도 "부" → 불합격 - ---- - -#### 5.2.1 절곡품 중간검사 (Bending Mid-Inspection) - -**양식 정보** - -| 항목 | 값 | -|------|-----| -| name | 절곡품 중간검사 성적서 | -| category | 품질/중간검사 | -| title | 절곡품 - 중간 검사 성적서 | -| 5130 DB필드 | `recordbendingMid` (JSON) | - -**섹션 구조**: 제품 코드별 다른 검사 항목 (동적 구성) - -구성품별 검사항목: - -| 구성품 | 검사 항목 | 비고 | -|--------|----------|------| -| 가이드레일 (벽면형 120×70) | 겉모양(절곡상태), 길이, 너비, 간격(4포인트) | S1: 30/80/45/40mm | -| 가이드레일 (측면형 120×120) | 겉모양(절곡상태), 길이, 너비, 간격(6포인트) | S1: 30/70/45/35/95/90mm | -| 하단마감재 (60×40) | 겉모양, 너비(60mm) | 길이 3000/4000mm | -| 하단 L-BAR (17×60) | 겉모양, 너비(17mm) | - | -| 케이스/셔터박스 | 겉모양, 높이, 하단, 차이, 위치 | 양면/밑면/후면 | -| 연기차단재 (가이드레일용) | 너비(50mm), 간격(12mm) | - | -| 연기차단재 (케이스용) | 너비(80mm), 간격(12mm) | - | - -**컬럼 구조** - -| # | label | column_type | 비고 | -|---|-------|-----------|------| -| 1 | 분류/제품명 | text | 자동매핑 (KSS01 등) | -| 2 | 타입 | text | 벽면형/측면형/규격 | -| 3 | 겉모양(절곡상태) | check | 양호/불량 체크 | -| 4 | 길이 | complex | sub_labels: ['도면치수', '측정값'] | -| 5 | 너비 | complex | sub_labels: ['도면치수', '측정값'] | -| 6 | 간격 | complex | sub_labels: POINT별 ['도면치수', '측정값'] (가변) | -| 7 | 판정(적/부) | select | 자동계산 가능 | - -**허용 공차** -- 길이: ±4mm -- 간격: ±2mm - -**참조 이미지**: `bending_inspection1.jpg`, `bending_inspection2.jpg`, `guiderail_*`, `box_*`, `Lbar_mid`, `smoke` - -**⚠️ 특이사항**: 절곡품은 제품 코드(KSS01/KSS02/KWE01)와 마감유형(S1/S2/S3)에 따라 검사 항목이 동적으로 변경됨. 현재 양식 시스템에서 이를 표현하려면 **가장 포괄적인 구성을 기본 양식으로 만들고**, 실제 문서 생성 시 해당 제품에 맞는 행만 활성화하는 방식 검토 필요. - ---- - -#### 5.2.2 스크린 중간검사 (Screen Mid-Inspection) - -**양식 정보** - -| 항목 | 값 | -|------|-----| -| name | 스크린 중간검사 성적서 | -| category | 품질/중간검사 | -| title | 스크린 - 중간 검사 성적서 | -| 5130 DB필드 | `recordscreenMid` (JSON) | - -**섹션: 스크린 검사 항목** - -| # | 검사항목 | 타입 | 기준 | 비고 | -|---|---------|------|------|------| -| 겉모양-1 | 가공상태 | check | 양호/불량 | - | -| 겉모양-2 | 재봉상태 | check | 양호/불량 | - | -| 겉모양-3 | 조립상태 | check | 양호/불량 | - | -| 치수-① | 길이 | measurement | 도면치수 ±4mm | col10_SW/col10 | -| 치수-② | 높이 | measurement | 도면치수 ±40mm | col11_SH/col11 | -| 치수-③ | 간격 | check | 400 이하 → OK/NG | 고정 기준 | - -**컬럼 구조** - -| # | label | column_type | 비고 | -|---|-------|-----------|------| -| 1 | 일련번호 | text | 자동 (제품 순번) | -| 2 | 가공상태 | check | 양호/불량 | -| 3 | 재봉상태 | check | 양호/불량 | -| 4 | 조립상태 | check | 양호/불량 | -| 5 | ①길이 | complex | sub_labels: ['도면치수', '측정값'] | -| 6 | ②높이 | complex | sub_labels: ['도면치수', '측정값'] | -| 7 | ③간격 | complex | sub_labels: ['기준치', 'OK/NG'] | -| 8 | 판정(적/부) | select | 자동계산 | - -**행 개수**: 발주 제품 수(estimateList)만큼 동적 생성 - ---- - -#### 5.2.3 슬랫(철재스라트) 중간검사 (Slat Mid-Inspection) - -**양식 정보** - -| 항목 | 값 | -|------|-----| -| name | 슬랫 중간검사 성적서 | -| category | 품질/중간검사 | -| title | 슬랫 - 중간 검사 성적서 | -| 5130 DB필드 | `recordslatMid` (JSON) | - -**섹션: 슬랫 검사 항목** - -| # | 검사항목 | 타입 | 기준 | 비고 | -|---|---------|------|------|------| -| 겉모양-1 | 가공상태 | check | 양호/불량 | - | -| 겉모양-2 | 조립상태 | check | 양호/불량 | 재봉상태 없음 (스크린과 차이) | -| 치수-① | 높이(1) | measurement | 16.5 ± 1mm | 고정 기준값 | -| 치수-② | 높이(2) | measurement | 14.5 ± 1mm | 고정 기준값 | -| 치수-③ | 길이(엔드락제외) | measurement | 도면치수 ±4mm | col10 | - -**컬럼 구조** - -| # | label | column_type | 비고 | -|---|-------|-----------|------| -| 1 | 일련번호 | text | 자동 | -| 2 | 가공상태 | check | 양호/불량 | -| 3 | 조립상태 | check | 양호/불량 | -| 4 | ①높이 | complex | sub_labels: ['기준(16.5±1)', '측정값'] | -| 5 | ②높이 | complex | sub_labels: ['기준(14.5±1)', '측정값'] | -| 6 | ③길이 | complex | sub_labels: ['도면치수', '측정값'] | -| 7 | 판정(적/부) | select | 자동계산 | - -**행 개수**: 발주 제품 수(estimateSlatList)만큼 동적 생성 - -**스크린 vs 슬랫 차이점** - -| 항목 | 스크린 | 슬랫 | -|------|--------|------| -| 겉모양 | 3개 (가공/재봉/조립) | 2개 (가공/조립) | -| 치수①② | 길이·높이 (도면치수) | 높이(1)(2) (고정값 16.5/14.5) | -| 치수③ | 간격 (400이하, OK/NG) | 길이 (도면치수 ±4mm) | -| 공차 | ±4mm, ±40mm | ±1mm, ±1mm, ±4mm | - ---- - -#### 5.2.4 조인트바 중간검사 (Jointbar Mid-Inspection) - -**양식 정보** - -| 항목 | 값 | -|------|-----| -| name | 조인트바 중간검사 성적서 | -| category | 품질/중간검사 | -| title | 조인트바 - 중간 검사 성적서 | -| 5130 DB필드 | `recordjointbar` (JSON) | - -**섹션: 조인트바 검사 항목** - -| # | 검사항목 | 타입 | 기준값 | 공차 | -|---|---------|------|-------|------| -| 겉모양-1 | 가공상태 | check | 양호/불량 | - | -| 겉모양-2 | 조립상태 | check | 양호/불량 | - | -| 치수-① | 높이(1) | measurement | 16.5mm | ±1mm | -| 치수-② | 높이(2) | measurement | 14.5mm | ±1mm | -| 치수-③ | 길이(엔드락제외) | measurement | 300mm | ±4mm | -| 치수-④ | 간격 | measurement | 150mm | ±4mm | - -**컬럼 구조** - -| # | label | column_type | 비고 | -|---|-------|-----------|------| -| 1 | 일련번호 | text | 자동 | -| 2 | 가공상태 | check | 양호/불량 | -| 3 | 조립상태 | check | 양호/불량 | -| 4 | ①높이 | complex | sub_labels: ['기준(16.5±1)', '측정값'] | -| 5 | ②높이 | complex | sub_labels: ['기준(14.5±1)', '측정값'] | -| 6 | ③길이 | complex | sub_labels: ['기준(300±4)', '측정값'] | -| 7 | ④간격 | complex | sub_labels: ['기준(150±4)', '측정값'] | -| 8 | 판정(적/부) | select | 자동계산 | - -**행 개수**: 단일 행 (제품 1건 단위 검사) - -**참조 이미지**: `jointbar_inspection.jpg` - ---- - -#### 5.2.5 4종 비교 요약 - -| 항목 | 절곡품 | 스크린 | 슬랫 | 조인트바 | -|------|--------|--------|------|---------| -| 겉모양 수 | 1 (절곡상태) | 3 (가공/재봉/조립) | 2 (가공/조립) | 2 (가공/조립) | -| 치수 항목 | 길이+너비+간격(가변) | 3 (길이/높이/간격) | 3 (높이×2/길이) | 4 (높이×2/길이/간격) | -| 행 구성 | 구성품별 (동적) | 발주제품별 (동적) | 발주제품별 (동적) | 단일 행 | -| 기준값 | 도면치수+포인트별 | 도면치수+고정(400) | 고정(16.5/14.5)+도면 | 전체 고정값 | -| 공차 | ±4mm/±2mm | ±4/±40mm | ±1/±1/±4mm | ±1/±1/±4/±4mm | -| 참조이미지 | 다수 (구성품별) | 별도 | 별도 | 1장 | -| 복잡도 | ★★★★★ (최고) | ★★★ | ★★☆ | ★☆ (최저) | - -#### 5.2.6 양식 시스템 매핑 전략 - -**현재 양식 시스템의 한계와 대응**: - -1. **조인트바/슬랫**: 현재 양식 구조(섹션+항목+컬럼)로 **그대로 표현 가능**. 수입검사와 동일 패턴으로 시더 생성. - -2. **스크린**: 겉모양 check 컬럼 + complex 측정 컬럼 조합으로 표현 가능. 행이 발주 제품별 동적이므로 **문서 생성 시 행 수를 결정**하는 로직 필요. - -3. **절곡품**: 제품 코드별로 검사 항목이 완전히 달라지므로 **가장 복잡**. 접근 방식: - - **Option A**: 포괄 양식 1개 + 문서 생성 시 해당 행만 활성화 - - **Option B**: 제품 유형별 양식 분리 (S1/S2/S3 별도) - - **Option C (권장)**: 기본 양식에 구성품 목록만 정의하고, **문서 생성 시 제품 코드에 따라 동적으로 행 구성** (Phase 3.3에서 구현) - -4. **check 컬럼 타입**: 현재 시스템에 `check` 컬럼 타입이 이미 존재. 양호/불량 체크박스로 사용 가능. - -### 5.3 중간검사 데이터 이관 설계 (Phase 3.2) - -> **5130 JSON 구조 → 새 양식 시스템(EAV) 매핑** - -#### 5.3.1 5130 JSON 공통 배열 구조 - -4종 모두 동일한 배열 인덱스 패턴: - -``` -recordXxxMid = [ - [0]: { approval: { writer: {name,date}, reviewer: {name,date}, approver: {name,date} } } - [1]: { inputValue: { ... } } ← 절곡: named object / 스크린·슬랫·조인트바: flat array - [2]: { num: "주문번호" } - [3]: { tablename: "output" } - [4]: { update_log: "..." } ← 슬랫·조인트바는 없음 - [5]: { checkboxData: [ {good:[], bad:[], judgement:""}, ... ] } ← 슬랫·조인트바는 [4] -] -``` - -#### 5.3.2 JSON → EAV 매핑 테이블 - -**결재 데이터 (JSON[0] → document_approvals)** - -| JSON 경로 | EAV 대상 | 비고 | -|-----------|---------|------| -| `[0].approval.writer.name` | `document_approvals` (step=1, user→name) | 작성자 | -| `[0].approval.writer.date` | `document_approvals` (step=1, acted_at) | mm/dd → datetime | -| `[0].approval.reviewer.name` | `document_approvals` (step=2, user→name) | 검토자 | -| `[0].approval.reviewer.date` | `document_approvals` (step=2, acted_at) | mm/dd → datetime | -| `[0].approval.approver.name` | `document_approvals` (step=3, user→name) | 승인자 | -| `[0].approval.approver.date` | `document_approvals` (step=3, acted_at) | mm/dd → datetime | - -**기본필드 (JSON[1].inputValue → document_data, section_id=null)** - -| JSON 경로 | field_key | 비고 | -|-----------|----------|------| -| `[1].inputValue.inspectdate` | `basic_inspectdate` | 검사일자 | -| `[1].inputValue.reviewer_sub` | `basic_reviewer` | 검사자 | -| `[1].inputValue.*_false_comment` | `footer_remark` | 부적합 내용 | -| `[1].inputValue.resultJudgement` | `footer_judgement` | 종합판정 | - -**절곡품 측정 데이터 (JSON[1].inputValue → document_data)** - -| JSON 경로 | field_key 패턴 | 비고 | -|-----------|---------------|------| -| `[1].inputValue.lengthMeasurement[i]` | `s{섹션}_r{i}_length` | 길이 측정값 | -| `[1].inputValue.widthMeasurement[i]` | `s{섹션}_r{i}_width` | 너비 측정값 | -| `[1].inputValue.gapMeasurement[i]` | `s{섹션}_r{i}_gap_{point}` | 간격 측정값 (포인트별) | - -**스크린/슬랫/조인트바 측정 데이터 (JSON[1].inputValue → document_data)** - -| JSON 경로 | field_key 패턴 | 비고 | -|-----------|---------------|------| -| `[1].inputValue[n]` (col{row}_input_{dim}) | `s{섹션}_r{row}_c{col}_sub{dim}` | 순차 인덱스 → 행·컬럼 매핑 | - -**체크박스 데이터 (JSON[5/4].checkboxData → document_data)** - -| JSON 경로 | field_key 패턴 | 비고 | -|-----------|---------------|------| -| `checkboxData[row].good[col]` | `s{섹션}_r{row}_c{checkCol}_good` | 양호 체크 | -| `checkboxData[row].bad[col]` | `s{섹션}_r{row}_c{checkCol}_bad` | 불량 체크 | -| `checkboxData[row].judgement` | `s{섹션}_r{row}_judgement` | 행별 판정 (적/부) | - -#### 5.3.3 이관 시 데이터 변환 규칙 - -| 변환 항목 | 5130 형식 | 새 시스템 형식 | 변환 로직 | -|----------|----------|-------------|----------| -| 날짜 (결재) | `"1/31"` (mm/dd) | `datetime` | 연도 추정 필요 (output.indate 기준) | -| 날짜 (검사) | `"2026-01-31"` | `date` | 그대로 사용 | -| 체크박스 | `true/false` | `"1"/"0"` | boolean → string | -| 판정 | `"적"/"부"` | `"적"/"부"` | 그대로 사용 | -| 종합판정 | `"합격"/"불합격"` | `"합격"/"불합격"` | 그대로 사용 | -| 측정값 | `number/string` | `string` | EAV field_value는 string | -| 결재자 이름 | `string` | `user_id (FK)` | 이름→사용자 테이블 매칭 필요 | - -#### 5.3.4 이관 프로세스 설계 - -``` -Step 1: output 테이블에서 recordXxxMid IS NOT NULL 레코드 추출 - ↓ -Step 2: 각 레코드에 대해 해당 양식 템플릿 매핑 - - recordbendingMid → 절곡품 중간검사 양식 (template_id) - - recordscreenMid → 스크린 중간검사 양식 - - recordslatMid → 슬랫 중간검사 양식 - - recordjointbar → 조인트바 중간검사 양식 - ↓ -Step 3: documents 테이블에 문서 생성 - - template_id, tenant_id, document_no (MID-YYMMDD-NN) - - title: "{양식명} - {현장명}" - - status: APPROVED (이미 완료된 검사) - - created_at: output.indate 기준 - ↓ -Step 4: document_approvals 생성 - - JSON[0].approval → 3개 결재 레코드 - - 이름→user_id 매칭 (매칭 실패 시 created_by = system) - - status: APPROVED, acted_at: 변환된 날짜 - ↓ -Step 5: document_data (EAV) 생성 - - 기본필드: inspectdate, reviewer → field_key 매핑 - - 체크박스: checkboxData → good/bad/judgement 매핑 - - 측정값: inputValue → 행·컬럼 인덱스 매핑 - - Footer: false_comment → footer_remark, resultJudgement → footer_judgement - ↓ -Step 6: 검증 - - 원본 JSON과 변환 결과 대조 - - 종합판정·행별 판정 일치 확인 -``` - -#### 5.3.5 이관 대상 규모 추정 - -| 검사 종류 | DB 필드 | 조건 | 비고 | -|----------|---------|------|------| -| 절곡품 | recordbendingMid | IS NOT NULL AND != '' AND != '{}' | output 테이블 | -| 스크린 | recordscreenMid | 동일 | output 테이블 | -| 슬랫 | recordslatMid | 동일 | output 테이블 | -| 조인트바 | recordjointbar | 동일 | output 테이블 | - -> ⚠️ 실제 레코드 수는 5130 DB 조회 필요 (Phase 3.2 완료 기준 설계만 완성, 실행은 Phase 4 이후) - -#### 5.3.6 이관 시 주의사항 - -1. **절곡품 inputValue 구조 차이**: 절곡품만 named object (`lengthMeasurement[]`, `widthMeasurement[]`, `gapMeasurement[]`), 나머지 3종은 flat array. 이관 스크립트에서 분기 처리 필요. - -2. **update_log 유무**: 스크린만 별도 `update_log` 컬럼 업데이트. 슬랫·조인트바는 JSON 내부에만 포함 (실제로는 비어있을 수 있음). - -3. **결재자 이름 매칭**: 5130의 결재자는 문자열 이름만 저장. 새 시스템의 user_id(FK)로 변환 시 users 테이블에서 name 매칭 필요. 동명이인 주의. - -4. **행 수 불일치 가능성**: 5130에서 발주 제품 수에 따라 행이 동적 생성됨. 이관 시 원본 행 수 보존 필요. - -5. **이미지 참조**: 5130 JSON에는 이미지 참조명(`guiderail_wall_mid` 등)이 포함됨. 이관 시 새 시스템의 이미지 경로로 변환 필요. - -### 5.4 검사 기준 이미지 이관 (Phase 3.4 완료) - -`5130/img/inspection/` → `mng/public/img/inspection/` (27개 파일) - -| 분류 | 파일명 | 용도 | -|------|--------|------| -| 절곡-기준서 | `bending_inspection1.jpg`, `bending_inspection2.jpg` | 가이드레일/케이스/하단 기준 | -| 가이드레일-벽면 | `guiderail_wall_mid.jpg`, `_KSS02.jpg`, `_add.jpg`, `_slat.jpg`, `_add_slat.jpg`, `_slatKQTS01.jpg` | S1/S2/S3/슬랫변형 | -| 가이드레일-측면 | `guiderail_side_mid.jpg`, `_KSS02.jpg`, `_add.jpg`, `_slat.jpg`, `_add_slat.jpg`, `_slatKQTS01.jpg` | S1/S2/S3/슬랫변형 | -| 하단마감재 | `bottombar_KSS01KWE01.jpg`, `_add.jpg`, `_KTE01KQTS01.jpg`, `_add.jpg` | 표준/특수마감 | -| 케이스 | `box_both.jpg`, `box_both500x380.jpg`, `box_bottom.jpg`, `box_rear.jpg` | 양면/밑면/후면 | -| 기타 | `Lbar_mid.jpg`, `smoke.jpg` | L-BAR, 연기차단재 | -| 스크린 | `screen_inspection.jpg` | 스크린 기준서 | -| 슬랫 | `slat_inspection.jpg` | 슬랫 기준서 | -| 조인트바 | `jointbar_inspection.jpg` | 조인트바 기준서 | - -**접근 URL**: `https://mng.sam.kr/img/inspection/{filename}.jpg` - ---- - -## 6. 기술 결정사항 - -### 6.1 확정된 사항 - -| 항목 | 결정 | 이유 | -|------|------|------| -| 양식 관리 위치 | mng (Laravel + Blade) | 관리자 전용, HTMX 기반 UI 이미 존재 | -| 데이터 저장 패턴 | EAV (document_data 테이블) | 이미 설계됨, 동적 필드 지원 | -| 문서 상태 | DRAFT -> PENDING -> APPROVED/REJECTED/CANCELLED | 이미 구현됨 | -| API 제공 | api 저장소 (Laravel REST API) | SAM 표준 아키텍처 | -| 프론트엔드 소비 | react (Next.js) JSON 렌더링 | 기존 document-system 컴포넌트 확장 | - -### 6.2 검토 완료 사항 (2026-01-31 확정) - -| # | 항목 | 결정 | 근거 | -|---|------|------|------| -| 1 | PDF 생성 | **추후 고려** | react에 이미 구현됨 (html2pdf.js + DocumentViewer). mng 단계에서는 PDF 불필요 | -| 2 | 검사 판정 로직 | **프론트에서 입력, 결과만 저장** | 양식이 검사항목/기준을 정의하고, 프론트에서 사용자가 입력. 저장 시 입력값+판정 결과를 그대로 저장. 별도 판정 엔진 불필요 | -| 3 | 양식 버전 관리 | **수정 시 새 버전 생성** | 요청마다 검사 기준이 다를 수 있으므로 버전 관리 필수. document_templates에 version 컬럼 추가 필요 | -| 4 | 기존 react 컴포넌트 전환 | **기존 react 미수정** | mng에서 JSON 기반 화면 구현까지만 개발. 이후 프론트엔드 담당자와 협의하여 react 전환 여부 결정 | - ---- - -## 7. 컨펌 대기 목록 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| 1 | API 엔드포인트 추가 | `/api/v1/document-templates` (2), `/api/v1/documents` (5+4결재) | api 저장소 | ✅ Phase 4.1 완료 | -| 2 | DB 마이그레이션 변경 여부 | 기존 테이블로 충분한지 vs version 컬럼 추가 필요 (6.2 #3 확정) | api 저장소 | ⏳ Phase 1 중 | -| 3 | ~~검사 판정 로직 위치~~ | ~~프론트 vs 백엔드~~ → **프론트 입력, 결과만 저장** | - | ✅ 해결됨 (6.2 #2) | -| 4 | ~~PDF 생성 방식~~ | ~~클라이언트 vs 서버~~ → **추후 고려** (react 기 구현) | - | ✅ 해결됨 (6.2 #1) | - ---- - -## 8. 변경 이력 - -> 📎 별도 파일로 관리: [`document-management-system-changelog.md`](./document-management-system-changelog.md) - ---- - -## 9. 참고 문서 및 파일 - -### 프로젝트 문서 -- `docs/specs/database-schema.md` - DB 스키마 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `mng/CLAUDE.md` - MNG 프로젝트 규칙 - -### 기존 코드 (mng) -- `mng/app/Models/DocumentTemplate.php` - 양식 모델 -- `mng/app/Models/Documents/Document.php` - 문서 모델 -- `mng/app/Http/Controllers/DocumentTemplateController.php` - 양식 컨트롤러 -- `mng/app/Http/Controllers/DocumentController.php` - 문서 컨트롤러 -- `mng/resources/views/document-templates/edit.blade.php` - 양식 편집 UI (44.5KB) -- `mng/routes/web.php` 340-353줄 - 라우트 - -### 기존 코드 (react) -- `react/src/components/document-system/` - 문서 공통 시스템 -- `react/src/app/[locale]/(protected)/quality/qms/components/documents/` - QMS 검사 문서 -- `react/src/components/production/WorkerScreen/WorkLogContent.tsx` - 작업일지 -- `react/src/components/production/WorkOrders/documents/` - 중간검사 - -### 5130 레거시 -- `5130/instock/fetch_inspection.php` - 수입검사 메인 로더 (21.8KB) -- `5130/instock/i_*.php` - 자재별 수입검사 페이지 (40+) -- `5130/output/viewMidInspect*.php` - 중간검사 성적서 -- `5130/output/viewinspectionJointbar.php` - 조인트바 검사 -- `5130/img/inspection/` - 검사 기준 이미지 (20+) - -### DB 마이그레이션 -- `api/database/migrations/2026_01_26_200000_create_document_templates_table.php` -- `api/database/migrations/2026_01_28_200000_create_documents_table.php` - ---- - -## 11. Phase별 상세 실행 절차 - -> 각 Phase 작업 시 이 섹션을 먼저 읽고 진행한다. - -### 11.1 Phase 1.1 - 기존 document-templates 편집 UI 점검 및 보완 - -**목표**: `mng.sam.kr/document-templates/{id}/edit`에서 수입검사 양식에 필요한 모든 구성요소를 관리할 수 있는지 확인하고 부족한 부분을 보완한다. - -**사전 조건**: 없음 (첫 번째 작업) - -**실행 절차**: - -``` -Step 1: 현재 UI 분석 -├── mng/resources/views/document-templates/edit.blade.php (44.5KB) 읽기 -├── 기존 기능 목록 정리: -│ - 양식 기본정보 (이름, 카테고리, 제목, 회사명) 편집 가능? -│ - 결재라인 (approval_lines) CRUD 가능? -│ - 기본필드 (basic_fields) CRUD 가능? -│ - 섹션 (sections) CRUD 가능? -│ - 섹션 항목 (section_items) CRUD 가능? -│ - 컬럼 (columns) CRUD 가능? -│ - footer_remark_label, footer_judgement_label, footer_judgement_options 편집 가능? -└── 누락된 기능 목록화 - -Step 2: 브라우저에서 실제 동작 확인 -├── https://mng.sam.kr/document-templates 접속 -├── 기존 양식 편집 시도 (or 새 양식 생성 후 편집) -├── 각 탭/섹션별 CRUD 동작 확인 -└── JS 에러, 저장 실패 등 이슈 기록 - -Step 3: 보완 작업 -├── 누락된 CRUD 기능 구현 (Blade + HTMX + Alpine.js) -├── DocumentTemplateController 메서드 보강 -├── 유효성 검증 추가 (FormRequest 패턴) -└── 섹션 항목(section_items)의 drag-drop 정렬 (있는 경우 확인, 없으면 sort_order 수동 관리) - -Step 4: 검증 -├── 새 양식 생성 → 모든 하위 요소 추가 → 저장 → DB 확인 -├── 기존 양식 수정 → 저장 → 정상 반영 확인 -└── 양식 삭제 → 하위 요소 cascade 삭제 확인 -``` - -### 11.2 Phase 1.2 - 5130 수입검사 데이터 분석 - -**목표**: 5130의 자재별 수입검사 파일을 분석하여, 양식 시드 데이터로 변환할 수 있는 구조화된 데이터를 생성한다. - -**상태**: ✅ 완료 (2026-01-31, 경량 분석) - -**분석 결과**: - -#### 라우팅 구조 - -`5130/instock/common/viewJS.php`의 `viewBoardInstock()` 함수가 **item_name(품명) 기준 switch-case**로 개별 검사 페이지(`i_*.php`)를 팝업 호출한다. - -- `fetch_inspection.php` = 데이터 입력 폼 (목록에서 호출) -- `i_*.php` = 검사 성적서 뷰 (viewinspection 버튼에서 호출) -- 총 23개 파일, 품명별 1:1 또는 N:1 매핑 - -#### 자재 → 검사파일 매핑 (23개) - -| 품명 | 파일 | 비고 | -|---|---|---| -| EGI1.55T, EGI1.15T, EGI1.6T | `i_EGI155.php` | 전기아연도금강판 | -| SUS1.55T, SUS1.5T, SUS1.2T | `i_SUSplate.php` | 스테인리스강판 | -| GI0.5T, GI0.45T | `i_GIplate.php` | 아연도금강판 | -| 앵글 | `i_angle.php` | | -| 받침용앵글 | `i_anglebottom.php` | | -| 방화유리 | `i_antifireglass.php` | | -| 절곡코일(EGI) | `i_bendingcoil.php` | spec 앞3자=EGI | -| 베어링부 | `i_bracket.php` | | -| 바이오세라크울96K | `i_cerakwool.php` | | -| 연동제어기 | `i_controller.php` | | -| 화이바원단 | `i_fiber.php` | | -| 내화충진재 | `i_Fireproof_sealings.php` | | -| 내화실 | `i_fireproofWire.php` | | -| 전동개폐기 | `i_motor.php` | | -| 평철 | `i_platesteel.php` | | -| 마환봉 | `i_pole.php` | | -| 각파이프 | `i_recpipe.php` | | -| 감기샤프트 | `i_shaft.php` | | -| 실리카원단 | `i_sillica.php` | | -| 슬랫코일 | `i_slatcoil.php` | | -| 절곡코일(SUS) | `i_SUScoil.php` | spec 앞3자=SUS | -| 와이어원단 | `i_wire.php` | 기본 | -| 와이어원단(대한) | `i_wireDaehan.php` | remarks에 '대한' 포함 시 | - -#### 대표 자재 분석: EGI 1.55T (`i_EGI155.php`) - -| NO | 검사항목 | 검사기준 | 검사방식 | 검사주기 | 데이터 타입 | -|---|---|---|---|---|---| -| 1 | 겉모양 | 사용상 해로울 결함이 없을 것 | 육안검사 | n=3, c=0 | OK/NG 체크 ×3 | -| 2 | 치수-두께 | 두께별 허용범위 (±0.07~±0.12, 4구간) | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 2 | 치수-너비 | 1250 미만: +7/-0 | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 2 | 치수-길이 | 길이별 허용범위 (+10~+20/-0, 3구간) | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 3 | 인장강도 (N/mm²) | 270 이상 | 밀시트 | 입고시 | 단일값 | -| 4 | 연신율 (%) | 두께별 36~38 이상 (3구간) | 밀시트 | 입고시 | 단일값 | -| 5 | 아연 최소 부착량 (g/m²) | 한면 17 이상 | 밀시트 | 입고시 | 단일값 | - -#### 대표 자재 분석: SUS Plate (`i_SUSplate.php`) - -| NO | 검사항목 | 검사기준 | 검사방식 | 검사주기 | 데이터 타입 | -|---|---|---|---|---|---| -| 1 | 겉모양 | 사용상 해로울 결함이 없을 것 | 육안검사 | n=3, c=0 | OK/NG 체크 ×3 | -| 2 | 치수-두께 | 두께별 허용범위 (±0.10~±0.12, 2구간) | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 2 | 치수-너비 | 1250 미만: +7/-0 | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 2 | 치수-길이 | 길이별 허용범위 (+10~+20/-0, 2구간) | 체크검사 | n=3, c=0 | 측정값 ×3 | -| 3 | 항복강도 (N/mm²) | 205 이상 | 밀시트 | 입고시 | 단일값 | -| 4 | 인장강도 (N/mm²) | 520 이상 | 밀시트 | 입고시 | 단일값 | -| 5 | 연신율 (%) | 40 이상 | 밀시트 | 입고시 | 단일값 | -| 6 | 경도 (HV) | 200 이하 | 밀시트 | 입고시 | 단일값 | - -#### 공통 패턴 요약 - -**공통 구조 (모든 자재 동일):** -- **결재**: 담당 / 부서장 (2단계) -- **기본정보**: 품명, 규격(두께×너비×길이), 납품업체/제조업체, 로트번호, 자재번호, 검사일자, 로트크기, 검사자 -- **검사 테이블 컬럼**: NO / 검사항목 / 검사기준 / 검사방식 / 검사주기 / 측정치(n1,n2,n3) / 판정(적/부) -- **Footer**: 부적합 내용 + 종합판정(합격/불합격) -- **판정 로직**: JS 자동 계산 (모든 항목 적→합격, 하나라도 부→불합격) -- **저장**: JSON(`iList` hidden field) → AJAX POST → `insert_iList.php` - -**자재별 차이점:** -- 검사항목 수/종류 (EGI: 5항목 7행, SUS: 6항목 8행) -- 기준값 범위 (두께별 허용 오차, 강도/경도 기준 등) -- 두께 범위 구간 수 (EGI: 4구간, SUS: 2구간) -- 밀시트 항목 차이 (EGI: 인장+연신+아연, SUS: 항복+인장+연신+경도) - -> **결론**: 나머지 21개 자재는 Phase 1.3 시드 데이터 생성 시 개별 분석하면서 병행 진행 - -### 11.3 Phase 1.3 - 수입검사 양식 시드 데이터 생성 - -**실행 절차**: - -``` -Step 1: Seeder 파일 생성 -├── mng/database/seeders/IncomingInspectionTemplateSeeder.php 생성 -├── 1.2에서 정리한 데이터 기반 -└── 주요 자재 10종 양식 생성 (EGI, SUS, GI, Wire, Motor, Angle 등) - -Step 2: 실행 및 검증 -├── php artisan db:seed --class=IncomingInspectionTemplateSeeder -├── mng.sam.kr/document-templates 에서 목록 확인 -└── 각 양식 편집 화면에서 데이터 정합성 확인 -``` - ---- - -## 12. 자기완결성 점검 결과 - -### 12.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 각 Phase 작업 항목에 "완료 기준" 컬럼 추가됨 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 3 Phase 1-5 + 섹션 11 상세 절차 | -| 4 | 의존성이 명시되어 있는가? | ✅ | 기존 DB/모델/컨트롤러 현황 + 새 세션 가이드 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 절대 경로 + 상대 경로 모두 명시 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 11 Phase별 Step-by-step 절차 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 각 Phase 완료 기준에 검증 방법 포함 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 파일/테이블/컬럼/URL 명시 | - -### 12.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 🚀 새 세션 시작 가이드 + 📍 현재 진행 상태 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 새 세션 가이드 "핵심 파일" + 2.2 + 9. 참고 파일 | -| Q4. 작업 완료 확인 방법은? | ✅ | 각 Phase "완료 기준" 컬럼 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서 + 새 세션 가이드 | -| Q6. mng 기술 스택과 로컬 환경은? | ✅ | 새 세션 가이드 "프로젝트 정보" | -| Q7. 모델 관계와 DB 구조는? | ✅ | 새 세션 가이드 "모델 관계 구조" + 2.1 | -| Q8. Phase 1.1의 구체적 첫 단계는? | ✅ | 11.1 상세 실행 절차 | - -**결과**: 8/8 통과 - 자기완결성 확보 - -### 12.3 보완 이력 - -| 날짜 | 항목 | 원본 | 보완 내용 | -|------|------|------|----------| -| 2026-01-31 | 초기 검증 | - | 5/5 통과 | -| 2026-01-31 | 자기완결성 강화 | 새 세션에서 시작 불가 | 🚀 새 세션 시작 가이드 추가, 절대 경로/기술 스택/모델 코드 인라인, Phase 완료 기준 추가, 섹션 11 상세 실행 절차 추가, 컨펌 대기 목록 해결 항목 반영 | - ---- - -*이 문서는 /plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/hotfix-20260119-action-plan.md b/plans/hotfix-20260119-action-plan.md deleted file mode 100644 index c355d72..0000000 --- a/plans/hotfix-20260119-action-plan.md +++ /dev/null @@ -1,286 +0,0 @@ -# Hotfix 단위테스트 분석 및 액션 플랜 (2026-01-19) - -## 개요 - -**분석 대상 커밋**: `121b427c899cd37e273eaf08459dd5a3072da670` -**커밋 메시지**: 1/19 단위테스트 -**분석 일시**: 2026-01-19 -**작성자**: Claude Code - ---- - -## 테스트 결과 요약 - -| 구분 | 건수 | 비율 | -|------|------|------| -| ✅ 통과 (PASS) | 37개 | 92.5% | -| ⚠️ 스킵 - 페이지 미구현 | 2개 | 5.0% | -| ⚠️ 스킵 - 데이터 없음 | 1개 | 2.5% | -| **총계** | **40개** | **100%** | - ---- - -## 🔴 긴급 (P0) - 페이지 미구현 - -### 1. 근태 설정 페이지 - -| 항목 | 내용 | -|------|------| -| **URL** | `/ko/settings/attendance` | -| **현재 상태** | 404 Not Found | -| **우선순위** | P0 (긴급) | -| **담당** | React 프론트엔드 | -| **비고** | API 이미 존재 (WorkSettingController) | - -#### 필요 작업 -- [x] API 존재 확인 완료 (WorkSettingController) -- [ ] React 페이지 개발 -- [ ] API 연동 - -#### 예상 기능 -- 출퇴근 시간 설정 -- 지각/조퇴 기준 설정 -- 휴일 설정 -- 근태 알림 설정 - ---- - -### 2. 미수금현황 페이지 - -| 항목 | 내용 | -|------|------| -| **URL** | `/ko/accounting/receivables` | -| **현재 상태** | 404 Not Found | -| **우선순위** | P0 (긴급) | -| **담당** | React 프론트엔드 | -| **비고** | API 이미 존재 (ReceivablesController) | - -#### 필요 작업 -- [x] API 존재 확인 완료 (ReceivablesController) - - `GET /api/v1/receivables` - 목록 - - `GET /api/v1/receivables/summary` - 요약 - - `PUT /api/v1/receivables/memos` - 메모 업데이트 - - `PUT /api/v1/receivables/overdue-status` - 연체 상태 -- [ ] React 페이지 개발 (프론트엔드) -- [ ] API 연동 - -#### 예상 기능 -- 거래처별 미수금 현황 -- 기간별 미수금 추이 -- 연체 미수금 관리 -- 미수금 알림 설정 - ---- - -## 🟡 중요 (P1) - 데이터 정합성 이슈 - -### 1. 입금관리 - 입금유형 미설정 - -| 항목 | 내용 | -|------|------| -| **페이지** | `/ko/accounting/deposits` | -| **문제** | 입금유형 미설정 59건 / 60건 (98.3%) | -| **영향** | 입금 분류 및 통계 정확도 저하 | -| **우선순위** | P1 | - -#### 개선 방안 -- [ ] 입금유형 일괄 설정 기능 추가 -- [ ] 입금 등록 시 유형 필수 선택 옵션 -- [ ] 미설정 데이터 경고 배너 추가 - ---- - -### 2. 출금관리 - 출금유형 미설정 - -| 항목 | 내용 | -|------|------| -| **페이지** | `/ko/accounting/withdrawals` | -| **문제** | 출금유형 미설정 58건 / 60건 (96.7%) | -| **영향** | 출금 분류 및 통계 정확도 저하 | -| **우선순위** | P1 | - -#### 개선 방안 -- [ ] 출금유형 일괄 설정 기능 추가 -- [ ] 출금 등록 시 유형 필수 선택 옵션 -- [ ] 미설정 데이터 경고 배너 추가 - ---- - -### 3. 매입관리 - 매입유형/세금계산서 미설정 ✅ 완료 - -| 항목 | 내용 | -|------|------| -| **페이지** | `/ko/accounting/purchase` | -| **문제** | 매입유형 미설정 69건, 세금계산서 수취 미확인 69건 / 70건 (98.6%) | -| **영향** | 매입 분류, 세무 처리 누락 가능성 | -| **우선순위** | P1 | -| **상태** | ✅ API 완료 (2026-01-19) | - -#### 개선 방안 -- [x] 매입유형/세금계산서 일괄 설정 기능 → API 완료 - - `POST /api/v1/purchases/bulk-update-type` - 매입유형 일괄 변경 - - `POST /api/v1/purchases/bulk-update-tax-received` - 세금계산서 수취 일괄 설정 -- [ ] 매입 등록 시 필수 항목 검증 강화 -- [ ] 세무 신고 전 미설정 데이터 체크 기능 - ---- - -### 4. 매출관리 - 세금계산서/거래명세서 미발행 ✅ API 완료 - -| 항목 | 내용 | -|------|------| -| **페이지** | `/ko/accounting/sales` | -| **문제** | 세금계산서 발행대기 81건, 거래명세서 발행대기 81건 (100%) | -| **영향** | 세금계산서/거래명세서 발행 누락 | -| **우선순위** | P1 | -| **상태** | ✅ API 완료 (2026-01-19) | - -#### 기존 API (개별 발행) -- `POST /api/v1/tax-invoices/{id}/issue` - 세금계산서 개별 발행 -- `POST /api/v1/sales/{id}/statement/issue` - 거래명세서 개별 발행 - -#### 일괄 발행 API (신규) -- [x] `POST /api/v1/tax-invoices/bulk-issue` - 세금계산서 일괄 발행 -- [x] `POST /api/v1/sales/bulk-issue-statement` - 거래명세서 일괄 발행 - -#### 개선 방안 -- [x] 세금계산서 일괄 발행 API 개발 → 완료 -- [x] 거래명세서 일괄 발행 API 개발 → 완료 -- [ ] 자동 발행 로직 검토 (매출 등록 시 자동 발행 옵션) -- [ ] 발행 대기 데이터 대시보드 알림 -- [ ] React 프론트엔드 연동 - ---- - -## 🟢 개선 (P2) - 선택 사항 - -### 1. 관리자 대시보드 알림 강화 -- [ ] 데이터 미설정 건수 위젯 추가 -- [ ] 미발행 문서 건수 알림 -- [ ] 페이지 미구현 상태 모니터링 - -### 2. 데이터 품질 관리 -- [ ] 데이터 미설정 시 경고 아이콘 표시 -- [ ] 일별/주별 데이터 품질 리포트 -- [ ] 자동 데이터 정합성 체크 배치 - ---- - -## 정상 동작 기능 목록 (37개) - -
-전체 목록 펼치기 - -### 결재 시스템 (3개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 결재함 | approval-box | /ko/approval/inbox | -| 기안함 | draft-box | /ko/approval/draft | -| 참조함 | reference-box | /ko/approval/reference | - -### 인사관리 (12개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 근태현황 | attendance-checkin | /hr/attendance | -| 근태관리 | attendance-management | /hr/attendance-management | -| 근태 사유 | attendance-reason | /hr/attendance-management | -| 근태 등록 | attendance-register | /hr/attendance-management | -| 사원관리 | employee-register | /ko/hr/employee-management | -| 부서관리 | department-add | /ko/hr/department-management | -| 직급관리 | rank-management | /ko/settings/ranks | -| 휴가관리 | vacation-management | /ko/hr/vacation-management | -| 휴가정책 | leave-policy | /ko/settings/leave-policy | -| 급여관리 | salary-management | /ko/hr/salary-management | -| 카드관리 | card-add | /ko/hr/card-management | -| 근무일정 | work-schedule | /ko/settings/work-schedule | - -### 회계관리 (10개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 입금관리 | deposit-management | /ko/accounting/deposits | -| 출금관리 | withdrawal-management | /ko/accounting/withdrawals | -| 매입관리 | purchase-management | /ko/accounting/purchase | -| 매출관리 | sales-management | /ko/accounting/sales | -| 거래처관리 | vendor-management | /ko/accounting/vendors | -| 거래처원장 | vendor-ledger | /ko/accounting/vendor-ledger | -| 카드거래 | card-transactions | /ko/accounting/card-transactions | -| 대손채권회수 | bad-debt-collection | /accounting/bad-debt-collection | -| 일일 일보 | daily-report | /ko/accounting/daily-report | -| 지출 예상 내역서 | expected-expenses | /ko/accounting/expected-expenses | - -### 게시판 (4개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 게시판관리 | board-management | /ko/board/board-management | -| 게시판 | board-test | /ko/boards/board_mjsgri54_1fmg | -| 자유게시판 | free-board | /ko/boards/free | -| 1:1 문의 | customer-inquiry | /ko/customer-center/qna | - -### 생산관리 (3개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 품목관리 | item-management | /ko/production/screen-production | -| 생산 현황판 | production-dashboard | /ko/production/dashboard | -| 작업지시 관리 | work-order-management | /ko/production/work-orders | - -### 설정 (4개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 회사정보 | company-info | /ko/company-info | -| 권한관리 | permission-management | /ko/settings/permissions | -| 알림설정 | notification-settings | /ko/settings/notification-settings | -| 팝업관리 | popup-management | /ko/settings/popup-management | - -### 기타 (2개) -| 기능 | 테스트 ID | URL | -|------|----------|-----| -| 로그인 | login | /login | -| 결제내역 | payment-history | /ko/payment-history | - -
- ---- - -## 작업 일정 (권장) - -```mermaid -gantt - title Hotfix 작업 일정 - dateFormat YYYY-MM-DD - section P0 긴급 - 근태 설정 페이지 개발 :2026-01-20, 3d - 미수금현황 페이지 개발 :2026-01-20, 3d - section P1 중요 - 입금/출금 유형 일괄설정 :2026-01-23, 2d - 매입/매출 데이터 정합성 :2026-01-25, 2d - section P2 개선 - 대시보드 알림 강화 :2026-01-27, 2d -``` - ---- - -## 담당자 배정 (제안) - -| 우선순위 | 작업 | 담당 | 상태 | -|----------|------|------|------| -| P0 | 근태 설정 페이지 | React 프론트엔드 | ⬜ 대기 (API 존재) | -| P0 | 미수금현황 페이지 | React 프론트엔드 | ⬜ 대기 (API 존재) | -| P1 | 입금유형 일괄설정 | React 프론트엔드 | ✅ API 이미 존재 | -| P1 | 출금유형 일괄설정 | React 프론트엔드 | ✅ API 이미 존재 | -| P1 | 매입 데이터 정합성 | React 프론트엔드 | ✅ API 완료 (2026-01-19) | -| P1 | 매출 문서 발행 | api 백엔드 + React 프론트엔드 | ✅ API 완료 (2026-01-19) | -| P2 | 대시보드 알림 | React 프론트엔드 | ⬜ 대기 | - ---- - -## 참고 자료 - -- 테스트 결과 파일: `hotfix/*_2026-01-19_test.md` (40개) -- Serena 메모리: `hotfix-test-analysis-20260119.md` -- 관련 커밋: `121b427c899cd37e273eaf08459dd5a3072da670` - ---- - -**문서 버전**: 1.0 -**최종 수정**: 2026-01-19 -**다음 검토**: 작업 완료 후 \ No newline at end of file diff --git a/plans/index_plans.md b/plans/index_plans.md index 9cd0f4d..6b42d60 100644 --- a/plans/index_plans.md +++ b/plans/index_plans.md @@ -1,7 +1,7 @@ # 기획 문서 인덱스 > SAM 시스템 개발 계획 및 기획 문서 모음 -> **최종 업데이트**: 2026-02-22 +> **최종 업데이트**: 2026-02-26 --- @@ -9,242 +9,157 @@ | 분류 | 개수 | 설명 | |------|------|------| -| 진행중/대기 계획서 | 44개 | 기능별 개발 계획 | -| 완료 아카이브 | 37개 | `archive/` 폴더에 보관 | +| 🟡 진행중 (ACTIVE) | 18개 | 현재 작업중인 계획 | +| ⚪ 대기 (PLANNED) | 19개 | 미착수/선행조건 대기 | +| 완료 히스토리 | 40건 | `archive/HISTORY.md`에 요약 | | 스토리보드 | 1개 | ERP 화면 설계 (D1.0) | -| 플로우 테스트 | 32개 | API 검증용 JSON 테스트 케이스 | +| 플로우 테스트 | 32개 | API 검증용 JSON | -> **Note**: 완료된 계획 37개는 `archive/` 폴더로 이동됨 (최종 정리: 2026-02-22) +> **문서 관리 가이드**: [GUIDE.md](./GUIDE.md) --- -## 개발 계획서 (진행중/대기) +## 진행중 (ACTIVE) - 18개 -### ERP API 개발 +### ERP API -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [erp-api-development-plan.md](./erp-api-development-plan.md) | 🟡 진행중 | Phase 3/L | SAM ERP API 전체 개발 계획, L-2 React 연동 대기 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [erp-api-development-plan.md](./erp-api-development-plan.md) | Phase L | SAM ERP API, L-2 React 연동 대기 | -### 견적/수주 (Quote/Order) +### 견적/수주 -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [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) | ⚪ 대기 | - | 견적 시스템 개발, 계획 수립 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [kd-quote-logic-plan.md](./kd-quote-logic-plan.md) | 80% | 경동 견적 로직, Phase 5 통합테스트 직전 | +| [product-code-traceability-plan.md](./product-code-traceability-plan.md) | - | 제품코드 추적성 개선 | -### 생산/절곡 (Production/Bending) +### 품목/BOM -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [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-mapping-plan.md](./bom-item-mapping-plan.md) | 66% | BOM 품목 매핑, Phase 3 검증 잔여 | +| [item-master-data-alignment-plan.md](./item-master-data-alignment-plan.md) | - | 품목 마스터 정합, 섀도잉 정리 | +| [fg-code-consolidation-plan.md](./fg-code-consolidation-plan.md) | 분석완료 | FG 코드 통합, Phase 1 착수 전 | -### 품목/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%) | 중간검사 보고서, 검토 대기 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [document-system-master.md](./document-system-master.md) | Phase 4-5 | 문서 시스템 마스터 | +| [document-system-mid-inspection.md](./document-system-mid-inspection.md) | 5/6 | 중간검사, 결재만 남음 | +| [document-system-work-log.md](./document-system-work-log.md) | 3/4+α | 작업일지, React 연동 잔여 | +| [incoming-inspection-templates-plan.md](./incoming-inspection-templates-plan.md) | 83% | 수입검사 템플릿, 4종 품목 대기 | ### 마이그레이션 & 연동 -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [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 | 후속 문서로 이관됨 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [5130-to-mng-migration-plan.md](./5130-to-mng-migration-plan.md) | 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) | 45% | CEO Dashboard API 연동 | ### 시스템/인프라 -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [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% | 채번 규칙 관리, 미착수 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [db-backup-system-plan.md](./db-backup-system-plan.md) | 79% | DB 백업, 서버 작업 3건 잔여 | ### 프론트엔드 & UI -| 문서 | 상태 | 진행률 | 설명 | -|------|------|--------|------| -| [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%) | 개발 툴바 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [simulator-ui-enhancement-plan.md](./simulator-ui-enhancement-plan.md) | 60% | 시뮬레이터 UI 개선 | +| [card-management-section-plan.md](./card-management-section-plan.md) | 50% | 카드 관리 섹션 | +| [dev-toolbar-plan.md](./dev-toolbar-plan.md) | 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 전환 잔여 작업 목록 | +| 문서 | 진행률 | 설명 | +|------|--------|------| +| [mng-menu-system-plan.md](./mng-menu-system-plan.md) | 구현완료 | 메뉴 시스템, Phase 3 테스트 잔여 | --- -## 완료 아카이브 (archive/) - 37개 +## 대기 (PLANNED) - 19개 -> 완료된 계획 문서들 - 참조용으로 보관 +### 견적/수주 -| 문서 | 완료일 | 설명 | -|------|--------|------| -| [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 | 견적 자동 계산 | -| [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 | 알림음 시스템 | -| [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 | MES 연동 분석 | +| 문서 | 설명 | +|------|------| +| [quote-management-8issues-plan.md](./quote-management-8issues-plan.md) | 견적관리 8개 이슈, 컨펌 대기 | +| [quote-calculation-api-plan.md](./quote-calculation-api-plan.md) | 견적 계산 API, 설계 완료 | +| [quote-order-sync-improvement-plan.md](./quote-order-sync-improvement-plan.md) | 견적-수주 동기화 개선, 승인 대기 | +| [kd-orders-migration-plan.md](./kd-orders-migration-plan.md) | 경동 수주 마이그레이션, 선행조건 미충족 | +| [receiving-management-analysis-plan.md](./receiving-management-analysis-plan.md) | 입고 관리, 분석 완료/개발 대기 | +| [monthly-expense-integration-plan.md](./monthly-expense-integration-plan.md) | 월별 경비 연동 | + +### 품목/BOM + +| 문서 | 설명 | +|------|------| +| [mng-item-field-management-plan.md](./mng-item-field-management-plan.md) | 품목 필드 관리 | +| [item-inventory-management-plan.md](./item-inventory-management-plan.md) | 품목 재고 관리, 설계 확정 | + +### 생산/절곡 + +| 문서 | 설명 | +|------|------| +| [bending-info-auto-generation-plan.md](./bending-info-auto-generation-plan.md) | 절곡 정보 자동 생성, 설계 확정 | +| [bending-material-input-mapping-plan.md](./bending-material-input-mapping-plan.md) | 절곡 자재투입 매핑, GAP 분석 완료 | + +### 문서/서식 + +| 문서 | 설명 | +|------|------| +| [incoming-inspection-document-integration-plan.md](./incoming-inspection-document-integration-plan.md) | 수입검사 서류 연동, 분석만 완료 | +| [intermediate-inspection-report-plan.md](./intermediate-inspection-report-plan.md) | 중간검사 보고서, 검토 대기 | + +### 시스템/인프라 + +| 문서 | 설명 | +|------|------| +| [tenant-id-compliance-plan.md](./tenant-id-compliance-plan.md) | 테넌트 ID 정합, 실행 대기 | +| [tenant-numbering-system-plan.md](./tenant-numbering-system-plan.md) | 테넌트 채번 | +| [mng-numbering-rule-management-plan.md](./mng-numbering-rule-management-plan.md) | 채번 규칙 관리 | + +### 기타 + +| 문서 | 설명 | +|------|------| +| [api-explorer-development-plan.md](./api-explorer-development-plan.md) | API Explorer | +| [employee-user-linkage-plan.md](./employee-user-linkage-plan.md) | 사원-회원 연결 | +| [esign-alimtalk-integration.md](./esign-alimtalk-integration.md) | 전자서명/알림톡, 카카오 채널 개설 후 착수 | +| [dummy-data-seeding-plan.md](./dummy-data-seeding-plan.md) | 더미 데이터 시딩 | + +--- + +## 완료 히스토리 + +> 40건 완료 작업 요약 → [archive/HISTORY.md](./archive/HISTORY.md) --- ## 스토리보드 -### SAM_ERP_Storyboard_D1.0_251218 (현재 버전) - -**경로**: `docs/plans/SAM_ERP_Storyboard_D1.0_251218/` -**일자**: 2025-12-18 -**슬라이드 수**: 38장 - -**내용**: D0.8 대비 변경/추가된 화면 (D1.0 버전) +**경로**: `SAM_ERP_Storyboard_D1.0_251218/` +**내용**: D0.8 대비 변경/추가된 화면 (D1.0, 2025-12-18, 38장) --- ## 플로우 테스트 -**경로**: `docs/plans/flow-tests/` -**용도**: Flow Tester (mng.sam.kr/dev-tools/flow-tester) 검증용 JSON - -### 인증/권한 - -| 파일 | 설명 | -|------|------| -| [auth-api-flow.json](./flow-tests/auth-api-flow.json) | 인증 API 플로우 | -| [auth-legacy-flow.json](./flow-tests/auth-legacy-flow.json) | 레거시 인증 플로우 | -| [user-invitation-flow.json](./flow-tests/user-invitation-flow.json) | 사용자 초대 | - -### 품목/BOM - -| 파일 | 설명 | -|------|------| -| [items-crud-api-flow.json](./flow-tests/items-crud-api-flow.json) | 품목 CRUD | -| [items-bom-api-flow.json](./flow-tests/items-bom-api-flow.json) | BOM API | -| [items-bom-test.json](./flow-tests/items-bom-test.json) | BOM 테스트 | -| [item-master-page-api-flow.json](./flow-tests/item-master-page-api-flow.json) | 품목 마스터 페이지 | -| [item-master-full-api-flow.json](./flow-tests/item-master-full-api-flow.json) | 품목 마스터 전체 | -| [item-master-init-api-flow.json](./flow-tests/item-master-init-api-flow.json) | 품목 마스터 초기화 | -| [item-master-field-api-flow.json](./flow-tests/item-master-field-api-flow.json) | 품목 필드 | -| [item-master-legacy-flow.json](./flow-tests/item-master-legacy-flow.json) | 레거시 품목 | -| [item-delete-legacy-flow.json](./flow-tests/item-delete-legacy-flow.json) | 품목 삭제 (레거시) | -| [item-delete-force-delete.json](./flow-tests/item-delete-force-delete.json) | 품목 강제 삭제 | -| [item-fields-is-active-test.json](./flow-tests/item-fields-is-active-test.json) | 필드 활성화 테스트 | - -### 거래처/영업 - -| 파일 | 설명 | -|------|------| -| [client-api-flow.json](./flow-tests/client-api-flow.json) | 거래처 API | -| [client-legacy-flow.json](./flow-tests/client-legacy-flow.json) | 레거시 거래처 | -| [client-group-api-flow.json](./flow-tests/client-group-api-flow.json) | 거래처 그룹 | -| [pricing-crud-flow.json](./flow-tests/pricing-crud-flow.json) | 단가 CRUD | -| [pricing-validation-test.json](./flow-tests/pricing-validation-test.json) | 단가 검증 | - -### 인사/급여 - -| 파일 | 설명 | -|------|------| -| [employee-api-crud.json](./flow-tests/employee-api-crud.json) | 사원 CRUD | -| [attendance-api-crud.json](./flow-tests/attendance-api-crud.json) | 근태 CRUD | -| [department-tree-api.json](./flow-tests/department-tree-api.json) | 부서 트리 | - -### 회계/재무 - -| 파일 | 설명 | -|------|------| -| [account-management-flow.json](./flow-tests/account-management-flow.json) | 계정 관리 | -| [sales-statement-flow.json](./flow-tests/sales-statement-flow.json) | 매출 전표 | -| [payment-flow.json](./flow-tests/payment-flow.json) | 결제 플로우 | -| [bad-debt-flow.json](./flow-tests/bad-debt-flow.json) | 대손 처리 | - -### 기타 - -| 파일 | 설명 | -|------|------| -| [popup-flow.json](./flow-tests/popup-flow.json) | 팝업 플로우 | -| [company-request-flow.json](./flow-tests/company-request-flow.json) | 회사 요청 | -| [notification-settings-flow.json](./flow-tests/notification-settings-flow.json) | 알림 설정 | -| [subscription-flow.json](./flow-tests/subscription-flow.json) | 구독 플로우 | -| [branching-example-flow.json](./flow-tests/branching-example-flow.json) | 분기 예제 | +**경로**: `flow-tests/` +**용도**: Flow Tester (mng.sam.kr/dev-tools/flow-tester) 검증용 JSON, 32개 --- ## 관련 문서 - [docs/INDEX.md](../INDEX.md) - 전체 문서 인덱스 -- [docs/projects/index_projects.md](../projects/index_projects.md) - 프로젝트 문서 인덱스 +- [GUIDE.md](./GUIDE.md) - 문서 관리 가이드 --- -**범례**: -- 🟡 진행중: 현재 작업 중 또는 일부 완료 -- ⚪ 대기: 미착수 또는 선행조건 대기 -- 📚 참조: 분석/참조용 문서 +**범례**: 🟡 진행중 | ⚪ 대기 \ No newline at end of file diff --git a/plans/items-migration-kyungdong-plan.md b/plans/items-migration-kyungdong-plan.md deleted file mode 100644 index 6995ecc..0000000 --- a/plans/items-migration-kyungdong-plan.md +++ /dev/null @@ -1,1399 +0,0 @@ -# [ARCHIVED] 경동기업(5130) 레거시 → SAM 전체 데이터 마이그레이션 계획 - -> ⚠️ **이 문서는 분리되었습니다** (2026-01-28) -> -> 이 통합 문서는 다음 2개 문서로 분리되었습니다: -> -> 1. **📦 품목/단가/BOM**: [`kd-items-migration-plan.md`](./kd-items-migration-plan.md) ← **먼저 작업** -> 2. **📋 입고/재고/주문**: [`kd-orders-migration-plan.md`](./kd-orders-migration-plan.md) ← 품목 완료 후 작업 -> -> 아래 내용은 참고용으로 보존됩니다. - ---- - -> **작성일**: 2026-01-28 -> **목적**: 경동기업 레거시 시스템(5130/)의 **전체 운영 데이터**를 SAM으로 이관 -> **기준 문서**: `5130/` 폴더 분석 결과 -> **상태**: ✅ 문서 분리 완료 (2026-01-28) -> **데이터 규모**: ~30,000+ 레코드 (items + prices + receipts + orders) - ---- - -## 🚀 새 세션 시작 가이드 (Quick Start) - -### 이 문서만 보고 작업을 재개하려면: - -```bash -# 1. Docker 서비스 확인 -docker ps | grep sam - -# 2. 레거시 DB (chandj) 접속 테스트 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM models;" - -# 3. 현재 진행 상태 확인 -# → 아래 "📍 현재 진행 상태" 섹션 참조 - -# 4. 다음 작업 시작 -# → "📍 현재 진행 상태" > "다음 작업" 참조 -``` - -### 환경 정보 - -| 항목 | 값 | -|------|-----| -| **프로젝트 루트** | `/Users/kent/Works/@KD_SAM/SAM` | -| **레거시 소스** | `5130/` (프로젝트 루트 직하) | -| **API 프로젝트** | `api/` | -| **Docker 컨테이너** | `sam-mysql-1` | -| **레거시 DB** | `chandj` (MySQL) | -| **SAM DB** | `samdb` (MySQL) ⚠️ | -| **대상 테넌트 ID** | `287` (경동기업) | -| **생성자 사용자 ID** | `1` | - -### DB 접속 명령어 - -```bash -# 레거시 DB (chandj) 접속 -docker exec -it sam-mysql-1 mysql -uroot -proot chandj - -# SAM DB 접속 -docker exec -it sam-mysql-1 mysql -uroot -proot samdb - -# 레거시 테이블 목록 확인 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SHOW TABLES;" - -# SAM items 테이블 확인 -docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" -``` - -### 전제 조건 (작업 전 확인) - -- [x] Docker 서비스 실행 중 -- [x] `sam-mysql-1` 컨테이너 실행 중 -- [x] chandj 데이터베이스 접근 가능 -- [ ] SAM items 마이그레이션 실행 완료 (`php artisan migrate`) -- [ ] SAM prices 마이그레이션 실행 완료 - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | 전체 범위 분석 완료 (KDunitprice 603건, output 24,564건 발견) | -| **다음 작업** | Phase 1.0: KDunitprice → items 마스터 INSERT | -| **진행률** | 2/6 (33%) - 분석 완료, 구현 대기 | -| **마지막 업데이트** | 2026-01-28 | - -### 다음 작업 상세 - -**Phase 1.0: KDunitprice → items (마스터) INSERT** ⭐ 최우선! - -1. KDunitprice 데이터 확인: - ```bash - docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*), item_div FROM KDunitprice GROUP BY item_div;" - ``` - -2. 섹션 5.0의 SQL 쿼리를 SAM DB에서 실행: - - KDunitprice → items (603건) - - KDunitprice → prices (603건) - -3. 중복 확인 후 추가 items 생성: - - models, category_l4 중 KDunitprice에 없는 것만 추가 - -4. ⚠️ 실행 전 사용자 승인 필요 - ---- - -## 0. 성공 기준 - -| 기준 | 목표값 | 확인 방법 | -|------|-------|----------| -| **items 합계** | **~800건** | `SELECT COUNT(*) FROM items WHERE tenant_id=287` | -| items (FG) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='FG'` | -| items (PT) | ~250건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='PT'` | -| items (SM) | ~300건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='SM'` | -| items (RM) | ~100건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='RM'` | -| items (CS) | ~50건 | `SELECT COUNT(*) FROM items WHERE tenant_id=287 AND item_type='CS'` | -| **prices 합계** | **~500건** | `SELECT COUNT(*) FROM prices WHERE tenant_id=287` | -| **BOM 관계** | ~300건 | `SELECT COUNT(*) FROM item_bom_items WHERE tenant_id=287` | -| **입고 기록** | ~2,300건 | `SELECT COUNT(*) FROM item_receipts WHERE tenant_id=287` | -| **주문 기록** | ~24,600건 | `SELECT COUNT(*) FROM orders WHERE tenant_id=287` | -| **로트 기록** | ~200건 | `SELECT COUNT(*) FROM lots WHERE tenant_id=287` | -| code 유일성 | 100% | `SELECT code, COUNT(*) FROM items WHERE tenant_id=287 GROUP BY code HAVING COUNT(*) > 1` (0건) | -| API 테스트 | 100% | `/api/v1/items` 목록 조회 성공 | - ---- - -## 1. 개요 - -### 1.1 배경 - -경동기업은 **모델(Model) + 제품(Product)** 분리 구조를 사용하지만, SAM은 **통합 items 테이블** 구조로 기획됨. 레거시 5130/ 폴더의 데이터를 SAM 구조에 맞게 변환하여 이관 필요. - -### 1.2 핵심 차이점 - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ 레거시 (chandj) → SAM (samdb) │ -├────────────────────────────────────────────────────────────────────────────┤ -│ 📦 품목 마스터 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ KDunitprice (603건, 핵심!) → items (마스터, code로 구분) │ -│ models (18건) → items (FG) │ -│ parts, parts_sub (170건) → item_bom_items │ -│ category_l1~l4 → items 카테고리 참조 │ -│ guiderail, bottombar, bending 등 → item_details │ -│ │ -│ 💰 단가 정보 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ price_* (10개 테이블) → prices │ -│ KDunitprice.출고가/입고가 → prices (기본가) │ -│ │ -│ 📥 입고/재고 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ instock (2,286건) → item_receipts + stocks │ -│ lot, lot_sales → lots + lot_sales │ -│ │ -│ 📋 주문/출고 │ -│ ───────────────────────────────────────────────────────────────────────── │ -│ output (24,564건) → orders + order_items │ -│ output.iList (JSON 파일 참조) → orders.options │ -│ estimate → orders (type=견적) │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.2.1 중복 제거 전략 ⭐ - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심: KDunitprice가 마스터, code 필드로 중복 방지 │ -├────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 1️⃣ KDunitprice (603건) → items 먼저 생성 │ -│ - item_div로 item_type 결정 │ -│ - code = 품목코드 그대로 사용 │ -│ │ -│ 2️⃣ price_* 테이블 → items 중복 확인 후 prices만 생성 │ -│ - code로 items 조회 │ -│ - 존재하면 → prices만 추가 (item_id 연결) │ -│ - 없으면 → items 생성 후 prices 추가 │ -│ │ -│ 3️⃣ 매핑 테이블 불필요 │ -│ - item_id_mappings ❌ (양방향 조회 불필요) │ -│ - chandj는 손 안댐, samdb에만 밀어 넣음 │ -│ │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.3 SAM items 구조 (Target) - -```sql --- items 테이블 (tenant_id=287 for 경동기업) -CREATE TABLE items ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, -- 287 (경동기업) - item_type VARCHAR(15) NOT NULL, -- FG, PT, SM, RM, CS - code VARCHAR(100) NOT NULL, -- 품목코드 - name VARCHAR(255) NOT NULL, -- 품목명 - unit VARCHAR(20), -- 단위 - category_id BIGINT, -- 카테고리 ID - bom JSON, -- [{child_item_id, quantity}, ...] - attributes JSON, -- 동적 필드 값 - options JSON, -- 추가 옵션 - description TEXT, -- 설명 - is_active BOOLEAN DEFAULT TRUE, - created_by, updated_by, deleted_by, timestamps, soft_deletes -); -``` - -### 1.4 item_type 분류 - -| SAM item_type | 설명 | 레거시 소스 | -|---------------|------|-------------| -| **FG** | 완제품 (Finished Goods) | KDunitprice[제품], KDunitprice[상품], models | -| **PT** | 부품 (Parts) | KDunitprice[반제품], parts, parts_sub, category_l4 | -| **SM** | 부자재 (Sub-Materials) | KDunitprice[부재료], price_* 테이블들 | -| **RM** | 원자재 (Raw Materials) | KDunitprice[원재료], price_raw_materials | -| **CS** | 소모품 (Consumables) | KDunitprice[무형상품], 기타 | - -### 1.4.1 KDunitprice.item_div → item_type 매핑 ⭐ - -```sql --- KDunitprice.item_div 값 목록 (603건 중) --- [제품], [상품], [부재료], [원재료], [반제품], [무형상품] - -CASE item_div - WHEN '[제품]' THEN 'FG' -- 완제품 - WHEN '[상품]' THEN 'FG' -- 상품도 완제품으로 분류 - WHEN '[반제품]' THEN 'PT' -- 반제품 = 부품 - WHEN '[부재료]' THEN 'SM' -- 부자재 - WHEN '[원재료]' THEN 'RM' -- 원자재 - WHEN '[무형상품]' THEN 'CS' -- 소모품/무형 - ELSE 'SM' -- 기본값 -END AS item_type -``` - ---- - -## 2. 레거시 DB 구조 분석 - -### 2.1 핵심 테이블 및 레코드 수 (전체 목록) - -#### 📦 품목 마스터 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| **`KDunitprice`** ⭐ | **603** | **품목 마스터 (핵심!)** | **items (마스터)** | -| `models` | 18 | 모델 마스터 (스크린/철재) | items (FG) | -| `BDmodels` | 59 | 모델별 BOM + 단가 (JSON) | item_bom_items + prices | -| `parts` | 36 | 부품 | item_bom_items | -| `parts_sub` | 134 | 하위 부품 | item_bom_items | -| `category_l1` | 2 | 1단계 카테고리 (스크린/철재) | 참조용 | -| `category_l2` | 14 | 2단계 카테고리 | 참조용 | -| `category_l3` | 24 | 3단계 카테고리 | 참조용 | -| `category_l4` | 37 | 4단계 카테고리 (부품) | items (PT) | -| `item_list` | 5+ | 품목 마스터 | items (PT) | - -#### 🔧 제품 상세 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| `guiderail` | - | 가이드레일 상세 | item_details | -| `bottombar` | - | 하단바 상세 | item_details | -| `shutterbox` | - | 셔터박스 상세 | item_details | -| `bending` | - | 벤딩 상세 | item_details | -| `lift` | - | 리프트 상세 | item_details | - -#### 💰 단가 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| `price_motor` | 2 (JSON) | 모터 단가 | prices | -| `price_shaft` | 2 (JSON) | 감기샤프트 단가 | prices | -| `price_pipe` | 2 (JSON) | 파이프 단가 | prices | -| `price_angle` | 2 (JSON) | 앵글 단가 | prices | -| `price_raw_materials` | 6 (JSON) | 주자재 단가 | prices | -| `price_bend` | 3 (JSON) | 절곡 단가 | prices | -| `price_pole` | 2 (JSON) | 폴 단가 | prices | -| `price_screenplate` | 2 (JSON) | 스크린플레이트 단가 | prices | -| `price_smokeban` | 2 (JSON) | 연기차단 단가 | prices | - -#### 📥 입고/재고 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| **`instock`** ⭐ | **2,286** | 입고 기록 | item_receipts + stocks | -| `lot` | - | 로트 관리 | lots | -| `lot_sales` | - | 로트 소진 | lot_sales | - -#### 📋 주문/출고 테이블 - -| 테이블 | 레코드 수 | 역할 | SAM 매핑 | -|--------|----------|------|----------| -| **`output`** ⭐ | **24,564** | 주문/출고 기록 | orders + order_items | -| `estimate` | - | 견적 | orders (type=견적) | - -### 2.2 models 테이블 구조 - -```sql --- models: 제품 모델 마스터 -model_id INT PRIMARY KEY, -model_name VARCHAR(255), -- KSS01, KSE01, KWE01 등 -major_category ENUM('스크린','철재'), -finishing_type ENUM('SUS마감','EGI마감'), -guiderail_type VARCHAR(20), -- 벽면형, 측면형, 혼합형 -description TEXT, -is_deleted, created_at, updated_at -``` - -**샘플 데이터**: -- KSS01/스크린/SUS마감/벽면형 -- KSS01/스크린/SUS마감/측면형 -- KSE01/스크린/EGI마감/벽면형 -- KWE01/스크린/SUS마감/벽면형 - -### 2.3 KDunitprice 테이블 구조 ⭐ (핵심 마스터) - -```sql --- KDunitprice: 품목 마스터 (603건) - 가장 중요한 테이블! -품목코드 VARCHAR(50), -- items.code (유니크 키!) -품목명 VARCHAR(255), -- items.name -규격 VARCHAR(100), -- items.attributes.spec -단위 VARCHAR(20), -- items.unit -item_div VARCHAR(20), -- [제품]/[상품]/[부재료]/[원재료]/[반제품]/[무형상품] → item_type -입고가 DECIMAL, -- prices.purchase_price -출고가 DECIMAL, -- prices.sales_price -비고 TEXT -- items.description -``` - -**item_div 분포 (예상)**: -```sql -SELECT item_div, COUNT(*) FROM KDunitprice GROUP BY item_div; --- [제품] ~100건 → FG --- [상품] ~50건 → FG --- [반제품] ~100건 → PT --- [부재료] ~200건 → SM --- [원재료] ~100건 → RM --- [무형상품] ~53건 → CS -``` - -### 2.3.1 output.iList JSON 파일 구조 ⭐ - -```sql --- output 테이블의 iList 컬럼 --- 값: "../output/i_json/22545.json" (파일 경로!) --- 실제 파일 위치: 5130/output/i_json/{output_id}.json -``` - -**JSON 파일 내용 예시 (5130/output/i_json/22545.json)**: -```json -{ - "inputValue": [ - "2024-12-03", // 날짜 - "명보에스티", // 거래처명 - "KWE01 전체적인 테스트", // 모델/설명 - // ... 추가 입력값들 - ], - "beforeWidth": ["8000", "7000"], // 변경전 폭 - "beforeHeight": ["4000", "3500"], // 변경전 높이 - "afterWidth": ["8000", "7000"], // 변경후 폭 - "afterHeight": ["4000", "3500"], // 변경후 높이 - "pages": [ - { - "page": "1", - "inputItems": { - "openWidth": "8000", - "openHeight": "4000", - // ... 기타 치수 정보 - }, - "checkboxData": [...] - } - ], - "approval": { - "writer": {"name": "개발자", "date": "25/01/02"}, - "approver": {"name": "관리자", "date": "25/01/03"} - } -} -``` - -**SAM 매핑**: -- `inputValue` → `orders.options` (JSON) -- `pages` → `order_items.options` (JSON) -- `approval` → `orders.approved_by`, `orders.approved_at` -- `beforeWidth/Height`, `afterWidth/Height` → `order_items.options.dimensions` - -### 2.4 BDmodels 테이블 구조 (BOM + 단가) - -```sql --- BDmodels: 모델별 BOM 및 단가 정보 -num INT PRIMARY KEY, -major_category VARCHAR(10), -- 스크린/철재 -spec VARCHAR(30), -- 규격 (60*40, 120*70 등) -model_name VARCHAR(255), -- 모델명 -finishing_type ENUM('SUS마감','EGI마감'), -check_type VARCHAR(20), -- 벽면형/측면형/혼합형 -seconditem VARCHAR(30), -- 부품명 (가이드레일, 하단마감재, L-BAR 등) -unitprice TEXT, -- 단가 (문자열) -savejson TEXT, -- BOM 상세 JSON -description TEXT, -is_deleted, priceDate DATE -``` - -**savejson 예시** (가이드레일 BOM): -```json -[ - {"col1":"1번(마감재)","col2":"SUS 1.2T","col3":"-3","col4":"203","col5":"206","col6":"51,000","col7":"10,506","col8":"2","col9":"21,012","col10":"삭제"}, - {"col1":"2번(본체)","col2":"EGI 1.55T","col3":"-5","col4":"294","col5":"299","col6":"27,000","col7":"8,073","col8":"1","col9":"8,073","col10":"삭제"}, - {"col1":"3번(벽면형-C)","col2":"EGI 1.55T","col3":"-1","col4":"104","col5":"105","col6":"27,000","col7":"2,835","col8":"1","col9":"2,835","col10":"삭제"}, - {"col1":"4번(벽면형-D)","col2":"EGI 1.55T","col3":"-3","col4":"105","col5":"108","col6":"27,000","col7":"2,916","col8":"1","col9":"2,916","col10":"삭제"} -] -``` - -### 2.4 카테고리 계층 구조 (4단계) - -``` -category_l1 (2개) -├── 스크린 -│ ├── category_l2 (앵글, 환봉, 각파이프, 감기샤프트, 전동개폐기, 원단, 절곡물) -│ │ ├── category_l3 (받침앵글, 브라켓트, 와이어, 실리카, 마구리, 케이스, 가이드레일, 하단마감재...) -│ │ │ └── category_l4 (점검구양면, 점검구후면, 점검구밑면, 연기차단재, 상부덮개, 마구리, 벽면형, 측면형, 혼합형, L-bar, 하장바, 보강평철, 무게평철...) -│ -└── 철재 - ├── category_l2 (환봉, 앵글, 각파이프, 감기샤프트, 전동개폐기, 슬랫, 절곡물) - │ ├── category_l3 (브라켓트, 받침앵글, 슬랫, 조인트바, 가이드레일, 연동제어기, 모터, 하단마감재, 케이스) - │ │ └── category_l4 (하부베이스, 매립형, 노출형, 유선, 무선, L-bar, 하장바, 보강평철, 점검구양면, 점검구후면) -``` - -### 2.5 price_* 테이블 구조 (단가 정보) - -```sql --- 공통 구조 (price_motor, price_shaft, price_pipe, price_raw_materials 등) -num INT PRIMARY KEY, -registedate DATE, -- 등록일 -itemList TEXT, -- JSON 배열 (단가 정보) -is_deleted TINYINT DEFAULT 0, -update_log TEXT, -created_at TIMESTAMP -``` - -**price_motor itemList 예시**: -```json -[ - {"col1":"220","col2":"150K(S)","col3":"368","col4":"124","col5":"188","col6":"","col7":"680","col8":"6.79","col9":"100.1","col10":"1300","col11":"130,130","col12":"156,156","col13":"285,000","col14":"128,844","col15":"45.2"}, - {"col1":"380","col2":"300K","col3":"420","col4":"180","col5":"188","col6":"","col7":"788","col8":"6.79","col9":"116.1","col10":"1300","col11":"150,930","col12":"181,116","col13":"300,000","col14":"118,884","col15":"39.6"}, - {"col1":"제어기","col2":"노출형","col3":"","col4":"","col5":"300","col6":"","col7":"300","col8":"6.79","col9":"44.2","col10":"1300","col11":"57,460","col12":"68,952","col13":"130000","col14":"61,048","col15":"47"} -] -``` - -### 2.6 단가 시스템 상세 분석 ⭐ - -#### 2.6.1 레거시 단가 테이블 전체 목록 (10개) - -| 테이블명 | 레코드 수 | 최신 날짜 | 용도 | -|---------|----------|----------|------| -| `price_motor` | 2 | 2024-08-25 | 전동개폐기, 제어기 단가 | -| `price_shaft` | 2 | 2024-08-25 | 감기샤프트 단가 | -| `price_pipe` | 2 | 2024-08-26 | 파이프 단가 | -| `price_angle` | 2 | 2024-08-26 | 앵글 단가 | -| `price_raw_materials` | 6 | 2025-06-18 | 슬랫, 스크린 원자재 단가 | -| `price_bend` | 3 | 2025-03-09 | 절곡 단가 | -| `price_pole` | 2 | 2024-08-26 | 폴 단가 | -| `price_screenplate` | 2 | 2024-08-26 | 스크린플레이트 단가 | -| `price_smokeban` | 2 | 2024-08-26 | 연기차단 단가 | -| `price_etc` | 0 | - | 기타 (개별 컬럼 방식, 비활성) | - -#### 2.6.2 공통 테이블 구조 - -```sql --- 9개 테이블 공통 구조 (price_etc 제외) -num INT PRIMARY KEY, -registedate DATE, -- 적용일 (버전 관리 핵심!) -itemList TEXT, -- JSON 배열 (단가 정보) -is_deleted TINYINT DEFAULT 0, -update_log TEXT, -searchtag VARCHAR(255), -created_at TIMESTAMP, -memo TEXT -``` - -#### 2.6.3 각 테이블의 JSON 스키마 분석 - -**price_motor (모터/제어기)**: -``` -col1: 분류 (220/380/제어기/방화/방범) -col2: 용량/타입 (150K, 300K, 노출형, 매립형...) -col3-col10: 치수, 무게, 계산값 -col11: 원가 (VAT 제외) -col12: 원가 (VAT 포함) -col13: 판매단가 ⭐ -col14: 이익금액 -col15: 이익률 (%) -``` - -**price_shaft (감기샤프트)**: -``` -col1: 품목명 (샤프트(BS)) -col2-col5: 규격 (두께, 외경, 두께, 외경) -col6-col10: 길이, 무게, 계산값 -col11-col16: 가공비, 원가 -col17-col20: 단가 옵션들 (길이별) -``` - -**price_raw_materials (원자재)**: -``` -col1: 분류 (슬랫/스크린) -col2: 종류 (방화/방범/실리카/화이바/조인트바) -col3-col12: 규격, 무게, 계산값 -col13: 기준단가 -col14: 품목코드 -col15: 현재단가 ⭐ -``` - -**price_pipe (파이프)**: -``` -col1: 품목 (각파이프) -col2: 길이 (3,000/6,000) -col3: 규격 (50*30, 100*50) -col4: 두께 -col5: 수량 -col6-col7: 원가 -col8: 단가 ⭐ -``` - -#### 2.6.4 SAM prices 테이블 구조 (Target) - -```sql -CREATE TABLE prices ( - id BIGINT PRIMARY KEY, - tenant_id BIGINT NOT NULL, -- 287 (경동기업) - - -- 품목 연결 - item_type_code VARCHAR(20), -- FG/PT/SM/RM/CS - item_id BIGINT, -- items.id FK - client_group_id BIGINT NULL, -- NULL = 기본가 - - -- 원가 정보 - purchase_price DECIMAL(15,4), -- 매입단가 (원가) - processing_cost DECIMAL(15,4), -- 가공비 - loss_rate DECIMAL(5,2), -- LOSS율 (%) - - -- 판매가 정보 - margin_rate DECIMAL(5,2), -- 마진율 (%) - sales_price DECIMAL(15,4), -- 판매단가 ⭐ - rounding_rule ENUM('round','ceil','floor'), - rounding_unit INT DEFAULT 1, -- 반올림 단위 - - -- 메타 정보 - supplier VARCHAR(255), -- 공급업체 - effective_from DATE, -- 적용 시작일 ⭐ - effective_to DATE NULL, -- 적용 종료일 - note TEXT, - - -- 상태 관리 - status ENUM('draft','active','inactive','finalized'), - is_final BOOLEAN DEFAULT FALSE, - - -- 감사 컬럼 - created_by, updated_by, deleted_by, timestamps, soft_deletes -); -``` - -#### 2.6.5 Legacy → SAM 단가 매핑 전략 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 단가 마이그레이션 플로우 │ -├─────────────────────────────────────────────────────────────────┤ -│ │ -│ Legacy (chandj) SAM │ -│ ────────────── ─── │ -│ │ -│ 1. price_motor.itemList[i] │ -│ ├── col1,col2 (전압,용량) ───→ items (SM) 생성 │ -│ │ └── code: SM-MOTOR-220-150K │ -│ │ │ -│ └── col11,col13 (원가,판매가) ─→ prices 생성 │ -│ ├── item_id: 위에서 생성된 items.id │ -│ ├── purchase_price: col11 │ -│ ├── sales_price: col13 │ -│ └── effective_from: registedate │ -│ │ -│ 2. 날짜별 버전 관리 │ -│ ├── registedate 2024-08-25 → effective_from │ -│ └── 다음 레코드 존재 시 → effective_to 설정 │ -│ │ -│ 3. 최신 레코드만 active, 나머지는 inactive │ -│ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -#### 2.6.6 items와 prices 관계 - -``` -items (품목 마스터) prices (단가 이력) -┌──────────────────────┐ ┌──────────────────────┐ -│ id: 1001 │ │ id: 5001 │ -│ code: SM-MOTOR-220-150K │◄────────────│ item_id: 1001 │ -│ name: 전동개폐기 220V 150K │ │ sales_price: 285000 │ -│ item_type: SM │ │ effective_from: 2024-08-25 │ -│ attributes: {...} │ │ status: active │ -└──────────────────────┘ └──────────────────────┘ - │ - ┌──────────────────────┐ - │ id: 5002 │ - │ item_id: 1001 │ - │ sales_price: 270000 │ - │ effective_from: 2024-01-01 │ - │ effective_to: 2024-08-24 │ - │ status: inactive │ - └──────────────────────┘ -``` - ---- - -## 3. 매핑 설계 - -### 3.1 models → items (FG 완제품) - -| 레거시 (models) | SAM (items) | 비고 | -|----------------|-------------|------| -| model_id | (신규 생성) | | -| model_name | code | KSS01 → FG-KSS01 | -| - | name | 모델명 + 마감타입 + 가이드타입 조합 | -| major_category | attributes.major_category | 스크린/철재 | -| finishing_type | attributes.finishing_type | SUS마감/EGI마감 | -| guiderail_type | attributes.guiderail_type | 벽면형/측면형/혼합형 | -| - | item_type | 'FG' | -| - | tenant_id | 287 | - -**코드 생성 규칙**: -``` -FG-{model_name}-{guiderail_type}-{finishing_type} -예: FG-KSS01-벽면형-SUS -``` - -### 3.2 BDmodels → items (FG 세부 + BOM) - -| 레거시 (BDmodels) | SAM (items) | 비고 | -|------------------|-------------|------| -| seconditem | code (부품) | 가이드레일 → PT-GR-120x70-SUS-벽면형 | -| savejson | bom | JSON 변환 | -| unitprice | attributes.unit_price | | -| spec | attributes.spec | 120*70 | -| priceDate | attributes.price_date | | - -### 3.3 category_l4 → items (PT 부품) - -| 레거시 (category_l4) | SAM (items) | 비고 | -|---------------------|-------------|------| -| name | name | 부품명 | -| - | code | PT-L1-L2-L3-{name} 조합 | -| - | item_type | 'PT' | -| parent_id | attributes.parent_category_id | | - -### 3.4 price_* → prices 테이블 (단가 연동) ⭐ - -> **중요**: 단가 데이터는 items.attributes가 아닌 **prices 테이블**에 별도 관리 - -| 레거시 (price_*) | SAM (prices) | 비고 | -|-----------------|--------------|------| -| registedate | effective_from | 적용 시작일 | -| itemList.col13 (판매가) | sales_price | | -| itemList.col11 (원가) | purchase_price | | -| itemList.col12 (VAT포함) | - | 계산으로 도출 | -| - | item_type_code | FG/PT/SM/RM/CS | -| - | item_id | items.id FK | -| - | client_group_id | NULL (기본가) | -| - | status | 'active' | - ---- - -## 4. 대상 범위 - -### 4.1 Phase 1: 마스터 데이터 이관 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| **1.0** | **KDunitprice → items (마스터)** ⭐ | ⏳ | **603건 (최우선!)** | -| 1.1 | models → items (FG) INSERT 쿼리 작성 | ⏳ | 18건 (중복 확인 후) | -| 1.2 | item_list → items (PT) INSERT 쿼리 작성 | ⏳ | 5건+ (중복 확인 후) | -| 1.3 | category_l4 → items (PT) INSERT 쿼리 작성 | ⏳ | 37건 (중복 확인 후) | -| 1.4 | price_motor 파싱 → prices 연결 | ⏳ | code로 items 조회 후 prices 생성 | -| 1.5 | price_shaft 파싱 → prices 연결 | ⏳ | ~15건 | -| 1.6 | price_raw_materials 파싱 → prices 연결 | ⏳ | ~20건 | -| 1.7 | ⚠️ **사용자 승인**: Phase 1 INSERT 실행 | ⏳ | | - -### 4.2 Phase 2: BOM 및 상세 데이터 이관 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | BDmodels.savejson → item_bom_items | ⏳ | 59건 | -| 2.2 | parts → item_bom_items | ⏳ | 36건 | -| 2.3 | parts_sub → item_bom_items | ⏳ | 134건 | -| 2.4 | guiderail/bottombar/bending 등 → item_details | ⏳ | 제품 상세 | -| 2.5 | parent_item_id, child_item_id 매핑 | ⏳ | code 기반 조회 | - -### 4.3 Phase 3: 검증 및 배포 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | 로컬 테스트 | ⏳ | | -| 3.2 | API 테스트 | ⏳ | | -| 3.3 | 개발서버 배포 | ⏳ | ⚠️ 컨펌 필요 | - -### 4.4 Phase 4: 단가 데이터 이관 ⭐ - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 4.1 | price_motor → items (SM) + prices | ⏳ | ~35건 품목 + 단가 | -| 4.2 | price_shaft → items (SM) + prices | ⏳ | ~15건 | -| 4.3 | price_pipe → items (SM) + prices | ⏳ | ~10건 | -| 4.4 | price_angle → items (SM) + prices | ⏳ | ~10건 | -| 4.5 | price_raw_materials → items (RM) + prices | ⏳ | ~20건 | -| 4.6 | price_bend → items (SM) + prices | ⏳ | ~10건 | -| 4.7 | price_pole → items (SM) + prices | ⏳ | ~5건 | -| 4.8 | price_screenplate → items (SM) + prices | ⏳ | ~5건 | -| 4.9 | price_smokeban → items (SM) + prices | ⏳ | ~5건 | -| 4.10 | 단가 버전 이력 정리 | ⏳ | effective_from/to 설정 | -| 4.11 | ⚠️ **사용자 승인**: 단가 INSERT 실행 | ⏳ | | - -### 4.5 Phase 5: 입고/재고 데이터 이관 ⭐ (신규) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 5.1 | instock → item_receipts | ⏳ | 2,286건 | -| 5.2 | instock 재고 계산 → stocks | ⏳ | 현재고 집계 | -| 5.3 | lot → lots | ⏳ | 로트 관리 | -| 5.4 | lot_sales → lot_sales | ⏳ | 로트 소진 | -| 5.5 | ⚠️ **사용자 승인**: 입고/재고 INSERT 실행 | ⏳ | | - -### 4.6 Phase 6: 주문/출고 데이터 이관 ⭐ (신규) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 6.1 | output → orders 헤더 | ⏳ | 24,564건 | -| 6.2 | output.iList JSON 파일 파싱 | ⏳ | 파일 경로 → JSON 읽기 | -| 6.3 | JSON → order_items 생성 | ⏳ | pages 배열 처리 | -| 6.4 | JSON.approval → orders 승인 정보 | ⏳ | approved_by, approved_at | -| 6.5 | estimate → orders (type=견적) | ⏳ | 견적 데이터 | -| 6.6 | ⚠️ **사용자 승인**: 주문/출고 INSERT 실행 | ⏳ | | - ---- - -## 5. SQL 쿼리 (예상) - -### 5.0 KDunitprice → items (마스터) ⭐ 최우선! - -```sql --- KDunitprice: 품목 마스터 (603건) → SAM items --- ⚠️ 이 쿼리를 가장 먼저 실행하여 items 마스터 생성 - -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, description, is_active, - created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - -- item_div → item_type 매핑 - CASE item_div - WHEN '[제품]' THEN 'FG' - WHEN '[상품]' THEN 'FG' - WHEN '[반제품]' THEN 'PT' - WHEN '[부재료]' THEN 'SM' - WHEN '[원재료]' THEN 'RM' - WHEN '[무형상품]' THEN 'CS' - ELSE 'SM' - END AS item_type, - 품목코드 AS code, -- 유니크 키! - 품목명 AS name, - 단위 AS unit, - JSON_OBJECT( - 'spec', 규격, - 'item_div', item_div, - 'legacy_source', 'KDunitprice' - ) AS attributes, - 비고 AS description, - 1 AS is_active, - 1 AS created_by, - NOW(), NOW() -FROM chandj.KDunitprice -WHERE 품목코드 IS NOT NULL AND 품목코드 != ''; - --- 결과 확인 -SELECT item_type, COUNT(*) -FROM samdb.items -WHERE tenant_id = 287 -GROUP BY item_type; -``` - -### 5.0.1 KDunitprice → prices (기본 단가) - -```sql --- KDunitprice의 입고가/출고가 → prices 테이블 -INSERT INTO samdb.prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, - effective_from, status, - created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - i.item_type AS item_type_code, - i.id AS item_id, - NULL AS client_group_id, -- 기본가 - COALESCE(k.입고가, 0) AS purchase_price, - COALESCE(k.출고가, 0) AS sales_price, - CURDATE() AS effective_from, -- 적용일 - 'active' AS status, - 1 AS created_by, - NOW(), NOW() -FROM chandj.KDunitprice k -JOIN samdb.items i ON i.code = k.품목코드 AND i.tenant_id = 287 -WHERE k.품목코드 IS NOT NULL AND k.품목코드 != ''; -``` - -### 5.1 models → items (FG) - -```sql --- 레거시 chandj.models → SAM items (FG) -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - 'FG' AS item_type, - CONCAT('FG-', model_name, '-', - COALESCE(guiderail_type, 'STD'), '-', - CASE finishing_type - WHEN 'SUS마감' THEN 'SUS' - WHEN 'EGI마감' THEN 'EGI' - ELSE 'STD' - END - ) AS code, - CONCAT(model_name, ' ', major_category, ' ', finishing_type, ' ', COALESCE(guiderail_type, '')) AS name, - 'EA' AS unit, - JSON_OBJECT( - 'major_category', major_category, - 'finishing_type', finishing_type, - 'guiderail_type', guiderail_type, - 'legacy_model_id', model_id - ) AS attributes, - CASE WHEN is_deleted = 0 THEN 1 ELSE 0 END AS is_active, - 1 AS created_by, - created_at, - updated_at -FROM chandj.models -WHERE is_deleted = 0; -``` - -### 5.2 category_l4 → items (PT) - -```sql --- 레거시 4단계 카테고리 → SAM items (PT) -INSERT INTO samdb.items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at -) -SELECT - 287 AS tenant_id, - 'PT' AS item_type, - CONCAT('PT-', l1.name, '-', l2.name, '-', l3.name, '-', l4.name) AS code, - l4.name AS name, - 'EA' AS unit, - JSON_OBJECT( - 'category_l1', l1.name, - 'category_l2', l2.name, - 'category_l3', l3.name, - 'category_l4', l4.name, - 'legacy_l4_id', l4.id - ) AS attributes, - 1 AS is_active, - 1 AS created_by, - NOW(), NOW() -FROM chandj.category_l4 l4 -JOIN chandj.category_l3 l3 ON l4.parent_id = l3.id -JOIN chandj.category_l2 l2 ON l3.parent_id = l2.id -JOIN chandj.category_l1 l1 ON l2.parent_id = l1.id; -``` - -### 5.3 price_motor → items (SM) + prices [PHP 스크립트] - -```php -query(" - SELECT num, registedate, itemList - FROM price_motor - WHERE is_deleted = 0 - ORDER BY registedate DESC -"); -$priceRecords = $stmt->fetchAll(PDO::FETCH_ASSOC); - -// 최신 단가의 itemList 파싱 → items 생성 -$latestRecord = $priceRecords[0]; -$itemList = json_decode($latestRecord['itemList'], true); - -foreach ($itemList as $idx => $item) { - $voltage = $item['col1']; // 220, 380, 제어기, 방화, 방범 - $capacity = $item['col2']; // 150K(S), 300K, 노출형, 매립형... - $purchasePrice = (float)str_replace(',', '', $item['col11'] ?? '0'); - $salesPrice = (float)str_replace(',', '', $item['col13'] ?? '0'); - - // 품목 코드 생성 - $code = "SM-MOTOR-" . preg_replace('/[^A-Za-z0-9가-힣]/', '', $voltage) - . "-" . preg_replace('/[^A-Za-z0-9가-힣()]/', '', $capacity); - - // 품목명 생성 - if (in_array($voltage, ['220', '380'])) { - $name = "전동개폐기 {$voltage}V {$capacity}"; - $itemType = 'SM'; - } elseif ($voltage === '제어기') { - $name = "연동제어기 {$capacity}"; - $itemType = 'SM'; - } else { - $name = "{$voltage} {$capacity}"; - $itemType = 'SM'; - } - - // 1단계: items INSERT - $itemStmt = $pdo->prepare(" - INSERT INTO items ( - tenant_id, item_type, code, name, unit, - attributes, is_active, created_by, created_at, updated_at - ) VALUES (?, ?, ?, ?, 'EA', ?, 1, ?, NOW(), NOW()) - ON DUPLICATE KEY UPDATE name = VALUES(name) - "); - $attributes = json_encode([ - 'voltage' => $voltage, - 'capacity' => $capacity, - 'legacy_source' => 'price_motor', - 'legacy_col_index' => $idx - ]); - $itemStmt->execute([$tenantId, $itemType, $code, $name, $attributes, $userId]); - $itemId = $pdo->lastInsertId(); - - // 2단계: prices INSERT (모든 버전) - foreach ($priceRecords as $priceIdx => $priceRecord) { - $priceItemList = json_decode($priceRecord['itemList'], true); - if (!isset($priceItemList[$idx])) continue; - - $priceItem = $priceItemList[$idx]; - $pPrice = (float)str_replace(',', '', $priceItem['col11'] ?? '0'); - $sPrice = (float)str_replace(',', '', $priceItem['col13'] ?? '0'); - $effectiveFrom = $priceRecord['registedate']; - - // 다음 레코드가 있으면 effective_to 설정 - $effectiveTo = isset($priceRecords[$priceIdx + 1]) - ? date('Y-m-d', strtotime($effectiveFrom . ' -1 day')) - : null; - - $status = ($priceIdx === 0) ? 'active' : 'inactive'; - - $priceStmt = $pdo->prepare(" - INSERT INTO prices ( - tenant_id, item_type_code, item_id, client_group_id, - purchase_price, sales_price, effective_from, effective_to, - status, created_by, created_at, updated_at - ) VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, NOW(), NOW()) - "); - $priceStmt->execute([ - $tenantId, $itemType, $itemId, - $pPrice, $sPrice, $effectiveFrom, $effectiveTo, - $status, $userId - ]); - } - - echo "✓ {$code} - items + prices 생성 완료\n"; -} -``` - -### 5.4 단가 마이그레이션 요약 스크립트 - -```php - ['item_type' => 'SM', 'prefix' => 'MOTOR'], - 'price_shaft' => ['item_type' => 'SM', 'prefix' => 'SHAFT'], - 'price_pipe' => ['item_type' => 'SM', 'prefix' => 'PIPE'], - 'price_angle' => ['item_type' => 'SM', 'prefix' => 'ANGLE'], - 'price_raw_materials' => ['item_type' => 'RM', 'prefix' => 'RAW'], - 'price_bend' => ['item_type' => 'SM', 'prefix' => 'BEND'], - 'price_pole' => ['item_type' => 'SM', 'prefix' => 'POLE'], - 'price_screenplate' => ['item_type' => 'SM', 'prefix' => 'SCREEN'], - 'price_smokeban' => ['item_type' => 'SM', 'prefix' => 'SMOKE'], -]; - -$totalItems = 0; -$totalPrices = 0; - -foreach ($priceTables as $table => $config) { - echo "\n📦 Processing: {$table}\n"; - - // 각 테이블별 JSON 스키마에 맞는 파싱 로직 호출 - list($itemCount, $priceCount) = migratePrice($table, $config); - - $totalItems += $itemCount; - $totalPrices += $priceCount; - - echo " → items: {$itemCount}, prices: {$priceCount}\n"; -} - -echo "\n✅ 마이그레이션 완료!\n"; -echo " 총 items: {$totalItems}\n"; -echo " 총 prices: {$totalPrices}\n"; -``` - ---- - -## 6. 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 📦 데이터 전략 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - KDunitprice(603건)가 품목 마스터 → items 최우선 생성 │ -│ - code 필드로 중복 방지 (ON DUPLICATE KEY UPDATE) │ -│ - BOM은 item_bom_items 테이블 사용 (items.bom JSON ❌) │ -│ - 단가 정보는 prices 테이블에 별도 저장 (items.attributes ❌) │ -│ │ -│ ❌ 불필요한 것 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - item_id_mappings 테이블 (양방향 조회 불필요) │ -│ - chandj 수정 (손 안댐, samdb에만 밀어 넣음) │ -│ - 레거시 소스 확인 (마이그레이션 후 검증만) │ -│ │ -│ ✅ 필수 사항 │ -│ ───────────────────────────────────────────────────────────────────── │ -│ - 경동기업 기준으로 맞춤 (이미 사용중인 시스템) │ -│ - 전체 이관 (instock 2,286건, output 24,564건 포함) │ -│ - SQL 쿼리 + PHP 스크립트 혼용 (JSON 파싱 필요) │ -│ - 로컬 검증 완료 후 개발서버 배포 │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 6.1 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | SELECT 쿼리, 분석, 매핑 설계 | 불필요 | -| ⚠️ 컨펌 필요 | INSERT 실행, TRUNCATE, 개발서버 배포 | **필수** | -| 🔴 금지 | 운영서버 직접 작업 | 별도 협의 | - ---- - -## 7. 데이터 규모 예상 (전체 마이그레이션) - -### 7.1 items 테이블 예상 - -| 소스 | 레코드 수 | SAM item_type | 예상 items 건수 | -|------|----------|---------------|----------------| -| **KDunitprice** ⭐ | **603** | FG/PT/SM/RM/CS | **~603 (마스터)** | -| models | 18 | FG | ~0 (중복 제외) | -| category_l4 | 37 | PT | ~20 (일부 신규) | -| item_list | 5 | PT | ~0 (중복 제외) | -| price_* 테이블 | ~130 항목 | SM/RM | ~100 (신규만) | -| **items 합계** | - | - | **~700~800건** | - -**item_type별 분포 예상**: -| item_type | 설명 | 예상 건수 | -|-----------|------|----------| -| FG | 완제품 | ~100건 | -| PT | 부품 | ~250건 | -| SM | 부자재 | ~300건 | -| RM | 원자재 | ~100건 | -| CS | 소모품 | ~50건 | - -### 7.2 prices 테이블 예상 ⭐ - -| 소스 | 버전 수 | 품목당 단가 | 예상 prices 건수 | -|------|--------|------------|-----------------| -| KDunitprice | 1 | 603 | ~603 | -| price_motor | 2 | 35 | ~70 | -| price_shaft | 2 | 15 | ~30 | -| price_pipe | 2 | 10 | ~20 | -| price_angle | 2 | 10 | ~20 | -| price_raw_materials | 6 | 20 | ~120 | -| price_bend | 3 | 10 | ~30 | -| 기타 price_* | 2 | 15 | ~30 | -| **prices 합계** | - | - | **~500건** (중복 제외) - -### 7.3 입고/재고 테이블 예상 ⭐ (신규) - -| 소스 | 레코드 수 | SAM 테이블 | 예상 건수 | -|------|----------|------------|----------| -| instock | 2,286 | item_receipts | ~2,286 | -| instock (집계) | - | stocks | ~500 (품목별 현재고) | -| lot | - | lots | ~200 | -| lot_sales | - | lot_sales | ~300 | -| **합계** | - | - | **~3,300건** | - -### 7.4 주문/출고 테이블 예상 ⭐ (신규) - -| 소스 | 레코드 수 | SAM 테이블 | 예상 건수 | -|------|----------|------------|----------| -| output | 24,564 | orders | ~24,564 | -| output.iList (JSON) | ~24,564 파일 | order_items | ~50,000 (주문당 2건 평균) | -| estimate | - | orders (type=견적) | ~500 | -| **합계** | - | - | **~75,000건** | - -### 7.5 전체 마이그레이션 요약 - -| SAM 테이블 | 예상 건수 | 비고 | -|------------|----------|------| -| items | ~800 | 품목 마스터 | -| item_bom_items | ~300 | BOM 관계 | -| item_details | ~200 | 제품 상세 | -| prices | ~500 | 단가 정보 | -| item_receipts | ~2,300 | 입고 기록 | -| stocks | ~500 | 현재고 | -| lots | ~200 | 로트 | -| lot_sales | ~300 | 로트 소진 | -| orders | ~25,000 | 주문 헤더 | -| order_items | ~50,000 | 주문 상세 | -| **총계** | **~80,000건** | | - ---- - -## 8. 체크리스트 - -### Phase 1: 마스터 데이터 이관 -- [x] 레거시 DB 구조 분석 완료 -- [x] KDunitprice 테이블 발견 및 분석 (603건, 핵심 마스터) -- [x] 중복 제거 전략 수립 (code 기반, 매핑 테이블 불필요) -- [ ] **KDunitprice → items 마이그레이션 스크립트 작성** ⭐ -- [ ] models → items (FG) INSERT 쿼리 작성 (중복 확인) -- [ ] category_l4 → items (PT) INSERT 쿼리 작성 (중복 확인) -- [ ] ⚠️ **사용자 승인**: 로컬 INSERT 실행 - -### Phase 2: BOM 데이터 이관 -- [ ] BDmodels.savejson 파싱 로직 작성 -- [ ] child_item_id 매핑 테이블 생성 -- [ ] items.bom JSON 생성 -- [ ] ⚠️ **사용자 승인**: BOM 데이터 INSERT 실행 - -### Phase 3: 검증 및 배포 -- [ ] 건수 검증 -- [ ] API 테스트 -- [ ] ⚠️ **사용자 승인**: 개발서버 배포 - -### Phase 4: 단가 데이터 이관 ⭐ -- [x] 레거시 price_* 테이블 구조 분석 (10개) -- [x] 각 테이블별 JSON 스키마 분석 -- [x] SAM prices 테이블 구조 확인 -- [x] Legacy → SAM 단가 매핑 전략 수립 -- [ ] price_motor → prices 연결 스크립트 작성 -- [ ] price_shaft → prices 연결 스크립트 작성 -- [ ] price_pipe → prices 연결 스크립트 작성 -- [ ] price_angle → prices 연결 스크립트 작성 -- [ ] price_raw_materials → prices 연결 스크립트 작성 -- [ ] 기타 price_* 테이블 처리 -- [ ] 단가 버전 이력 정리 (effective_from/to) -- [ ] ⚠️ **사용자 승인**: 단가 INSERT 실행 - -### Phase 5: 입고/재고 데이터 이관 ⭐ (신규) -- [ ] instock 테이블 구조 분석 -- [ ] instock → item_receipts 매핑 설계 -- [ ] 재고 집계 → stocks 매핑 설계 -- [ ] lot/lot_sales 구조 분석 -- [ ] 마이그레이션 스크립트 작성 -- [ ] ⚠️ **사용자 승인**: 입고/재고 INSERT 실행 - -### Phase 6: 주문/출고 데이터 이관 ⭐ (신규) -- [ ] output 테이블 구조 분석 -- [ ] output.iList JSON 파일 구조 분석 (완료) -- [ ] output → orders 매핑 설계 -- [ ] JSON → order_items 매핑 설계 -- [ ] estimate → orders 매핑 설계 -- [ ] 마이그레이션 스크립트 작성 (24,564건) -- [ ] ⚠️ **사용자 승인**: 주문/출고 INSERT 실행 - ---- - -## 9. 참고 문서 - -- **레거시 소스**: `5130/` 폴더 -- **SAM items 마이그레이션**: `api/database/migrations/2025_12_13_152507_create_items_table.php` -- **SAM prices 마이그레이션**: `api/database/migrations/2025_12_08_154633_create_prices_table.php` -- **SAM price_revisions 마이그레이션**: `api/database/migrations/2025_12_08_154634_create_price_revisions_table.php` -- **품목 분석**: `docs/data/analysis/item-db-analysis.md` -- **DummyItemSeeder**: `api/database/seeders/Dummy/DummyItemSeeder.php` -- **DummyDataSeeder**: `api/database/seeders/DummyDataSeeder.php` (TENANT_ID=287, USER_ID=1 상수 참조) -- **prices item_type_code 마이그레이션**: `api/database/migrations/2025_12_21_165524_update_prices_item_type_code_to_actual_item_type.php` - ---- - -## 10. 세션 및 메모리 관리 정책 - -### 10.1 세션 시작 시 (Load Strategy) -```bash -# 1. Docker 확인 -docker ps | grep sam - -# 2. DB 접속 테스트 -docker exec sam-mysql-1 mysql -uroot -proot chandj -e "SELECT COUNT(*) FROM KDunitprice;" -docker exec sam-mysql-1 mysql -uroot -proot samdb -e "SELECT COUNT(*) FROM items WHERE tenant_id=287;" - -# 3. 현재 진행 상태 확인 -# → 이 문서의 "📍 현재 진행 상태" 섹션 참조 - -# 4. 마이그레이션 상태 확인 (API 프로젝트) -cd /Users/kent/Works/@KD_SAM/SAM/api && php artisan migrate:status -``` - -### 10.2 작업 중 관리 - -| 작업 완료 시 | 조치 | -|-------------|------| -| Phase 완료 | "📍 현재 진행 상태" 업데이트 | -| INSERT 실행 | "10. 변경 이력" 추가 | -| 스키마 변경 | 관련 섹션 업데이트 + 변경 이력 추가 | -| 오류 발생 | 체크리스트에 메모 추가 | - -### 10.3 컨텍스트 관리 - -| 컨텍스트 잔량 | 조치 | -|--------------|------| -| **30% 이하** | 현재 작업 중단점 문서에 기록 | -| **20% 이하** | "📍 현재 진행 상태" 최종 업데이트 | -| **10% 이하** | 세션 정리 및 다음 세션 가이드 작성 | - ---- - -## 11. 자기완결성 점검 결과 - -### 11.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.1 배경 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 0 성공 기준 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 4 대상 범위 | -| 4 | 의존성이 명시되어 있는가? | ✅ | Quick Start 전제 조건 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 9 참고 문서 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 5 SQL 쿼리 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 0 확인 방법 SQL | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 건수, 테이블명, 컬럼 명시 | - -### 11.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경, 문서 헤더 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 📍 현재 진행 상태 → 다음 작업 상세 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 9. 참고 문서 | -| Q4. 작업 완료 확인 방법은? | ✅ | 0. 성공 기준 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 9. 참고 문서, Quick Start DB 접속 명령어 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - -### 11.3 핵심 정보 요약 (새 세션용) - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ 📋 핵심 정보 요약 │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ 🎯 목표: 경동기업 레거시(chandj) → SAM(samdb) 전체 데이터 이관 │ -│ │ -│ 📊 데이터 규모 (총 ~80,000건): │ -│ - items: ~800건 (KDunitprice 603 + 추가) │ -│ - prices: ~500건 │ -│ - item_receipts: ~2,300건 (입고) │ -│ - orders + order_items: ~75,000건 (주문) │ -│ │ -│ 🔑 핵심 상수: │ -│ - tenant_id = 287 (경동기업) │ -│ - user_id = 1 (생성자) │ -│ - Docker: sam-mysql-1 │ -│ - 레거시 DB: chandj / SAM DB: samdb ⚠️ │ -│ │ -│ ⭐ 마이그레이션 순서: │ -│ 1. KDunitprice → items (마스터, 603건) ← 최우선! │ -│ 2. code 기반 중복 확인 후 추가 items 생성 │ -│ 3. prices 연결 (item_id 참조) │ -│ 4. BOM, 입고, 주문 순서대로 진행 │ -│ │ -│ 📍 현재 상태: Phase 1 대기 (KDunitprice → items 마스터 INSERT) │ -│ │ -│ ❌ 불필요한 것: item_id_mappings (양방향 조회 불필요, chandj 손 안댐) │ -│ │ -│ ⚠️ 주의: 모든 INSERT 실행 전 사용자 승인 필요 │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 12. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-28 | 문서 재작성 | 레거시 5130/ 분석 기반으로 완전 재작성 | - | - | -| 2026-01-28 | 단가 시스템 추가 | price_* 테이블 분석, SAM prices 매핑 전략 | - | - | -| 2026-01-28 | 자기완결성 보완 | Quick Start, 성공 기준, 세션 관리, 자기완결성 점검 섹션 추가 | - | - | -| 2026-01-28 | **전체 범위 확장** | KDunitprice(603건) 발견, Phase 5/6 추가, ~80,000건 전체 이관 | - | - | -| 2026-01-28 | 중복 제거 전략 | code 기반 단순화, item_id_mappings 제거 | - | - | -| 2026-01-28 | DB 이름 수정 | sam → samdb 수정 | - | - | -| 2026-01-28 | output.iList | JSON 파일 구조 분석 및 문서화 | - | - | - ---- - -## 13. 트러블슈팅 가이드 - -### 13.1 일반적인 문제 - -| 문제 | 원인 | 해결책 | -|------|------|--------| -| Docker 컨테이너 없음 | Docker 미실행 | `docker-compose up -d` 실행 | -| DB 접속 실패 | 컨테이너명 변경 | `docker ps`로 정확한 컨테이너명 확인 | -| chandj DB 없음 | 레거시 DB 미설정 | Docker 볼륨 확인 또는 덤프 복원 | -| tenant_id 287 없음 | 경동기업 테넌트 미생성 | SAM에서 테넌트 생성 필요 | -| items 테이블 없음 | 마이그레이션 미실행 | `php artisan migrate` 실행 | -| **SAM DB 이름 오류** | `sam` 대신 `samdb` | 모든 쿼리에서 `samdb` 사용 확인 | -| KDunitprice 테이블 없음 | 레거시 덤프 불완전 | chandj 전체 덤프 확인 | -| output.iList 파일 없음 | JSON 파일 경로 오류 | `5130/output/i_json/` 폴더 확인 | - -### 13.2 JSON 파싱 오류 - -```php -// price_* 테이블의 itemList 파싱 시 주의사항 -$itemList = json_decode($record['itemList'], true); - -// 빈 값 또는 잘못된 JSON 처리 -if (empty($itemList) || !is_array($itemList)) { - // 스킵하고 로그 기록 - error_log("Invalid itemList in {$table} num={$record['num']}"); - continue; -} - -// 숫자 형식 변환 (콤마 제거) -$price = (float)str_replace(',', '', $item['col13'] ?? '0'); -``` - -### 13.3 중복 코드 처리 (code 기반) - -```sql --- 이미 존재하는 품목 확인 (code 유일성 검사) -SELECT code, COUNT(*) AS cnt -FROM samdb.items -WHERE tenant_id=287 -GROUP BY code -HAVING cnt > 1; - --- INSERT 시 ON DUPLICATE KEY UPDATE 사용 --- ⚠️ items 테이블에 (tenant_id, code) UNIQUE 인덱스 필요 -INSERT INTO samdb.items (...) VALUES (...) -ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW(); - --- KDunitprice와 price_* 중복 확인 -SELECT k.품목코드, '모터 150K' AS price_item -FROM chandj.KDunitprice k -WHERE k.품목명 LIKE '%모터%150K%'; --- → KDunitprice가 마스터, price_*는 가격만 추가 -``` - -### 13.4 output.iList JSON 파일 처리 - -```php -// output.iList 값 예시: "../output/i_json/22545.json" -$iListPath = $output['iList']; // "../output/i_json/22545.json" - -// 실제 파일 경로로 변환 -$basePath = '/Users/kent/Works/@KD_SAM/SAM/5130'; -$jsonFile = str_replace('../', '', $iListPath); -$fullPath = $basePath . '/' . $jsonFile; - -// JSON 파일 읽기 -if (file_exists($fullPath)) { - $jsonContent = json_decode(file_get_contents($fullPath), true); - // $jsonContent['inputValue'], $jsonContent['pages'] 등 사용 -} else { - // 파일 없음 - 로그 기록 후 스킵 - error_log("JSON file not found: {$fullPath}"); -} -``` - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다.* \ No newline at end of file diff --git a/plans/quote-management-url-migration-plan.md b/plans/quote-management-url-migration-plan.md deleted file mode 100644 index 9f2ce61..0000000 --- a/plans/quote-management-url-migration-plan.md +++ /dev/null @@ -1,1282 +0,0 @@ -# 견적관리 URL 구조 마이그레이션 계획 - -> **작성일**: 2026-01-26 -> **목적**: 견적관리 페이지 URL 구조를 Query 기반(?mode=new)에서 RESTful 경로 기반(/test-new, /test/[id])으로 마이그레이션 -> **기준 문서**: docs/standards/api-rules.md, docs/specs/database-schema.md -> **상태**: 📋 계획 수립 완료 (Serena ID: quote-url-migration-state) - ---- - -## 📍 현재 진행 상태 - -| 항목 | 내용 | -|------|------| -| **마지막 완료 작업** | Phase 3 코드 작업 완료 - 목록 페이지 링크 V2 적용 | -| **다음 작업** | Step 3.2: 통합 테스트 (사용자 수동 테스트) | -| **진행률** | 11/12 (92%) - Phase 1 ✅, Phase 2 ✅, Phase 3 (테스트 제외) ✅ | -| **마지막 업데이트** | 2026-01-26 | - ---- - -## 1. 개요 - -### 1.1 배경 - -현재 견적관리 시스템에는 두 가지 URL 패턴이 공존합니다: - -**V1 (기존 - Query 기반):** -- 목록: `/sales/quote-management` -- 등록: `/sales/quote-management?mode=new` -- 상세: `/sales/quote-management/[id]` -- 수정: `/sales/quote-management/[id]?mode=edit` - -**V2 (신규 - RESTful 경로 기반):** -- 목록: `/sales/quote-management` (동일) -- 등록: `/sales/quote-management/test-new` -- 상세: `/sales/quote-management/test/[id]` -- 수정: `/sales/quote-management/test/[id]?mode=edit` - -V2는 `IntegratedDetailTemplate` + `QuoteRegistrationV2` 컴포넌트를 사용하며, 현재 테스트(Mock 데이터) 상태입니다. - -### 1.2 목표 - -1. V2 페이지에 실제 API 연동 완료 -2. V2 URL 패턴을 정식 경로로 채택 (test 접두사 제거) -3. V1 페이지 삭제 또는 V2로 리다이렉트 처리 -4. DB 스키마 변경 없이 기존 API 활용 - -### 1.3 기준 원칙 - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 🎯 핵심 원칙 │ -├─────────────────────────────────────────────────────────────────┤ -│ - DB 스키마 변경 없음 (기존 quotes, quote_items 테이블 활용) │ -│ - 기존 API 엔드포인트 재사용 (POST/PUT /api/v1/quotes) │ -│ - V1 → V2 단계적 마이그레이션 (병행 기간 최소화) │ -│ - IntegratedDetailTemplate 표준 적용 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 1.4 변경 승인 정책 - -| 분류 | 예시 | 승인 | -|------|------|------| -| ✅ 즉시 가능 | 컴포넌트 수정, API 연동 코드, 타입 정의 | 불필요 | -| ⚠️ 컨펌 필요 | 라우트 경로 변경, 기존 페이지 삭제/리다이렉트 | **필수** | -| 🔴 금지 | DB 스키마 변경, 기존 API 엔드포인트 삭제 | 별도 협의 | - -### 1.5 준수 규칙 -- `docs/quickstart/quick-start.md` - 빠른 시작 가이드 -- `docs/standards/quality-checklist.md` - 품질 체크리스트 -- `docs/standards/api-rules.md` - API 개발 규칙 - ---- - -## 2. 현재 상태 분석 - -### 2.1 파일 구조 비교 - -#### V1 (기존) -``` -react/src/app/[locale]/(protected)/sales/quote-management/ -├── page.tsx # 목록 + mode=new 감지 → QuoteRegistration -├── new/page.tsx # 리다이렉트용 (거의 미사용) -├── [id]/page.tsx # 상세 + mode=edit 감지 → QuoteRegistration -└── [id]/edit/page.tsx # 리다이렉트용 (거의 미사용) -``` - -#### V2 (신규) -``` -react/src/app/[locale]/(protected)/sales/quote-management/ -├── test-new/page.tsx # 등록 (IntegratedDetailTemplate) -├── test/[id]/page.tsx # 상세/수정 (IntegratedDetailTemplate) -└── test/[id]/edit/page.tsx # 리다이렉트 → test/[id]?mode=edit -``` - -### 2.2 컴포넌트 비교 - -| 항목 | V1 (QuoteRegistration) | V2 (QuoteRegistrationV2) | -|------|------------------------|--------------------------| -| 파일 크기 | ~50KB | ~45KB | -| 레이아웃 | 단일 폼 | 좌우 분할 (개소 목록 \| 상세) | -| 템플릿 | 자체 레이아웃 | IntegratedDetailTemplate | -| 데이터 구조 | `QuoteFormData` | `QuoteFormDataV2` + `LocationItem` | -| API 연동 | ✅ 완료 | ❌ Mock 데이터 | -| 상태 관리 | `status: string` | `status: 'draft' \| 'temporary' \| 'final'` | - -### 2.3 데이터 구조 비교 - -#### V1: QuoteFormData -```typescript -interface QuoteFormData { - id?: string; - quoteNumber?: string; - registrationDate?: string; - clientId?: string | number; - clientName?: string; - siteName?: string; - manager?: string; - contact?: string; - dueDate?: string; - remarks?: string; - status?: string; - items?: QuoteItem[]; // 층별 항목 - bomMaterials?: BomMaterial[]; - calculationInputs?: Record; -} -``` - -#### V2: QuoteFormDataV2 -```typescript -interface QuoteFormDataV2 { - id?: string; - registrationDate: string; - writer: string; - clientId: string; - clientName: string; - siteName: string; - manager: string; - contact: string; - dueDate: string; - remarks: string; - status: 'draft' | 'temporary' | 'final'; - locations: LocationItem[]; // 개소별 항목 (더 상세한 구조) -} - -interface LocationItem { - id: string; - floor: string; - code: string; - openWidth: number; - openHeight: number; - productCode: string; - productName: string; - quantity: number; - guideRailType: string; - motorPower: string; - controller: string; - wingSize: number; - inspectionFee: number; - unitPrice?: number; - totalPrice?: number; - bomResult?: BomCalculationResult; -} -``` - -### 2.4 API 엔드포인트 (변경 없음) - -| HTTP | Endpoint | 설명 | V1 사용 | V2 사용 | -|------|----------|------|:-------:|:-------:| -| GET | `/api/v1/quotes` | 목록 조회 | ✅ | ✅ | -| GET | `/api/v1/quotes/{id}` | 단건 조회 | ✅ | 🔲 (TODO) | -| POST | `/api/v1/quotes` | 생성 | ✅ | 🔲 (TODO) | -| PUT | `/api/v1/quotes/{id}` | 수정 | ✅ | 🔲 (TODO) | -| POST | `/api/v1/quotes/calculate/bom/bulk` | BOM 자동산출 | ✅ | ✅ | - -### 2.5 DB 스키마 (변경 없음) - -**quotes 테이블** - 그대로 사용 -```sql --- 핵심 필드 -id, tenant_id, quote_number -registration_date, author -client_id, client_name, manager, contact -site_name, site_code -product_category, product_id, product_code, product_name -open_size_width, open_size_height, quantity -material_cost, labor_cost, install_cost -subtotal, discount_rate, discount_amount, total_amount -status, is_final -calculation_inputs (JSON) -options (JSON) -``` - -**quote_items 테이블** - 그대로 사용 -```sql -id, quote_id, tenant_id -item_id, item_code, item_name, specification, unit -base_quantity, calculated_quantity -unit_price, total_price -formula, formula_result, formula_source, formula_category -sort_order -``` - ---- - -## 3. 대상 범위 - -### 3.1 Phase 1: V2 API 연동 (프론트엔드) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1.1 | V2 데이터 변환 함수 구현 | ✅ | `transformV2ToApi`, `transformApiToV2` (2026-01-26) | -| 1.2 | test-new 페이지 API 연동 (createQuote) | ✅ | Mock → 실제 API (2026-01-26) | -| 1.3 | test/[id] 페이지 API 연동 (getQuoteById) | ✅ | Mock → 실제 API (2026-01-26) | -| 1.4 | test/[id] 수정 API 연동 (updateQuote) | ✅ | Mock → 실제 API (2026-01-26) | - -### 3.2 Phase 2: URL 경로 정식화 (라우팅) - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 2.1 | test-new → new 경로 변경 | ✅ | V2 버전으로 교체 완료 (2026-01-26) | -| 2.2 | test/[id] → [id] 경로 통합 | ✅ | V2 버전으로 교체 완료 (2026-01-26) | -| 2.3 | 기존 V1 페이지 처리 결정 | ✅ | V1 백업 보존, test 폴더 삭제 | - -### 3.3 Phase 3: 정리 및 테스트 - -| # | 작업 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 3.1 | V1 컴포넌트/페이지 정리 | ✅ | test 폴더 삭제 완료, V1 백업 보존 | -| 3.2 | 통합 테스트 | ⏳ | CRUD + 문서출력 + 상태전환 (사용자 테스트) | -| 3.3 | 목록 페이지 링크 업데이트 | ✅ | QuoteManagementClient, DevToolbar 완료 | -| 3.4 | 문서 업데이트 | ✅ | 계획 문서 완료 | - ---- - -## 4. 작업 절차 - -### 4.1 단계별 절차 - -``` -Phase 1: V2 API 연동 -├── Step 1.1: 데이터 변환 함수 -│ ├── transformV2ToApi() - V2 → API 요청 형식 -│ ├── transformApiToV2() - API 응답 → V2 형식 -│ └── actions.ts에 추가 -│ -├── Step 1.2: test-new 페이지 연동 -│ ├── handleSave에서 createQuote 호출 -│ ├── 성공 시 /sales/quote-management/test/{id}로 이동 -│ └── 에러 처리 -│ -├── Step 1.3: test/[id] 상세 페이지 연동 -│ ├── useEffect에서 getQuoteById 호출 -│ ├── transformApiToV2로 데이터 변환 -│ └── 로딩/에러 상태 처리 -│ -└── Step 1.4: test/[id] 수정 연동 - ├── handleSave에서 updateQuote 호출 - ├── 성공 시 view 모드로 복귀 - └── 에러 처리 - -Phase 2: URL 경로 정식화 -├── Step 2.1: 새 경로 생성 -│ ├── new/page.tsx → IntegratedDetailTemplate 버전 -│ └── 기존 new/page.tsx 백업 -│ -├── Step 2.2: 상세 경로 통합 -│ ├── [id]/page.tsx를 V2 버전으로 교체 -│ └── 기존 [id]/page.tsx 백업 -│ -└── Step 2.3: V1 처리 - ├── 옵션 A: V1 페이지 삭제 - └── 옵션 B: V1 → V2 리다이렉트 - -Phase 3: 정리 및 테스트 -├── Step 3.1: 파일 정리 -│ ├── test-new, test/[id] 폴더 삭제 -│ ├── V1 백업 파일 삭제 (확인 후) -│ └── 미사용 컴포넌트 정리 -│ -├── Step 3.2: 통합 테스트 -│ ├── 신규 등록 → 저장 → 상세 확인 -│ ├── 상세 → 수정 → 저장 → 상세 확인 -│ ├── 문서 출력 (견적서, 산출내역서, 발주서) -│ ├── 최종확정 → 수주전환 -│ └── 목록 링크 동작 확인 -│ -├── Step 3.3: 목록 페이지 링크 -│ └── QuoteManagementClient의 라우팅 경로 확인 -│ -└── Step 3.4: 문서 업데이트 - ├── 이 계획 문서 완료 처리 - └── 필요시 claudedocs에 작업 기록 -``` - -### 4.2 데이터 변환 상세 - -#### V2 → API (저장 시) -```typescript -function transformV2ToApi(data: QuoteFormDataV2) { - return { - registration_date: data.registrationDate, - author: data.writer, - client_id: data.clientId || null, - client_name: data.clientName, - site_name: data.siteName, - manager: data.manager, - contact: data.contact, - completion_date: data.dueDate, - remarks: data.remarks, - status: data.status === 'final' ? 'finalized' : data.status, - - // locations → items 변환 - items: data.locations.map((loc, index) => ({ - floor: loc.floor, - code: loc.code, - product_code: loc.productCode, - product_name: loc.productName, - open_width: loc.openWidth, - open_height: loc.openHeight, - quantity: loc.quantity, - guide_rail_type: loc.guideRailType, - motor_power: loc.motorPower, - controller: loc.controller, - wing_size: loc.wingSize, - inspection_fee: loc.inspectionFee, - unit_price: loc.unitPrice, - total_price: loc.totalPrice, - sort_order: index, - })), - - // calculation_inputs 생성 (첫 번째 location 기준) - calculation_inputs: data.locations.length > 0 ? { - W0: data.locations[0].openWidth, - H0: data.locations[0].openHeight, - QTY: data.locations[0].quantity, - GT: data.locations[0].guideRailType, - MP: data.locations[0].motorPower, - } : null, - }; -} -``` - -#### API → V2 (조회 시) -```typescript -function transformApiToV2(apiData: QuoteResponse): QuoteFormDataV2 { - return { - id: apiData.id, - registrationDate: apiData.registrationDate, - writer: apiData.author || '', - clientId: String(apiData.clientId || ''), - clientName: apiData.clientName || '', - siteName: apiData.siteName || '', - manager: apiData.manager || '', - contact: apiData.contact || '', - dueDate: apiData.completionDate || '', - remarks: apiData.remarks || '', - status: mapApiStatusToV2(apiData.status), - - // items → locations 변환 - locations: (apiData.items || []).map(item => ({ - id: String(item.id), - floor: item.floor || '', - code: item.code || '', - openWidth: item.openWidth || 0, - openHeight: item.openHeight || 0, - productCode: item.productCode || '', - productName: item.productName || '', - quantity: item.quantity || 1, - guideRailType: item.guideRailType || 'wall', - motorPower: item.motorPower || 'single', - controller: item.controller || 'basic', - wingSize: item.wingSize || 50, - inspectionFee: item.inspectionFee || 0, - unitPrice: item.unitPrice, - totalPrice: item.totalPrice, - })), - }; -} - -function mapApiStatusToV2(apiStatus: string): 'draft' | 'temporary' | 'final' { - switch (apiStatus) { - case 'finalized': - case 'converted': - return 'final'; - case 'draft': - case 'sent': - case 'approved': - return 'draft'; - default: - return 'draft'; - } -} -``` - ---- - -## 5. 컨펌 대기 목록 - -> API 내부 로직 변경 등 승인 필요 항목 - -| # | 항목 | 변경 내용 | 영향 범위 | 상태 | -|---|------|----------|----------|------| -| C-1 | URL 경로 정식화 | test-new → new, test/[id] → [id] | 라우팅 전체 | ⏳ 대기 | -| C-2 | V1 페이지 처리 | 삭제 vs 리다이렉트 결정 | 기존 사용자 | ⏳ 대기 | -| C-3 | 컴포넌트 정리 | QuoteRegistration.tsx 삭제 여부 | 코드베이스 | ⏳ 대기 | - ---- - -## 6. 변경 이력 - -| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | -|------|------|----------|------|------| -| 2026-01-26 | Step 3.3, 3.4 | 목록 페이지 V2 URL 적용, 문서 업데이트 | page.tsx, QuoteManagementClient.tsx, DevToolbar.tsx | ✅ | -| 2026-01-26 | Step 3.1 | test 폴더 삭제, V1 백업 보존 | test-new/, test/ 삭제 | ✅ | -| 2026-01-26 | Step 2.1, 2.2 | URL 경로 정식화 (Phase 2 완료) | new/page.tsx, [id]/page.tsx | ✅ | -| 2026-01-26 | Step 1.3, 1.4 | test/[id] 상세/수정 API 연동 (Phase 1 완료) | test/[id]/page.tsx | ✅ | -| 2026-01-26 | Step 1.2 | test-new 페이지 createQuote API 연동 | test-new/page.tsx | ✅ | -| 2026-01-26 | Step 1.1 | V2 데이터 변환 함수 구현 완료 | types.ts | ✅ | -| 2026-01-26 | - | 계획 문서 초안 작성 | - | - | - ---- - -## 7. 참고 문서 - -- **빠른 시작**: `docs/quickstart/quick-start.md` -- **품질 체크리스트**: `docs/standards/quality-checklist.md` -- **API 규칙**: `docs/standards/api-rules.md` -- **DB 스키마**: `docs/specs/database-schema.md` - -### 7.1 핵심 파일 경로 - -#### 프론트엔드 (React) -``` -# V1 (기존) -react/src/app/[locale]/(protected)/sales/quote-management/page.tsx -react/src/app/[locale]/(protected)/sales/quote-management/[id]/page.tsx -react/src/components/quotes/QuoteRegistration.tsx (50KB) -react/src/components/quotes/actions.ts (28KB) -react/src/components/quotes/types.ts - -# V2 (신규) -react/src/app/[locale]/(protected)/sales/quote-management/test-new/page.tsx -react/src/app/[locale]/(protected)/sales/quote-management/test/[id]/page.tsx -react/src/components/quotes/QuoteRegistrationV2.tsx -react/src/components/quotes/LocationListPanel.tsx -react/src/components/quotes/LocationDetailPanel.tsx -react/src/components/quotes/QuoteSummaryPanel.tsx -react/src/components/quotes/QuoteFooterBar.tsx -react/src/components/quotes/quoteConfig.ts -``` - -#### 백엔드 (Laravel API) - 변경 없음 -``` -api/app/Http/Controllers/Api/V1/QuoteController.php -api/app/Http/Requests/Quote/QuoteStoreRequest.php -api/app/Http/Requests/Quote/QuoteUpdateRequest.php -api/app/Models/Quote/Quote.php -api/app/Models/Quote/QuoteItem.php -api/app/Services/Quote/QuoteService.php -api/app/Services/Quote/QuoteCalculationService.php -``` - ---- - -## 8. 세션 및 메모리 관리 정책 (Serena Optimized) - -### 8.1 세션 시작 시 (Load Strategy) -```javascript -read_memory("quote-url-migration-state") // 1. 상태 파악 -read_memory("quote-url-migration-snapshot") // 2. 사고 흐름 복구 -``` - -### 8.2 작업 중 관리 (Context Defense) -| 컨텍스트 잔량 | Action | 내용 | -|--------------|--------|------| -| **30% 이하** | 🛠 **Snapshot** | 현재까지의 코드 변경점과 논의 핵심 요약 | -| **20% 이하** | 🧹 **Context Purge** | 수정 중인 핵심 파일 및 함수 목록 | -| **10% 이하** | 🛑 **Stop & Save** | 최종 상태 저장 후 세션 교체 권고 | - -### 8.3 Serena 메모리 구조 -- `quote-url-migration-state`: { phase, progress, next_step, last_decision } -- `quote-url-migration-snapshot`: 현재까지의 코드 변경 및 논의 요약 -- `quote-url-migration-active-files`: 수정 중인 파일 목록 - ---- - -## 9. 검증 결과 - -> 작업 완료 후 이 섹션에 검증 결과 추가 - -### 9.1 테스트 케이스 - -| 시나리오 | 입력 | 예상 결과 | 실제 결과 | 상태 | -|---------|------|----------|----------|------| -| 신규 등록 | 견적 정보 입력 후 저장 | DB 저장, 상세 페이지 이동 | - | ⏳ | -| 상세 조회 | /quote-management/[id] 접근 | 저장된 데이터 표시 | - | ⏳ | -| 수정 | mode=edit에서 수정 후 저장 | DB 업데이트, view 모드 복귀 | - | ⏳ | -| 문서 출력 | 견적서 버튼 클릭 | 견적서 모달 표시 | - | ⏳ | -| 최종확정 | 최종확정 버튼 클릭 | status → finalized | - | ⏳ | - -### 9.2 성공 기준 달성 현황 - -| 기준 | 달성 | 비고 | -|------|:----:|------| -| V2 API 연동 완료 | ✅ | Phase 1 완료 | -| URL 경로 정식화 | ✅ | Phase 2 완료 | -| V1 정리 완료 | ✅ | test 폴더 삭제, 백업 보존 | -| 통합 테스트 통과 | ⏳ | 사용자 테스트 필요 | - ---- - -## 10. 자기완결성 점검 결과 - -### 10.1 체크리스트 검증 - -| # | 검증 항목 | 상태 | 비고 | -|---|----------|:----:|------| -| 1 | 작업 목적이 명확한가? | ✅ | 섹션 1.2 목표 참조 | -| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 참조 | -| 3 | 작업 범위가 구체적인가? | ✅ | 섹션 3 대상 범위 참조 | -| 4 | 의존성이 명시되어 있는가? | ✅ | DB/API 변경 없음 명시 | -| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 7.1 검증 완료 | -| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 상세 절차 참조 | -| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 | -| 8 | 모호한 표현이 없는가? | ✅ | 구체적 함수명, 경로 명시 | - -### 10.2 새 세션 시뮬레이션 테스트 - -| 질문 | 답변 가능 | 참조 섹션 | -|------|:--------:|----------| -| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.2 목표 | -| Q2. 어디서부터 시작해야 하는가? | ✅ | 4.1 Step 1.1 | -| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 7.1 핵심 파일 경로 | -| Q4. 작업 완료 확인 방법은? | ✅ | 9.1 테스트 케이스 | -| Q5. 막혔을 때 참고 문서는? | ✅ | 7. 참고 문서 | - -**결과**: 5/5 통과 → ✅ 자기완결성 확보 - ---- - -## 부록 A: API 스키마 상세 - -> V2 연동 시 참고할 실제 API 요청/응답 스키마 - -### A.1 API 응답 타입 (QuoteApiData) - -```typescript -// react/src/components/quotes/types.ts 에서 발췌 - -interface QuoteApiData { - id: number; - quote_number: string; - registration_date: string; - - // 발주처 정보 - client_id: number | null; - client_name: string; - client?: { id: number; name: string; }; // with('client') 로드 시 - - // 현장 정보 - site_name: string | null; - site_code: string | null; - - // 담당자 정보 (API 실제 필드명) - manager?: string | null; // 담당자명 - contact?: string | null; // 연락처 - manager_name?: string | null; // 레거시 호환 - manager_contact?: string | null; // 레거시 호환 - - // 제품 정보 - product_category: 'screen' | 'steel'; - quantity: number; - unit_symbol?: string | null; // 단위 (개소, set 등) - - // 금액 정보 - supply_amount: string | number; - tax_amount: string | number; - total_amount: string | number; - - // 상태 - status: 'draft' | 'sent' | 'approved' | 'rejected' | 'finalized' | 'converted'; - current_revision: number; - is_final: boolean; - - // 비고/납기 - remarks?: string | null; // API 실제 필드명 - completion_date?: string | null; // API 실제 필드명 - description?: string | null; // 레거시 호환 - delivery_date?: string | null; // 레거시 호환 - - // 자동산출 입력값 (JSON) - calculation_inputs?: { - items?: Array<{ - productCategory?: string; - productName?: string; - openWidth?: string; - openHeight?: string; - guideRailType?: string; - motorPower?: string; - controller?: string; - wingSize?: string; - inspectionFee?: number; - floor?: string; - code?: string; - quantity?: number; - }>; - } | null; - - // 품목 목록 - items?: QuoteItemApiData[]; - bom_materials?: BomMaterialApiData[]; - - // 감사 정보 - created_at: string; - updated_at: string; - created_by: number | null; - updated_by: number | null; - finalized_at: string | null; - finalized_by: number | null; - - // 관계 데이터 (with 로드 시) - creator?: { id: number; name: string; } | null; - updater?: { id: number; name: string; } | null; - finalizer?: { id: number; name: string; } | null; -} -``` - -### A.2 품목 API 타입 (QuoteItemApiData) - -```typescript -interface QuoteItemApiData { - id: number; - quote_id: number; - - // 품목 정보 - item_id?: number | null; - item_code?: string | null; - item_name: string; - product_id?: number | null; // 레거시 호환 - product_name?: string; // 레거시 호환 - specification: string | null; - unit: string | null; - - // 수량 (API는 calculated_quantity 사용) - base_quantity?: number; // 1개당 BOM 수량 - calculated_quantity?: number; // base × 주문 수량 - quantity?: number; // 레거시 호환 - - // 금액 - unit_price: string | number; - total_price?: string | number; // API 실제 필드 - supply_amount?: string | number; // 레거시 호환 - tax_amount?: string | number; - total_amount?: string | number; // 레거시 호환 - - sort_order: number; - note: string | null; -} -``` - -### A.3 API 요청 형식 (POST/PUT /api/v1/quotes) - -```typescript -// transformFormDataToApi() 출력 형식 - -interface QuoteApiRequest { - registration_date: string; // "2026-01-26" - author: string | null; // 작성자명 - client_id: number | null; - client_name: string; - site_name: string | null; - manager: string | null; // 담당자명 - contact: string | null; // 연락처 - completion_date: string | null; // 납기일 "2026-02-01" - remarks: string | null; - product_category: 'screen' | 'steel'; - quantity: number; // 총 수량 (items.quantity 합계) - unit_symbol: string; // "개소" | "SET" - total_amount: number; // 총액 (공급가 + 세액) - - // 자동산출 입력값 저장 (폼 복원용) - calculation_inputs: { - items: Array<{ - productCategory: string; - productName: string; - openWidth: string; - openHeight: string; - guideRailType: string; - motorPower: string; - controller: string; - wingSize: string; - inspectionFee: number; - floor: string; - code: string; - quantity: number; - }>; - }; - - // BOM 자재 기반 items - items: Array<{ - item_name: string; - item_code: string; - specification: string | null; - unit: string; - quantity: number; // 주문 수량 - base_quantity: number; // 1개당 BOM 수량 - calculated_quantity: number; // base × 주문 수량 - unit_price: number; - total_price: number; - sort_order: number; - note: string | null; - item_index?: number; // calculation_inputs.items 인덱스 - finished_goods_code?: string; // 완제품 코드 - formula_category?: string; // 공정 그룹 - }>; -} -``` - ---- - -## 부록 B: 기존 변환 함수 코드 - -> 새 세션에서 바로 사용할 수 있도록 V1 변환 함수 전체 코드 포함 - -### B.1 API → 프론트엔드 변환 (transformApiToFrontend) - -```typescript -// react/src/components/quotes/types.ts - -export function transformApiToFrontend(apiData: QuoteApiData): Quote { - return { - id: String(apiData.id), - quoteNumber: apiData.quote_number, - registrationDate: apiData.registration_date, - clientId: apiData.client_id ? String(apiData.client_id) : '', - clientName: apiData.client?.name || apiData.client_name || '', - siteName: apiData.site_name || undefined, - siteCode: apiData.site_code || undefined, - // API 실제 필드명 우선, 레거시 폴백 - managerName: apiData.manager || apiData.manager_name || undefined, - managerContact: apiData.contact || apiData.manager_contact || undefined, - productCategory: apiData.product_category, - quantity: apiData.quantity || 0, - unitSymbol: apiData.unit_symbol || undefined, - supplyAmount: parseFloat(String(apiData.supply_amount)) || 0, - taxAmount: parseFloat(String(apiData.tax_amount)) || 0, - totalAmount: parseFloat(String(apiData.total_amount)) || 0, - status: apiData.status, - currentRevision: apiData.current_revision || 0, - isFinal: apiData.is_final || false, - description: apiData.remarks || apiData.description || undefined, - validUntil: apiData.valid_until || undefined, - deliveryDate: apiData.completion_date || apiData.delivery_date || undefined, - deliveryLocation: apiData.delivery_location || undefined, - paymentTerms: apiData.payment_terms || undefined, - items: (apiData.items || []).map(transformItemApiToFrontend), - calculationInputs: apiData.calculation_inputs || undefined, - bomMaterials: (apiData.bom_materials || []).map(transformBomMaterialApiToFrontend), - createdAt: apiData.created_at, - updatedAt: apiData.updated_at, - createdBy: apiData.creator?.name || undefined, - updatedBy: apiData.updater?.name || undefined, - finalizedAt: apiData.finalized_at || undefined, - finalizedBy: apiData.finalizer?.name || undefined, - }; -} -``` - -### B.2 프론트엔드 → API 변환 (transformFormDataToApi) - -```typescript -// react/src/components/quotes/types.ts (핵심 부분) - -export function transformFormDataToApi(formData: QuoteFormData): Record { - let itemsData = []; - - // calculationResults가 있으면 BOM 자재 기반으로 items 생성 - if (formData.calculationResults && formData.calculationResults.items.length > 0) { - let sortOrder = 1; - formData.calculationResults.items.forEach((calcItem) => { - const formItem = formData.items[calcItem.index]; - const orderQuantity = formItem?.quantity || 1; - - calcItem.result.items.forEach((bomItem) => { - const baseQuantity = bomItem.quantity; - const calculatedQuantity = bomItem.unit === 'EA' - ? Math.round(baseQuantity * orderQuantity) - : parseFloat((baseQuantity * orderQuantity).toFixed(2)); - const totalPrice = bomItem.unit_price * calculatedQuantity; - - itemsData.push({ - item_name: bomItem.item_name, - item_code: bomItem.item_code, - specification: bomItem.specification || null, - unit: bomItem.unit || 'EA', - quantity: orderQuantity, - base_quantity: baseQuantity, - calculated_quantity: calculatedQuantity, - unit_price: bomItem.unit_price, - total_price: totalPrice, - sort_order: sortOrder++, - note: `${formItem?.floor || ''} ${formItem?.code || ''}`.trim() || null, - item_index: calcItem.index, - finished_goods_code: calcItem.result.finished_goods.code, - formula_category: bomItem.process_group || undefined, - }); - }); - }); - } else { - // 기존 로직: 완제품 기준 items 생성 - itemsData = formData.items.map((item, index) => ({ - item_name: item.productName, - item_code: item.productName, - specification: item.openWidth && item.openHeight - ? `${item.openWidth}x${item.openHeight}mm` : null, - unit: item.unit || '개소', - quantity: item.quantity, - base_quantity: 1, - calculated_quantity: item.quantity, - unit_price: item.unitPrice || item.inspectionFee || 0, - total_price: (item.unitPrice || item.inspectionFee || 0) * item.quantity, - sort_order: index + 1, - note: `${item.floor || ''} ${item.code || ''}`.trim() || null, - })); - } - - // 총액 계산 - const totalSupply = itemsData.reduce((sum, item) => sum + item.total_price, 0); - const totalTax = Math.round(totalSupply * 0.1); - const grandTotal = totalSupply + totalTax; - - // 자동산출 입력값 저장 - const calculationInputs = { - items: formData.items.map(item => ({ - productCategory: item.productCategory, - productName: item.productName, - openWidth: item.openWidth, - openHeight: item.openHeight, - guideRailType: item.guideRailType, - motorPower: item.motorPower, - controller: item.controller, - wingSize: item.wingSize, - inspectionFee: item.inspectionFee, - floor: item.floor, - code: item.code, - quantity: item.quantity, - })), - }; - - return { - registration_date: formData.registrationDate, - author: formData.writer || null, - client_id: formData.clientId ? parseInt(formData.clientId, 10) : null, - client_name: formData.clientName, - site_name: formData.siteName || null, - manager: formData.manager || null, - contact: formData.contact || null, - completion_date: formData.dueDate || null, - remarks: formData.remarks || null, - product_category: formData.items[0]?.productCategory?.toLowerCase() || 'screen', - quantity: formData.items.reduce((sum, item) => sum + item.quantity, 0), - unit_symbol: formData.unitSymbol || '개소', - total_amount: grandTotal, - calculation_inputs: calculationInputs, - items: itemsData, - }; -} -``` - -### B.3 Quote → QuoteFormData 변환 (transformQuoteToFormData) - -```typescript -// react/src/components/quotes/types.ts - -export function transformQuoteToFormData(quote: Quote): QuoteFormData { - const calcInputs = quote.calculationInputs?.items || []; - - // BOM 자재(quote.items)의 총 금액 계산 - const totalBomAmount = quote.items.reduce((sum, item) => sum + (item.totalAmount || 0), 0); - const itemCount = calcInputs.length || 1; - const amountPerItem = Math.round(totalBomAmount / itemCount); - - return { - id: quote.id, - registrationDate: formatDateForInput(quote.registrationDate), - writer: quote.createdBy || '', - clientId: quote.clientId, - clientName: quote.clientName, - siteName: quote.siteName || '', - manager: quote.managerName || '', - contact: quote.managerContact || '', - dueDate: formatDateForInput(quote.deliveryDate), - remarks: quote.description || '', - unitSymbol: quote.unitSymbol, - - // calculation_inputs.items가 있으면 그것으로 items 복원 - items: calcInputs.length > 0 - ? calcInputs.map((calcInput, index) => ({ - id: `temp-${index}`, - floor: calcInput.floor || '', - code: calcInput.code || '', - productCategory: calcInput.productCategory || '', - productName: calcInput.productName || '', - openWidth: calcInput.openWidth || '', - openHeight: calcInput.openHeight || '', - guideRailType: calcInput.guideRailType || '', - motorPower: calcInput.motorPower || '', - controller: calcInput.controller || '', - quantity: calcInput.quantity || 1, - unit: undefined, - wingSize: calcInput.wingSize || '50', - inspectionFee: calcInput.inspectionFee || 50000, - unitPrice: Math.round(amountPerItem / (calcInput.quantity || 1)), - totalAmount: amountPerItem, - })) - : quote.items.map((item) => ({ - id: item.id, - floor: '', - code: '', - productCategory: '', - productName: item.productName, - openWidth: '', - openHeight: '', - guideRailType: '', - motorPower: '', - controller: '', - quantity: item.quantity || 1, - unit: item.unit, - wingSize: '50', - inspectionFee: item.unitPrice || 50000, - unitPrice: item.unitPrice, - totalAmount: item.totalAmount, - })), - - bomMaterials: calcInputs.length > 0 - ? quote.items.map((item, index) => ({ - itemIndex: index, - finishedGoodsCode: '', - itemCode: item.itemCode || '', - itemName: item.productName, - itemType: '', - itemCategory: '', - specification: item.specification || '', - unit: item.unit || '', - quantity: item.quantity, - unitPrice: item.unitPrice, - totalPrice: item.totalAmount, - processType: '', - })) - : quote.bomMaterials, - }; -} - -// 날짜 형식 변환 헬퍼 -function formatDateForInput(dateStr: string | null | undefined): string { - if (!dateStr) return ''; - if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr; - const date = new Date(dateStr); - if (isNaN(date.getTime())) return ''; - return date.toISOString().split('T')[0]; -} -``` - ---- - -## 부록 C: V2 ↔ API 필드 매핑표 - -> 새 변환 함수 작성 시 참고할 필드 매핑 - -### C.1 견적 마스터 필드 매핑 - -| V2 필드 (QuoteFormDataV2) | API 필드 (QuoteApiData) | DB 컬럼 (quotes) | 비고 | -|--------------------------|------------------------|-----------------|------| -| `id` | `id` | `id` | string ↔ number 변환 | -| `registrationDate` | `registration_date` | `registration_date` | | -| `writer` | `author` / `creator.name` | `author` | 저장: author, 조회: creator.name | -| `clientId` | `client_id` | `client_id` | string ↔ number 변환 | -| `clientName` | `client_name` / `client.name` | `client_name` | | -| `siteName` | `site_name` | `site_name` | | -| `manager` | `manager` | `manager` | | -| `contact` | `contact` | `contact` | | -| `dueDate` | `completion_date` | `completion_date` | | -| `remarks` | `remarks` | `remarks` | | -| `status` | `status` | `status` | V2: draft/temporary/final ↔ API: draft/sent/.../finalized | -| `locations` | `items` + `calculation_inputs.items` | - | 복합 변환 필요 | - -### C.2 개소 항목 필드 매핑 - -| V2 필드 (LocationItem) | API calculation_inputs.items | API items | 비고 | -|-----------------------|----------------------------|-----------|------| -| `id` | - | `id` | | -| `floor` | `floor` | `note` (일부) | | -| `code` | `code` | `note` (일부) | | -| `openWidth` | `openWidth` | `specification` (파싱) | "3000x2500mm" 형식 | -| `openHeight` | `openHeight` | `specification` (파싱) | | -| `productCode` | - | `finished_goods_code` | BOM 산출 시 사용 | -| `productName` | `productName` | `item_name` | | -| `quantity` | `quantity` | `quantity` | 주문 수량 | -| `guideRailType` | `guideRailType` | - | calculation_inputs에만 저장 | -| `motorPower` | `motorPower` | - | | -| `controller` | `controller` | - | | -| `wingSize` | `wingSize` | - | | -| `inspectionFee` | `inspectionFee` | - | | -| `unitPrice` | - | `unit_price` | | -| `totalPrice` | - | `total_price` | | - -### C.3 상태값 매핑 - -| V2 status | API status | 설명 | -|-----------|-----------|------| -| `draft` | `draft`, `sent`, `approved`, `rejected` | 작성중/진행중 | -| `temporary` | - | V2 전용 (임시저장) → API에는 `draft`로 저장 | -| `final` | `finalized`, `converted` | 최종확정/수주전환 | - ---- - -## 부록 D: 테스트 명령어 - -> Docker 환경에서 테스트하는 방법 - -### D.1 서비스 확인 - -```bash -# Docker 서비스 상태 확인 -cd /Users/kent/Works/@KD_SAM/SAM -docker compose ps - -# API 서버 로그 확인 -docker compose logs -f api - -# React 개발 서버 로그 확인 -docker compose logs -f react -``` - -### D.2 API 직접 테스트 - -```bash -# 견적 목록 조회 -curl -X GET "http://api.sam.kr/api/v1/quotes" \ - -H "Authorization: Bearer {TOKEN}" \ - -H "Accept: application/json" - -# 견적 상세 조회 -curl -X GET "http://api.sam.kr/api/v1/quotes/{ID}" \ - -H "Authorization: Bearer {TOKEN}" \ - -H "Accept: application/json" - -# 견적 생성 (예시) -curl -X POST "http://api.sam.kr/api/v1/quotes" \ - -H "Authorization: Bearer {TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{ - "registration_date": "2026-01-26", - "client_name": "테스트 발주처", - "site_name": "테스트 현장", - "product_category": "screen", - "quantity": 1, - "total_amount": 1000000, - "items": [] - }' -``` - -### D.3 브라우저 테스트 URL - -``` -# V1 (기존) -http://dev.sam.kr/sales/quote-management # 목록 -http://dev.sam.kr/sales/quote-management?mode=new # 등록 -http://dev.sam.kr/sales/quote-management/1 # 상세 -http://dev.sam.kr/sales/quote-management/1?mode=edit # 수정 - -# V2 (신규 - 테스트) -http://dev.sam.kr/sales/quote-management/test-new # 등록 -http://dev.sam.kr/sales/quote-management/test/1 # 상세 -http://dev.sam.kr/sales/quote-management/test/1?mode=edit # 수정 -``` - -### D.4 디버깅 - -```bash -# React 콘솔 로그 확인 (브라우저 개발자 도구) -# [QuoteActions] 접두사로 API 요청/응답 확인 - -# API 디버그 로그 확인 -docker compose exec api tail -f storage/logs/laravel.log -``` - ---- - -## 부록 E: V2 변환 함수 구현 가이드 - -> Phase 1.1에서 구현할 함수 상세 가이드 - -### E.1 transformV2ToApi 구현 - -```typescript -// react/src/components/quotes/types.ts에 추가 - -import type { QuoteFormDataV2, LocationItem } from './QuoteRegistrationV2'; - -/** - * V2 폼 데이터 → API 요청 형식 변환 - * - * 핵심 차이점: - * - V2는 locations[] 배열, API는 items[] + calculation_inputs.items[] 구조 - * - V2 status는 3가지, API status는 6가지 - * - BOM 산출 결과가 있으면 items에 자재 상세 포함 - */ -export function transformV2ToApi( - data: QuoteFormDataV2, - bomResults?: BomCalculationResult[] -): Record { - - // 1. calculation_inputs 생성 (폼 복원용) - const calculationInputs = { - items: data.locations.map(loc => ({ - productCategory: 'screen', // TODO: 실제 카테고리 - productName: loc.productName, - openWidth: String(loc.openWidth), - openHeight: String(loc.openHeight), - guideRailType: loc.guideRailType, - motorPower: loc.motorPower, - controller: loc.controller, - wingSize: String(loc.wingSize), - inspectionFee: loc.inspectionFee, - floor: loc.floor, - code: loc.code, - quantity: loc.quantity, - })), - }; - - // 2. items 생성 (BOM 결과 있으면 자재 상세, 없으면 완제품 기준) - let items: Array> = []; - - if (bomResults && bomResults.length > 0) { - // BOM 자재 기반 - let sortOrder = 1; - bomResults.forEach((bomResult, locIndex) => { - const loc = data.locations[locIndex]; - const orderQty = loc?.quantity || 1; - - bomResult.items.forEach(bomItem => { - const baseQty = bomItem.quantity; - const calcQty = bomItem.unit === 'EA' - ? Math.round(baseQty * orderQty) - : parseFloat((baseQty * orderQty).toFixed(2)); - - items.push({ - item_name: bomItem.item_name, - item_code: bomItem.item_code, - specification: bomItem.specification || null, - unit: bomItem.unit || 'EA', - quantity: orderQty, - base_quantity: baseQty, - calculated_quantity: calcQty, - unit_price: bomItem.unit_price, - total_price: bomItem.unit_price * calcQty, - sort_order: sortOrder++, - note: `${loc?.floor || ''} ${loc?.code || ''}`.trim() || null, - item_index: locIndex, - finished_goods_code: bomResult.finished_goods.code, - formula_category: bomItem.process_group || undefined, - }); - }); - }); - } else { - // 완제품 기준 (BOM 산출 전) - items = data.locations.map((loc, index) => ({ - item_name: loc.productName, - item_code: loc.productCode, - specification: `${loc.openWidth}x${loc.openHeight}mm`, - unit: '개소', - quantity: loc.quantity, - base_quantity: 1, - calculated_quantity: loc.quantity, - unit_price: loc.unitPrice || loc.inspectionFee || 0, - total_price: loc.totalPrice || (loc.unitPrice || loc.inspectionFee || 0) * loc.quantity, - sort_order: index + 1, - note: `${loc.floor} ${loc.code}`.trim() || null, - })); - } - - // 3. 총액 계산 - const totalSupply = items.reduce((sum, item) => sum + (item.total_price as number), 0); - const totalTax = Math.round(totalSupply * 0.1); - const grandTotal = totalSupply + totalTax; - - // 4. API 요청 객체 반환 - return { - registration_date: data.registrationDate, - author: data.writer || null, - client_id: data.clientId ? parseInt(data.clientId, 10) : null, - client_name: data.clientName, - site_name: data.siteName || null, - manager: data.manager || null, - contact: data.contact || null, - completion_date: data.dueDate || null, - remarks: data.remarks || null, - product_category: 'screen', // TODO: 동적으로 결정 - quantity: data.locations.reduce((sum, loc) => sum + loc.quantity, 0), - unit_symbol: '개소', - total_amount: grandTotal, - status: data.status === 'final' ? 'finalized' : 'draft', - calculation_inputs: calculationInputs, - items: items, - }; -} -``` - -### E.2 transformApiToV2 구현 - -```typescript -/** - * API 응답 → V2 폼 데이터 변환 - * - * 핵심: - * - calculation_inputs.items가 있으면 그것으로 locations 복원 - * - 없으면 items에서 추출 시도 (레거시 호환) - */ -export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 { - const calcInputs = apiData.calculation_inputs?.items || []; - - // calculation_inputs에서 locations 복원 - const locations: LocationItem[] = calcInputs.length > 0 - ? calcInputs.map((ci, index) => { - // 해당 인덱스의 BOM 자재에서 금액 계산 - const relatedItems = (apiData.items || []).filter( - item => item.item_index === index || item.note?.includes(ci.floor || '') - ); - const totalPrice = relatedItems.reduce( - (sum, item) => sum + parseFloat(String(item.total_price || 0)), 0 - ); - const qty = ci.quantity || 1; - - return { - id: `loc-${index}`, - floor: ci.floor || '', - code: ci.code || '', - openWidth: parseInt(ci.openWidth || '0', 10), - openHeight: parseInt(ci.openHeight || '0', 10), - productCode: '', // TODO: finished_goods_code에서 추출 - productName: ci.productName || '', - quantity: qty, - guideRailType: ci.guideRailType || 'wall', - motorPower: ci.motorPower || 'single', - controller: ci.controller || 'basic', - wingSize: parseInt(ci.wingSize || '50', 10), - inspectionFee: ci.inspectionFee || 50000, - unitPrice: Math.round(totalPrice / qty), - totalPrice: totalPrice, - }; - }) - : []; // TODO: items에서 복원 로직 추가 - - // 상태 매핑 - const mapStatus = (s: string): 'draft' | 'temporary' | 'final' => { - if (s === 'finalized' || s === 'converted') return 'final'; - return 'draft'; - }; - - return { - id: String(apiData.id), - registrationDate: formatDateForInput(apiData.registration_date), - writer: apiData.creator?.name || '', - clientId: apiData.client_id ? String(apiData.client_id) : '', - clientName: apiData.client?.name || apiData.client_name || '', - siteName: apiData.site_name || '', - manager: apiData.manager || apiData.manager_name || '', - contact: apiData.contact || apiData.manager_contact || '', - dueDate: formatDateForInput(apiData.completion_date || apiData.delivery_date), - remarks: apiData.remarks || apiData.description || '', - status: mapStatus(apiData.status), - locations: locations, - }; -} -``` - ---- - -*이 문서는 /sc:plan 스킬로 생성되었습니다. (2026-01-26 보완)* \ No newline at end of file diff --git a/plans/quote-system-development-plan.md b/plans/quote-system-development-plan.md deleted file mode 100644 index 66e8147..0000000 --- a/plans/quote-system-development-plan.md +++ /dev/null @@ -1,319 +0,0 @@ -# 견적 시스템 개발 계획 - -> **작성일**: 2025-12-24 -> **목표**: mng 수식 시뮬레이터를 완전한 견적 시스템으로 확장 후 React API 개발 - ---- - -## 1. 개발 단계 - -### Stage 1: mng 견적 시스템 완성 (현재) -**목표**: 스크린샷과 동일한 견적 시스템을 mng Plain Blade로 구현 - -### Stage 2: API 개발 -**목표**: React 프론트엔드에서 호출할 견적 산출 REST API 개발 - ---- - -## 2. 현재 상태 vs 목표 상태 - -### 2.1 입력 폼 비교 - -| 필드 | 현재 시뮬레이터 | 목표 (스크린샷) | 상태 | -|------|----------------|----------------|------| -| 층수 | ❌ | 예: 1층, B1, 지하1층 | 추가 필요 | -| 부호 | ❌ | 예: A, B, C | 추가 필요 | -| 제품 카테고리 (PC) | ✅ | 스크린, 철재 등 | 완료 | -| 제품명 | ✅ | 방화 스크린 셔터 등 | 완료 | -| 오픈사이즈 W0 | ✅ | 가로 | 완료 | -| 오픈사이즈 H0 | ✅ | 세로 | 완료 | -| 가이드레일 설치유형 (GT) | ✅ | 벽면형, 측면형 | 완료 | -| 모터 전원 (MP) | ✅ | 220V, 380V | 완료 | -| 연동제어기 (CT) | ✅ | 단독, 연동 | 완료 | -| 수량 (QTY) | ✅ | 1, 2, 3... | 완료 | -| 마구리 날개치수 (WS) | ❌ | 50 등 | 추가 필요 | -| 검사비 (INSP) | ❌ | 50000 등 | 추가 필요 | - -### 2.2 출력 결과 비교 - -| 섹션 | 현재 시뮬레이터 | 목표 (스크린샷) | 상태 | -|------|----------------|----------------|------| -| 입력 정보 요약 | ❌ | 제품명, 카테고리, 오픈사이즈, 설치유형 등 요약 | 추가 필요 | -| 기본 산출 공식 | ❌ | 제작폭(W1), 제작높이(H1), 면적(M), 중량(K) 표시 | 추가 필요 | -| BOM 목록 테이블 | ⚠️ 공정별 그룹화 | 순번, 품목코드, 품목명, 품목유형, 규격, 기준수량, 산출수량, 단위, 단가, 금액, 작업 | 구조 변경 필요 | -| 품목 추가/삭제 | ❌ | + 품목 추가 버튼, 휴지통 삭제 버튼 | 추가 필요 | -| 할인율 | ❌ | 할인율(%) 입력 | 추가 필요 | -| 금액 요약 | ⚠️ 합계만 | 합계, 공급가, 최종 금액 | 확장 필요 | - ---- - -## 3. Stage 1: mng 견적 시스템 상세 계획 - -### Phase 1: UI 확장 (1일) -**파일**: `resources/views/quote-formulas/simulator.blade.php` - -#### 1.1 입력 폼 확장 -``` -추가 필드: -- 층수 (floor): text input, placeholder "예: 1층, B1, 지하1층" -- 부호 (code): text input, placeholder "예: A, B, C" -- 마구리 날개치수 (WS): number input, default 50 -- 검사비 (INSP): number input, default 50000 -``` - -#### 1.2 견적 항목 다중 입력 -``` -- 견적 1, 견적 2, ... 탭 형태 -- "+ 견적 추가" 버튼 -- 복사, 삭제 버튼 -``` - -#### 1.3 결과 출력 섹션 -``` -1. 입력 정보 요약 카드 - - 제품명, 제품 카테고리, 오픈사이즈, 가이드레일 설치, 모터 전원, 연동제어기, 수량 - -2. 기본 산출 공식 카드 - - 제작폭 (W1): 값 + 계산식 - - 제작높이 (H1): 값 + 계산식 - - 면적 (M): 값 + 단위 - - 중량 (K): 값 + 단위 - -3. 부품구성표(BOM) 목록 테이블 - - 컬럼: 순번, 품목코드, 품목명, 품목유형, 규격, 기준수량, 산출수량, 단위, 단가, 금액, 작업 - - "+ 품목 추가" 버튼 - - 행별 삭제 버튼 - -4. 금액 요약 - - 할인율(%) 입력 - - 합계, 공급가, 최종 금액 -``` - -### Phase 2: 백엔드 로직 확장 (1일) -**파일**: `app/Services/Quote/FormulaEvaluatorService.php` - -#### 2.1 executeAll() 반환 구조 확장 -```php -return [ - 'input_summary' => [ - 'product_name' => '방화 스크린 셔터 (소형)', - 'product_category' => '스크린', - 'open_size' => 'W2000 × H2500', - 'guide_rail_type' => '벽면형', - 'motor_power' => '220V', - 'controller' => '단독', - 'quantity' => 1, - ], - 'calculation_formula' => [ - 'W1' => ['value' => 2140, 'formula' => 'W0 + 140'], - 'H1' => ['value' => 2850, 'formula' => 'H0 + 350'], - 'M' => ['value' => 6.10, 'unit' => '㎡'], - 'K' => ['value' => 0.00, 'unit' => 'kg'], - ], - 'bom_items' => [ - [ - 'seq' => 1, - 'item_code' => 'SF-SCR-F01', - 'item_name' => '스크린 원단', - 'item_type' => 'SF', - 'spec' => '-', - 'base_quantity' => 1.10, - 'calculated_quantity' => 6.099, - 'unit' => 'M2', - 'unit_price' => 213465, - 'total_price' => 1301923.035, - 'editable' => true, - ], - // ... more items - ], - 'summary' => [ - 'subtotal' => 2806523.035, - 'discount_rate' => 0, - 'discount_amount' => 0, - 'supply_price' => 2806523.035, - 'total_amount' => 2806523.035, - ], -]; -``` - -### Phase 3: 검사비 품목 추가 (0.5일) -**파일**: `database/seeders/DesignItemSeeder.php` - -```php -// 서비스 품목 추가 -$serviceItems = [ - ['code' => 'SVC-INSP', 'name' => '검사비', 'unit' => '식', 'price' => 50000, 'type' => 'CS'], - ['code' => 'SVC-INSTALL', 'name' => '설치비', 'unit' => '식', 'price' => 100000, 'type' => 'CS'], - ['code' => 'SVC-DELIVERY', 'name' => '운송비', 'unit' => '식', 'price' => 80000, 'type' => 'CS'], -]; -``` - -### Phase 4: 테스트 및 검증 (0.5일) -- Playwright로 전체 플로우 테스트 -- Design 시스템 결과와 비교 검증 - ---- - -## 4. Stage 2: API 개발 상세 계획 - -### Phase 1: API 엔드포인트 설계 (0.5일) - -#### 4.1 견적 산출 API -``` -POST /api/v1/quotes/calculate - -Request: -{ - "items": [ - { - "floor": "1층", - "code": "A", - "product_category": "screen", - "product_id": "screen_standard", - "open_width": 2000, - "open_height": 2500, - "guide_rail_type": "wall", - "motor_power": "220V", - "controller": "single", - "quantity": 1, - "wing_size": 50, - "inspection_fee": 50000 - } - ], - "discount_rate": 0 -} - -Response: -{ - "success": true, - "data": { - "quotes": [ - { - "quote_id": "quote-1", - "input_summary": { ... }, - "calculation_formula": { ... }, - "bom_items": [ ... ], - "summary": { ... } - } - ], - "total_summary": { - "total_items": 1, - "total_amount": 2806523.035 - } - } -} -``` - -#### 4.2 제품 목록 API -``` -GET /api/v1/quotes/products?category=screen - -Response: -{ - "success": true, - "data": [ - { - "id": "screen_standard", - "name": "스크린 셔터 (표준형)", - "category": "screen" - } - ] -} -``` - -#### 4.3 옵션 목록 API -``` -GET /api/v1/quotes/options - -Response: -{ - "success": true, - "data": { - "product_categories": [...], - "guide_rail_types": [...], - "motor_powers": [...], - "controllers": [...] - } -} -``` - -### Phase 2: API 컨트롤러 구현 (1일) -**파일**: `api/app/Http/Controllers/Api/V1/QuoteCalculationController.php` - -### Phase 3: API 테스트 (0.5일) -- Postman/Swagger 테스트 -- React 연동 테스트 - ---- - -## 5. 일정 요약 - -| Stage | Phase | 작업 내용 | 예상 일정 | -|-------|-------|----------|----------| -| **Stage 1** | Phase 1 | mng UI 확장 | 1일 | -| | Phase 2 | 백엔드 로직 확장 | 1일 | -| | Phase 3 | 검사비 품목 추가 | 0.5일 | -| | Phase 4 | 테스트 및 검증 | 0.5일 | -| | **소계** | | **3일** | -| **Stage 2** | Phase 1 | API 설계 | 0.5일 | -| | Phase 2 | API 구현 | 1일 | -| | Phase 3 | API 테스트 | 0.5일 | -| | **소계** | | **2일** | -| **합계** | | | **5일** | - ---- - -## 6. 파일 구조 - -### Stage 1 (mng) -``` -/SAM/mng/ -├── app/Services/Quote/ -│ └── FormulaEvaluatorService.php # 로직 확장 -├── database/seeders/ -│ └── DesignItemSeeder.php # 서비스 품목 추가 -└── resources/views/quote-formulas/ - └── simulator.blade.php # UI 확장 -``` - -### Stage 2 (api) -``` -/SAM/api/ -├── app/Http/Controllers/Api/V1/ -│ └── QuoteCalculationController.php # 신규 -├── app/Services/Quote/ -│ └── QuoteCalculationService.php # 신규 (또는 mng 서비스 공유) -└── routes/ - └── api.php # 라우트 추가 -``` - ---- - -## 7. 성공 기준 - -### Stage 1 -1. ✅ 스크린샷과 동일한 입력 폼 (층수, 부호, WS, INSP 포함) -2. ✅ 입력 정보 요약 섹션 표시 -3. ✅ 기본 산출 공식 섹션 표시 -4. ✅ BOM 테이블 (순번~금액 컬럼) -5. ✅ 품목 추가/삭제 기능 -6. ✅ 할인율 + 최종 금액 계산 - -### Stage 2 -1. ✅ POST /api/v1/quotes/calculate 정상 작동 -2. ✅ GET /api/v1/quotes/products 정상 작동 -3. ✅ GET /api/v1/quotes/options 정상 작동 -4. ✅ Swagger 문서화 완료 -5. ✅ React에서 API 호출 테스트 완료 - ---- - -## 8. 참고 문서 - -- `docs/plans/simulator-calculation-logic-mapping.md` - 계산 로직 상세 -- `react/src/components/quotes/QuoteRegistration.tsx` - React UI 참조 -- `design/src/components/AutoCalculationSimulator.tsx` - Design 시뮬레이터 참조 - ---- - -*이 문서는 mng 견적 시스템 완성 및 API 개발 계획을 정의합니다.* \ No newline at end of file diff --git a/plans/react-mock-remaining-tasks.md b/plans/react-mock-remaining-tasks.md deleted file mode 100644 index 2bdfbc5..0000000 --- a/plans/react-mock-remaining-tasks.md +++ /dev/null @@ -1,637 +0,0 @@ -# React Mock → API 마이그레이션 - 잔여 작업 - -> **작성일**: 2025-12-27 -> **목적**: 미완료 Mock → API 연동 작업 추적 -> **원본 문서**: `react-mock-to-api-migration-plan.md` -> **참조 구현**: 단가관리 (`/sales/pricing-management`) - ---- - -## 0. 로컬 개발 환경 - -### 도메인 구성 - -| 서비스 | 도메인 | 설명 | -|--------|--------|------| -| React (프론트엔드) | `http://dev.sam.kr` | 사용자 화면 | -| API (백엔드) | `http://api.sam.kr` | REST API 서버 | -| MNG (운영관리자) | `http://mng.sam.kr` | 관리자 패널 | - -### 테스트 URL 예시 - -``` -# 종합분석 페이지 -http://dev.sam.kr/reports/comprehensive-analysis - -# API 직접 호출 -http://api.sam.kr/api/v1/comprehensive-analysis -``` - -### 테스트 대상 테넌트 - -| 항목 | 값 | 비고 | -|------|-----|------| -| **Tenant ID** | 287 | 프론트_테스트회사 | -| **테스트 User ID** | 33 | 홍킬동 (hhhhhh@example.com) | -| **보조 User ID** | 12 | Ops Admin (결재함/참조함 테스트용 기안자) | - -> ⚠️ **주의**: Seeder 및 테스트 데이터 생성 시 반드시 `tenant_id = 287`, `user_id = 33` 사용 - -### 로그인 정보 - -| 사용자 | Email | 비밀번호 | Tenant | -|--------|-------|---------|--------| -| 홍킬동 | hhhhhh@example.com | (확인 필요) | 287 (기본) | - -### 종합분석 페이지 작업 시 주의사항 - -> ⚠️ **필수**: 종합분석은 여러 모듈의 데이터를 통합 표시하므로, 데이터 수정 시 관련 페이지 점검 필수 - -| 종합분석 섹션 | 원본 데이터 | 관련 페이지 (점검 대상) | -|--------------|------------|----------------------| -| 오늘의 이슈 (결재 대기) | `approvals`, `approval_steps` | `/approval/draft` (기안함), `/approval/pending` (결재함), `/approval/reference` (참조함) | -| 월간 예상 지출 | `expected_expenses` | `/accounting/expected-expenses` | -| 입금 현황 | `deposits` | `/accounting/deposits` | -| 채권추심 | `bad_debts` | `/accounting/bad-debts` | -| 미수금/여신한도 | `clients` | `/sales/clients` | - -**작업 흐름:** -``` -종합분석 데이터 수정 → 종합분석 페이지 확인 → 관련 원본 페이지 점검 -``` - ---- - -## 1. 작업 규칙 - -### 1.0 아키텍처 원칙 (필수) - -> **React는 오직 `api.sam.kr` (api 프로젝트)만 호출한다** - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ react/ │ ───► │ api/ │ │ mng/ │ -│ dev.sam.kr │ │ api.sam.kr │ │ mng.sam.kr │ -│ (프론트엔드) │ │ (REST API) │ │ (관리자패널) │ -└─────────────┘ └─────────────┘ └─────────────┘ - │ │ │ - │ ✅ 호출 허용 │ │ - └────────────────────┘ │ - │ - ❌ 절대 호출 금지 ─────────────────────────┘ -``` - -**규칙:** -- React에서 mng API 직접 호출 **절대 금지** -- 필요한 API가 api 프로젝트에 없으면 **api에 새로 개발** -- mng의 모델/로직은 **참조만** (코드 복사 또는 재구현) - -### 1.1 작업 진행 정책 - -> **단위 작업 → 검수 → 승인 → 문서 업데이트 → 커밋** 순서로 진행 - -### 1.2 세션 규칙 및 Serena 메모리 관리 - -> **세션 간 일관성 보장을 위한 필수 규칙** - -#### 세션 시작 프로토콜 (필수) - -``` -1. Serena 메모리 로드 - read_memory("mock-to-api-state") → 현재 Phase/작업 확인 - read_memory("mock-to-api-snapshot") → 마지막 작업 내용 확인 - -2. 현재 상태 확인 - - 이 문서 읽기 - - 현재 Phase의 기능별 상태 확인 - - "다음 작업은 [Phase]-[번호]의 [기능] 입니다" 명시 - -3. 작업 범위 명확화 - - 사용자에게 작업 범위 확인 - - "[Phase] 전체를 진행할까요, 특정 기능만 진행할까요?" -``` - -#### Serena 메모리 구조 - -```javascript -// mock-to-api-state -{ - "current_phase": "J", - "current_item": "J-1", - "current_feature": "게시판 목록", - "progress": { - "J-1": { "목록": "대기", "상세": "대기" } - }, - "last_update": "2025-12-27" -} - -// mock-to-api-snapshot -"Phase J 게시판 시스템 시작 예정" -``` - -#### 작업 완료 시 (필수) - -``` -1. 문서 업데이트 - - 해당 기능 상태 변경 (🔄 → ✅) - - 변경 이력 추가 - -2. Serena 메모리 저장 - write_memory("mock-to-api-state", 현재 상태) - write_memory("mock-to-api-snapshot", 작업 내용 요약) - -3. 커밋 - feat: [Phase]-[번호] [페이지명] Mock → API 연동 -``` - -### 1.3 작업 템플릿 (표준) - -```markdown -## [Phase-번호] 페이지명 - [기능명] 연동 - -**작업 대상:** -- 컴포넌트: `ComponentName.tsx` -- 액션: `actions.ts` -- API: `GET/POST/PUT/DELETE /api/v1/endpoint` - -**작업 절차:** -1. [ ] API 스펙 확인 (Swagger) -2. [ ] actions.ts 함수 확인/생성 -3. [ ] 타입 정의 확인 (API ↔ Frontend) -4. [ ] 컴포넌트에서 actions 호출 -5. [ ] console.log/MOCK 제거 -6. [ ] 브라우저 테스트 - -**결과:** -- [ ] 검수 요청 -- [ ] [승인] 문서 업데이트 -- [ ] [승인] 커밋 -``` - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ 📋 작업 흐름 (페이지 단위) │ -├─────────────────────────────────────────────────────────────────┤ -│ 1️⃣ 작업 시작: 대상 페이지 Mock → API 연동 작업 │ -│ 2️⃣ 작업 완료: 코드 수정 완료 후 사용자에게 검수 요청 │ -│ 3️⃣ 검수: 사용자가 기능 확인 (브라우저 테스트) │ -│ 4️⃣ [승인] 문서 업데이트: 이 문서의 상태 갱신 │ -│ 5️⃣ [승인] 커밋: Git 커밋 생성 │ -│ 6️⃣ 다음 페이지로 이동 │ -└─────────────────────────────────────────────────────────────────┘ -``` - -**⚠️ 중요 규칙:** -- 각 단계에서 `[승인]` 표시된 작업은 **사용자 승인 후** 진행 - ---- - -## 2. 잔여 작업 목록 - -### 2.1 Phase J: 게시판 시스템 - -> **상태**: ✅ api 프로젝트에 게시판/게시글 API 완비 → React 연동 작업 가능 - -#### ✅ api 프로젝트 게시판 API 아키텍처 - -> **핵심 설계**: 시스템 게시판과 테넌트 게시판을 **별도 엔드포인트**로 분리하고, **code 기반 URL** 사용 - -``` -시스템 게시판 (본사 운영) 테넌트 게시판 (테넌트 내부) -┌─────────────────────────────┐ ┌─────────────────────────────┐ -│ /api/v1/system-boards/{code}│ │ /api/v1/boards/{code} │ -│ - is_system = true │ │ - is_system = false │ -│ - tenant_id = null │ │ - tenant_id = {current} │ -│ - 메뉴 → global_menus │ │ - 메뉴 → menus │ -└─────────────────────────────┘ └─────────────────────────────┘ -``` - -**장점:** -- 동일한 `board_code`도 시스템/테넌트에서 독립 사용 가능 -- API 호출 시 `is_system` 플래그 불필요 -- URL만으로 게시판 유형 구분 가능 -- RESTful 원칙 준수 - -#### 📌 시스템 게시판 API (System Boards) - -| 기능 | Method | Endpoint (api.sam.kr) | 상태 | -|------|--------|----------------------|------| -| 시스템 게시판 목록 | GET | `/api/v1/system-boards` | ✅ | -| 시스템 게시판 상세 | GET | `/api/v1/system-boards/{code}` | ✅ | -| 시스템 게시판 필드 | GET | `/api/v1/system-boards/{code}/fields` | ✅ | -| 시스템 게시글 목록 | GET | `/api/v1/system-boards/{code}/posts` | ✅ | -| 시스템 게시글 상세 | GET | `/api/v1/system-boards/{code}/posts/{id}` | ✅ | -| 시스템 게시글 등록 | POST | `/api/v1/system-boards/{code}/posts` | ✅ | -| 시스템 게시글 수정 | PUT | `/api/v1/system-boards/{code}/posts/{id}` | ✅ | -| 시스템 게시글 삭제 | DELETE | `/api/v1/system-boards/{code}/posts/{id}` | ✅ | -| 시스템 댓글 CRUD | * | `/api/v1/system-boards/{code}/posts/{id}/comments/*` | ✅ | - -#### 📌 테넌트 게시판 API (Tenant Boards) - -| 기능 | Method | Endpoint (api.sam.kr) | 상태 | -|------|--------|----------------------|------| -| 테넌트 게시판 목록 | GET | `/api/v1/boards` | ✅ | -| 테넌트 게시판 상세 | GET | `/api/v1/boards/{code}` | 🔄 변경 필요 (ID→code) | -| 테넌트 게시판 필드 | GET | `/api/v1/boards/{code}/fields` | ✅ | -| 테넌트 게시글 목록 | GET | `/api/v1/boards/{code}/posts` | ✅ | -| 테넌트 게시글 상세 | GET | `/api/v1/boards/{code}/posts/{id}` | ✅ | -| 테넌트 게시글 등록 | POST | `/api/v1/boards/{code}/posts` | ✅ | -| 테넌트 게시글 수정 | PUT | `/api/v1/boards/{code}/posts/{id}` | ✅ | -| 테넌트 게시글 삭제 | DELETE | `/api/v1/boards/{code}/posts/{id}` | ✅ | -| 테넌트 댓글 CRUD | * | `/api/v1/boards/{code}/posts/{id}/comments/*` | ✅ | - -#### 📌 관리자 게시판 API (Admin - mng.sam.kr) - -| 기능 | Method | Endpoint (mng.sam.kr) | 상태 | -|------|--------|----------------------|------| -| 전체 게시판 목록 | GET | `/boards` (Blade) | ✅ | -| 게시판 등록 | POST | `/boards` | ✅ | -| 게시판 수정 | PUT | `/boards/{id}` | ✅ | -| 게시판 삭제 | DELETE | `/boards/{id}` | ✅ | -| **게시판 CRUD 시 메뉴 자동 연동** | - | mng + api 프로젝트 | ✅ 완료 | - -#### 📌 테넌트 게시판 메뉴 연동 (api 프로젝트) - -> **2025-12-29 추가**: 테넌트 게시판 생성/수정/삭제 시 메뉴 자동 연동 - -| 기능 | 트리거 | 메뉴 처리 | 상태 | -|------|--------|----------|------| -| 게시판 생성 | `BoardService::createTenantBoard()` | `/board` 하위에 메뉴 자동 추가 | ✅ | -| 게시판 수정 | `BoardService::updateTenantBoard()` | 코드/이름 변경 시 메뉴 URL/이름 동기화 | ✅ | -| 게시판 삭제 | `BoardService::deleteTenantBoard()` | 메뉴 Soft Delete | ✅ | - -**구현 파일:** -- `api/app/Services/MenuService.php` - 게시판 메뉴 연동 메서드 추가 -- `api/app/Services/Boards/BoardService.php` - MenuService 호출 로직 추가 - -#### 🏗️ 게시판 시스템 아키텍처 (참조용) - -**EAV (Entity-Attribute-Value) 패턴 기반 통합 게시판:** -``` -boards (게시판 정의) -├── board_settings (EAV 필드 스키마) -├── posts (게시글) -│ └── post_custom_field_values (EAV 값 저장) -└── 첨부파일 (Polymorphic: files → fileable) -``` - -**게시판 모델 주요 필드:** -```typescript -interface Board { - id: number; - tenant_id?: number; // null = 시스템 게시판 - is_system: boolean; // 시스템/테넌트 구분 - board_type: string; // notice, qna, faq, free, gallery, download - board_code: string; // 고유 코드 - name: string; - description?: string; - editor_type: 'wysiwyg' | 'markdown' | 'text'; - allow_files: boolean; - max_file_count: number; - max_file_size: number; // KB - extra_settings: { // JSON - allow_comment?: boolean; - allow_secret?: boolean; - write_roles?: string[]; - read_roles?: string[]; - }; - is_active: boolean; -} - -interface BoardSetting { // EAV 필드 스키마 - id: number; - board_id: number; - name: string; // 필드명 (예: 카테고리) - field_key: string; // 필드 키 (예: category) - field_type: 'text' | 'number' | 'select' | 'date' | 'textarea' | 'checkbox' | 'radio' | 'file'; - field_meta?: { // JSON (select 옵션, 기본값 등) - options?: string[]; - default?: string; - }; - is_required: boolean; - sort_order: number; -} -``` - -**템플릿 시스템 (`config/board_templates.php`):** -- **시스템 템플릿**: notice, qna, faq, popup (본사 ↔ 테넌트 소통용) -- **테넌트 템플릿**: free, gallery, download, notice, qna (테넌트 내부용) - -#### 📋 React 연동 작업 현황 - -| # | 페이지 | React 경로 | 조회 | 등록 | 수정 | 삭제 | API 연동 전략 | -|---|--------|-----------|------|------|------|------|--------------| -| J-1 | 게시판 목록 | `/board` | ✅ | ⏭️ | ⏭️ | ✅ | ✅ 완료 (2025-12-29) - `actions.ts` getPosts/getMyPosts | -| J-2 | 게시글 상세 | `/board/[boardCode]/[postId]` | ✅ | ⏭️ | ⏭️ | ✅ | ✅ 완료 (2025-12-29) - `actions.ts` getPost/deletePost | -| J-3 | 게시글 작성/수정 | `/board/[boardCode]/[postId]/edit` | ✅ | ✅ | ✅ | ⏭️ | ✅ 완료 (2025-12-29) - `actions.ts` createPost/updatePost | -| J-4 | 게시판 관리 | `/board/board-management` | ✅ | ✅ | ✅ | ✅ | ✅ 완료 (2025-12-27) | - -> ✅ **Phase J 완료** (2025-12-29): 모든 게시판 Mock → API 연동 완료 - -**파일 구조 (완료):** -``` -components/board/ -├── types.ts ← ✅ Post, Comment, PostApiData 등 API 타입 정의 -├── actions.ts ← ✅ Server Actions (getPosts, getPost, createPost, updatePost, deletePost) -├── BoardForm/ ← ✅ getBoards + createPost/updatePost API 연동 -├── BoardDetail/ ← ✅ 게시글 상세 + 삭제 API 연동 -├── BoardList/ ← ✅ 게시판별 필터링 + 페이지네이션 + 삭제 -└── BoardManagement/ - ├── types.ts ← ✅ Board 관리 타입 - ├── actions.ts ← ✅ getBoards, createBoard, updateBoard, deleteBoard - └── index.tsx ← ✅ 게시판 CRUD 완료 -``` - -**라우트 변경:** -- 기존: `/board/[id]` → 신규: `/board/[boardCode]/[postId]` -- 삭제된 파일: `board/[id]/page.tsx`, `board/[id]/edit/page.tsx` - ---- - -### 2.2 Phase K: 고객센터 - -> **상태**: ✅ React 연동 완료 (2025-12-29) - `shared/actions.ts` 통해 시스템 게시판 API 호출 - -#### 🎯 통합 전략: 고객센터 = 게시판 템플릿 활용 - -각 고객센터 메뉴를 별도 `board_code`로 생성하여 통합 관리: - -| 메뉴 | board_code | board_type | 템플릿 | 커스텀 필드 | -|------|------------|------------|--------|------------| -| FAQ | `system-faq` | faq | system/faq | category (select) | -| 공지사항 | `system-notice` | notice | system/notice | category (select) | -| 이벤트 | `system-event` | notice | - | start_date, end_date, image_url | -| 1:1 문의 | `system-qna` | qna | system/qna | inquiry_type, answer_status | - -**장점:** -- 코드 중복 제거 (게시판 CRUD 재사용) -- 커스텀 필드로 각 메뉴 특성 반영 -- 관리자 UI 통합 (mng.sam.kr/boards에서 일괄 관리) - -#### 📋 React 연동 작업 현황 - -| # | 페이지 | React 경로 | 조회 | 등록 | 수정 | 삭제 | API 연동 전략 | -|---|--------|-----------|------|------|------|------|--------------| -| K-1 | FAQ 관리 | `/customer-center/faq` | ✅ | ⏭️ | ⏭️ | ⏭️ | ✅ 완료 (2025-12-29) - `shared/actions.ts` | -| K-2 | 이벤트 관리 | `/customer-center/events` | ✅ | ⏭️ | ⏭️ | ⏭️ | ✅ 완료 (2025-12-29) - `shared/actions.ts` | -| K-3 | 공지사항 관리 | `/customer-center/notices` | ✅ | ⏭️ | ⏭️ | ⏭️ | ✅ 완료 (2025-12-29) - `shared/actions.ts` | -| K-4 | 문의 관리 | `/customer-center/inquiries` | ✅ | ✅ | ✅ | ✅ | ✅ 완료 (2025-12-29) - `shared/actions.ts` + 댓글 CRUD | - -**파일 구조 → 연동 계획:** -``` -components/customer-center/ -├── shared/ -│ ├── types.ts ← 🆕 공통 Post, BoardField 타입 -│ ├── actions.ts ← 🆕 게시글 CRUD Server Actions (board_code 파라미터) -│ └── PostForm.tsx ← 🆕 동적 폼 (커스텀필드 기반) -├── FAQManagement/ -│ ├── types.ts ← 🔄 FAQ 특화 타입 (category 필드) -│ ├── actions.ts ← 🆕 board_code='system-faq' 고정 -│ └── FAQList.tsx ← 🔄 카테고리별 그룹핑 UI -├── EventManagement/ -│ ├── types.ts ← 🔄 Event 특화 타입 (start_date, end_date) -│ ├── actions.ts ← 🆕 board_code='system-event' 고정 -│ └── EventList.tsx ← 🔄 진행중/예정/종료 필터 -├── NoticeManagement/ -│ ├── types.ts ← 🔄 Notice 특화 타입 -│ ├── actions.ts ← 🆕 board_code='system-notice' 고정 -│ └── NoticeList.tsx ← 🔄 공지 목록 UI -└── InquiryManagement/ - ├── types.ts ← 🔄 Inquiry 특화 타입 (answer_status) - ├── actions.ts ← 🆕 board_code='system-qna' 고정 - ├── InquiryList.tsx ← 🔄 답변대기/완료 필터 - ├── InquiryDetail.tsx ← 🔄 문의 상세 + 답변 작성 - └── InquiryForm.tsx ← 🔄 문의 등록 폼 -``` - -#### 🛠️ 구현 순서 (권장) - -**Step 1: mng에서 시스템 게시판 생성** -``` -mng.sam.kr/boards → 템플릿으로 생성: -- system-faq (FAQ 템플릿) -- system-notice (공지사항 템플릿) -- system-event (커스텀: 이벤트) -- system-qna (1:1문의 템플릿) -``` - -**Step 2: React 공통 모듈 개발** (✅ 게시판/게시글 API 이미 완비) -``` -react/src/lib/board/ -├── types.ts ← API 타입 정의 -├── actions.ts ← 게시글 CRUD Server Actions -└── utils.ts ← 커스텀필드 렌더링 유틸 -``` - -**Step 3: 각 메뉴별 연동** -``` -K-3 공지사항 (가장 단순) → K-1 FAQ → K-2 이벤트 → K-4 문의 (가장 복잡) -``` - -#### 💡 커스텀 필드 동적 렌더링 전략 - -```typescript -// 게시판 필드 스키마 기반 동적 폼 생성 -function renderCustomField(field: BoardSetting, value: string | null) { - switch (field.field_type) { - case 'text': return ; - case 'select': return