From 2692865b553b98b5e60fcdb27382d00bc3f522c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 11 Mar 2026 16:55:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[=EA=B2=AC=EC=A0=81]=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=EA=B8=B0=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?+=20=EA=B0=80=EC=9D=B4=EB=93=9C=EB=A0=88=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=ED=92=88=EC=97=B0=EB=8F=99=20+=20=EC=88=98=EC=8B=9D=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 제어기: 노출형/매립형(뒷박스포함)/매립형(뒷박스제외) 3가지로 변경 - 가이드레일: 제품코드 specification에서 벽면형/측면형/혼합형 자동 연동, Select 비활성 - FormulaViewModal: JSON 데이터를 범용 렌더러(GenericDataView)로 표시 - DevFill: 새 제어기 타입 + 제품 기반 가이드레일 적용 --- src/components/quotes/FormulaViewModal.tsx | 127 +++++++++++++++++- src/components/quotes/LocationDetailPanel.tsx | 22 ++- src/components/quotes/LocationEditModal.tsx | 13 +- src/components/quotes/LocationListPanel.tsx | 35 +++-- src/components/quotes/QuoteRegistration.tsx | 15 ++- src/components/quotes/types.ts | 2 +- 6 files changed, 184 insertions(+), 30 deletions(-) diff --git a/src/components/quotes/FormulaViewModal.tsx b/src/components/quotes/FormulaViewModal.tsx index 464ed9ae..3212ffa2 100644 --- a/src/components/quotes/FormulaViewModal.tsx +++ b/src/components/quotes/FormulaViewModal.tsx @@ -185,9 +185,7 @@ function DebugStepCard({ step }: { step: BomDebugStep }) { {hasFormulas ? ( ) : ( -
-              {JSON.stringify(step.data, null, 2)}
-            
+ )} )} @@ -308,10 +306,125 @@ function FormulaTable({ formulas, stepName: _stepName }: { formulas: FormulaItem ); } - // 기본 폴백 + // 기본 폴백 — 범용 렌더러 + return ; +} + +// ============================================================================= +// 범용 JSON 데이터 렌더러 (formulas 없는 step용) +// ============================================================================= + +// 금액성 키 감지 +const AMOUNT_KEYS = ['subtotal', 'grand_total', 'total_price', 'unit_price', 'total']; +const LABEL_MAP: Record = { + tenant_id: '테넌트 ID', + handler: '계산 핸들러', + handler_class: '핸들러 클래스', + finished_goods: '완제품 코드', + code: '코드', + name: '제품명', + item_category: '품목 카테고리', + total_items: '총 품목 수', + item_codes: '품목 코드', + items: '품목 목록', + groups: '카테고리 그룹', + count: '품목 수', + subtotal: '소계', + grand_total: '총합계', + formatted: '금액', + item_count: '품목 수', + formula: '수식', + calculation: '계산', + result: '결과', + category: '카테고리', + item_categories: '품목 카테고리', +}; + +function isAmountKey(key: string): boolean { + return AMOUNT_KEYS.some(k => key.toLowerCase().includes(k)); +} + +function formatValue(key: string, value: unknown): string { + if (value === null || value === undefined) return '-'; + if (typeof value === 'number') { + if (isAmountKey(key)) return `${formatNumber(value)}원`; + return formatNumber(value); + } + return String(value); +} + +function GenericDataView({ data }: { data: unknown }) { + if (data === null || data === undefined) return null; + + // 배열 처리 + if (Array.isArray(data)) { + // 객체 배열 → 카드 리스트 + if (data.length > 0 && typeof data[0] === 'object') { + return ( +
+ {data.map((item, i) => ( +
+ +
+ ))} +
+ ); + } + // 단순 배열 → 태그 + return ( +
+ {data.map((item, i) => ( + + {String(item)} + + ))} +
+ ); + } + + // 객체 처리 + if (typeof data === 'object') { + const entries = Object.entries(data as Record); + return ( +
+ {entries.map(([key, value]) => ( + + ))} +
+ ); + } + + // 원시 값 + return {String(data)}; +} + +function GenericField({ fieldKey, value }: { fieldKey: string; value: unknown }) { + const label = LABEL_MAP[fieldKey] || fieldKey; + + // 중첩 객체/배열 → 접이식 + if (typeof value === 'object' && value !== null) { + const isArray = Array.isArray(value); + const count = isArray ? value.length : Object.keys(value as object).length; + + return ( +
+
+ {label} + {isArray && ({count}개)} +
+ +
+ ); + } + + // 단순 key-value + const isAmount = isAmountKey(fieldKey); return ( -
-      {JSON.stringify(formulas, null, 2)}
-    
+
+ {label} + + {formatValue(fieldKey, value)} + +
); } \ No newline at end of file diff --git a/src/components/quotes/LocationDetailPanel.tsx b/src/components/quotes/LocationDetailPanel.tsx index 44f95e43..6ce3283e 100644 --- a/src/components/quotes/LocationDetailPanel.tsx +++ b/src/components/quotes/LocationDetailPanel.tsx @@ -74,17 +74,26 @@ const GUIDE_RAIL_TYPES = [ { value: "mixed", label: "혼합형" }, ]; +// 제품 specification에서 가이드레일 타입 추출 +function getGuideRailFromSpec(spec?: string, itemName?: string): string | null { + const text = spec || itemName || ''; + if (text.includes('혼합')) return 'mixed'; + if (text.includes('벽면')) return 'wall'; + if (text.includes('측면')) return 'floor'; + return null; +} + // 모터 전원 const MOTOR_POWERS = [ { value: "single", label: "단상(220V)" }, { value: "three", label: "삼상(380V)" }, ]; -// 연동제어기 +// 제어기 설치 방식 const CONTROLLERS = [ - { value: "basic", label: "단독" }, - { value: "smart", label: "연동" }, - { value: "premium", label: "매립형-뒷박스포함" }, + { value: "exposed", label: "노출형" }, + { value: "embedded", label: "매립형(뒷박스 포함)" }, + { value: "embedded_no_box", label: "매립형(뒷박스 제외)" }, ]; // 탭 인터페이스 정의 @@ -334,10 +343,12 @@ export function LocationDetailPanel({ value={location.productCode} onValueChange={(value) => { const product = finishedGoods.find((fg) => fg.item_code === value); + const guideRail = product ? getGuideRailFromSpec(product.specification, product.item_name) : null; onUpdateLocation(location.id, { productCode: value, productName: product?.item_name || value, itemCategory: product?.item_category, + ...(guideRail ? { guideRailType: guideRail } : {}), }); }} disabled={disabled} @@ -361,11 +372,12 @@ export function LocationDetailPanel({
handleFieldChange("guideRailType", value)} + disabled > diff --git a/src/components/quotes/LocationListPanel.tsx b/src/components/quotes/LocationListPanel.tsx index 352872f6..d919755d 100644 --- a/src/components/quotes/LocationListPanel.tsx +++ b/src/components/quotes/LocationListPanel.tsx @@ -52,19 +52,29 @@ const FLOOR_OPTIONS = [ const GUIDE_RAIL_TYPES = [ { value: "wall", label: "벽면형" }, { value: "floor", label: "측면형" }, + { value: "mixed", label: "혼합형" }, ]; +// 제품 specification에서 가이드레일 타입 추출 +function getGuideRailFromSpec(spec?: string, itemName?: string): string | null { + const text = spec || itemName || ''; + if (text.includes('혼합')) return 'mixed'; + if (text.includes('벽면')) return 'wall'; + if (text.includes('측면')) return 'floor'; + return null; +} + // 모터 전원 const MOTOR_POWERS = [ { value: "single", label: "단상(220V)" }, { value: "three", label: "삼상(380V)" }, ]; -// 연동제어기 +// 제어기 설치 방식 const CONTROLLERS = [ - { value: "basic", label: "단독" }, - { value: "smart", label: "연동" }, - { value: "premium", label: "매립형-뒷박스포함" }, + { value: "exposed", label: "노출형" }, + { value: "embedded", label: "매립형(뒷박스 포함)" }, + { value: "embedded_no_box", label: "매립형(뒷박스 제외)" }, ]; // ============================================================================= @@ -116,7 +126,7 @@ export function LocationListPanel({ quantity: "1", guideRailType: "wall", motorPower: "single", - controller: "basic", + controller: "exposed", }); // 삭제 확인 다이얼로그 @@ -190,7 +200,7 @@ export function LocationListPanel({ 수량: 1, 가이드레일: "wall", 전원: "single", - 제어기: "basic", + 제어기: "exposed", }, ]; @@ -245,7 +255,7 @@ export function LocationListPanel({ quantity: parseInt(row["수량"]) || 1, guideRailType: row["가이드레일"] || "wall", motorPower: row["전원"] || "single", - controller: row["제어기"] || "basic", + controller: row["제어기"] || "exposed", wingSize: 50, inspectionFee: 50000, }; @@ -338,7 +348,14 @@ export function LocationListPanel({ handleFormChange("guideRailType", value)} + disabled > diff --git a/src/components/quotes/QuoteRegistration.tsx b/src/components/quotes/QuoteRegistration.tsx index f3883e85..f1dab8f4 100644 --- a/src/components/quotes/QuoteRegistration.tsx +++ b/src/components/quotes/QuoteRegistration.tsx @@ -119,7 +119,7 @@ const _createNewLocation = (): LocationItem => ({ quantity: 1, guideRailType: "wall", motorPower: "single", - controller: "basic", + controller: "exposed", wingSize: 50, inspectionFee: 50000, }); @@ -231,9 +231,16 @@ export function QuoteRegistration({ const codePrefixes = ["SD", "FSS", "FD", "SS", "DS"]; const fixedPrefix = codePrefixes[Math.floor(Math.random() * codePrefixes.length)]; - const guideRailTypes = ["wall", "floor", "mixed"]; + // 가이드레일: 제품 specification에서 자동 추출 + const getGuideRailFromSpec = (spec?: string, name?: string): string => { + const text = spec || name || ''; + if (text.includes('혼합')) return 'mixed'; + if (text.includes('측면')) return 'floor'; + if (text.includes('벽면')) return 'wall'; + return 'wall'; + }; const motorPowers = ["single", "three"]; - const controllers = ["basic", "smart", "premium"]; + const controllers = ["exposed", "embedded", "embedded_no_box"]; // 1~5개 랜덤 개소 생성 const locationCount = Math.floor(Math.random() * 5) + 1; @@ -257,7 +264,7 @@ export function QuoteRegistration({ productCode: fixedProduct?.item_code || "FG-SCR-001", productName: fixedProduct?.item_name || "방화 스크린 셔터 (소형)", quantity: 1, - guideRailType: guideRailTypes[Math.floor(Math.random() * guideRailTypes.length)], + guideRailType: getGuideRailFromSpec(fixedProduct?.specification, fixedProduct?.item_name), motorPower: motorPowers[Math.floor(Math.random() * motorPowers.length)], controller: controllers[Math.floor(Math.random() * controllers.length)], wingSize: [50, 60, 70][Math.floor(Math.random() * 3)], diff --git a/src/components/quotes/types.ts b/src/components/quotes/types.ts index 5fae9e10..32de0e42 100644 --- a/src/components/quotes/types.ts +++ b/src/components/quotes/types.ts @@ -971,7 +971,7 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 { quantity: qty, guideRailType: ci.guideRailType || 'wall', motorPower: ci.motorPower || 'single', - controller: ci.controller || 'basic', + controller: ci.controller || 'exposed', wingSize: parseInt(ci.wingSize || '50', 10), inspectionFee: ci.inspectionFee || 50000, unitPrice,