refactor(WEB): 견적관리 URL 구조 마이그레이션 Phase 2 완료

- test-new → new 경로 정식화 (V2 등록 페이지)
- test/[id] → [id] 경로 정식화 (V2 상세/수정 페이지)
- test 폴더 삭제 (test-new/, test/[id]/)
- V1 페이지 백업 파일 보존 (.v1-backup)
- LocationDetailPanel, QuoteSummaryPanel 개선
- types.ts 변환 함수 정리

V2 URL 패턴:
- 등록: /sales/quote-management/new
- 상세: /sales/quote-management/[id]
- 수정: /sales/quote-management/[id]?mode=edit
This commit is contained in:
2026-01-26 21:34:13 +09:00
parent f9dafbc02c
commit 05b0ba73be
10 changed files with 991 additions and 1028 deletions

View File

@@ -35,6 +35,10 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { ItemSearchModal } from "./ItemSearchModal";
import type { LocationItem } from "./QuoteRegistrationV2";
import type { FinishedGoods } from "./actions";
import type { BomCalculationResultItem } from "./types";
// 납품길이 옵션
const DELIVERY_LENGTH_OPTIONS = [
{ value: "3000", label: "3000" },
@@ -43,40 +47,16 @@ const DELIVERY_LENGTH_OPTIONS = [
{ value: "6000", label: "6000" },
];
// 목데이터 - 탭별 품목 아이템 (각 탭마다 다른 구조)
const MOCK_BOM_ITEMS = {
// 본체 (스크린/슬랫): 품목명, 제작사이즈, 수량, 작업
body: [
{ id: "b1", item_name: "실리카 스크린", manufacture_size: "5280*3280", quantity: 1, unit: "EA", total_price: 1061676 },
],
// 절곡품 - 가이드레일: 품목명, 재질, 규격, 납품길이, 수량, 작업
"guide-rail": [
{ id: "g1", item_name: "벽면형 마감재", material: "알루미늄", spec: "50mm", delivery_length: "4000", quantity: 2, unit: "EA", total_price: 84048 },
{ id: "g2", item_name: "본체 가이드 레일", material: "스틸", spec: "20mm", delivery_length: "4000", quantity: 2, unit: "EA", total_price: 32508 },
],
// 절곡품 - 케이스: 품목명, 재질, 규격, 납품길이, 수량, 작업
case: [
{ id: "c1", item_name: "전면부 케이스", material: "알루미늄", spec: "30mm", delivery_length: "4000", quantity: 1, unit: "EA", total_price: 30348 },
],
// 절곡품 - 하단마감재: 품목명, 재질, 규격, 납품길이, 수량, 작업
bottom: [
{ id: "bt1", item_name: "하단 하우징", material: "스틸", spec: "40mm", delivery_length: "4000", quantity: 1, unit: "EA", total_price: 15420 },
],
// 모터 & 제어기: 품목명, 유형, 사양, 수량, 작업
motor: [
{ id: "m1", item_name: "직류 모터", type: "220V", spec: "1/2HP", quantity: 1, unit: "EA", total_price: 250000 },
{ id: "m2", item_name: "제어기", type: "디지털", spec: "", quantity: 1, unit: "EA", total_price: 150000 },
],
// 부자재: 품목명, 규격, 납품길이, 수량, 작업
accessory: [
{ id: "a1", item_name: "각파이프 25mm", spec: "25*25*2.0t", delivery_length: "4000", quantity: 2, unit: "EA", total_price: 17000 },
{ id: "a2", item_name: "플랫바 20mm", spec: "20*3.0t", delivery_length: "4000", quantity: 1, unit: "EA", total_price: 4200 },
],
// 빈 BOM 아이템 (bomResult 없을 때 사용)
const EMPTY_BOM_ITEMS: Record<string, BomCalculationResultItem[]> = {
body: [],
"guide-rail": [],
case: [],
bottom: [],
motor: [],
accessory: [],
};
import type { LocationItem } from "./QuoteRegistrationV2";
import type { FinishedGoods } from "./actions";
// =============================================================================
// 상수
// =============================================================================
@@ -148,11 +128,11 @@ export function LocationDetailPanel({
return finishedGoods.find((fg) => fg.item_code === location.productCode);
}, [location?.productCode, finishedGoods]);
// BOM 아이템을 탭별로 분류 (목데이터 사용)
// BOM 아이템을 탭별로 분류
const bomItemsByTab = useMemo(() => {
// bomResult가 없으면 목데이터 사용
// bomResult가 없으면 빈 배열 반환
if (!location?.bomResult?.items) {
return MOCK_BOM_ITEMS;
return EMPTY_BOM_ITEMS;
}
const items = location.bomResult.items;

View File

@@ -33,59 +33,7 @@ interface DetailCategory {
items: DetailItem[];
}
const MOCK_DETAIL_TOTALS: DetailCategory[] = [
{
label: "본체 (스크린/슬랫)",
count: 1,
amount: 1061676,
items: [
{ name: "실리카 스크린", quantity: 1, unitPrice: 1061676, totalPrice: 1061676 },
]
},
{
label: "절곡품 - 가이드레일",
count: 2,
amount: 116556,
items: [
{ name: "벽면형 마감재", quantity: 2, unitPrice: 42024, totalPrice: 84048 },
{ name: "본체 가이드 레일", quantity: 2, unitPrice: 16254, totalPrice: 32508 },
]
},
{
label: "절곡품 - 케이스",
count: 1,
amount: 30348,
items: [
{ name: "전면부 케이스", quantity: 1, unitPrice: 30348, totalPrice: 30348 },
]
},
{
label: "절곡품 - 하단마감재",
count: 1,
amount: 15420,
items: [
{ name: "하단 하우징", quantity: 1, unitPrice: 15420, totalPrice: 15420 },
]
},
{
label: "모터 & 제어기",
count: 2,
amount: 400000,
items: [
{ name: "직류 모터", quantity: 1, unitPrice: 250000, totalPrice: 250000 },
{ name: "제어기", quantity: 1, unitPrice: 150000, totalPrice: 150000 },
]
},
{
label: "부자재",
count: 2,
amount: 21200,
items: [
{ name: "각파이프 25mm", quantity: 2, unitPrice: 8500, totalPrice: 17000 },
{ name: "플랫바 20mm", quantity: 1, unitPrice: 4200, totalPrice: 4200 },
]
},
];
// Mock 데이터 제거 - bomResult 없으면 빈 배열 반환
// =============================================================================
// Props
@@ -132,11 +80,11 @@ export function QuoteSummaryPanel({
}));
}, [locations]);
// 선택 개소의 상세별 합계 (공정별) - 목데이터 포함
// 선택 개소의 상세별 합계 (공정별)
const detailTotals = useMemo((): DetailCategory[] => {
// bomResult가 없으면 목데이터 사용
// bomResult가 없으면 빈 배열 반환
if (!selectedLocation?.bomResult?.subtotals) {
return selectedLocation ? MOCK_DETAIL_TOTALS : [];
return [];
}
const subtotals = selectedLocation.bomResult.subtotals;

View File

@@ -704,6 +704,7 @@ export function transformV2ToApi(
const calculationInputs: CalculationInputs & { bomResults?: BomCalculationResult[] } = {
items: data.locations.map(loc => ({
productCategory: 'screen', // TODO: 동적으로 결정
productCode: loc.productCode, // BOM 재계산용
productName: loc.productName,
openWidth: String(loc.openWidth),
openHeight: String(loc.openHeight),
@@ -868,26 +869,41 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 {
if (calcInputs.length > 0) {
locations = calcInputs.map((ci, index) => {
// 해당 인덱스의 BOM 자재에서 금액 계산
const relatedItems = (apiData.items || []).filter(
item => (item as QuoteItemApiData & { item_index?: number }).item_index === index ||
(item.note && ci.floor && item.note.includes(ci.floor))
);
const totalPrice = relatedItems.reduce(
(sum, item) => sum + parseFloat(String(item.total_price ?? item.total_amount ?? 0)), 0
);
const qty = ci.quantity || 1;
// 해당 인덱스의 BOM 결과 복원
const bomResult = savedBomResults[index];
// 금액 계산: bomResult.grand_total 우선, 없으면 apiData.items에서 계산
let unitPrice: number | undefined;
let totalPrice: number | undefined;
if (bomResult?.grand_total) {
// BOM 결과에서 금액 가져오기
unitPrice = Math.round(bomResult.grand_total);
totalPrice = Math.round(bomResult.grand_total * qty);
} else {
// Fallback: apiData.items에서 계산
const relatedItems = (apiData.items || []).filter(
item => (item as QuoteItemApiData & { item_index?: number }).item_index === index ||
(item.note && ci.floor && item.note.includes(ci.floor))
);
const itemsTotal = relatedItems.reduce(
(sum, item) => sum + parseFloat(String(item.total_price ?? item.total_amount ?? 0)), 0
);
if (itemsTotal > 0) {
unitPrice = Math.round(itemsTotal / qty);
totalPrice = itemsTotal;
}
}
return {
id: `loc-${index}`,
floor: ci.floor || '',
code: ci.code || '',
openWidth: parseInt(ci.openWidth || '0', 10),
openHeight: parseInt(ci.openHeight || '0', 10),
productCode: '', // calculation_inputs에 없음, 필요시 items에서 추출
productCode: (ci as { productCode?: string }).productCode || bomResult?.finished_goods?.code || '',
productName: ci.productName || '',
quantity: qty,
guideRailType: ci.guideRailType || 'wall',
@@ -895,8 +911,8 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 {
controller: ci.controller || 'basic',
wingSize: parseInt(ci.wingSize || '50', 10),
inspectionFee: ci.inspectionFee || 50000,
unitPrice: totalPrice > 0 ? Math.round(totalPrice / qty) : undefined,
totalPrice: totalPrice > 0 ? totalPrice : undefined,
unitPrice,
totalPrice,
bomResult: bomResult, // BOM 결과 복원
};
});