chore: 완료 계획 문서 22개 archive 이동 및 인덱스 업데이트
- 완료된 계획 문서 22개를 plans/archive/로 이동 - tracked 16개 (git mv): bending-lot-pipeline, docs-update, fcm-notification 등 - untracked 6개 (mv): bending-worklog, formula-engine, mng-item 등 - index_plans.md 전면 업데이트 - 진행중 44개 / 완료 37개 현황 반영 - 각 문서별 실제 진행률 기재 (0%~94%) - 카테고리별 재정리 (견적/생산/품목/문서/마이그레이션/시스템/UI) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
406
plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md
Normal file
406
plans/archive/AI_리포트_키워드_색상체계_가이드_v1.4.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# 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일 이내 | 정상 발행 | - |
|
||||
|
||||
---
|
||||
|
||||
*— 문서 끝 —*
|
||||
128
plans/archive/SEEDERS_LIST.md
Normal file
128
plans/archive/SEEDERS_LIST.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# 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개)
|
||||
434
plans/archive/api-analysis-report.md
Normal file
434
plans/archive/api-analysis-report.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# 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 스킬로 생성되었습니다.*
|
||||
1097
plans/archive/bending-lot-pipeline-dev-plan.md
Normal file
1097
plans/archive/bending-lot-pipeline-dev-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
860
plans/archive/bending-worklog-reimplementation-plan.md
Normal file
860
plans/archive/bending-worklog-reimplementation-plan.md
Normal file
@@ -0,0 +1,860 @@
|
||||
# 절곡 작업일지 완전 재구현 계획
|
||||
|
||||
> **작성일**: 2026-02-19
|
||||
> **목적**: PHP viewBendingWork_slat.php와 동일한 구조로 React BendingWorkLogContent.tsx 완전 재구현
|
||||
> **기준 문서**: `5130/output/viewBendingWork_slat.php` (~1400줄)
|
||||
> **상태**: ✅ 구현 완료 (커밋: 59b9b1b)
|
||||
|
||||
---
|
||||
|
||||
## 📍 현재 진행 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| **마지막 완료 작업** | Phase 1~5 전체 구현 완료 + 슬랫 입고 LOT NO 개소별 표시 버그 수정 |
|
||||
| **다음 작업** | 실 데이터 테스트 (bending_info가 채워진 작업지시로 화면 확인) |
|
||||
| **진행률** | 15/15 (100%) |
|
||||
| **마지막 업데이트** | 2026-02-19 |
|
||||
| **Git 커밋** | `59b9b1b` feat(WEB): 절곡 작업일지 완전 재구현 + 슬랫 입고 LOT NO 개소별 표시 수정 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 배경
|
||||
|
||||
현재 React `BendingWorkLogContent.tsx`는 **빈 껍데기 상태**로, 단순 테이블에 `item.productName`, `item.specification`, `item.quantity`만 평면 나열함. PHP 원본(`viewBendingWork_slat.php`)의 4개 카테고리 구조를 전혀 지원하지 않음.
|
||||
|
||||
**현재 React 컴포넌트 상태:**
|
||||
- 헤더 + 결재란 (ConstructionApprovalTable 사용) ✅
|
||||
- 신청업체 / 신청내용 테이블 ✅
|
||||
- 제품 정보 테이블 (빈 칸) ❌ 데이터 바인딩 없음
|
||||
- 작업내역 (유형명/세부품명/재질/LOT/길이/수량) ❌ 단순 flat 리스트
|
||||
- 생산량 합계 [kg] SUS/EGI ❌ 빈 칸
|
||||
- **4개 카테고리 섹션 완전 부재** ❌
|
||||
|
||||
**PHP 원본 구조 (구현 목표):**
|
||||
- 가이드레일: 벽면형/측면형 분류, 이미지 + 세부품명별 길이/수량/LOT NO/무게 계산
|
||||
- 하단마감재: 3000/4000mm 길이별 수량, 별도마감재
|
||||
- 셔터박스: 동적 이미지 + 구성요소(전면부/린텔부/점검구/후면코너부/상부덮개/측면부)
|
||||
- 연기차단재: W50 레일용, W80 케이스용
|
||||
- 생산량 합계: SUS(7.93g/cm3) / EGI(7.85g/cm3) 무게 자동 계산
|
||||
|
||||
### 1.2 데이터 흐름 (전체 파이프라인)
|
||||
|
||||
```
|
||||
[수주 시스템]
|
||||
order_nodes.options.bending_info (JSON)
|
||||
│
|
||||
▼ WorkOrderService.php (Line 276)
|
||||
│ $nodeOptions['bending_info'] ?? null
|
||||
│
|
||||
▼
|
||||
work_order_items.options (JSON)
|
||||
│ { floor, code, width, height, bending_info, slat_info, cutting_info, wip_info }
|
||||
│
|
||||
▼ API GET /work-orders/{id} → items[].options.bending_info
|
||||
│
|
||||
▼ Frontend getWorkOrderById() → WorkOrder.items
|
||||
│
|
||||
▼ WorkLogModal.tsx (Line 207-213)
|
||||
│ <BendingWorkLogContent data={order} />
|
||||
│ ※ 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<number, string> = {
|
||||
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 스킬로 생성되었습니다.*
|
||||
309
plans/archive/docs-update-plan.md
Normal file
309
plans/archive/docs-update-plan.md
Normal file
@@ -0,0 +1,309 @@
|
||||
# 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 스킬로 생성되었습니다.*
|
||||
28
plans/archive/document-management-system-changelog.md
Normal file
28
plans/archive/document-management-system-changelog.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 문서관리 시스템 - 변경 이력
|
||||
|
||||
> **본 문서**: `docs/plans/document-management-system-plan.md`의 변경 이력
|
||||
> **최종 업데이트**: 2026-01-31
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
|
||||
| 날짜 | 항목 | 변경 내용 | 관련 섹션 | 승인 |
|
||||
|------|------|----------|----------|------|
|
||||
| 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 | - |
|
||||
375
plans/archive/document-system-product-inspection.md
Normal file
375
plans/archive/document-system-product-inspection.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# Phase 5.2: 제품검사(FQC) 폼 구현 계획
|
||||
|
||||
> **작성일**: 2026-02-10
|
||||
> **마스터 문서**: [`document-system-master.md`](./document-system-master.md)
|
||||
> **상태**: 🔄 진행 중
|
||||
> **선행 조건**: Phase 5.0 (공통: 검사기준서↔컬럼 연동) 완료 필요, Phase 5.1과 병렬 진행 가능
|
||||
> **최종 분석일**: 2026-02-12
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 목적
|
||||
mng에서 제품검사(FQC) 양식 템플릿을 관리하고, React 품질관리 화면(`/quality/inspections`)에서 수주건의 **개소별** 제품검사 문서를 생성/입력/결재할 수 있도록 한다.
|
||||
|
||||
### 1.2 제품검사 = 품질검사
|
||||
- 동일 개념. "제품검사(FQC: Final Quality Control)"로 통일
|
||||
- 수주건(Order) + 개소(OrderItem) 단위로 관리
|
||||
- **전수검사**: 수주 50개소 → 제품검사 문서 50건 생성
|
||||
|
||||
### 1.3 현재 상태 (2026-02-12 분석)
|
||||
|
||||
| 항목 | 상태 | 비고 |
|
||||
|------|:----:|------|
|
||||
| React InspectionManagement | ✅ | `components/quality/InspectionManagement/` - 요청관리 CRUD (목록/등록/상세/캘린더) |
|
||||
| React ProductInspectionDocument | ✅ | `quality/qms/components/documents/` - 하드코딩 11개 항목 |
|
||||
| React 제품검사 모달 | ✅ | InspectionReportModal, ProductInspectionInputModal |
|
||||
| React 문서시스템 뷰어 | ✅ | `components/document-system/` - DocumentViewer, TemplateInspectionContent |
|
||||
| API Inspection 모델 | ✅ | `/api/v1/inspections` - JSON 기반, 단순 status (waiting→completed) |
|
||||
| API Document 모델 | ✅ | EAV 정규화, 결재 워크플로우 (DRAFT→APPROVED) |
|
||||
| mng 양식 템플릿 | ❌ | 미존재 (신규 생성 필요) |
|
||||
| 개소별 문서 자동생성 | ❌ | 미구현 |
|
||||
|
||||
### 1.4 핵심 발견 사항
|
||||
|
||||
**두 개의 독립적 검사 시스템 존재:**
|
||||
|
||||
| 시스템 | 데이터 모델 | 특징 |
|
||||
|--------|------------|------|
|
||||
| InspectionManagement | `inspections` 테이블 (JSON) | 요청관리, 단순 상태, 결재 없음 |
|
||||
| Document System | `documents` 테이블 (EAV) | 양식 기반, 결재 워크플로우, 이력 관리 |
|
||||
|
||||
**세 가지 검사항목 세트 발견:**
|
||||
|
||||
| 출처 | 항목 | 용도 |
|
||||
|------|------|------|
|
||||
| types.ts ProductInspectionData | 겉모양(가공/재봉/조립/연기차단재/하단마감재), 모터, 재질/치수, 시험 | 공장출하검사 |
|
||||
| 계획문서 (이 문서) | 외관, 작동, 개폐속도, 방연/차연/내화, 안전, 비상개방, 전기배선, 설치, 부속 | **설치 후 최종검사 ← 채택** |
|
||||
| QMS ProductInspectionDocument | 가공상태, 외관검사, 절단면, 도포상태, 조립, 슬릿, 규격치수, 마감처리, 내벽/마감/배색시트 | 제조품질검사 |
|
||||
|
||||
### 1.5 통합 전략 (확정)
|
||||
|
||||
> **InspectionManagement의 요청관리 흐름(목록/등록/상세/캘린더)은 유지하고,
|
||||
> 검사 성적서 생성/입력/결재만 documents 시스템으로 전환한다.**
|
||||
|
||||
- `inspections` 테이블: 검사 요청/일정/상태 관리 (meta 정보) → **유지**
|
||||
- `documents` 테이블: 검사 성적서 (양식 기반 상세 데이터, 결재) → **신규 연동**
|
||||
- 연결: `documents.linkable_type = 'order_item'`, `document_links`로 Order/Inspection 연결
|
||||
- 기존 InspectionReportModal/ProductInspectionInputModal → TemplateInspectionContent 기반 전환
|
||||
|
||||
### 1.6 성공 기준
|
||||
1. mng에서 제품검사 양식 편집/미리보기 정상 동작
|
||||
2. 수주 1건 선택 시 개소(OrderItem) 수만큼 Document 자동생성
|
||||
3. 각 Document에 해당 개소의 정보(층-부호, 규격, 수량) 자동매핑
|
||||
4. 개소별 검사 데이터 입력/저장/조회 가능
|
||||
5. 결재 워크플로우 정상 동작
|
||||
6. 기존 InspectionManagement 요청관리 기능 정상 유지
|
||||
|
||||
---
|
||||
|
||||
## 2. 데이터 흐름
|
||||
|
||||
```
|
||||
Order (수주)
|
||||
├─ order_no: "KD-TS-260210-01"
|
||||
├─ client_name: "발주처명"
|
||||
├─ site_name: "현장명"
|
||||
├─ quantity: 50 (총 개소 수)
|
||||
└─ items: OrderItem[] (50건)
|
||||
├─ [0] floor_code="1F", symbol_code="A", specification="W7400×H2950"
|
||||
├─ [1] floor_code="1F", symbol_code="B", specification="W5200×H3100"
|
||||
└─ [49] ...
|
||||
|
||||
제품검사 요청 시:
|
||||
↓
|
||||
Document (50건 자동생성)
|
||||
├─ Document[0]
|
||||
│ ├─ template_id → 제품검사 양식
|
||||
│ ├─ linkable_type = 'App\Models\OrderItem'
|
||||
│ ├─ linkable_id = OrderItem[0].id
|
||||
│ ├─ document_no = "FQC-260210-01"
|
||||
│ ├─ title = "제품검사 - 1F-A (W7400×H2950)"
|
||||
│ └─ document_data (EAV)
|
||||
│ ├─ 기본필드: 납품명, 제품명, 발주처, LOT NO, 로트크기, 검사일자, 검사자
|
||||
│ ├─ 검사데이터: 11개 항목별 적합/부적합
|
||||
│ └─ Footer: 종합판정(합격/불합격)
|
||||
├─ Document[1] → OrderItem[1]
|
||||
└─ Document[49] → OrderItem[49]
|
||||
|
||||
+ document_links 연결:
|
||||
├─ link_key="order" → Order.id
|
||||
└─ link_key="inspection" → Inspection.id (있는 경우)
|
||||
```
|
||||
|
||||
### 2.1 linkable 다형성 연결
|
||||
|
||||
| 필드 | 값 | 설명 |
|
||||
|------|-----|------|
|
||||
| `linkable_type` | `App\Models\OrderItem` | OrderItem 모델 |
|
||||
| `linkable_id` | OrderItem.id | 개소 PK |
|
||||
|
||||
추가로 `document_links` 테이블을 통해:
|
||||
- Order(수주) 연결: link_key="order"
|
||||
- Inspection(검사요청) 연결: link_key="inspection" (InspectionManagement에서 연결 시)
|
||||
- Process(공정) 연결: link_key="process" (해당되는 경우)
|
||||
|
||||
---
|
||||
|
||||
## 3. 작업 항목
|
||||
|
||||
| # | 작업 | 상태 | 완료 기준 | 비고 |
|
||||
|---|------|:----:|----------|------|
|
||||
| 5.2.1 | mng 제품검사 양식 시더 생성 | ✅ | ProductInspectionTemplateSeeder 작성 (template_id: 65). 결재3+기본필드7+섹션2+항목11+section_fields | 2026-02-12 |
|
||||
| 5.2.2 | mng 양식 편집/미리보기 검증 | ✅ | 양식 edit → 미리보기 → 저장 정상 동작 확인 | 2026-02-12 |
|
||||
| 5.2.3 | API 개소별 문서 일괄생성 | ✅ | `POST /api/v1/documents/bulk-create-fqc` + `GET /api/v1/documents/fqc-status`. DocumentService에 bulkCreateFqc/fqcStatus 추가 | 2026-02-12 |
|
||||
| 5.2.4 | React 제품검사 모달 → 양식 기반 전환 | ✅ | fqcActions.ts + FqcDocumentContent.tsx 신규. InspectionReportModal/ProductInspectionInputModal 듀얼모드(FQC/legacy) | 2026-02-12 |
|
||||
| 5.2.5 | 개소 목록/진행현황 UI | ✅ | InspectionDetail에 FQC 진행현황 통계 바 + 개소별 상태 뱃지(합격/불합격/진행중/미생성) + 조회 버튼 | 2026-02-12 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 제품검사 항목 (설치 후 최종검사 11항목 - 확정)
|
||||
|
||||
| # | 카테고리 | 검사항목 | 검사기준 | 검사방식 | 측정유형 |
|
||||
|---|---------|---------|---------|---------|---------|
|
||||
| 1 | 외관 | 외관검사 | 사용상 결함이 없을 것 | visual | checkbox |
|
||||
| 2 | 기능 | 작동상태 | 정상 작동 | visual | checkbox |
|
||||
| 3 | 기능 | 개폐속도 | 규정 속도 범위 이내 | visual | checkbox |
|
||||
| 4 | 성능 | 방연성능 | 기준 적합 | visual | checkbox |
|
||||
| 5 | 성능 | 차연성능 | 기준 적합 | visual | checkbox |
|
||||
| 6 | 성능 | 내화성능 | 기준 적합 | visual | checkbox |
|
||||
| 7 | 안전 | 안전장치 | 정상 작동 | visual | checkbox |
|
||||
| 8 | 안전 | 비상개방 | 정상 작동 | visual | checkbox |
|
||||
| 9 | 설치 | 전기배선 | 규정 적합 | visual | checkbox |
|
||||
| 10 | 설치 | 설치상태 | 규정 적합 | visual | checkbox |
|
||||
| 11 | 부속 | 부속품 | 누락 없음 | visual | checkbox |
|
||||
|
||||
**특성:**
|
||||
- 모든 항목이 visual/checkbox (적합/부적합)
|
||||
- numeric 측정값 없음 → columns 구조가 중간검사보다 훨씬 단순
|
||||
- **columns 자동 파생(방안1)**: checkbox → 판정(select) 컬럼
|
||||
|
||||
**결재라인**: 작성(품질) → 검토(품질QC) → 승인(경영)
|
||||
**Footer**: 부적합 내용 + 종합판정(합격/불합격)
|
||||
**자동판정**: 모든 항목 적합 → 합격, 1개라도 부적합 → 불합격
|
||||
|
||||
### 4.1 양식 시더 구조 (MidInspectionTemplateSeeder 패턴)
|
||||
|
||||
```php
|
||||
// ProductInspectionTemplateSeeder
|
||||
[
|
||||
'name' => '제품검사 성적서',
|
||||
'category' => '품질/제품검사',
|
||||
'title' => '제 품 검 사 성 적 서',
|
||||
'company_name' => '케이디산업',
|
||||
'footer_remark_label' => '부적합 내용',
|
||||
'footer_judgement_label' => '종합판정',
|
||||
'footer_judgement_options' => ['합격', '불합격'],
|
||||
|
||||
'approval_lines' => [
|
||||
['name' => '작성', 'dept' => '품질', 'role' => '담당자', 'sort_order' => 1],
|
||||
['name' => '검토', 'dept' => '품질', 'role' => 'QC', 'sort_order' => 2],
|
||||
['name' => '승인', 'dept' => '경영', 'role' => '대표', 'sort_order' => 3],
|
||||
],
|
||||
|
||||
'basic_fields' => [
|
||||
['label' => '납품명', 'field_type' => 'text'],
|
||||
['label' => '제품명', 'field_type' => 'text'],
|
||||
['label' => '발주처', 'field_type' => 'text'],
|
||||
['label' => 'LOT NO', 'field_type' => 'text'],
|
||||
['label' => '로트크기', 'field_type' => 'text'],
|
||||
['label' => '검사일자', 'field_type' => 'date'],
|
||||
['label' => '검사자', 'field_type' => 'text'],
|
||||
],
|
||||
|
||||
'sections' => [
|
||||
[
|
||||
'title' => '제품검사 기준서',
|
||||
'items' => [], // 기준서 섹션 (빈 섹션, 향후 확장)
|
||||
],
|
||||
[
|
||||
'title' => '제품검사 DATA',
|
||||
'items' => [
|
||||
['category' => '외관', 'item' => '외관검사', ...],
|
||||
// ... 11개 항목 (모두 visual/checkbox)
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// columns는 자동 파생 (Phase 5.0 방안1)
|
||||
// checkbox → [NO, 검사항목, 검사기준, 판정(select)]
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 개소별 문서 일괄생성 로직
|
||||
|
||||
### 5.1 API 엔드포인트 (계획)
|
||||
|
||||
```
|
||||
POST /api/v1/orders/{orderId}/create-fqc
|
||||
Request: { template_id: number }
|
||||
Response: { documents: Document[], created_count: number }
|
||||
```
|
||||
|
||||
### 5.2 생성 로직
|
||||
|
||||
```php
|
||||
// 1. Order + OrderItems 조회
|
||||
$order = Order::with('items')->findOrFail($orderId);
|
||||
|
||||
// 2. 개소별 Document 생성
|
||||
foreach ($order->items as $index => $orderItem) {
|
||||
$document = Document::create([
|
||||
'template_id' => $templateId,
|
||||
'document_no' => "FQC-" . date('ymd') . "-" . str_pad($index + 1, 2, '0', STR_PAD_LEFT),
|
||||
'title' => "제품검사 - {$orderItem->floor_code}-{$orderItem->symbol_code} ({$orderItem->specification})",
|
||||
'status' => DocumentStatus::DRAFT,
|
||||
'linkable_type' => OrderItem::class,
|
||||
'linkable_id' => $orderItem->id,
|
||||
]);
|
||||
|
||||
// 3. 기본필드 자동매핑
|
||||
$autoFillData = [
|
||||
'납품명' => $order->title,
|
||||
'제품명' => $orderItem->item_name,
|
||||
'발주처' => $order->client_name,
|
||||
'LOT NO' => $order->order_no,
|
||||
'로트크기' => "1 EA",
|
||||
];
|
||||
|
||||
// 4. document_data에 기본필드 저장
|
||||
foreach ($autoFillData as $key => $value) {
|
||||
DocumentData::create([
|
||||
'document_id' => $document->id,
|
||||
'field_key' => Str::slug($key),
|
||||
'field_value' => $value,
|
||||
]);
|
||||
}
|
||||
|
||||
// 5. document_links 연결
|
||||
DocumentLink::create([
|
||||
'document_id' => $document->id,
|
||||
'link_key' => 'order',
|
||||
'linkable_type' => Order::class,
|
||||
'linkable_id' => $order->id,
|
||||
]);
|
||||
|
||||
// 6. 결재라인 초기화
|
||||
// ... (기존 패턴 재사용)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 개소 진행현황 조회
|
||||
|
||||
```
|
||||
GET /api/v1/orders/{orderId}/fqc-status
|
||||
Response: {
|
||||
total: 50,
|
||||
inspected: 30,
|
||||
passed: 28,
|
||||
failed: 2,
|
||||
pending: 20,
|
||||
items: [
|
||||
{ order_item_id: 1, floor_code: "1F", symbol_code: "A", document_id: 101, status: "APPROVED", result: "합격" },
|
||||
{ order_item_id: 2, floor_code: "1F", symbol_code: "B", document_id: 102, status: "DRAFT", result: null },
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 핵심 파일 경로
|
||||
|
||||
### mng
|
||||
| 파일 | 용도 | 상태 |
|
||||
|------|------|:----:|
|
||||
| `mng/database/seeders/ProductInspectionTemplateSeeder.php` | 제품검사 양식 시더 | 🔄 작성 중 |
|
||||
| `mng/database/seeders/MidInspectionTemplateSeeder.php` | 참조 패턴 (중간검사) | ✅ |
|
||||
|
||||
### api
|
||||
| 파일 | 용도 | 상태 |
|
||||
|------|------|:----:|
|
||||
| `api/app/Models/Order.php` | 수주 모델 | ✅ |
|
||||
| `api/app/Models/OrderItem.php` | 수주 상세(개소) 모델 | ✅ |
|
||||
| `api/app/Models/Documents/Document.php` | 문서 모델 | ✅ |
|
||||
| `api/app/Models/Qualitys/Inspection.php` | 기존 검사 모델 (IQC/PQC/FQC) | ✅ |
|
||||
| `api/app/Http/Controllers/Api/V1/OrderController.php` | 수주 컨트롤러 (createFqc 추가 필요) | ⏳ |
|
||||
| `api/app/Services/DocumentService.php` | 문서 생성 서비스 | ✅ |
|
||||
|
||||
### react
|
||||
| 파일 | 용도 | 상태 |
|
||||
|------|------|:----:|
|
||||
| `react/src/components/quality/InspectionManagement/` | 품질검사 요청관리 (15+ 파일) | ✅ 유지 |
|
||||
| `react/src/components/quality/InspectionManagement/InspectionList.tsx` | 검사 목록 | ✅ 유지 |
|
||||
| `react/src/components/quality/InspectionManagement/InspectionDetail.tsx` | 검사 상세 | 🔄 수정 필요 |
|
||||
| `react/src/components/quality/InspectionManagement/modals/InspectionReportModal.tsx` | 성적서 모달 | 🔄 전환 필요 |
|
||||
| `react/src/components/quality/InspectionManagement/modals/ProductInspectionInputModal.tsx` | 입력 모달 | 🔄 전환 필요 |
|
||||
| `react/src/components/document-system/viewer/DocumentViewer.tsx` | 문서 뷰어 | ✅ |
|
||||
| `react/src/components/document-system/content/TemplateInspectionContent.tsx` | 양식 기반 렌더링 | ✅ |
|
||||
| `react/src/app/[locale]/(protected)/quality/qms/components/documents/ProductInspectionDocument.tsx` | 하드코딩 문서 | ❌ 대체 예정 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 기존 Inspection 모델과의 관계 (통합 전략)
|
||||
|
||||
### 7.1 현재 구조
|
||||
|
||||
```
|
||||
inspections 테이블 (JSON 기반)
|
||||
├─ inspection_type: IQC/PQC/FQC
|
||||
├─ status: waiting → in_progress → completed
|
||||
├─ meta: { ... } (JSON)
|
||||
├─ items: { ... } (JSON - 검사 결과)
|
||||
└─ extra: { ... } (JSON)
|
||||
|
||||
documents 테이블 (EAV 정규화)
|
||||
├─ template_id → document_templates
|
||||
├─ status: DRAFT → PENDING → APPROVED/REJECTED
|
||||
├─ linkable_type + linkable_id (다형성)
|
||||
├─ document_data (EAV - 섹션/컬럼/행 기반)
|
||||
└─ document_approvals (결재 이력)
|
||||
```
|
||||
|
||||
### 7.2 통합 후 구조
|
||||
|
||||
```
|
||||
InspectionManagement (요청관리 레이어) - 유지
|
||||
├─ 검사 목록/등록/상세/캘린더
|
||||
├─ inspections 테이블 (요청/일정/상태)
|
||||
└─ API: /api/v1/inspections (CRUD)
|
||||
|
||||
Document System (성적서 레이어) - 신규 연동
|
||||
├─ 양식 기반 검사 데이터 입력
|
||||
├─ documents 테이블 (EAV + 결재)
|
||||
├─ linkable → OrderItem (개소별)
|
||||
└─ document_links → Order, Inspection
|
||||
|
||||
연결 포인트:
|
||||
├─ InspectionDetail에서 "성적서 작성/조회" 시 → Document System 호출
|
||||
├─ InspectionReportModal → TemplateInspectionContent 기반 전환
|
||||
└─ ProductInspectionInputModal → 양식 기반 입력으로 전환
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 변경 이력
|
||||
|
||||
| 날짜 | 내용 |
|
||||
|------|------|
|
||||
| 2026-02-10 | Phase 5.2 계획 문서 신규 생성 |
|
||||
| 2026-02-10 | 방안1 반영: 시더에 section_fields 필수, columns 자동 파생. 선행조건 Phase 5.0 추가 |
|
||||
| 2026-02-12 | 코드베이스 분석 반영: InspectionManagement 발견, 3개 검사항목 세트 정리, 통합 전략 확정 |
|
||||
| 2026-02-12 | 설치 후 최종검사 11항목 확정, documents 기반 통합 방향 확정 |
|
||||
| 2026-02-12 | 5.2.1 ProductInspectionTemplateSeeder 작성 완료 (template_id: 65) |
|
||||
| 2026-02-12 | 5.2.2 mng 양식 편집/미리보기 검증 완료 |
|
||||
| 2026-02-12 | 5.2.3 API bulk-create-fqc + fqc-status 엔드포인트 구현 완료 |
|
||||
| 2026-02-12 | 5.2.4 React fqcActions.ts + FqcDocumentContent + 모달 듀얼모드 전환 완료 |
|
||||
| 2026-02-12 | 5.2.5 InspectionDetail FQC 진행현황 통계 바 + 개소별 상태/조회 UI 완료 |
|
||||
| 2026-02-12 | **Phase 5.2 전체 완료 (5/5)** |
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 /plan 스킬로 생성되었습니다.*
|
||||
369
plans/archive/fcm-user-targeted-notification-plan.md
Normal file
369
plans/archive/fcm-user-targeted-notification-plan.md
Normal file
@@ -0,0 +1,369 @@
|
||||
# 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 스킬로 생성되었습니다.*
|
||||
1077
plans/archive/formula-engine-real-data-plan.md
Normal file
1077
plans/archive/formula-engine-real-data-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
589
plans/archive/items-table-unification-plan.md
Normal file
589
plans/archive/items-table-unification-plan.md
Normal file
@@ -0,0 +1,589 @@
|
||||
# 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 테이블 유지 상태에서)
|
||||
# 신규 테이블만 삭제하면 됨
|
||||
```
|
||||
1293
plans/archive/kd-items-migration-plan.md
Normal file
1293
plans/archive/kd-items-migration-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
482
plans/archive/material-input-per-item-mapping-plan.md
Normal file
482
plans/archive/material-input-per-item-mapping-plan.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# 개소별 자재 투입 매핑 계획
|
||||
|
||||
> **작성일**: 2026-02-12
|
||||
> **목적**: Worker Screen 자재 투입 시 개소(work_order_item)별 매핑 추적 기능 구현
|
||||
> **기준 문서**: `docs/specs/database-schema.md`, `docs/standards/api-rules.md`
|
||||
> **상태**: 🔄 진행중
|
||||
|
||||
---
|
||||
|
||||
## 📍 현재 진행 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| **마지막 완료 작업** | Phase 1~3 전체 구현 완료 |
|
||||
| **다음 작업** | 테스트 및 검증 |
|
||||
| **진행률** | 8/8 (100%) |
|
||||
| **마지막 업데이트** | 2026-02-12 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 배경
|
||||
|
||||
현재 자재 투입은 **작업지시(WorkOrder) 단위**로만 처리됨:
|
||||
- `POST /api/v1/work-orders/{id}/material-inputs` → `{inputs: [{stock_lot_id, qty}]}`
|
||||
- `stock_transactions.reference_id` = `work_order_id` (개소 정보 없음)
|
||||
- 어떤 개소(work_order_item)에 어떤 자재가 투입되었는지 추적 불가
|
||||
|
||||
**필요**: 개소별로 자재 투입을 추적하여:
|
||||
- 개소별 투입 완료 여부 확인
|
||||
- 개소별 필요 자재 vs 실투입 비교
|
||||
- 검사서에 개소별 투입 자재 LOT 번호 기록
|
||||
|
||||
### 1.2 기준 원칙
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🎯 핵심 원칙 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 1. 신규 테이블(work_order_material_inputs)로 개소별 매핑 추적 │
|
||||
│ 2. 기존 stock_transactions 구조 변경 없음 (재고 이력은 그대로) │
|
||||
│ 3. 기존 작업지시 단위 API는 유지, 개소별 API를 추가 │
|
||||
│ 4. BOM 기반 필요 자재 계산은 기존 로직 재활용 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 변경 승인 정책
|
||||
|
||||
| 분류 | 예시 | 승인 |
|
||||
|------|------|------|
|
||||
| ✅ 즉시 가능 | 타입 정의 추가, 프론트 UI 변경 | 불필요 |
|
||||
| ⚠️ 컨펌 필요 | 새 마이그레이션, 새 API 엔드포인트, 서비스 로직 변경 | **필수** |
|
||||
| 🔴 금지 | 기존 stock_transactions 구조 변경, 기존 API 삭제 | 별도 협의 |
|
||||
|
||||
### 1.4 준수 규칙
|
||||
- `docs/standards/api-rules.md` - Service-First, FormRequest, ApiResponse::handle()
|
||||
- `docs/standards/quality-checklist.md` - 품질 체크리스트
|
||||
- `docs/specs/database-schema.md` - DB 스키마 규칙
|
||||
- MEMORY.md: 멀티테넌시 원칙 (FK/조인키만 컬럼, 나머지 options JSON)
|
||||
|
||||
---
|
||||
|
||||
## 2. 대상 범위
|
||||
|
||||
### 2.1 Phase 1: Database & Model (백엔드 기반)
|
||||
|
||||
| # | 작업 항목 | 상태 | 비고 |
|
||||
|---|----------|:----:|------|
|
||||
| 1.1 | `work_order_material_inputs` 마이그레이션 생성 | ✅ | api/ 프로젝트에서 |
|
||||
| 1.2 | `WorkOrderMaterialInput` 모델 생성 | ✅ | BelongsToTenant 필수 |
|
||||
| 1.3 | 관계 설정 (WorkOrderItem, WorkOrder) | ✅ | |
|
||||
|
||||
### 2.2 Phase 2: Backend API (서비스 + 컨트롤러)
|
||||
|
||||
| # | 작업 항목 | 상태 | 비고 |
|
||||
|---|----------|:----:|------|
|
||||
| 2.1 | `getMaterialsForItem()` 서비스 메서드 | ✅ | 개소별 BOM 자재 조회 |
|
||||
| 2.2 | `registerMaterialInputForItem()` 서비스 메서드 | ✅ | 개소별 투입 + 매핑 저장 |
|
||||
| 2.3 | `getMaterialInputsForItem()` 서비스 메서드 | ✅ | 개소별 투입 이력 조회 |
|
||||
| 2.4 | 컨트롤러 엔드포인트 추가 | ✅ | 3개 엔드포인트 |
|
||||
| 2.5 | FormRequest 생성 | ✅ | 투입 요청 검증 |
|
||||
| 2.6 | 라우트 등록 | ✅ | production.php |
|
||||
|
||||
### 2.3 Phase 3: Frontend (React)
|
||||
|
||||
| # | 작업 항목 | 상태 | 비고 |
|
||||
|---|----------|:----:|------|
|
||||
| 3.1 | Server Actions 추가 | ✅ | 개소별 API 호출 함수 |
|
||||
| 3.2 | MaterialInputModal props 확장 | ✅ | workOrderItemId 추가 |
|
||||
| 3.3 | 자재투입 버튼 → 개소별 호출 연결 | ✅ | WorkerScreen에서 |
|
||||
| 3.4 | 투입 이력/상태 표시 | ✅ | 개소 카드에 투입 완료 표시 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 상세 설계
|
||||
|
||||
### 3.1 신규 테이블: `work_order_material_inputs`
|
||||
|
||||
```sql
|
||||
CREATE TABLE work_order_material_inputs (
|
||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
work_order_id BIGINT UNSIGNED NOT NULL COMMENT '작업지시 ID',
|
||||
work_order_item_id BIGINT UNSIGNED NOT NULL COMMENT '개소(작업지시품목) ID',
|
||||
stock_lot_id BIGINT UNSIGNED NOT NULL COMMENT '투입 로트 ID',
|
||||
item_id BIGINT UNSIGNED NOT NULL COMMENT '자재 품목 ID',
|
||||
qty DECIMAL(12,3) NOT NULL COMMENT '투입 수량',
|
||||
input_by BIGINT UNSIGNED NULL COMMENT '투입자 ID',
|
||||
input_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '투입 시각',
|
||||
created_at TIMESTAMP NULL,
|
||||
updated_at TIMESTAMP NULL,
|
||||
|
||||
-- FK
|
||||
FOREIGN KEY (work_order_id) REFERENCES work_orders(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (work_order_item_id) REFERENCES work_order_items(id) ON DELETE CASCADE,
|
||||
|
||||
-- Index
|
||||
INDEX idx_womi_tenant (tenant_id),
|
||||
INDEX idx_womi_wo_item (work_order_id, work_order_item_id),
|
||||
INDEX idx_womi_lot (stock_lot_id)
|
||||
) COMMENT='개소별 자재 투입 이력';
|
||||
```
|
||||
|
||||
**설계 근거**:
|
||||
- `work_order_id`: 작업지시 단위 조회용 (기존 호환)
|
||||
- `work_order_item_id`: 개소별 매핑 핵심
|
||||
- `stock_lot_id`: 어떤 LOT에서 투입했는지
|
||||
- `item_id`: 어떤 자재(품목)인지
|
||||
- `qty`: 투입 수량
|
||||
- `input_by`, `input_at`: 투입자/시간 추적
|
||||
|
||||
### 3.2 API 엔드포인트
|
||||
|
||||
#### GET `/api/v1/work-orders/{workOrderId}/items/{itemId}/materials`
|
||||
- **용도**: 특정 개소의 BOM 기반 필요 자재 + 재고 LOT 조회
|
||||
- **응답**: 기존 `MaterialForInput[]`과 동일 구조
|
||||
- **로직**: 기존 `getMaterials()` 중 해당 item_id의 BOM만 추출
|
||||
|
||||
#### POST `/api/v1/work-orders/{workOrderId}/items/{itemId}/material-inputs`
|
||||
- **용도**: 특정 개소에 자재 투입 등록
|
||||
- **요청**:
|
||||
```json
|
||||
{
|
||||
"inputs": [
|
||||
{ "stock_lot_id": 456, "qty": 100 }
|
||||
]
|
||||
}
|
||||
```
|
||||
- **처리 순서**:
|
||||
1. `StockService::decreaseFromLot()` 호출 (기존 재고 차감 로직 재사용)
|
||||
2. `work_order_material_inputs` 레코드 생성 (개소 매핑)
|
||||
3. 감사 로그 기록
|
||||
- **응답**:
|
||||
```json
|
||||
{
|
||||
"work_order_id": 123,
|
||||
"work_order_item_id": 789,
|
||||
"material_count": 2,
|
||||
"input_results": [...],
|
||||
"input_at": "2026-02-12T14:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/api/v1/work-orders/{workOrderId}/items/{itemId}/material-inputs`
|
||||
- **용도**: 특정 개소의 투입 이력 조회
|
||||
- **응답**:
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"stock_lot_id": 456,
|
||||
"lot_no": "LOT-2026-001",
|
||||
"item_id": 100,
|
||||
"material_code": "MAT-001",
|
||||
"material_name": "내화실",
|
||||
"qty": 100,
|
||||
"unit": "EA",
|
||||
"input_by": 5,
|
||||
"input_by_name": "홍길동",
|
||||
"input_at": "2026-02-12T14:30:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 서비스 메서드 설계
|
||||
|
||||
#### WorkOrderService::getMaterialsForItem(int $workOrderId, int $itemId): array
|
||||
|
||||
```
|
||||
1. WorkOrderItem 조회 (workOrderId + itemId 검증)
|
||||
2. 해당 item의 BOM 추출
|
||||
3. BOM child_item별 required_qty = bom_qty × item.quantity
|
||||
4. 각 자재의 StockLot 조회 (FIFO)
|
||||
5. 이미 투입된 수량 차감 계산 (work_order_material_inputs에서 SUM)
|
||||
6. 반환: MaterialForInput[] (remaining_required_qty 포함)
|
||||
```
|
||||
|
||||
#### WorkOrderService::registerMaterialInputForItem(int $workOrderId, int $itemId, array $inputs): array
|
||||
|
||||
```
|
||||
DB::transaction {
|
||||
1. WorkOrderItem 조회 + 검증
|
||||
2. foreach (inputs as input):
|
||||
a. StockService::decreaseFromLot() (기존 로직 재사용)
|
||||
b. WorkOrderMaterialInput::create({
|
||||
tenant_id, work_order_id, work_order_item_id,
|
||||
stock_lot_id, item_id (로트의 품목),
|
||||
qty, input_by, input_at
|
||||
})
|
||||
3. 감사 로그 기록
|
||||
4. 결과 반환
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 프론트엔드 변경
|
||||
|
||||
#### MaterialInputModal Props 확장
|
||||
```typescript
|
||||
interface MaterialInputModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
order: WorkOrder | null;
|
||||
workOrderItemId?: number; // ← 추가: 개소 ID
|
||||
workOrderItemName?: string; // ← 추가: 개소명 (모달 헤더용)
|
||||
isCompletionFlow?: boolean;
|
||||
onComplete?: () => void;
|
||||
onSaveMaterials?: (...) => void;
|
||||
savedMaterials?: MaterialInput[];
|
||||
}
|
||||
```
|
||||
|
||||
#### Server Actions 추가
|
||||
```typescript
|
||||
// 개소별 자재 조회
|
||||
getMaterialsForItem(workOrderId: string, itemId: number): Promise<{
|
||||
success: boolean;
|
||||
data: MaterialForInput[];
|
||||
}>
|
||||
|
||||
// 개소별 자재 투입
|
||||
registerMaterialInputForItem(workOrderId: string, itemId: number, inputs: ...): Promise<{
|
||||
success: boolean;
|
||||
}>
|
||||
|
||||
// 개소별 투입 이력
|
||||
getMaterialInputsForItem(workOrderId: string, itemId: number): Promise<{
|
||||
success: boolean;
|
||||
data: MaterialInputHistory[];
|
||||
}>
|
||||
```
|
||||
|
||||
#### MaterialInputModal 로직 변경
|
||||
```
|
||||
useEffect에서:
|
||||
if (workOrderItemId) {
|
||||
getMaterialsForItem(order.id, workOrderItemId) // 개소별 조회
|
||||
} else {
|
||||
getMaterialsForWorkOrder(order.id) // 기존 전체 조회 (하위호환)
|
||||
}
|
||||
|
||||
handleSubmit에서:
|
||||
if (workOrderItemId) {
|
||||
registerMaterialInputForItem(order.id, workOrderItemId, inputs)
|
||||
} else {
|
||||
registerMaterialInput(order.id, inputs)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 기존 API와의 관계
|
||||
|
||||
```
|
||||
기존 API (유지, 하위 호환):
|
||||
GET /work-orders/{id}/materials → 전체 자재 조회
|
||||
POST /work-orders/{id}/material-inputs → 전체 단위 투입
|
||||
|
||||
신규 API (추가):
|
||||
GET /work-orders/{id}/items/{itemId}/materials → 개소별 자재 조회
|
||||
POST /work-orders/{id}/items/{itemId}/material-inputs → 개소별 투입
|
||||
GET /work-orders/{id}/items/{itemId}/material-inputs → 개소별 투입 이력
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 작업 절차
|
||||
|
||||
### Step 1: 마이그레이션 + 모델 (Phase 1)
|
||||
```
|
||||
1.1 api/ 프로젝트에서 마이그레이션 파일 생성
|
||||
- 파일: api/database/migrations/2026_02_12_XXXXXX_create_work_order_material_inputs_table.php
|
||||
- 테이블: work_order_material_inputs (섹션 3.1 참조)
|
||||
|
||||
1.2 WorkOrderMaterialInput 모델 생성
|
||||
- 파일: api/app/Models/Production/WorkOrderMaterialInput.php
|
||||
- traits: BelongsToTenant, SoftDeletes (선택)
|
||||
- $fillable: tenant_id, work_order_id, work_order_item_id, stock_lot_id, item_id, qty, input_by, input_at
|
||||
- 관계: belongsTo(WorkOrder), belongsTo(WorkOrderItem), belongsTo(StockLot)
|
||||
|
||||
1.3 기존 모델에 역관계 추가
|
||||
- WorkOrderItem: hasMany(WorkOrderMaterialInput)
|
||||
- WorkOrder: hasMany(WorkOrderMaterialInput)
|
||||
|
||||
검증: docker exec sam-api-1 php artisan migrate → 테이블 생성 확인
|
||||
```
|
||||
|
||||
### Step 2: Backend Service (Phase 2.1-2.3)
|
||||
```
|
||||
2.1 WorkOrderService에 getMaterialsForItem() 추가
|
||||
- 기존 getMaterials() 로직 재활용
|
||||
- 해당 item의 BOM만 필터링
|
||||
- 이미 투입된 수량 차감 표시
|
||||
|
||||
2.2 WorkOrderService에 registerMaterialInputForItem() 추가
|
||||
- 기존 registerMaterialInput() 로직 기반
|
||||
- work_order_material_inputs 레코드 추가 생성
|
||||
- 트랜잭션 내에서 처리
|
||||
|
||||
2.3 WorkOrderService에 getMaterialInputsForItem() 추가
|
||||
- work_order_material_inputs 조회
|
||||
- lot_no, material_name 등 조인
|
||||
|
||||
검증: API 테스트 (curl 또는 Swagger)
|
||||
```
|
||||
|
||||
### Step 3: Controller + Route (Phase 2.4-2.6)
|
||||
```
|
||||
2.4 WorkOrderController에 3개 메서드 추가
|
||||
- materialsForItem(int $workOrderId, int $itemId)
|
||||
- registerMaterialInputForItem(Request, int $workOrderId, int $itemId)
|
||||
- materialInputsForItem(int $workOrderId, int $itemId)
|
||||
|
||||
2.5 MaterialInputForItemRequest FormRequest 생성 (투입 검증)
|
||||
- inputs: required|array|min:1
|
||||
- inputs.*.stock_lot_id: required|integer
|
||||
- inputs.*.qty: required|numeric|gt:0
|
||||
|
||||
2.6 라우트 등록: api/routes/api/v1/production.php
|
||||
- Route::get('work-orders/{id}/items/{itemId}/materials', ...)
|
||||
- Route::post('work-orders/{id}/items/{itemId}/material-inputs', ...)
|
||||
- Route::get('work-orders/{id}/items/{itemId}/material-inputs', ...)
|
||||
|
||||
검증: php artisan route:list | grep material
|
||||
```
|
||||
|
||||
### Step 4: Frontend (Phase 3)
|
||||
```
|
||||
3.1 actions.ts에 3개 Server Action 추가
|
||||
- getMaterialsForItem()
|
||||
- registerMaterialInputForItem()
|
||||
- getMaterialInputsForItem()
|
||||
|
||||
3.2 MaterialInputModal 수정
|
||||
- workOrderItemId prop 추가
|
||||
- useEffect에서 조건부 API 호출
|
||||
- handleSubmit에서 조건부 API 호출
|
||||
- 모달 헤더에 개소명 표시
|
||||
|
||||
3.3 WorkerScreen에서 개소별 자재투입 연결
|
||||
- 자재투입 버튼 클릭 시 workOrderItemId 전달
|
||||
|
||||
3.4 개소 카드에 투입 상태 표시
|
||||
- 투입 완료/미완료 뱃지
|
||||
|
||||
검증: dev.sam.kr에서 실제 플로우 테스트
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 핵심 파일 참조
|
||||
|
||||
### Backend (api/)
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `app/Services/WorkOrderService.php` | getMaterials() (line 1117), registerMaterialInput() (line 1264) |
|
||||
| `app/Services/StockService.php` | decreaseFromLot() (line 618) - 재고 차감 |
|
||||
| `app/Http/Controllers/Api/V1/WorkOrderController.php` | materials(), registerMaterialInput() |
|
||||
| `routes/api/v1/production.php` (line 67-70) | 자재 관련 라우트 |
|
||||
| `app/Models/Production/WorkOrderItem.php` | 작업지시 품목 모델 |
|
||||
|
||||
### Frontend (react/)
|
||||
| 파일 | 역할 |
|
||||
|------|------|
|
||||
| `src/components/production/WorkerScreen/MaterialInputModal.tsx` | 자재 투입 모달 UI |
|
||||
| `src/components/production/WorkerScreen/actions.ts` | getMaterialsForWorkOrder(), registerMaterialInput() |
|
||||
| `src/components/production/WorkerScreen/types.ts` | MaterialForInput, MaterialInput 타입 |
|
||||
|
||||
### Database
|
||||
| 테이블 | 역할 |
|
||||
|--------|------|
|
||||
| `work_order_items` | 작업지시 품목(개소). options JSON에 공정별 상세 |
|
||||
| `stock_lots` | 재고 LOT. available_qty, fifo_order |
|
||||
| `stock_transactions` | 재고 거래 이력. reference_type='work_order_input' |
|
||||
| `work_order_material_inputs` | **신규** - 개소별 투입 매핑 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 컨펌 대기 목록
|
||||
|
||||
| # | 항목 | 변경 내용 | 영향 범위 | 상태 |
|
||||
|---|------|----------|----------|------|
|
||||
| 1 | 마이그레이션 | work_order_material_inputs 테이블 생성 | DB | ⚠️ 컨펌 필요 |
|
||||
| 2 | API 엔드포인트 3개 추가 | 개소별 자재 조회/투입/이력 | api | ⚠️ 컨펌 필요 |
|
||||
| 3 | 기존 API 유지 여부 | 작업지시 단위 API 유지 (하위호환) | api | ✅ 유지 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 변경 이력
|
||||
|
||||
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|
||||
|------|------|----------|------|------|
|
||||
| 2026-02-12 | - | 문서 초안 작성 | - | - |
|
||||
|
||||
---
|
||||
|
||||
## 8. 참고 문서
|
||||
|
||||
- **API 규칙**: `docs/standards/api-rules.md`
|
||||
- **DB 스키마**: `docs/specs/database-schema.md`
|
||||
- **품질 체크리스트**: `docs/standards/quality-checklist.md`
|
||||
- **기존 분석**: Explore Agent 분석 결과 (세션 내)
|
||||
- **품목 정책**: `docs/rules/item-policy.md` (BOM, lot_managed 등)
|
||||
- **MEMORY.md**: 멀티테넌시 원칙, 품목 options 체계
|
||||
|
||||
---
|
||||
|
||||
## 9. 검증 결과
|
||||
|
||||
### 9.1 테스트 케이스
|
||||
|
||||
| # | 시나리오 | 예상 결과 | 실제 결과 | 상태 |
|
||||
|---|---------|----------|----------|------|
|
||||
| 1 | 개소별 자재 조회 (BOM 있는 품목) | 해당 개소 BOM의 자재 + LOT 목록 반환 | | ✅ |
|
||||
| 2 | 개소별 자재 조회 (BOM 없는 품목) | 품목 자체를 자재로 반환 | | ✅ |
|
||||
| 3 | 개소별 자재 투입 | stock_lot 차감 + material_inputs 레코드 생성 | | ✅ |
|
||||
| 4 | 이미 투입된 자재 재조회 | remaining_required_qty 감소 확인 | | ✅ |
|
||||
| 5 | 가용수량 초과 투입 시도 | 에러 반환 (재고 부족) | | ✅ |
|
||||
| 6 | 투입 이력 조회 | lot_no, 자재명, 수량, 투입자 확인 | | ✅ |
|
||||
| 7 | 프론트 자재투입 모달에서 개소별 투입 | 해당 개소 자재만 표시, 투입 성공 | | ✅ |
|
||||
|
||||
### 9.2 성공 기준
|
||||
|
||||
| 기준 | 달성 | 비고 |
|
||||
|------|------|------|
|
||||
| 개소별 자재 조회 API 동작 | ✅ | BOM 기반 필터링 |
|
||||
| 개소별 자재 투입 API 동작 | ✅ | 재고 차감 + 매핑 저장 |
|
||||
| 프론트에서 개소별 투입 플로우 | ✅ | MaterialInputModal 연동 |
|
||||
| 기존 작업지시 단위 API 호환 유지 | ✅ | 기존 기능 미파손 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 자기완결성 점검 결과
|
||||
|
||||
### 10.1 체크리스트 검증
|
||||
|
||||
| # | 검증 항목 | 상태 | 비고 |
|
||||
|---|----------|:----:|------|
|
||||
| 1 | 작업 목적이 명확한가? | ✅ | 개소별 자재 투입 매핑 |
|
||||
| 2 | 성공 기준이 정의되어 있는가? | ✅ | 섹션 9.2 |
|
||||
| 3 | 작업 범위가 구체적인가? | ✅ | Phase 1-3, 8개 작업 항목 |
|
||||
| 4 | 의존성이 명시되어 있는가? | ✅ | 기존 API, StockService 재사용 |
|
||||
| 5 | 참고 파일 경로가 정확한가? | ✅ | 섹션 5 핵심 파일 참조 |
|
||||
| 6 | 단계별 절차가 실행 가능한가? | ✅ | 섹션 4 Step 1-4 |
|
||||
| 7 | 검증 방법이 명시되어 있는가? | ✅ | 섹션 9.1 테스트 케이스 |
|
||||
| 8 | 모호한 표현이 없는가? | ✅ | SQL, API 스키마 구체적 명시 |
|
||||
|
||||
### 10.2 새 세션 시뮬레이션 테스트
|
||||
|
||||
| 질문 | 답변 가능 | 참조 섹션 |
|
||||
|------|:--------:|----------|
|
||||
| Q1. 이 작업의 목적은 무엇인가? | ✅ | 1.1 배경 |
|
||||
| Q2. 어디서부터 시작해야 하는가? | ✅ | 4. 작업 절차 Step 1 |
|
||||
| Q3. 어떤 파일을 수정해야 하는가? | ✅ | 5. 핵심 파일 참조 |
|
||||
| Q4. 작업 완료 확인 방법은? | ✅ | 9. 검증 결과 |
|
||||
| Q5. 막혔을 때 참고 문서는? | ✅ | 8. 참고 문서 |
|
||||
|
||||
**결과**: 5/5 통과 → ✅ 자기완결성 확보
|
||||
|
||||
---
|
||||
|
||||
*이 문서는 /plan 스킬로 생성되었습니다.*
|
||||
837
plans/archive/mng-item-formula-integration-plan.md
Normal file
837
plans/archive/mng-item-formula-integration-plan.md
Normal file
@@ -0,0 +1,837 @@
|
||||
# MNG 품목관리 - 견적수식 엔진(FormulaEvaluatorService) 연동 계획
|
||||
|
||||
> **작성일**: 2026-02-19
|
||||
> **목적**: 가변사이즈 완제품(FG) 선택 시 오픈사이즈(W,H) 입력 → FormulaEvaluatorService로 동적 자재 산출 → 중앙 패널에 트리 표시
|
||||
> **기준 문서**: docs/plans/mng-item-management-plan.md, api/app/Services/Quote/FormulaEvaluatorService.php
|
||||
> **선행 작업**: 3-Panel 품목관리 페이지 구현 완료 (Phase 1~2 of mng-item-management-plan.md)
|
||||
> **상태**: 🔄 진행중
|
||||
|
||||
---
|
||||
|
||||
## 📍 현재 진행 상태
|
||||
|
||||
| 항목 | 내용 |
|
||||
|------|------|
|
||||
| **마지막 완료 작업** | Phase 2.5: 로딩/에러 상태 처리 (전체 구현 완료) |
|
||||
| **다음 작업** | 검증 (브라우저 테스트) |
|
||||
| **진행률** | 8/8 (100%) |
|
||||
| **마지막 업데이트** | 2026-02-19 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 배경
|
||||
|
||||
MNG 품목관리 페이지(`mng.sam.kr/item-management`)에서 완제품(FG) `FG-KQTS01-벽면형-SUS`를 선택하면 `items.bom` JSON에 등록된 정적 BOM(PT 2개: 가이드레일, 하단마감재)만 표시된다.
|
||||
그러나 견적관리(`dev.sam.kr/sales/quote-management/46`)에서는 `FormulaEvaluatorService`가 W=3000, H=3000 입력으로 17종 51개의 자재를 동적 산출한다.
|
||||
|
||||
**핵심 문제**: items.bom(정적)과 FormulaEvaluatorService(동적) 두 시스템이 분리되어 있어, 품목관리 페이지에서 실제 필요 자재를 볼 수 없다.
|
||||
|
||||
**해결**: 가변사이즈 품목(`item_details.is_variable_size = true`) 선택 시 오픈사이즈(W, H) 입력 UI를 제공하고, API의 기존 엔드포인트(`POST /api/v1/quotes/calculate/bom`)를 MNG에서 HTTP로 호출하여 산출 결과를 중앙 패널에 표시한다.
|
||||
|
||||
### 1.2 기준 원칙
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 🎯 핵심 원칙 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ - MNG에서 마이그레이션 파일 생성 금지 (API에서만) │
|
||||
│ - MNG ↔ API 통신은 HTTP API 호출 (코드 공유/직접 서비스 호출 X) │
|
||||
│ - 기존 API 엔드포인트 재사용 (POST /api/v1/quotes/calculate/bom)│
|
||||
│ - Docker nginx 내부 라우팅 + SSL 우회 패턴 사용 │
|
||||
│ - Blade + HTMX + Tailwind + Vanilla JS (Alpine.js 미사용) │
|
||||
│ - 정적 BOM과 수식 산출 결과를 탭으로 전환 가능하게 │
|
||||
│ - Controller에서 직접 DB 쿼리 금지 (Service-First) │
|
||||
│ - Controller에서 직접 validate() 금지 (FormRequest 필수) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 변경 승인 정책
|
||||
|
||||
| 분류 | 예시 | 승인 |
|
||||
|------|------|------|
|
||||
| ✅ 즉시 가능 | 서비스 생성, 컨트롤러 메서드 추가, Blade 수정, JS 추가 | 불필요 |
|
||||
| ⚠️ 컨펌 필요 | API 라우트 추가, 기존 Blade 구조 변경 | **필수** |
|
||||
| 🔴 금지 | mng에서 마이그레이션 생성, API 소스 수정 | 별도 협의 |
|
||||
|
||||
### 1.4 MNG 절대 금지 규칙
|
||||
|
||||
```
|
||||
❌ mng/database/migrations/ 에 파일 생성 금지
|
||||
❌ docker exec sam-mng-1 php artisan migrate 실행 금지
|
||||
❌ php artisan db:seed --class=*MenuSeeder 실행 금지
|
||||
❌ Controller에서 직접 DB 쿼리 금지 (Service-First)
|
||||
❌ Controller에서 직접 validate() 금지 (FormRequest 필수)
|
||||
❌ api/ 프로젝트 소스 코드 수정 금지
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 대상 범위
|
||||
|
||||
### 2.1 Phase 1: MNG 백엔드 (HTTP API 호출 서비스)
|
||||
|
||||
| # | 작업 항목 | 상태 | 비고 |
|
||||
|---|----------|:----:|------|
|
||||
| 1.1 | FormulaApiService 생성 (MNG→API HTTP 호출 래퍼) | ✅ | 신규 파일 |
|
||||
| 1.2 | ItemManagementApiController에 calculateFormula 메서드 추가 | ✅ | 기존 파일 수정 |
|
||||
| 1.3 | API 라우트 추가 (POST /api/admin/items/{id}/calculate-formula) | ✅ | 기존 파일 수정 |
|
||||
|
||||
### 2.2 Phase 2: MNG 프론트엔드 (UI 연동)
|
||||
|
||||
| # | 작업 항목 | 상태 | 비고 |
|
||||
|---|----------|:----:|------|
|
||||
| 2.1 | 중앙 패널 헤더에 탭 UI 추가 (정적 BOM / 수식 산출) | ✅ | index.blade.php 수정 |
|
||||
| 2.2 | 오픈사이즈 입력 폼 (W, H, 수량) + 산출 버튼 | ✅ | index.blade.php 수정 |
|
||||
| 2.3 | 수식 산출 결과 트리 렌더링 (카테고리 그룹별) | ✅ | JS 추가 |
|
||||
| 2.4 | 가변사이즈 품목 감지 → 자동 탭 전환 | ✅ | item-detail.blade.php + JS 수정 |
|
||||
| 2.5 | 로딩/에러 상태 처리 | ✅ | JS 추가 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 이미 구현된 코드 (선행 작업 - 수정 대상)
|
||||
|
||||
> 새 세션에서 현재 코드 상태를 파악할 수 있도록 이미 존재하는 파일 전체 목록과 핵심 구조를 기록.
|
||||
|
||||
### 3.1 파일 구조 (이미 존재)
|
||||
|
||||
```
|
||||
mng/
|
||||
├── app/
|
||||
│ ├── Http/Controllers/
|
||||
│ │ ├── ItemManagementController.php # Web (HX-Redirect 패턴)
|
||||
│ │ └── Api/Admin/
|
||||
│ │ └── ItemManagementApiController.php # API (index, bomTree, detail)
|
||||
│ ├── Models/
|
||||
│ │ ├── Items/
|
||||
│ │ │ ├── Item.php # BelongsToTenant, 관계, 스코프, 상수
|
||||
│ │ │ └── ItemDetail.php # 1:1 확장 (is_variable_size 필드 포함)
|
||||
│ │ └── Commons/
|
||||
│ │ └── File.php # 파일 모델
|
||||
│ ├── Services/
|
||||
│ │ └── ItemManagementService.php # getItemList, getBomTree, getItemDetail
|
||||
│ └── Traits/
|
||||
│ └── BelongsToTenant.php # 테넌트 격리 Trait
|
||||
├── resources/views/item-management/
|
||||
│ ├── index.blade.php # 3-Panel 메인 (★ 수정 대상)
|
||||
│ └── partials/
|
||||
│ ├── item-list.blade.php # 좌측 패널 (변경 없음)
|
||||
│ ├── bom-tree.blade.php # 중앙 패널 초기 상태 (변경 없음)
|
||||
│ └── item-detail.blade.php # 우측 패널 (★ 수정 대상)
|
||||
├── routes/
|
||||
│ ├── web.php # Route: GET /item-management (변경 없음)
|
||||
│ └── api.php # Route: items group (★ 수정 대상 - 라우트 추가)
|
||||
└── config/
|
||||
└── api-explorer.php # FLOW_TESTER_API_KEY 설정 참조
|
||||
```
|
||||
|
||||
### 3.2 현재 ItemManagementApiController 전체 (수정 대상)
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace App\Http\Controllers\Api\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\ItemManagementService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ItemManagementApiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ItemManagementService $service
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$items = $this->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
|
||||
<!-- 현재 중앙 패널 -->
|
||||
<div class="flex-1 bg-white rounded-lg shadow-sm flex flex-col overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<h2 class="text-sm font-semibold text-gray-700">BOM 구성 (재귀 트리)</h2>
|
||||
</div>
|
||||
<div id="bom-tree-container" class="flex-1 overflow-y-auto p-4">
|
||||
<p class="text-gray-400 text-center py-10">좌측에서 품목을 선택하세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FormulaApiService
|
||||
{
|
||||
/**
|
||||
* API 서버의 FormulaEvaluatorService를 HTTP로 호출하여 BOM 산출
|
||||
*
|
||||
* Docker 내부 통신 패턴:
|
||||
* - URL: https://nginx/api/v1/quotes/calculate/bom (Docker nginx 컨테이너)
|
||||
* - Host 헤더: api.sam.kr (nginx가 올바른 서버 블록으로 라우팅)
|
||||
* - SSL 우회: withoutVerifying() (내부 자체 서명 인증서)
|
||||
* - 인증: X-API-KEY 헤더 (FLOW_TESTER_API_KEY 환경변수)
|
||||
*/
|
||||
public function calculateBom(string $finishedGoodsCode, array $variables, int $tenantId): array
|
||||
{
|
||||
try {
|
||||
$apiKey = config('api-explorer.default_environments.0.api_key')
|
||||
?: env('FLOW_TESTER_API_KEY', '');
|
||||
|
||||
$response = Http::timeout(30)
|
||||
->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
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<h2 class="text-sm font-semibold text-gray-700">BOM 구성 (재귀 트리)</h2>
|
||||
</div>
|
||||
<div id="bom-tree-container" class="flex-1 overflow-y-auto p-4">
|
||||
```
|
||||
|
||||
**변경 후**:
|
||||
```html
|
||||
<div class="px-4 py-3 border-b border-gray-200">
|
||||
<div class="flex items-center gap-1">
|
||||
<button type="button" id="tab-static-bom"
|
||||
class="bom-tab px-3 py-1.5 text-xs font-medium rounded-md bg-blue-100 text-blue-800"
|
||||
onclick="switchBomTab('static')">
|
||||
정적 BOM
|
||||
</button>
|
||||
<button type="button" id="tab-formula-bom"
|
||||
class="bom-tab px-3 py-1.5 text-xs font-medium rounded-md bg-gray-100 text-gray-600 hover:bg-gray-200"
|
||||
onclick="switchBomTab('formula')"
|
||||
style="display:none;">
|
||||
수식 산출
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 수식 산출 입력 폼 (가변사이즈 품목 선택 시에만 표시) -->
|
||||
<div id="formula-input-panel" style="display:none;" class="p-4 bg-gray-50 border-b border-gray-200">
|
||||
<div class="flex items-end gap-3">
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">폭 W (mm)</label>
|
||||
<input type="number" id="input-width" value="1000" min="100" max="10000" step="1"
|
||||
class="w-24 px-2 py-1.5 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">높이 H (mm)</label>
|
||||
<input type="number" id="input-height" value="1000" min="100" max="10000" step="1"
|
||||
class="w-24 px-2 py-1.5 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:outline-none">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs text-gray-500 mb-1">수량</label>
|
||||
<input type="number" id="input-qty" value="1" min="1" max="100" step="1"
|
||||
class="w-16 px-2 py-1.5 text-sm border border-gray-300 rounded focus:ring-2 focus:ring-blue-500 focus:outline-none">
|
||||
</div>
|
||||
<button type="button" id="btn-calculate" onclick="calculateFormula()"
|
||||
class="px-4 py-1.5 text-sm font-medium text-white bg-blue-600 rounded hover:bg-blue-700 transition-colors">
|
||||
산출
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 정적 BOM 영역 -->
|
||||
<div id="bom-tree-container" class="flex-1 overflow-y-auto p-4">
|
||||
<p class="text-gray-400 text-center py-10">좌측에서 품목을 선택하세요.</p>
|
||||
</div>
|
||||
|
||||
<!-- 수식 산출 결과 영역 (초기 숨김) -->
|
||||
<div id="formula-result-container" class="flex-1 overflow-y-auto p-4" style="display:none;">
|
||||
<p class="text-gray-400 text-center py-10">오픈사이즈를 입력하고 산출 버튼을 클릭하세요.</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 2.2 item-detail.blade.php에 메타 데이터 추가
|
||||
|
||||
**수정 파일**: `mng/resources/views/item-management/partials/item-detail.blade.php`
|
||||
|
||||
**파일 맨 위에 추가** (기존 `<div class="space-y-4">` 앞):
|
||||
```html
|
||||
<!-- 품목 메타 데이터 (JS에서 가변사이즈 감지용) -->
|
||||
<div id="item-meta-data"
|
||||
data-item-id="{{ $item->id }}"
|
||||
data-item-code="{{ $item->code }}"
|
||||
data-is-variable-size="{{ $item->details?->is_variable_size ? 'true' : 'false' }}"
|
||||
style="display:none;"></div>
|
||||
```
|
||||
|
||||
#### 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 = '<div class="flex justify-center py-10"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div></div>';
|
||||
|
||||
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 = `
|
||||
<div class="text-center py-10">
|
||||
<p class="text-red-500 text-sm mb-2">${data.error || '산출 실패'}</p>
|
||||
<button onclick="calculateFormula()" class="text-blue-600 text-sm hover:underline">다시 시도</button>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
renderFormulaTree(data, container);
|
||||
})
|
||||
.catch(err => {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-10">
|
||||
<p class="text-red-500 text-sm mb-2">서버 연결 실패</p>
|
||||
<button onclick="calculateFormula()" class="text-blue-600 text-sm hover:underline">다시 시도</button>
|
||||
</div>`;
|
||||
});
|
||||
};
|
||||
|
||||
// ── 수식 산출 결과 트리 렌더링 ──
|
||||
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 = `
|
||||
<span class="text-sm font-medium text-blue-800">
|
||||
${data.finished_goods?.name || ''} (${data.finished_goods?.code || ''})
|
||||
<span class="text-xs text-blue-600 ml-2">W:${data.variables?.W0} H:${data.variables?.H0}</span>
|
||||
</span>
|
||||
<span class="text-sm font-bold text-blue-900">합계: ${Number(data.grand_total).toLocaleString()}원</span>
|
||||
`;
|
||||
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 = `
|
||||
<span class="text-xs text-gray-400">▼</span>
|
||||
<span>${groupIcons[group] || '📦'}</span>
|
||||
<span class="text-sm font-semibold text-gray-700">${groupLabels[group] || group}</span>
|
||||
<span class="text-xs text-gray-500">(${items.length}건)</span>
|
||||
<span class="ml-auto text-xs font-medium text-gray-600">소계: ${Number(subtotal).toLocaleString()}원</span>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">PT</span>
|
||||
<span class="font-mono text-xs text-gray-500 w-32 truncate">${item.item_code || ''}</span>
|
||||
<span class="text-gray-700 flex-1 truncate">${item.item_name || ''}</span>
|
||||
<span class="text-xs text-gray-500 w-16 text-right">${item.quantity || 0} ${item.unit || ''}</span>
|
||||
<span class="text-xs text-blue-600 font-medium w-20 text-right">${Number(item.total_price || 0).toLocaleString()}원</span>
|
||||
`;
|
||||
// 아이템 클릭 시 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 = '<p class="text-gray-400 text-center py-10">산출된 자재가 없습니다.</p>';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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*
|
||||
1447
plans/archive/mng-item-management-plan.md
Normal file
1447
plans/archive/mng-item-management-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
831
plans/archive/order-location-management-plan.md
Normal file
831
plans/archive/order-location-management-plan.md
Normal file
@@ -0,0 +1,831 @@
|
||||
# 수주 하위 구조 관리 시스템 구축 계획
|
||||
|
||||
> **작성일**: 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<string, unknown> | 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 (
|
||||
<div style={{ marginLeft: depth * 24 }}>
|
||||
{/* 노드 헤더 */}
|
||||
<NodeHeader node={node} />
|
||||
|
||||
{/* 해당 노드의 자재 테이블 */}
|
||||
{node.items.length > 0 && <ItemsTable items={node.items} />}
|
||||
|
||||
{/* 하위 노드 재귀 렌더링 */}
|
||||
{node.children.map(child => (
|
||||
<OrderNodeCard key={child.id} node={child} depth={depth + 1} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**역호환**:
|
||||
```typescript
|
||||
{order.nodes && order.nodes.length > 0 ? (
|
||||
order.nodes.map(node => <OrderNodeCard key={node.id} node={node} />)
|
||||
) : (
|
||||
<LegacyFlatTableView items={order.items} />
|
||||
)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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)*
|
||||
659
plans/archive/order-workorder-shipment-integration-plan.md
Normal file
659
plans/archive/order-workorder-shipment-integration-plan.md
Normal file
@@ -0,0 +1,659 @@
|
||||
# 수주-작업지시-출하 하이브리드 연동 구조 구현 계획
|
||||
|
||||
> **작성일**: 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
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('shipments', function (Blueprint $table) {
|
||||
$table->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 스킬로 생성되었습니다.*
|
||||
262
plans/archive/quote-v2-auto-calculation-fix-plan.md
Normal file
262
plans/archive/quote-v2-auto-calculation-fix-plan.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# 견적 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 스킬로 생성되었습니다.*
|
||||
1294
plans/archive/sam-stat-database-design-plan.md
Normal file
1294
plans/archive/sam-stat-database-design-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
1057
plans/archive/simulator-calculation-logic-mapping.md
Normal file
1057
plans/archive/simulator-calculation-logic-mapping.md
Normal file
File diff suppressed because it is too large
Load Diff
421
plans/archive/stock-integration-plan.md
Normal file
421
plans/archive/stock-integration-plan.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# 재고 통합 시스템 개발 계획
|
||||
|
||||
> **작성일**: 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 스킬로 생성되었습니다.*
|
||||
1021
plans/archive/welfare-section-plan.md
Normal file
1021
plans/archive/welfare-section-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user