From 4693acfd0104bc6883ba0e3e63231ea021966022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Sat, 14 Mar 2026 08:02:58 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20[plans]=20QA=20=EC=A0=90=EA=B2=80=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95=20=EA=B3=84=ED=9A=8D=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 경동dev 모듈별 기능/UI 점검 37건 이슈 분석 - Phase 0~4 단계별 수정 계획 수립 - 코드 레벨 심층 분석 (파일경로, 라인번호, 근본원인) - 컨펌 6건 결정 반영 (5건 확정, 1건 보류) --- plans/qa-bugfix-plan.md | 704 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 704 insertions(+) create mode 100644 plans/qa-bugfix-plan.md diff --git a/plans/qa-bugfix-plan.md b/plans/qa-bugfix-plan.md new file mode 100644 index 0000000..9ba5b82 --- /dev/null +++ b/plans/qa-bugfix-plan.md @@ -0,0 +1,704 @@ +# 경동dev QA 점검 이슈 수정 계획 + +> **작성일**: 2026-03-13 +> **목적**: 경동dev 모듈별 기능 및 UI 점검 결과 37건 이슈 체계적 수정 +> **기준 문서**: `경동dev_모듈별_기능 및 UI점검 - 시트1.csv` +> **상태**: 🔄 진행중 + +--- + +## 📍 현재 진행 상태 + +| 항목 | 내용 | +|------|------| +| **마지막 완료 작업** | 컨펌 대기 6건 결정 완료 | +| **다음 작업** | Phase 1: Critical 수정 | +| **진행률** | 0/37 (0%) — 컨펌 완료, 수정 대기 | +| **마지막 업데이트** | 2026-03-14 | + +--- + +## 1. 개요 + +### 1.1 배경 +경동dev 환경에서 모듈별 기능 및 UI를 점검한 결과 37건의 이슈가 발견됨. +Critical 2건은 데이터 정합성에 직접 영향을 주므로 즉시 수정 필요. + +### 1.2 이슈 통계 + +| 중요도 | 건수 | 비고 | +|--------|------|------| +| **Critical** | 2건 | 데이터 정합성 파괴 | +| **Major** | 12건 | 핵심 기능 오류 | +| **Minor** | 14건 | UI/UX 개선 | +| **확인 필요** | 9건 | 정책 확인 후 결정 | + +| 모듈 | 건수 | +|------|------| +| 견적관리 | 20건 | +| 수주관리 | 5건 | +| 거래처관리 | 2건 | +| 단가관리 | 2건 | +| 품목관리 | 2건 | +| 생산/작업지시 | 2건 | +| 공통 | 4건 | + +### 1.3 변경 승인 정책 + +| 분류 | 예시 | 승인 | +|------|------|------| +| ✅ 즉시 가능 | UI 스타일, 라벨 변경, 필터 수정 | 불필요 | +| ⚠️ 컨펌 필요 | 상태 전이 로직, 데이터 차단 로직, API 변경 | **필수** | +| 🔴 금지 | 테이블 구조 변경, 기존 데이터 마이그레이션 | 별도 협의 | + +### 1.4 준수 규칙 +- `docs/dev/standards/api-rules.md` — API 개발 규칙 +- `docs/dev/standards/quality-checklist.md` — 품질 체크리스트 +- `docs/dev/standards/git-conventions.md` — Git 커밋 컨벤션 +- `docs/features/quotes/README.md` — 견적 시스템 문서 +- `docs/rules/pricing-policy.md` — 단가 정책 + +--- + +## 2. Phase 구조 + +### 2.0 Phase 0: 사전 조사 (확인 필요 9건) — 예상 1일 + +> 코드 분석 사전 조사 완료. 정책 결정만 남음. + +| # | 이슈 | 코드 분석 결과 | 결과 | 배정 | +|---|------|---------------|------|------| +| 9 | 견적 목록 정렬 정책 | `QuoteManagementClient.tsx:297` customSortFn에서 registrationDate DESC 하드코딩. API도 동일(QuoteService:46). **정책 맞음** | ✅ 정상 | - | +| 10 | 목록 작업 컬럼 빈 값 | 어떤 데이터가 들어가야 하는지 기획 확인 필요 | ⏳ 기획 확인 (보류) | - | +| 11 | 견적 접수일 날짜 하루 밀림 | `api/config/app.php:60` timezone=Asia/Seoul ✅ 정상. `QuoteService.php:344` registration_date는 프론트 전달값 사용. **React에서 날짜 전송 시 UTC 변환 여부 확인 필요** | ⏳ 프론트 확인 | Phase 2 | +| 12 | 연락처 필수값 안내 시점 | UX 정책 결정 필요: 저장 시 vs 실시간 검증 | ⏳ UX 결정 | Phase 4 | +| 13 | 수동 품목 단가 0원 | 기획 확인 필요: 의도된 동작인지, 단가 입력 필드 필요 여부 | ⏳ 기획 확인 | Phase 2 | +| 15 | 부가세 값 저장/노출 안 됨 | **Quote 모델 fillable에 `tax_amount` 없음**. Order 모델에만 존재. 견적에 부가세 저장 로직 자체가 미구현 | 🔴 버그 확정 | Phase 2 | +| 17 | PDF 생성 안 됨 | `QuoteDocumentService.php` 존재. DomPDF 설정/폰트 확인 필요 (개발서버 환경 이슈 가능) | ⏳ 환경 확인 | Phase 3 | +| 23 | 견적 수정→저장=확정 프로세스 | **결정: 저장/확정 분리** — [저장]=draft 유지, [견적확정]=finalized 전환. 확정 후 수정 시 리비전 생성 | ✅ 결정 완료 | Phase 2 | +| 29 | 수주 수정 시 유효성 에러 | **`UpdateOrderRequest.php:40-42`에서 `items.*.item_name` required**. 프론트에서 item_name 누락 시 에러 발생 | 🔴 버그 확정 | Phase 3 | + +--- + +### 2.1 Phase 1: Critical 수정 (2건) — 예상 1일 + +> 데이터 정합성에 직접 영향. 최우선 수정. + +#### #27 수주등록 - 견적 불러오기 실패 `Critical` + +| 항목 | 내용 | +|------|------| +| **현상** | 견적 선택 시 전환 가능한 견적 0건, 데이터 호출 실패 | +| **경로** | `/sales/order-management-sales?mode=new` | +| **비고** | 견적번호 검색으로는 데이터 불러와짐 | + +**근본 원인 (코드 분석 완료):** + +1. **프론트 호출**: `react/src/components/orders/QuotationSelectDialog.tsx:46-52` + ```typescript + const handleFetchData = useCallback(async (query: string) => { + const result = await getQuotesForSelect({ q: query || undefined, size: 50 }); + // ... + }, []); + ``` + - `SearchableSelectionModal`의 `loadOnOpen` 활성화 + - 초기 로드 시 빈 검색어(`query: undefined`) 전달 + +2. **API 호출**: `react/src/components/orders/actions.ts:1246-1266` + ```typescript + url: buildApiUrl('/api/v1/quotes', { + status: 'finalized', // ← finalized 상태만 + with_items: 'true', + for_order: 'true', // ← 수주 미생성 필터 + q: params?.q, + }), + ``` + +3. **백엔드 필터**: `api/app/Services/Quote/QuoteService.php:48-59,76-81` + ```php + // for_order 필터 (라인 48-59) + if ($forOrder) { + $query->whereNull('order_id'); + $query->whereDoesntHave('orders'); + } + // status 필터 (라인 76-81) + if ($status === Quote::STATUS_CONVERTED) { + $query->whereNotNull('order_id'); + } elseif ($status) { + $query->where('status', $status)->whereNull('order_id'); + } + ``` + - `status=finalized` + `for_order=true` → `whereNull('order_id')` 2회 적용 (중복이지만 논리적 문제 없음) + - **실제 원인 추정**: `status` 컬럼에 실제로 'finalized'가 아닌 다른 값으로 저장되어 있거나, `whereDoesntHave('orders')` 관계가 잘못 설정되어 있을 가능성 + - 검색어(`q`) 입력 시에는 `search()` 메서드가 다른 필터를 우회할 수 있음 + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `api/app/Services/Quote/QuoteService.php` | 48-81 | `for_order` + `status` 필터 조건 상호작용 디버깅. `whereDoesntHave('orders')` 관계 확인 | +| `react/src/components/orders/actions.ts` | 1246-1266 | `getQuotesForSelect()` 파라미터 검증 | +| `react/src/components/orders/QuotationSelectDialog.tsx` | 46-52 | 초기 로드 시 API 응답 로그 확인 | + +**검증:** +- [ ] finalized 상태 견적이 DB에 존재하는지 직접 쿼리 +- [ ] API 직접 호출하여 응답 확인: `GET /api/v1/quotes?status=finalized&for_order=true` +- [ ] 검색어 있을 때와 없을 때 쿼리 차이 확인 (`DB::enableQueryLog()`) +- [ ] 견적 선택 → 수주 등록 정상 동작 확인 + +--- + +#### #31 생산지시 후 견적 수정 가능 → 금액 불일치 `Critical` + +| 항목 | 내용 | +|------|------| +| **현상** | 생산지시 완료된 수주의 견적을 수정 가능 → 견적-수주-생산지시 금액 불일치 | +| **경로** | `/sales/order-management-sales/[id]` → 견적 수정 | + +> **✅ 컨펌 완료 (2026-03-14)**: 수정 허용 + 변경 전파 + 리비전 관리. 생산지시 있으면 차단. + +**결정된 흐름:** +``` +견적 수정 클릭 + ├─ 생산지시 존재? → ❌ 차단 "생산지시가 진행된 건은 수정할 수 없습니다" + └─ 생산지시 없음? + ├─ 수주 연결? → ⚠️ "연결된 수주건이 함께 변경됩니다" [확인/취소] + │ ├─ [확인] → 리비전 생성(rev.N) + 견적 수정 + 수주 자동 동기화 + │ └─ [취소] → 변경 안 함 + └─ 수주 미연결 → 리비전 생성(rev.N) + 견적 수정 +``` + +**기존 리비전 시스템 (이미 구현됨):** +- `quote_revisions` 테이블 존재 (스냅샷 JSON 저장) +- `QuoteRevision` 모델 + `createRevision()` 메서드 구현 완료 +- `current_revision` 컬럼으로 수정 차수 추적 +- `syncFromQuote()` 수주 동기화 호출 코드 존재 + +**근본 원인 (코드 분석 완료):** + +1. **Quote 모델**: `api/app/Models/Quote/Quote.php:337-340` + ```php + public function isEditable(): bool + { + return true; // ← 항상 true! 상태 체크 없음 + } + ``` + +2. **QuoteService update()**: `api/app/Services/Quote/QuoteService.php:377-379` + - `isEditable()`이 항상 true이므로 이 검증이 무의미 + +3. **프론트 수정 버튼**: `react/src/components/quotes/QuoteFooterBar.tsx:142-153` + - `orderId`가 있으면 수정 버튼을 숨기지만, 백엔드에서 차단하지 않음 + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `api/app/Models/Quote/Quote.php` | 337-340 | `isEditable()` → 생산지시 존재 시 false 반환 | +| `api/app/Services/Quote/QuoteService.php` | update() | 수주 연결 시 `syncFromQuote()` 동작 검증 + 강화 | +| `react/src/components/quotes/QuoteFooterBar.tsx` | 142-153 | 생산지시 존재 시 수정 버튼 비활성화. 수주 연결 시 확인 모달 추가 | +| `react/src/components/quotes/QuoteRegistration.tsx` | - | 저장 전 "연결된 수주건이 함께 변경됩니다" 확인 모달 | + +**수정 코드 (제안):** +```php +// Quote.php - isEditable() 수정 +public function isEditable(): bool +{ + // 생산지시가 존재하는 수주에 연결된 견적은 수정 불가 + if ($this->orders()->whereHas('workOrders')->exists()) { + return false; + } + return true; // draft, finalized, converted(수주연결) 모두 수정 가능 +} +``` + +**검증:** +- [ ] 생산지시 있는 견적 수정 시 차단 메시지 노출 +- [ ] 수주 연결 견적 수정 시 확인 모달 → 수주 자동 동기화 +- [ ] 수주 미연결 견적 수정 시 정상 동작 +- [ ] 리비전 생성 확인 (revision_number 증가, previous_data 스냅샷) +- [ ] 기존 불일치 데이터 현황 파악 스크립트 실행 + +--- + +### 2.2 Phase 2: Major - 견적관리 (8건) — 예상 2.5일 + +#### 2.2.1 BOM 탭 순서 통일 (#18, #19, #20, #22) — 4건 묶음 + +| 항목 | 내용 | +|------|------| +| **현상** | 등록/상세, 엑셀/수동, 산출 전/후에서 BOM 탭 순서 불일치 + inspection 라벨 | +| **기대** | 모든 케이스에서 동일한 탭 순서 | + +> **✅ 컨펌 완료 (2026-03-14)**: **주자재 → 모터 → 제어기 → 절곡품 → 부자재 → 검사비 → 기타** (inspection → 검사비 라벨 변경 포함) + +**근본 원인 (코드 분석 완료):** + +1. **프론트 탭 생성**: `react/src/components/quotes/LocationDetailPanel.tsx:157-179` + ```typescript + const detailTabs = useMemo((): TabDefinition[] => { + const subtotals = location.bomResult.subtotals; + const tabs: TabDefinition[] = []; + Object.entries(subtotals).forEach(([key, value]) => { + tabs.push({ value: key, label: obj.name || key }); + }); + tabs.push({ value: "etc", label: "기타" }); + return tabs; + }, [location?.bomResult?.subtotals]); + ``` + - **`Object.entries()` 순서가 JavaScript 객체 키 삽입 순서에 의존** → 비결정적 + +2. **백엔드 그룹화**: `api/app/Services/Quote/FormulaEvaluatorService.php:1852-1865` + ```php + // Step 8: 카테고리별 그룹화 + $groupedItems = []; + foreach ($calculatedItems as $item) { + $category = $item['category_group']; + if (!isset($groupedItems[$category])) { + $groupedItems[$category] = [...]; + } + } + ``` + - **foreach 루프 순서가 $calculatedItems 배열 순서에 의존** → 비결정적 + +3. **카테고리명 하드코딩**: `FormulaEvaluatorService.php:1923-1933` + ```php + private function getTenantCategoryName(string $category): string { + return match ($category) { + 'material' => '주자재', 'motor' => '모터', 'controller' => '제어기', + 'steel' => '절곡품', 'parts' => '부자재', + default => $category, // ← 'inspection' 매핑 없음! + }; + } + ``` + - **'inspection' → '검사비' 매핑이 없음** → 탭 라벨 불일치 + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `react/src/components/quotes/types.ts` | 신규 | `BOM_CATEGORY_ORDER` 정렬 순서 상수 정의 | +| `react/src/components/quotes/LocationDetailPanel.tsx` | 157-179 | 탭 생성 시 `BOM_CATEGORY_ORDER` 기준 정렬 적용 | +| `api/app/Services/Quote/FormulaEvaluatorService.php` | 1852-1865 | `$groupedItems`를 카테고리 순서대로 정렬 후 반환 | +| `api/app/Services/Quote/FormulaEvaluatorService.php` | 1923-1933 | `'inspection' => '검사비'` 매핑 추가 | + +--- + +#### 2.2.2 스크린+스틸 혼합 등록 차단 (#16) + +| 항목 | 내용 | +|------|------| +| **현상** | 혼합 제품 등록 시 무조건 스크린으로 표기, 혼합 필터 미적용 | +| **기대** | ~~"혼합"으로 표시~~ → **스크린+스틸 동시 등록 자체를 차단** | + +> **✅ 컨펌 완료 (2026-03-14)**: 스크린과 스틸이 동시에 들어오는 수주는 없음 (인정 검사 때문). MIXED 타입 추가 불필요. 프론트에서 혼합 등록 밸리데이션 차단. + +**수정 방향 변경:** +- ~~MIXED 타입 추가~~ → 불필요 +- ~~deriveProductCategory() 수정~~ → 불필요 +- **스크린+스틸 동시 품목 추가 시 프론트 경고 + 차단** 추가 + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `react/src/components/quotes/QuoteRegistration.tsx` | 품목 추가 핸들러 | 기존 품목과 다른 카테고리(스크린↔스틸) 추가 시 경고 모달 + 차단 | +| `react/src/components/quotes/QuoteManagementClient.tsx` | 312-323 | 혼합 필터 옵션 제거 (데드 코드 정리) | + +--- + +#### 2.2.3 견적 저장/확정 분리 (#23) + +| 항목 | 내용 | +|------|------| +| **현상** | 견적 수정 → 저장하면 바로 확정(finalized)되어 사용자 혼란 | +| **기대** | 저장과 확정을 분리하여 명시적 프로세스 제공 | + +> **✅ 컨펌 완료 (2026-03-14)**: 저장/확정 분리 — [저장]=draft 유지, [견적확정]=finalized 전환. 확정 후 수정 시 리비전 생성. + +**결정된 흐름:** +``` +견적 등록 → [저장] → draft (자유롭게 수정, 리비전 안 쌓임) + ↓ + [견적확정] 버튼 클릭 → finalized (리비전 시작, 수주 전환 가능) + ↓ + 수정하면 → rev.N 생성 + 수주 동기화 (#31 규칙 적용) +``` + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `react/src/components/quotes/QuoteFooterBar.tsx` | 142-153 | [임시저장] → [저장], [저장] → [견적확정] 버튼 라벨/동작 분리 | +| `react/src/components/quotes/QuoteRegistration.tsx` | submit 핸들러 | 저장=draft 유지, 견적확정=finalized 전환 API 분리 | +| `api/app/Services/Quote/QuoteService.php` | update() | draft 저장 시 리비전 미생성, finalize 시에만 상태 전환 | +| `api/app/Http/Controllers/Quote/QuoteController.php` | - | `finalize()` 액션 추가 (또는 기존 활용) | + +--- + +#### 2.2.4 기타 품목 수동추가 이슈 (#14, #21) + +**근본 원인**: `LocationDetailPanel.tsx:175-176` +- 기타 탭은 `tabs.push({ value: "etc", label: "기타" })`로 항상 마지막 추가 +- **TabsList에 overflow/scroll 처리 없음** → 탭이 많으면 화면 밖으로 밀림 +- 수동 추가 시 새로운 카테고리가 생성되어 탭이 분리됨 + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `react/src/components/quotes/LocationDetailPanel.tsx` | 157-179 | 수동 추가 품목을 "기타" 탭에 병합하는 로직 | +| `react/src/components/quotes/LocationDetailPanel.tsx` | 200+ | TabsList에 `overflow-x-auto` + `flex-nowrap` 스크롤 처리 | + +--- + +#### 2.2.5 필터 셀렉트박스 라벨 (#8) + +**근본 원인**: `QuoteManagementClient.tsx:312-336` +- 두 셀렉트박스 모두 `placeholder` 설정은 있음 ("제품분류", "상태") +- 하지만 초기값이 `'all'`로 설정되어 **"전체"** 텍스트만 보임 → 어떤 필터인지 구분 불가 + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `react/src/components/quotes/QuoteManagementClient.tsx` | 312-336 | SelectTrigger 앞에 라벨 텍스트 추가 또는 "전체" → "제품분류: 전체" / "상태: 전체"로 변경 | + +--- + +#### 2.2.6 거래처 선택 시 담당자 자동 채움 (#25) + +**근본 원인**: `QuoteRegistration.tsx:776-790` +```typescript +const handleClientChange = (selectedClient: Client | null) => { + setFormData((prev) => ({ + ...prev, + clientId: String(selectedClient.id), + clientName: selectedClient.name, + manager: selectedClient?.managerName || selectedClient?.representativeName || prev.manager, + contact: selectedClient?.managerPhone || selectedClient?.mobile || selectedClient?.phone || prev.contact, + })); +}; +``` +- **자동 채움 로직은 구현되어 있음** (라인 783-784) +- **원인 추정**: 거래처 선택 모달에서 반환하는 Client 객체에 `managerName`/`managerPhone` 필드가 **포함되지 않음** (목록 조회 시 select 컬럼에서 누락) + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `react/src/components/quotes/actions.ts` 또는 `QuoteRegistration.tsx` | - | 거래처 선택 시 상세 API 추가 호출하여 담당자 정보 로드 | +| 또는 `api/app/Services/ClientService.php` | index() | 목록 조회 시 `contact_person`, `phone`, `manager_name` 컬럼 포함 | + +--- + +### 2.3 Phase 3: Major - 기타 모듈 (4건+) — 예상 2.25일 + +#### #6 거래처 등록 미노출 + +| 항목 | 내용 | +|------|------| +| **현상** | 신규 거래처 등록 시 회계관리 거래처 목록에 미노출 | + +**수정 대상:** +| 파일 | 변경 내용 | +|------|----------| +| `api/app/Services/ClientService.php` | 등록 프로세스 확인 (mng DB 동기화 여부) | +| `mng/` | 회계관리 거래처 목록 데이터 소스 확인 (samdb vs codebridge DB) | + +--- + +#### #32, #33 단가 등록/수정 오류 — 2건 묶음 + +**근본 원인 (코드 분석 완료):** + +1. **PricingService 미구현**: `api/app/Services/Pricing/PricingService.php` + ```php + public function getItemPrice(...): array { + // TODO: 실제 가격 조회 로직 구현 + // 현재는 임시로 0원 반환 + return ['price' => 0, 'warning' => null]; + } + ``` + - **store(), update() 메서드가 미구현 (TODO 상태)** + +2. **프론트 폼 필드 불일치**: `react/src/components/pricing/PricingFormClient.tsx:70-76` + ```typescript + const displayItemCode = initialData?.itemCode || itemInfo?.itemCode || ''; + ``` + - API 응답 필드명(`item_code`)과 프론트 필드명(`itemCode`) 간 매핑 불일치 가능 + +3. **FormRequest 검증 부족**: `PriceUpdateRequest.php:15-17` + ```php + 'sales_price' => 'nullable|numeric|min:0', // 0원 허용, max 제한 없음 + ``` + +> **✅ 컨펌 완료 (2026-03-14)**: 단가 0원 허용. `min:0` 유지. PricingService 구현만 수정. + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `api/app/Services/Pricing/PricingService.php` | 전체 | store(), update() 메서드 **구현** (현재 TODO) | +| `react/src/components/pricing/PricingFormClient.tsx` | 70-76, 152-159 | 품목코드 매핑 + submit payload 필드명 정렬 | +| `api/app/Http/Requests/Pricing/PriceStoreRequest.php` | - | item_id, sales_price 유효성 강화 | +| `api/app/Http/Requests/Pricing/PriceUpdateRequest.php` | 15-17 | `nullable|numeric|min:0` 유지 (0원 허용) | + +--- + +#### #34 품목목록 규격 컬럼 비어있음 + +**분석**: `api/app/Models/Items/Item.php:21-29` +- 규격은 `attributes` JSON 컬럼에 저장 (cast: array) +- **API 목록 조회 시 `attributes` 필드가 응답에 포함되는지, 프론트에서 올바르게 바인딩하는지 확인 필요** + +| 파일 | 변경 내용 | +|------|----------| +| `api/app/Services/ItemService.php` | 목록 조회 시 attributes에서 규격 값 추출하여 응답에 포함 | +| 프론트 품목 목록 컴포넌트 | 규격 컬럼 데이터 바인딩 확인 | + +--- + +#### #37 작업지시 품목 수량 수정 안 됨 + +**수정 대상:** +| 파일 | 변경 내용 | +|------|----------| +| `api/app/Services/WorkOrderService.php` | updateItem 메서드 확인, 수량 업데이트 로직 | +| `react/src/components/production/WorkOrders/WorkOrderDetail.tsx` | 수량 수정 API 호출 로직 및 파라미터 | + +--- + +#### #29 수주 수정 시 유효성 에러 (Phase 0에서 버그 확정) + +**근본 원인**: `api/app/Http/Requests/Order/UpdateOrderRequest.php:40-42` +```php +'items.*.item_name' => 'required|string|max:200', +'items.*.quantity' => 'required|numeric|min:0', +'items.*.unit_price' => 'required|numeric|min:0', +``` +- **items 배열 전송 시 모든 item에 `item_name` 필수** → 프론트에서 누락 가능 + +**수정 대상:** +| 파일 | 라인 | 변경 내용 | +|------|------|----------| +| `react/src/components/orders/` | - | 수주 수정 submit 시 items 배열에 item_name 포함 확인 | +| `api/app/Http/Requests/Order/UpdateOrderRequest.php` | 40-42 | 부분 수정 시 items 유효성 규칙 조정 (sometimes 적용 검토) | + +--- + +### 2.4 Phase 4: Minor 수정 (14건) — 예상 1.75일 + +#### 4.1 공통 UI (4건) + +| # | 이슈 | 파일:라인 | 원인 및 수정 | +|---|------|----------|-------------| +| 1 | 리스트 열 너비 정책 | `react/src/components/templates/UniversalListPage/index.tsx:227-240` | 컬럼별 `w-[Npx]`/`min-w-[Npx]` 설정. `useColumnSettings()` 훅(라인 845) 활용하여 열 너비 고정 정책 적용 | +| 2 | 스티키 취소 레이어 | `react/src/components/organisms/FormActions.tsx:22-73` | FormActions 자체는 스티키 미적용. 부모에서 `sticky bottom-0` 추가 필요. 취소/저장 영역 구분 강화 (배경색, 구분선) | +| 3 | 모달 닫힘 정책 | `react/src/components/organisms/SearchableSelectionModal/SearchableSelectionModal.tsx:233` | shadcn/ui Dialog의 `onOpenChange` 콜백 사용 중. 외부 클릭 시 닫힘은 기본 동작이나, 특정 모달에서 비활성화되었을 수 있음 | +| 4 | 밸리데이션 워딩 | 공통 에러 메시지 파일 | 에러 텍스트 전체 검증 및 일률 조정 필요 | + +#### 4.2 견적 UI (4건) + +| # | 이슈 | 파일:라인 | 원인 및 수정 | +|---|------|----------|-------------| +| 7 | 필터 버튼 활성화 스타일 | `QuoteManagementClient.tsx` | 선택된 날짜 필터 버튼에 active 스타일 클래스 추가 | +| 22 | inspection → 검사비 | `LocationDetailPanel.tsx:112` | **Phase 2 #18-20에서 함께 처리 완료** — `getTenantCategoryName()`에 inspection→검사비 매핑 추가 | +| 24 | 견적상태 3곳 불일치 | `QuoteFooterBar.tsx:23` status 타입, `QuoteManagementClient.tsx:115` getRevisionBadge(), `QuoteSummaryPanel.tsx:43` (상태 미표시) | 3곳의 상태 참조 체계가 다름. 통일된 상태 표시 컴포넌트 또는 유틸 함수 필요 | +| 26 | 수식 모달 하단 여백 | `FormulaViewModal.tsx` | padding-bottom 추가 | + +#### 4.3 수주 UI (2건) + +| # | 이슈 | 파일:라인 | 원인 및 수정 | +|---|------|----------|-------------| +| 28 | 수신처 필드 | OrderRegistration.tsx | `react/src/components/ui/phone-input.tsx` 컴포넌트 존재. 수신처 필드에 phone-input 교체 | +| 30 | "만원원" 이중 표시 | `react/src/lib/utils/amount.ts:53-61` | `formatAmount()`는 정상 ("만원" 반환). **수주 상세에서 별도로 "원"을 붙이는 코드가 있을 가능성** → 호출처 확인 필요 | + +#### 4.4 기타 (3건) + +| # | 이슈 | 파일:라인 | 원인 및 수정 | +|---|------|----------|-------------| +| 5 | 거래처 카운트 불일치 | `api/app/Services/ClientService.php:28-54` | index()에서 `is_active`/`client_type` 필터 적용 후 카운트 vs 전체 카운트 쿼리 불일치. stats() 메서드와 index() 필터 조건 동기화 필요 | +| 35-1 | 품목 수정이력 | `api/app/Services/ItemService.php` update() | AuditLogger 호출 여부 확인. 누락 시 추가 | +| 35-2 | 생산현황판 데이터 불일치 | `ProductionDashboard/actions.ts` vs `WorkOrders/actions.ts` | 두 곳에서 `getWorkOrders` 호출 시 `per_page`, 필터 파라미터 차이. 대시보드는 `per_page: 100`, 목록은 별도 필터 | + +--- + +## 3. 작업 일정 + +``` +Phase 0: 사전 조사 (대부분 완료) [0.5일] ██ +Phase 1: Critical [1일] ████ +Phase 2: 견적 Major [2.5일] ██████████ +Phase 3: 기타 Major + #15,#29 [2.5일] ██████████ +Phase 4: Minor [1.75일] ███████ + ───────────────── +총계 약 8.25일 +``` + +**병렬 처리 가능:** +- Phase 2 (견적) + Phase 3 (기타 모듈): 독립적 → **2.5일로 단축** +- Phase 4: Phase 1~3 중 동일 파일 수정 시 함께 처리 + +**최적 일정 (병렬 적용):** +``` +Day 1 : Phase 0 마무리 + Phase 1 (Critical 2건) +Day 2-3.5 : Phase 2 + Phase 3 (병렬) +Day 4-5 : Phase 4 (Minor) + Phase 0 결과 반영 + ───────────────── +최적 총계 약 5일 +``` + +--- + +## 4. 의존성 맵 + +``` +Phase 0 (사전 조사) +├─→ #9 (정렬 정책) ────→ ✅ 정상 확인 완료 +├─→ #11 (날짜 밀림) ──→ Phase 2로 배정 (프론트 날짜 전송 확인) +├─→ #15 (부가세) ────→ 🔴 Phase 3 배정 (Quote 모델에 tax_amount 미구현) +├─→ #17 (PDF) ──────→ Phase 3 (개발서버 환경 확인 필요) +├─→ #23 (확정 프로세스) → ✅ 결정 완료 → Phase 2 배정 (저장/확정 분리) +└─→ #29 (수주 수정 에러) → 🔴 Phase 3 배정 (UpdateOrderRequest 규칙 수정) + +Phase 1 (Critical) +├─→ #27 (견적 불러오기) ── 독립 +└─→ #31 (견적 수정 + 전파) ── 생산지시 차단 + 수주 동기화 + 리비전 + +Phase 2 (견적 Major) +├─→ #18,19,20 (BOM 탭 순서) ── 상호 의존 (함께 수정), #22와 연관 +├─→ #23 (저장/확정 분리) ── #31과 연관 (리비전 생성 시점) +├─→ #16 (혼합 등록 차단) ── 독립 (프론트 밸리데이션) +├─→ #14,21 (레이아웃) ── #18-20과 부분 연관 (탭 구조) +├─→ #8 (필터 라벨) ── 독립 +└─→ #25 (담당자 자동채움) ── 독립 + +Phase 3 (기타 Major) ── 모두 독립 +Phase 4 (Minor) ── 모두 독립 (#22는 Phase 2에서 함께 처리) +``` + +--- + +## 5. 리스크 + +| 리스크 | 영향 | 대응 | +|--------|------|------| +| BOM 탭 순서 통일 시 기존 데이터 영향 | 저장된 calculation_inputs의 순서 변경 가능 | 기존 데이터는 순서 변경 없이 **조회 시만 정렬** 적용 | +| #16 혼합 등록 차단 | 프론트 밸리데이션만 추가 → 우회 가능 | 백엔드에서도 검증 추가 검토 (FormRequest) | +| #31 견적 수정 + 수주 동기화 | syncFromQuote() 동작 검증 필요 | 개발서버에서 시나리오 테스트 후 반영 | +| #32,33 PricingService 미구현 | store/update 전체 구현 필요 → 작업량 증가 | 기존 패턴(QuoteService) 참고하여 구현 | +| #29 UpdateOrderRequest 수정 | items 유효성 규칙 변경 → 다른 수주 기능에 사이드이펙트 | `sometimes` 규칙 적용 시 기존 store도 영향 없는지 확인 | + +--- + +## 6. 컨펌 결과 + +| # | 항목 | 결정 | 상태 | +|---|------|------|------| +| 1 | #31 견적 수정 범위 | 수정 허용 + 변경 전파(수주 동기화) + 리비전 관리. **생산지시 있으면 차단** | ✅ 확정 | +| 2 | #18-20 BOM 탭 순서 | **주자재→모터→제어기→절곡품→부자재→검사비→기타** (inspection→검사비 라벨 변경) | ✅ 확정 | +| 3 | #16 혼합 제품 | 혼합 수주 없음(인정검사). **스크린+스틸 동시 등록 프론트 차단**. MIXED 타입 불필요 | ✅ 확정 | +| 4 | #23 견적 확정 프로세스 | **저장/확정 분리** — [저장]=draft, [견적확정]=finalized. 확정 후 수정 시 리비전 | ✅ 확정 | +| 5 | #33 단가 0원 | **0원 허용**. `min:0` 유지. PricingService 미구현 버그만 수정 | ✅ 확정 | +| 6 | #10 작업 컬럼 | **보류** — 기획 의도 확인 후 결정 | ⏳ 보류 | + +--- + +## 7. 변경 이력 + +| 날짜 | 항목 | 변경 내용 | 파일 | 승인 | +|------|------|----------|------|------| +| 2026-03-13 | - | 문서 초안 작성 | - | - | +| 2026-03-13 | - | 코드 레벨 심층 분석 반영 (4개 에이전트 병렬 분석) | - | - | +| 2026-03-14 | #31,#18-20,#16,#23,#33,#10 | 컨펌 대기 6건 결정 반영 (5건 확정, 1건 보류) | - | ✅ | + +--- + +## 8. 참고 문서 + +| 문서 | 경로 | +|------|------| +| 문서 인덱스 | `docs/INDEX.md` | +| 견적 시스템 | `docs/features/quotes/README.md` | +| 단가 정책 | `docs/rules/pricing-policy.md` | +| API 규칙 | `docs/dev/standards/api-rules.md` | +| 품질 체크리스트 | `docs/dev/standards/quality-checklist.md` | +| Git 컨벤션 | `docs/dev/standards/git-conventions.md` | +| 프론트엔드 아키텍처 | `docs/frontend/v1/01-architecture.md` | +| 통합 개선 마스터 | `docs/dev/dev_plans/integrated-master-plan.md` | + +--- + +## 9. 파일 경로 인덱스 + +### 견적관리 +| 파일 | 역할 | 관련 이슈 | +|------|------|----------| +| `react/src/components/quotes/types.ts` | 타입 정의, 변환 함수 | #16(L39,54-59), #18-20(탭순서) | +| `react/src/components/quotes/actions.ts` | 서버 액션 | #25(거래처 API) | +| `react/src/components/quotes/QuoteManagementClient.tsx` | 목록 페이지 | #8(L312-336), #16(L256-262), #24(L115) | +| `react/src/components/quotes/QuoteRegistration.tsx` | 등록/수정 폼 | #25(L776-790) | +| `react/src/components/quotes/LocationListPanel.tsx` | 개소 목록 (V2) | #14,21(엑셀업로드 L228-286) | +| `react/src/components/quotes/LocationDetailPanel.tsx` | 개소 상세 (V2) | #18-20(L157-179), #22(L112) | +| `react/src/components/quotes/QuoteFooterBar.tsx` | 하단 액션 바 | #31(L142-153), #24(L176-187) | +| `api/app/Services/Quote/QuoteService.php` | CRUD + 상태관리 | #27(L48-81), #31(L377-379) | +| `api/app/Services/Quote/FormulaEvaluatorService.php` | 수식 평가 엔진 | #18-20(L1852-1865,1923-1933) | +| `api/app/Models/Quote/Quote.php` | 견적 모델 | #31(L337-340 isEditable) | + +### 수주관리 +| 파일 | 역할 | 관련 이슈 | +|------|------|----------| +| `react/src/components/orders/QuotationSelectDialog.tsx` | 견적 선택 모달 | #27(L46-52) | +| `react/src/components/orders/actions.ts` | 서버 액션 | #27(L1246-1266) | +| `api/app/Http/Requests/Order/UpdateOrderRequest.php` | 수주 수정 검증 | #29(L40-42) | + +### 단가관리 +| 파일 | 역할 | 관련 이슈 | +|------|------|----------| +| `react/src/components/pricing/PricingFormClient.tsx` | 단가 폼 | #32(L70-76), #33(L139-148) | +| `api/app/Services/Pricing/PricingService.php` | CRUD (미구현!) | #32,33(TODO 상태) | +| `api/app/Http/Requests/Pricing/PriceUpdateRequest.php` | 수정 검증 | #33(L15-17) | + +### 공통 +| 파일 | 역할 | 관련 이슈 | +|------|------|----------| +| `react/src/components/templates/UniversalListPage/index.tsx` | 공통 목록 | #1(L227-240,845) | +| `react/src/components/organisms/FormActions.tsx` | 폼 액션 바 | #2(L22-73) | +| `react/src/components/organisms/SearchableSelectionModal/` | 공통 모달 | #3(L233) | +| `react/src/lib/utils/amount.ts` | 금액 포맷 | #30(L53-61) | +| `api/app/Services/ClientService.php` | 거래처 CRUD | #5(L28-54) | + +--- + +## 10. 검증 결과 + +> 각 Phase 완료 후 이 섹션에 검증 결과 추가 + +### 10.1 Phase 1 검증 + +| 테스트 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| `GET /api/v1/quotes?status=finalized&for_order=true` 직접 호출 | 전환 가능 견적 N건 | | ⏳ | +| DB 직접 조회: `SELECT * FROM quotes WHERE status='finalized' AND order_id IS NULL` | N건 존재 | | ⏳ | +| 검색어 유무에 따른 쿼리 로그 비교 | 동일 결과 | | ⏳ | +| converted 견적 수정 시도 (API) | 403 에러 + "수정 불가" 메시지 | | ⏳ | +| converted 견적 상세 (프론트) | 수정 버튼 미노출 | | ⏳ | +| draft 견적 수정 | 정상 수정 | | ⏳ | + +### 10.2 Phase 2 검증 + +| 테스트 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| BOM 탭 순서 (수동 등록) | 통일된 순서 | | ⏳ | +| BOM 탭 순서 (상세 조회) | 동일 순서 | | ⏳ | +| BOM 탭 순서 (엑셀 업로드) | 동일 순서 | | ⏳ | +| 스크린+스틸 동시 품목 추가 | 경고 모달 + 차단 | | ⏳ | +| 혼합 필터 옵션 | 제거 확인 (데드 코드 정리) | | ⏳ | +| 거래처 선택 → 담당자 | 자동 채움 | | ⏳ | +| 기타 품목 수동 추가 | 레이아웃 정상, 기타 탭에 병합 | | ⏳ | + +### 10.3 Phase 3 검증 + +| 테스트 | 예상 결과 | 실제 결과 | 상태 | +|--------|----------|----------|------| +| 단가 등록 → 상세 확인 | 원본 품목코드/품목명 유지 | | ⏳ | +| 단가 수정 → 저장 | 입력 금액 정상 저장 | | ⏳ | +| 수주 수정 → 저장 | 유효성 에러 없이 저장 | | ⏳ | + +--- + +*이 문서는 /plan 스킬로 생성되었습니다. (2026-03-13)* +*코드 레벨 분석: Sequential Thinking MCP + Explore Agent x8 (2회차)* +*컨펌 반영: 2026-03-14 (6건 중 5건 확정, 1건 보류)*