From 0784b2a40e33cc6e604fb7ade1d343cf1286a184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Sat, 21 Feb 2026 01:06:48 +0900 Subject: [PATCH] =?UTF-8?q?fix(WEB):=20=EA=B2=AC=EC=A0=81=20=EA=B0=9C?= =?UTF-8?q?=EC=86=8C=20=EC=9E=85=EB=A0=A5=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20BOM=20=EB=B3=80=ED=99=98=20=EC=95=88=EC=A0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 층/부호 필수 검증 제거, 빈값 시 "-" 대체 - DevFill 제품 1개 고정 + 수량 1 고정 (모델별 인증 반영) - note에서 "-" 값 필터링, formula_source 필드 추가 - FG 조회 시 has_bom 필터 제거 --- src/components/dev/generators/quoteData.ts | 24 ++++- src/components/quotes/LocationListPanel.tsx | 8 +- src/components/quotes/QuoteRegistration.tsx | 101 +++++++++++--------- src/components/quotes/actions.ts | 1 - src/components/quotes/types.ts | 17 +++- 5 files changed, 88 insertions(+), 63 deletions(-) diff --git a/src/components/dev/generators/quoteData.ts b/src/components/dev/generators/quoteData.ts index 2ffa3365..15cc4a6e 100644 --- a/src/components/dev/generators/quoteData.ts +++ b/src/components/dev/generators/quoteData.ts @@ -43,13 +43,16 @@ const WRITERS = ['드미트리', '김철수', '이영희', '박지민', '최서 export function generateQuoteFormItem( index: number, products?: Array<{ code: string; name: string; category?: string }>, - category?: string + category?: string, + fixedProductCode?: string ): QuoteFormItem { const selectedCategory = category || randomPick(PRODUCT_CATEGORIES); // 카테고리에 맞는 제품 필터링 let productCode = ''; - if (products && products.length > 0) { + if (fixedProductCode) { + productCode = fixedProductCode; + } else if (products && products.length > 0) { const categoryProducts = products.filter(p => p.category?.toUpperCase() === selectedCategory || !p.category ); @@ -70,7 +73,7 @@ export function generateQuoteFormItem( guideRailType: randomPick(GUIDE_RAIL_TYPES), motorPower: randomPick(MOTOR_POWERS), controller: randomPick(CONTROLLERS), - quantity: randomInt(1, 10), + quantity: 1, wingSize: '50', inspectionFee: 50000, }; @@ -104,11 +107,22 @@ export function generateQuoteData(options: GenerateQuoteDataOptions = {}): Quote // 품목 수 결정 const count = itemCount ?? randomInt(1, 5); - // 품목 생성 (동일 카테고리 사용) + // 제품 1개 고정 선택 (모델별 인증이라 섞을 수 없음) const selectedCategory = category || randomPick(PRODUCT_CATEGORIES); + let fixedProductCode = ''; + if (products && products.length > 0) { + const categoryProducts = products.filter(p => + p.category?.toUpperCase() === selectedCategory || !p.category + ); + if (categoryProducts.length > 0) { + fixedProductCode = randomPick(categoryProducts).code; + } + } + + // 품목 생성 (동일 제품, 수량 1) const items: QuoteFormItem[] = []; for (let i = 0; i < count; i++) { - items.push(generateQuoteFormItem(i, products, selectedCategory)); + items.push(generateQuoteFormItem(i, products, selectedCategory, fixedProductCode)); } return { diff --git a/src/components/quotes/LocationListPanel.tsx b/src/components/quotes/LocationListPanel.tsx index 264b868e..ebb5a7ca 100644 --- a/src/components/quotes/LocationListPanel.tsx +++ b/src/components/quotes/LocationListPanel.tsx @@ -134,10 +134,6 @@ export function LocationListPanel({ // 개소 추가 (BOM 계산 성공 시에만 폼 초기화) const handleAdd = useCallback(async () => { // 유효성 검사 - if (!formData.floor || !formData.code) { - toast.error("층과 부호를 입력해주세요."); - return; - } if (!formData.openWidth || !formData.openHeight) { toast.error("가로와 세로를 입력해주세요."); return; @@ -150,8 +146,8 @@ export function LocationListPanel({ const product = finishedGoods.find((fg) => fg.item_code === formData.productCode); const newLocation: Omit = { - floor: formData.floor, - code: formData.code, + floor: formData.floor || "-", + code: formData.code || "-", openWidth: parseFloat(formData.openWidth) || 0, openHeight: parseFloat(formData.openHeight) || 0, productCode: formData.productCode, diff --git a/src/components/quotes/QuoteRegistration.tsx b/src/components/quotes/QuoteRegistration.tsx index 627fae81..fce69942 100644 --- a/src/components/quotes/QuoteRegistration.tsx +++ b/src/components/quotes/QuoteRegistration.tsx @@ -221,47 +221,49 @@ export function QuoteRegistration({ // DevFill (개발/테스트용 자동 채우기) // --------------------------------------------------------------------------- useDevFill("quoteV2", useCallback(() => { - // BOM이 있는 제품만 필터링 - const productsWithBom = finishedGoods.filter((fg) => fg.has_bom === true || (fg.bom && Array.isArray(fg.bom) && fg.bom.length > 0)); + // 제품 1개 고정 선택 (모델별 인증이라 섞을 수 없음) + const fixedProduct = finishedGoods.length > 0 + ? finishedGoods[Math.floor(Math.random() * finishedGoods.length)] + : null; - // 랜덤 개소 생성 함수 - const createRandomLocation = (index: number): LocationItem => { - const floors = ["B2", "B1", "1F", "2F", "3F", "4F", "5F", "R"]; - const codePrefix = ["SD", "FSS", "FD", "SS", "DS"]; - const guideRailTypes = ["wall", "floor", "mixed"]; - const motorPowers = ["single", "three"]; - const controllers = ["basic", "smart", "premium"]; + // 층 순서 (정렬된 상태로 순차 할당) + const sortedFloors = ["B2", "B1", "1F", "2F", "3F", "4F", "5F", "R"]; + // 부호 접두사 1개 고정 + const codePrefixes = ["SD", "FSS", "FD", "SS", "DS"]; + const fixedPrefix = codePrefixes[Math.floor(Math.random() * codePrefixes.length)]; - const randomFloor = floors[Math.floor(Math.random() * floors.length)]; - const randomPrefix = codePrefix[Math.floor(Math.random() * codePrefix.length)]; - const randomWidth = (Math.floor(Math.random() * 40) + 20) * 100; // 2000~6000 (100단위) - const randomHeight = (Math.floor(Math.random() * 30) + 20) * 100; // 2000~5000 (100단위) - // BOM이 있는 제품 중에서 랜덤 선택 (없으면 전체에서 선택) - const productPool = productsWithBom.length > 0 ? productsWithBom : finishedGoods; - const randomProduct = productPool[Math.floor(Math.random() * productPool.length)]; + const guideRailTypes = ["wall", "floor", "mixed"]; + const motorPowers = ["single", "three"]; + const controllers = ["basic", "smart", "premium"]; - return { - id: `loc-${Date.now()}-${index}`, - floor: randomFloor, - code: `${randomPrefix}-${String(index + 1).padStart(2, "0")}`, + // 1~5개 랜덤 개소 생성 + const locationCount = Math.floor(Math.random() * 5) + 1; + + // 층을 순차 할당할 시작 인덱스 (랜덤 시작점, 순서대로 올라감) + const maxStartIdx = Math.max(0, sortedFloors.length - locationCount); + const floorStartIdx = Math.floor(Math.random() * (maxStartIdx + 1)); + + const testLocations: LocationItem[] = []; + for (let i = 0; i < locationCount; i++) { + const floorIdx = Math.min(floorStartIdx + i, sortedFloors.length - 1); + const randomWidth = (Math.floor(Math.random() * 40) + 20) * 100; + const randomHeight = (Math.floor(Math.random() * 30) + 20) * 100; + + testLocations.push({ + id: `loc-${Date.now()}-${i}`, + floor: sortedFloors[floorIdx], + code: `${fixedPrefix}-${String(i + 1).padStart(2, "0")}`, openWidth: randomWidth, openHeight: randomHeight, - productCode: randomProduct?.item_code || "FG-SCR-001", - productName: randomProduct?.item_name || "방화 스크린 셔터 (소형)", - quantity: Math.floor(Math.random() * 3) + 1, // 1~3 + productCode: fixedProduct?.item_code || "FG-SCR-001", + productName: fixedProduct?.item_name || "방화 스크린 셔터 (소형)", + quantity: 1, guideRailType: guideRailTypes[Math.floor(Math.random() * guideRailTypes.length)], 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)], inspectionFee: [50000, 60000, 70000][Math.floor(Math.random() * 3)], - }; - }; - - // 1~5개 랜덤 개소 생성 - const locationCount = Math.floor(Math.random() * 5) + 1; - const testLocations: LocationItem[] = []; - for (let i = 0; i < locationCount; i++) { - testLocations.push(createRandomLocation(i)); + }); } // 로그인 사용자 정보 가져오기 @@ -511,29 +513,36 @@ export function QuoteRegistration({ const source = formData.locations.find((loc) => loc.id === locationId); if (!source) return; - // 부호에서 접두어와 번호 분리 (예: "DS-01" → prefix="DS-", num=1) - const codeMatch = source.code.match(/^(.*?)(\d+)$/); - let newCode = source.code + "-copy"; + // 층/부호가 없거나 "-"이면 그대로 유지 + let newFloor = source.floor || "-"; + let newCode = source.code || "-"; - if (codeMatch) { - const prefix = codeMatch[1]; // "DS-" - const numLength = codeMatch[2].length; // 2 (자릿수 보존) + if (newCode !== "-") { + // 부호에서 접두어와 번호 분리 (예: "DS-01" → prefix="DS-", num=1) + const codeMatch = source.code.match(/^(.*?)(\d+)$/); + if (codeMatch) { + const prefix = codeMatch[1]; // "DS-" + const numLength = codeMatch[2].length; // 2 (자릿수 보존) - // 같은 접두어를 가진 부호 중 최대 번호 찾기 - let maxNum = 0; - formData.locations.forEach((loc) => { - const m = loc.code.match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\d+)$`)); - if (m) { - maxNum = Math.max(maxNum, parseInt(m[1], 10)); - } - }); + // 같은 접두어를 가진 부호 중 최대 번호 찾기 + let maxNum = 0; + formData.locations.forEach((loc) => { + const m = loc.code.match(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\d+)$`)); + if (m) { + maxNum = Math.max(maxNum, parseInt(m[1], 10)); + } + }); - newCode = prefix + String(maxNum + 1).padStart(numLength, "0"); + newCode = prefix + String(maxNum + 1).padStart(numLength, "0"); + } else { + newCode = source.code + "-copy"; + } } const clonedLocation: LocationItem = { ...source, id: `loc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + floor: newFloor, code: newCode, }; diff --git a/src/components/quotes/actions.ts b/src/components/quotes/actions.ts index 3ca7e558..d14f2d57 100644 --- a/src/components/quotes/actions.ts +++ b/src/components/quotes/actions.ts @@ -309,7 +309,6 @@ export async function getFinishedGoods(category?: string): Promise<{ const result = await executeServerAction[]>({ url: buildApiUrl('/api/v1/items', { item_type: 'FG', - has_bom: '1', item_category: category, size: '5000', }), diff --git a/src/components/quotes/types.ts b/src/components/quotes/types.ts index 27b07f69..373de7b7 100644 --- a/src/components/quotes/types.ts +++ b/src/components/quotes/types.ts @@ -766,6 +766,7 @@ export function transformV2ToApi( total_price: number; sort_order: number; note: string | null; + formula_source?: string; item_index?: number; finished_goods_code?: string; formula_category?: string; @@ -796,7 +797,8 @@ export function transformV2ToApi( unit_price: bomItem.unit_price, total_price: bomItem.unit_price * calcQty, sort_order: sortOrder++, - note: `${loc?.floor || ''} ${loc?.code || ''}`.trim() || null, + note: [loc?.floor, loc?.code].filter(v => v && v !== '-').join(' ') || null, + formula_source: `product_${locIndex}`, item_index: locIndex, finished_goods_code: bomResult.finished_goods.code, formula_category: bomItem.process_group || undefined, @@ -827,7 +829,8 @@ export function transformV2ToApi( unit_price: bomItem.unit_price, total_price: bomItem.unit_price * calcQty, sort_order: sortOrder++, - note: `${loc.floor || ''} ${loc.code || ''}`.trim() || null, + note: [loc.floor, loc.code].filter(v => v && v !== '-').join(' ') || null, + formula_source: `product_${locIndex}`, item_index: locIndex, finished_goods_code: loc.bomResult!.finished_goods.code, formula_category: bomItem.process_group || undefined, @@ -850,7 +853,8 @@ export function transformV2ToApi( unit_price: loc.unitPrice || loc.inspectionFee || 0, total_price: loc.totalPrice || (loc.unitPrice || loc.inspectionFee || 0) * loc.quantity, sort_order: index + 1, - note: `${loc.floor} ${loc.code}`.trim() || null, + note: [loc.floor, loc.code].filter(v => v && v !== '-').join(' ') || null, + formula_source: `product_${index}`, })); } @@ -1027,6 +1031,7 @@ export function transformFormDataToApi(formData: QuoteFormData): Record v && v !== '-').join(' ') || null, + formula_source: `product_${calcItem.index}`, item_index: calcItem.index, finished_goods_code: calcItem.result.finished_goods.code, formula_category: bomItem.process_group || undefined, @@ -1084,7 +1090,8 @@ export function transformFormDataToApi(formData: QuoteFormData): Record v && v !== '-').join(' ') || null, + formula_source: `product_${index}`, }; }); }