From 2465d739fe1f7630cb293379d62b813cbda18530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Fri, 16 Jan 2026 19:35:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=AC=EC=A0=81=EC=84=9C=20=EB=AA=A9?= =?UTF-8?q?=EC=97=85=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=E2=86=92=20API=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EstimateDetailTableSection: 하드코딩된 셀렉트 옵션 → API 데이터 연동 - 재료/도장/모터/제어기/시공비: getCommonCodeOptions() 사용 - 공과 품목: getExpenseItemOptions() 사용 - EstimateListClient: 거래처/견적자 필터 API 연동 - MOCK_PARTNERS → getClientOptions() - MOCK_ESTIMATORS → getUserOptions() - actions.ts: 공통코드/거래처/사용자/공과품목 API 함수 추가 - constants.ts: MOCK_MATERIALS 제거 - EstimateDetailForm: MOCK_MATERIALS import 제거 --- .../estimates/EstimateDetailForm.tsx | 9 +- .../estimates/EstimateListClient.tsx | 41 ++-- .../construction/estimates/actions.ts | 198 ++++++++++++++++++ .../sections/EstimateDetailTableSection.tsx | 104 +++++++-- .../construction/estimates/utils/constants.ts | 16 +- 5 files changed, 317 insertions(+), 51 deletions(-) diff --git a/src/components/business/construction/estimates/EstimateDetailForm.tsx b/src/components/business/construction/estimates/EstimateDetailForm.tsx index 669a32ba..09cf0a63 100644 --- a/src/components/business/construction/estimates/EstimateDetailForm.tsx +++ b/src/components/business/construction/estimates/EstimateDetailForm.tsx @@ -20,7 +20,7 @@ import type { import { getEmptyEstimateDetailFormData, estimateDetailToFormData } from './types'; import { ElectronicApprovalModal } from './modals/ElectronicApprovalModal'; import { EstimateDocumentModal } from './modals/EstimateDocumentModal'; -import { MOCK_MATERIALS } from './utils'; +// MOCK_MATERIALS 제거됨 - API 데이터 사용 import { EstimateInfoSection, EstimateSummarySection, @@ -95,6 +95,11 @@ export default function EstimateDetailForm({ // ===== 저장 핸들러 (IntegratedDetailTemplate용) ===== const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { try { + // 🔍 디버깅: 저장 전 formData 확인 (브라우저 콘솔) + console.log('🔍 [handleConfirmSave] formData.detailItems:', formData.detailItems?.length, '개'); + console.log('🔍 [handleConfirmSave] formData.priceAdjustmentData:', formData.priceAdjustmentData); + console.log('🔍 [handleConfirmSave] formData 전체:', formData); + // 현재 사용자 이름을 견적자로 설정하여 저장 const result = await updateEstimate(estimateId, { ...formData, @@ -295,7 +300,7 @@ export default function EstimateDetailForm({ id: String(Date.now() + Math.random() + i), no: currentLength + i + 1, name: '', - material: MOCK_MATERIALS[0]?.value || '', + material: 'screen', // 기본값: 스크린 (API 옵션 첫번째 값) width: 0, height: 0, quantity: 1, diff --git a/src/components/business/construction/estimates/EstimateListClient.tsx b/src/components/business/construction/estimates/EstimateListClient.tsx index f1f26a7a..5decd122 100644 --- a/src/components/business/construction/estimates/EstimateListClient.tsx +++ b/src/components/business/construction/estimates/EstimateListClient.tsx @@ -31,7 +31,8 @@ import { STATUS_STYLES, STATUS_LABELS, } from './types'; -import { getEstimateList, getEstimateStats, deleteEstimate, deleteEstimates } from './actions'; +import { getEstimateList, getEstimateStats, deleteEstimate, deleteEstimates, getClientOptions, getUserOptions } from './actions'; +import type { ClientOption, UserOption } from './actions'; // 테이블 컬럼 정의 const tableColumns = [ @@ -48,19 +49,6 @@ const tableColumns = [ { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; -// 거래처/견적자 옵션 (다중선택용) -const MOCK_PARTNERS = [ - { value: '1', label: '회사명' }, - { value: '2', label: '야사 대림아파트' }, - { value: '3', label: '여의 현장아파트' }, -]; - -const MOCK_ESTIMATORS = [ - { value: 'hong', label: '홍길동' }, - { value: 'kim', label: '김철수' }, - { value: 'lee', label: '이영희' }, -]; - // 금액 포맷팅 function formatAmount(amount: number): string { return new Intl.NumberFormat('ko-KR').format(amount); @@ -82,6 +70,9 @@ export default function EstimateListClient({ initialData = [], initialStats }: E const [endDate, setEndDate] = useState(''); // Stats 데이터 const [stats, setStats] = useState(initialStats || null); + // 필터 옵션 데이터 + const [partnerOptions, setPartnerOptions] = useState([]); + const [estimatorOptions, setEstimatorOptions] = useState([]); // Stats 로드 useEffect(() => { @@ -94,6 +85,22 @@ export default function EstimateListClient({ initialData = [], initialStats }: E } }, [initialStats]); + // 거래처/견적자 옵션 로드 + useEffect(() => { + // 거래처 옵션 로드 + getClientOptions().then((result) => { + if (result.success && result.data) { + setPartnerOptions(result.data); + } + }); + // 견적자(사용자) 옵션 로드 + getUserOptions().then((result) => { + if (result.success && result.data) { + setEstimatorOptions(result.data); + } + }); + }, []); + // ===== 핸들러 ===== const handleRowClick = useCallback( (item: Estimate) => { @@ -172,13 +179,13 @@ export default function EstimateListClient({ initialData = [], initialStats }: E key: 'partner', label: '거래처', type: 'multi', - options: MOCK_PARTNERS, + options: partnerOptions, }, { key: 'estimator', label: '견적자', type: 'multi', - options: MOCK_ESTIMATORS, + options: estimatorOptions, }, { key: 'status', @@ -382,7 +389,7 @@ export default function EstimateListClient({ initialData = [], initialStats }: E /> ), }), - [startDate, endDate, activeStatTab, stats, handleRowClick, handleEdit] + [startDate, endDate, activeStatTab, stats, partnerOptions, estimatorOptions, handleRowClick, handleEdit] ); return ; diff --git a/src/components/business/construction/estimates/actions.ts b/src/components/business/construction/estimates/actions.ts index 174a93c1..f2f2763d 100644 --- a/src/components/business/construction/estimates/actions.ts +++ b/src/components/business/construction/estimates/actions.ts @@ -940,4 +940,202 @@ export async function getExpenseItemOptions(): Promise<{ console.error('공과 품목 목록 조회 오류:', error); return { success: false, error: '공과 품목 목록을 불러오는데 실패했습니다.' }; } +} + +// ==================== 공통코드/거래처/사용자 API ==================== + +/** + * 공통코드 옵션 타입 + */ +export interface CommonCodeOption { + value: string; + label: string; + code: string; + price?: number; + attributes?: Record; +} + +/** + * 특정 그룹의 공통코드 목록 조회 + * GET /api/v1/settings/common/{group} + */ +export async function getCommonCodes(group: string): Promise<{ + success: boolean; + data?: CommonCodeOption[]; + error?: string; +}> { + try { + const response = await apiClient.get<{ + success: boolean; + message: string; + data: Array<{ + id: number; + code: string; + name: string; + attributes: string | null; + sort_order: number; + }>; + }>(`/settings/common/${group}`); + + const items = Array.isArray(response.data) ? response.data : []; + const options: CommonCodeOption[] = items.map((item) => { + const attrs = item.attributes ? JSON.parse(item.attributes) : {}; + return { + value: attrs.price !== undefined ? String(attrs.price) : item.code, + label: item.name, + code: item.code, + price: attrs.price, + attributes: attrs, + }; + }); + + return { success: true, data: options }; + } catch (error) { + console.error(`공통코드(${group}) 목록 조회 오류:`, error); + return { success: false, error: `공통코드(${group}) 목록을 불러오는데 실패했습니다.` }; + } +} + +/** + * 거래처 옵션 타입 + */ +export interface ClientOption { + value: string; + label: string; +} + +/** + * 거래처 목록 조회 (셀렉트 옵션용) + * GET /api/v1/clients + */ +export async function getClientOptions(): Promise<{ + success: boolean; + data?: ClientOption[]; + error?: string; +}> { + try { + const response = await apiClient.get<{ + success: boolean; + message: string; + data: { + data: Array<{ + id: number; + name: string; + code?: string; + }>; + }; + }>('/clients', { + params: { + active: '1', + size: '100', + }, + }); + + const paginatedData = response.data; + const items = Array.isArray(paginatedData.data) ? paginatedData.data : []; + const options: ClientOption[] = items.map((item) => ({ + value: String(item.id), + label: item.name, + })); + + return { success: true, data: options }; + } catch (error) { + console.error('거래처 목록 조회 오류:', error); + return { success: false, error: '거래처 목록을 불러오는데 실패했습니다.' }; + } +} + +/** + * 사용자 옵션 타입 + */ +export interface UserOption { + value: string; + label: string; +} + +/** + * 사용자 목록 조회 (견적자 셀렉트 옵션용) + * GET /api/v1/users + */ +export async function getUserOptions(): Promise<{ + success: boolean; + data?: UserOption[]; + error?: string; +}> { + try { + const response = await apiClient.get<{ + success: boolean; + message: string; + data: { + data: Array<{ + id: number; + name: string; + email?: string; + }>; + }; + }>('/users', { + params: { + active: '1', + size: '100', + }, + }); + + const paginatedData = response.data; + const items = Array.isArray(paginatedData.data) ? paginatedData.data : []; + const options: UserOption[] = items.map((item) => ({ + value: String(item.id), + label: item.name, + })); + + return { success: true, data: options }; + } catch (error) { + console.error('사용자 목록 조회 오류:', error); + return { success: false, error: '사용자 목록을 불러오는데 실패했습니다.' }; + } +} + +/** + * 견적서 옵션 데이터 일괄 조회 + * 제품, 도장, 모터, 제어기, 시공비 등 모든 셀렉트 옵션 + */ +export interface EstimateOptionsData { + materials: CommonCodeOption[]; + paintings: CommonCodeOption[]; + motors: CommonCodeOption[]; + controllers: CommonCodeOption[]; + widthConstructions: CommonCodeOption[]; + heightConstructions: CommonCodeOption[]; +} + +export async function getEstimateOptions(): Promise<{ + success: boolean; + data?: EstimateOptionsData; + error?: string; +}> { + try { + const [materials, paintings, motors, controllers, widthConstructions, heightConstructions] = + await Promise.all([ + getCommonCodes('material_type'), + getCommonCodes('painting_type'), + getCommonCodes('motor_type'), + getCommonCodes('controller_type'), + getCommonCodes('width_construction_cost'), + getCommonCodes('height_construction_cost'), + ]); + + return { + success: true, + data: { + materials: materials.data || [], + paintings: paintings.data || [], + motors: motors.data || [], + controllers: controllers.data || [], + widthConstructions: widthConstructions.data || [], + heightConstructions: heightConstructions.data || [], + }, + }; + } catch (error) { + console.error('견적서 옵션 일괄 조회 오류:', error); + return { success: false, error: '견적서 옵션을 불러오는데 실패했습니다.' }; + } } \ No newline at end of file diff --git a/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx b/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx index 4f4452a9..8d369c27 100644 --- a/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx +++ b/src/components/business/construction/estimates/sections/EstimateDetailTableSection.tsx @@ -27,8 +27,9 @@ import { TableRow, } from '@/components/ui/table'; import type { EstimateDetailItem } from '../types'; -import { formatAmount, MOCK_MATERIALS } from '../utils'; +import { formatAmount } from '../utils'; import { calculateItemValuesWithApplied, calculateTotalsWithApplied } from '../hooks/useEstimateCalculations'; +import type { CommonCodeOption } from '../actions'; // 계산식 정보 const FORMULA_INFO: Record = { @@ -85,10 +86,21 @@ export interface AppliedPrices { controller: number; } +// 옵션 데이터 타입 +export interface EstimateDetailOptions { + materials: CommonCodeOption[]; + paintings: CommonCodeOption[]; + motors: CommonCodeOption[]; + controllers: CommonCodeOption[]; + widthConstructions: CommonCodeOption[]; + heightConstructions: CommonCodeOption[]; +} + interface EstimateDetailTableSectionProps { detailItems: EstimateDetailItem[]; appliedPrices: AppliedPrices | null; isViewMode: boolean; + options?: EstimateDetailOptions; onAddItems: (count: number) => void; onRemoveItem: (id: string) => void; onRemoveSelected: () => void; @@ -99,10 +111,45 @@ interface EstimateDetailTableSectionProps { onReset: () => void; } +// API 데이터 로드 전 기본 옵션 (폴백용) +const DEFAULT_OPTIONS: EstimateDetailOptions = { + materials: [ + { value: 'screen', label: '스크린', code: 'screen' }, + { value: 'slat', label: '슬랫', code: 'slat' }, + { value: 'bending', label: '벤딩', code: 'bending' }, + { value: 'jointbar', label: '조인트바', code: 'jointbar' }, + ], + paintings: [ + { value: '0', label: '직접입력', code: '0', price: 0 }, + { value: '50000', label: '도장A', code: 'painting_a', price: 50000 }, + { value: '80000', label: '도장B', code: 'painting_b', price: 80000 }, + ], + motors: [ + { value: '300000', label: '모터 300,000', code: 'motor_300k', price: 300000 }, + { value: '500000', label: '모터 500,000', code: 'motor_500k', price: 500000 }, + ], + controllers: [ + { value: '150000', label: '제어기 150,000', code: 'ctrl_150k', price: 150000 }, + { value: '250000', label: '제어기 250,000', code: 'ctrl_250k', price: 250000 }, + ], + widthConstructions: [ + { value: '300000', label: '3.01~4.0M', code: 'w_3_4m', price: 300000 }, + { value: '400000', label: '4.01~5.0M', code: 'w_4_5m', price: 400000 }, + { value: '500000', label: '5.01~6.0M', code: 'w_5_6m', price: 500000 }, + { value: '600000', label: '6.01~7.0M', code: 'w_6_7m', price: 600000 }, + ], + heightConstructions: [ + { value: '5000', label: '3.51~4.5M', code: 'h_3_4m', price: 5000 }, + { value: '8000', label: '4.51~5.5M', code: 'h_4_5m', price: 8000 }, + { value: '10000', label: '5.51~6.5M', code: 'h_5_6m', price: 10000 }, + ], +}; + export function EstimateDetailTableSection({ detailItems, appliedPrices, isViewMode, + options, onAddItems, onRemoveItem, onRemoveSelected, @@ -112,6 +159,8 @@ export function EstimateDetailTableSection({ onApplyAdjustedPrice, onReset, }: EstimateDetailTableSectionProps) { + // API 옵션이 없으면 기본 옵션 사용 + const opts = options || DEFAULT_OPTIONS; const selectedCount = detailItems.filter((item) => (item as unknown as { selected?: boolean }).selected).length; const allSelected = detailItems.length > 0 && detailItems.every((item) => (item as unknown as { selected?: boolean }).selected); const totals = calculateTotalsWithApplied(detailItems, appliedPrices); @@ -279,7 +328,7 @@ export function EstimateDetailTableSection({ {/* 01: 명칭 */} onItemChange(item.id, 'name', e.target.value)} disabled={isViewMode} className={`w-full min-w-[80px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`} @@ -288,7 +337,7 @@ export function EstimateDetailTableSection({ {/* 02: 제품 */} onItemChange(item.id, 'width', Number(e.target.value))} disabled={isViewMode} className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`} @@ -320,7 +369,7 @@ export function EstimateDetailTableSection({ onItemChange(item.id, 'height', Number(e.target.value))} disabled={isViewMode} className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`} @@ -419,9 +468,11 @@ export function EstimateDetailTableSection({ - 직접입력 - 도장A - 도장B + {opts.paintings.map((option) => ( + + {option.label} + + ))} @@ -436,8 +487,11 @@ export function EstimateDetailTableSection({ - 모터 300,000 - 모터 500,000 + {opts.motors.map((option) => ( + + {option.label} + + ))} @@ -452,8 +506,11 @@ export function EstimateDetailTableSection({ - 제어기 150,000 - 제어기 250,000 + {opts.controllers.map((option) => ( + + {option.label} + + ))} @@ -468,10 +525,11 @@ export function EstimateDetailTableSection({ - 3.01~4.0M - 4.01~5.0M - 5.01~6.0M - 6.01~7.0M + {opts.widthConstructions.map((option) => ( + + {option.label} + + ))} @@ -486,9 +544,11 @@ export function EstimateDetailTableSection({ - 3.51~4.5M - 4.51~5.5M - 5.51~6.5M + {opts.heightConstructions.map((option) => ( + + {option.label} + + ))} @@ -507,7 +567,7 @@ export function EstimateDetailTableSection({ onItemChange(item.id, 'expense', Number(e.target.value))} disabled={isViewMode} className={`text-right min-w-[60px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`} @@ -528,7 +588,7 @@ export function EstimateDetailTableSection({ onItemChange(item.id, 'quantity', Number(e.target.value))} disabled={isViewMode} className={`text-right min-w-[40px] ${isViewMode ? 'bg-gray-50' : 'bg-white'}`} diff --git a/src/components/business/construction/estimates/utils/constants.ts b/src/components/business/construction/estimates/utils/constants.ts index ac9fc236..8f88862d 100644 --- a/src/components/business/construction/estimates/utils/constants.ts +++ b/src/components/business/construction/estimates/utils/constants.ts @@ -1,10 +1,6 @@ -// 목업 재료 목록 -export const MOCK_MATERIALS = [ - { value: 'screen', label: '스크린' }, - { value: 'slat', label: '슬랫' }, - { value: 'bending', label: '벤딩' }, - { value: 'jointbar', label: '조인트바' }, -]; - -// 공과 품목은 Items API (type=RM)에서 조회 -// MOCK_EXPENSES 제거됨 - getExpenseItemOptions() 사용 \ No newline at end of file +// 견적서 상수 정의 +// 모든 MOCK 데이터는 API로 대체됨: +// - 재료(material_type): getCommonCodeOptions('material_type') +// - 공과 품목: getExpenseItemOptions() +// - 도장/모터/제어기/시공비: getCommonCodeOptions('{group}') +// - 거래처/견적자: getClientOptions(), getUserOptions() \ No newline at end of file