"use client"; /** * 판매품목정보 — Type B 마스터-디테일 리디자인 * * 좌측: 판매품목 목록 (item_info, 판매 관련 필터) * 우측: 선택한 품목의 거래처 정보 (customer_item_mapping → customer_mng 조인) * * 거래처관리와 양방향 연동 (같은 customer_item_mapping 테이블) */ import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Checkbox } from "@/components/ui/checkbox"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Users, Search, X, Settings2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; import { exportToExcel } from "@/lib/utils/excelExport"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { useTableSettings } from "@/hooks/useTableSettings"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const ITEM_TABLE = "item_info"; const MAPPING_TABLE = "customer_item_mapping"; const CUSTOMER_TABLE = "customer_mng"; // 숫자 포맷 헬퍼 const formatNum = (val: any): string => { if (val === null || val === undefined || val === "") return ""; const n = Number(val); return isNaN(n) ? String(val) : n.toLocaleString(); }; const ITEM_GRID_COLUMNS = [ { key: "item_number", label: "품번" }, { key: "item_name", label: "품명" }, { key: "size", label: "규격" }, { key: "unit", label: "단위" }, { key: "standard_price", label: "기준단가" }, { key: "selling_price", label: "판매가격" }, { key: "currency_code", label: "통화" }, { key: "status", label: "상태" }, ]; export default function SalesItemPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); const ts = useTableSettings("c16-sales-item", ITEM_TABLE, ITEM_GRID_COLUMNS); // 좌측: 품목 const [items, setItems] = useState([]); const [itemLoading, setItemLoading] = useState(false); const [itemCount, setItemCount] = useState(0); const [selectedItemId, setSelectedItemId] = useState(null); // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); // 우측: 거래처 const [customerItems, setCustomerItems] = useState([]); const [customerLoading, setCustomerLoading] = useState(false); const [customerCheckedIds, setCustomerCheckedIds] = useState([]); // 카테고리 const [categoryOptions, setCategoryOptions] = useState>({}); const [priceCategoryOptions, setPriceCategoryOptions] = useState>({}); // 거래처 추가 모달 const [custSelectOpen, setCustSelectOpen] = useState(false); const [custSearchKeyword, setCustSearchKeyword] = useState(""); const [custSearchResults, setCustSearchResults] = useState([]); const [custSearchLoading, setCustSearchLoading] = useState(false); const [custCheckedIds, setCustCheckedIds] = useState>(new Set()); // 품목 수정 모달 const [editItemOpen, setEditItemOpen] = useState(false); const [editItemForm, setEditItemForm] = useState>({}); const [saving, setSaving] = useState(false); // 엑셀 const [excelUploadOpen, setExcelUploadOpen] = useState(false); // 거래처 상세 입력 모달 (거래처 품번/품명 + 단가) const [custDetailOpen, setCustDetailOpen] = useState(false); const [selectedCustsForDetail, setSelectedCustsForDetail] = useState([]); const [custMappings, setCustMappings] = useState>>({}); const [custPrices, setCustPrices] = useState>>({}); const [editCustData, setEditCustData] = useState(null); // 카테고리 로드 useEffect(() => { const load = async () => { const optMap: Record = {}; const flatten = (vals: any[]): { code: string; label: string; isDefault?: boolean }[] => { const result: { code: string; label: string; isDefault?: boolean }[] = []; for (const v of vals) { result.push({ code: v.valueCode, label: v.valueLabel, isDefault: v.isDefault }); if (v.children?.length) result.push(...flatten(v.children)); } return result; }; for (const col of ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]) { try { const res = await apiClient.get(`/table-categories/${ITEM_TABLE}/${col}/values`); if (res.data?.success) optMap[col] = flatten(res.data.data || []); } catch { /* skip */ } } setCategoryOptions(optMap); // 단가 카테고리 const priceOpts: Record = {}; for (const col of ["base_price_type", "currency_code", "discount_type", "rounding_type", "rounding_unit_value"]) { try { const res = await apiClient.get(`/table-categories/customer_item_prices/${col}/values`); if (res.data?.success) priceOpts[col] = flatten(res.data.data || []); } catch { /* skip */ } } setPriceCategoryOptions(priceOpts); }; load(); }, []); const resolve = (col: string, code: string) => { if (!code) return ""; return categoryOptions[col]?.find((o) => o.code === code)?.label || code; }; // 좌측: 품목 조회 const fetchItems = useCallback(async () => { setItemLoading(true); try { const filters: { columnName: string; operator: string; value: any }[] = []; // 판매품목/영업관리 division 필터 (다중값 컬럼이므로 contains로 매칭) filters.push({ columnName: "division", operator: "contains", value: "CAT_ML8ZFVEL_1TOR" }); // DynamicSearchFilter에서 전달된 필터 추가 for (const f of searchFilters) { filters.push({ columnName: f.columnName, operator: f.operator, value: f.value }); } const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const raw = res.data?.data?.data || res.data?.data?.rows || []; const CATS = ["unit", "material", "division", "type", "status", "inventory_unit", "currency_code", "user_type01", "user_type02"]; const data = raw.map((r: any) => { const converted = { ...r }; for (const col of CATS) { if (converted[col]) converted[col] = resolve(col, converted[col]); } return converted; }); setItems(data); setItemCount(res.data?.data?.total || raw.length); } catch (err) { console.error("품목 조회 실패:", err); toast.error("품목 목록을 불러오는데 실패했습니다."); } finally { setItemLoading(false); } }, [searchFilters, categoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { fetchItems(); }, [fetchItems]); // 선택된 품목 const selectedItem = items.find((i) => i.id === selectedItemId); // 우측: 거래처 목록 조회 useEffect(() => { if (!selectedItem?.item_number) { setCustomerItems([]); setCustomerCheckedIds([]); return; } setCustomerCheckedIds([]); const itemKey = selectedItem.item_number; const fetchCustomerItems = async () => { setCustomerLoading(true); try { // 1. customer_item_mapping에서 해당 품목의 매핑 조회 const mapRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/data`, { page: 1, size: 500, dataFilter: { enabled: true, filters: [{ columnName: "item_id", operator: "equals", value: itemKey }] }, autoFilter: true, }); const mappings = mapRes.data?.data?.data || mapRes.data?.data?.rows || []; // 2. customer_id → customer_mng 조인 (거래처명) const custIds = [...new Set(mappings.map((m: any) => m.customer_id).filter(Boolean))]; let custMap: Record = {}; if (custIds.length > 0) { try { const custRes = await apiClient.post(`/table-management/tables/${CUSTOMER_TABLE}/data`, { page: 1, size: custIds.length + 10, dataFilter: { enabled: true, filters: [{ columnName: "customer_code", operator: "in", value: custIds }] }, autoFilter: true, }); for (const c of (custRes.data?.data?.data || custRes.data?.data?.rows || [])) { custMap[c.customer_code] = c; } } catch { /* skip */ } } // 3. customer_item_prices 조회 (단가 정보) let allPrices: any[] = []; if (mappings.length > 0) { try { const priceRes = await apiClient.post(`/table-management/tables/customer_item_prices/data`, { page: 1, size: 500, dataFilter: { enabled: true, filters: [ { columnName: "item_id", operator: "equals", value: itemKey }, ]}, autoFilter: true, }); allPrices = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; } catch { /* skip */ } } // 4. 거래처별 중복 제거 + 오늘 날짜 기준 단가 매칭 const priceResolve = (col: string, code: string) => { if (!code) return ""; return priceCategoryOptions[col]?.find((o: any) => o.code === code)?.label || code; }; const today = new Date().toISOString().split("T")[0]; const seenCustIds = new Set(); const sortedMappings = [...mappings].sort((a: any, b: any) => (a.customer_id || "").localeCompare(b.customer_id || "")); setCustomerItems(sortedMappings.map((m: any) => { const custKey = m.customer_id || ""; const isFirstOfGroup = !seenCustIds.has(custKey); if (custKey) seenCustIds.add(custKey); const custPriceList = allPrices.filter((p: any) => p.customer_id === custKey); const todayPrice = custPriceList.find((p: any) => (!p.start_date || p.start_date <= today) && (!p.end_date || p.end_date >= today) ) || custPriceList[0] || {}; return { ...m, customer_code: isFirstOfGroup ? custKey : "", customer_name: isFirstOfGroup ? (custMap[custKey]?.customer_name || "") : "", customer_item_code: m.customer_item_code || "", customer_item_name: m.customer_item_name || "", base_price: todayPrice.base_price || "", calculated_price: todayPrice.calculated_price || todayPrice.unit_price || "", currency_code: priceResolve("currency_code", todayPrice.currency_code || ""), }; })); } catch (err) { console.error("거래처 조회 실패:", err); } finally { setCustomerLoading(false); } }; fetchCustomerItems(); }, [selectedItem?.item_number, priceCategoryOptions]); // eslint-disable-line react-hooks/exhaustive-deps // 거래처 검색 const searchCustomers = async () => { setCustSearchLoading(true); try { const filters: any[] = []; if (custSearchKeyword) filters.push({ columnName: "customer_name", operator: "contains", value: custSearchKeyword }); const res = await apiClient.post(`/table-management/tables/${CUSTOMER_TABLE}/data`, { page: 1, size: 50, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, autoFilter: true, }); const all = res.data?.data?.data || res.data?.data?.rows || []; // 이미 등록된 거래처 제외 const existing = new Set(customerItems.map((c: any) => c.customer_id || c.customer_code)); setCustSearchResults(all.filter((c: any) => !existing.has(c.customer_code))); } catch { /* skip */ } finally { setCustSearchLoading(false); } }; // 거래처 선택 → 상세 모달로 이동 const goToCustDetail = () => { const selected = custSearchResults.filter((c) => custCheckedIds.has(c.id)); if (selected.length === 0) { toast.error("거래처를 선택해주세요."); return; } setSelectedCustsForDetail(selected); const mappings: typeof custMappings = {}; const prices: typeof custPrices = {}; for (const cust of selected) { const key = cust.customer_code || cust.id; mappings[key] = []; prices[key] = [{ _id: `p_${Date.now()}_${Math.random()}`, start_date: "", end_date: "", currency_code: "CAT_MLAMDKVN_PZJI", base_price_type: "CAT_MLAMFGFT_4RZW", base_price: selectedItem?.standard_price || selectedItem?.selling_price || "", discount_type: "", discount_value: "", rounding_type: "", rounding_unit_value: "", calculated_price: selectedItem?.standard_price || selectedItem?.selling_price || "", }]; } setCustMappings(mappings); setCustPrices(prices); setCustSelectOpen(false); setCustDetailOpen(true); }; const addMappingRow = (custKey: string) => { setCustMappings((prev) => ({ ...prev, [custKey]: [...(prev[custKey] || []), { _id: `m_${Date.now()}_${Math.random()}`, customer_item_code: "", customer_item_name: "" }], })); }; const removeMappingRow = (custKey: string, rowId: string) => { setCustMappings((prev) => ({ ...prev, [custKey]: (prev[custKey] || []).filter((r) => r._id !== rowId), })); }; const updateMappingRow = (custKey: string, rowId: string, field: string, value: string) => { setCustMappings((prev) => ({ ...prev, [custKey]: (prev[custKey] || []).map((r) => r._id === rowId ? { ...r, [field]: value } : r), })); }; const addPriceRow = (custKey: string) => { setCustPrices((prev) => ({ ...prev, [custKey]: [...(prev[custKey] || []), { _id: `p_${Date.now()}_${Math.random()}`, start_date: "", end_date: "", currency_code: "CAT_MLAMDKVN_PZJI", base_price_type: "CAT_MLAMFGFT_4RZW", base_price: "", discount_type: "", discount_value: "", rounding_type: "", rounding_unit_value: "", calculated_price: "", }], })); }; const removePriceRow = (custKey: string, rowId: string) => { setCustPrices((prev) => ({ ...prev, [custKey]: (prev[custKey] || []).filter((r) => r._id !== rowId), })); }; const updatePriceRow = (custKey: string, rowId: string, field: string, value: string) => { setCustPrices((prev) => ({ ...prev, [custKey]: (prev[custKey] || []).map((r) => { if (r._id !== rowId) return r; const updated = { ...r, [field]: value }; if (["base_price", "discount_type", "discount_value"].includes(field)) { const bp = Number(updated.base_price) || 0; const dv = Number(updated.discount_value) || 0; const dt = updated.discount_type; let calc = bp; if (dt === "CAT_MLAMBEC8_URQA") calc = bp * (1 - dv / 100); else if (dt === "CAT_MLAMBLFM_JTLO") calc = bp - dv; updated.calculated_price = String(Math.round(calc)); } return updated; }), })); }; const openEditCust = async (row: any) => { const custKey = row.customer_code || row.customer_id; // customer_mng에서 거래처 정보 조회 let custInfo: any = { customer_code: custKey, customer_name: row.customer_name || "" }; try { const res = await apiClient.post(`/table-management/tables/customer_mng/data`, { page: 1, size: 1, dataFilter: { enabled: true, filters: [{ columnName: "customer_code", operator: "equals", value: custKey }] }, autoFilter: true, }); const found = (res.data?.data?.data || res.data?.data?.rows || [])[0]; if (found) custInfo = found; } catch { /* skip */ } const mappingRows = [{ _id: `m_existing_${row.id}`, customer_item_code: row.customer_item_code || "", customer_item_name: row.customer_item_name || "", }].filter((m) => m.customer_item_code || m.customer_item_name); const priceRows = [{ _id: `p_existing_${row.id}`, start_date: row.start_date || "", end_date: row.end_date || "", currency_code: row.currency_code || "CAT_MLAMDKVN_PZJI", base_price_type: row.base_price_type || "CAT_MLAMFGFT_4RZW", base_price: row.base_price ? String(row.base_price) : "", discount_type: row.discount_type || "", discount_value: row.discount_value ? String(row.discount_value) : "", rounding_type: row.rounding_type || "", rounding_unit_value: row.rounding_unit_value || "", calculated_price: row.calculated_price ? String(row.calculated_price) : "", }].filter((p) => p.base_price || p.start_date); if (priceRows.length === 0) { priceRows.push({ _id: `p_${Date.now()}`, start_date: "", end_date: "", currency_code: "CAT_MLAMDKVN_PZJI", base_price_type: "CAT_MLAMFGFT_4RZW", base_price: "", discount_type: "", discount_value: "", rounding_type: "", rounding_unit_value: "", calculated_price: "", }); } setSelectedCustsForDetail([custInfo]); setCustMappings({ [custKey]: mappingRows }); setCustPrices({ [custKey]: priceRows }); setEditCustData(row); setCustDetailOpen(true); }; const handleCustDetailSave = async () => { if (!selectedItem) return; const isEditingExisting = !!editCustData; setSaving(true); try { for (const cust of selectedCustsForDetail) { const custKey = cust.customer_code || cust.id; const mappingRows = custMappings[custKey] || []; if (isEditingExisting && editCustData?.id) { await apiClient.put(`/table-management/tables/${MAPPING_TABLE}/edit`, { originalData: { id: editCustData.id }, updatedData: { customer_item_code: mappingRows[0]?.customer_item_code || "", customer_item_name: mappingRows[0]?.customer_item_name || "", }, }); // 기존 prices 삭제 후 재등록 try { const existingPrices = await apiClient.post(`/table-management/tables/customer_item_prices/data`, { page: 1, size: 100, dataFilter: { enabled: true, filters: [ { columnName: "mapping_id", operator: "equals", value: editCustData.id }, ]}, autoFilter: true, }); const existing = existingPrices.data?.data?.data || existingPrices.data?.data?.rows || []; if (existing.length > 0) { await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, { data: existing.map((p: any) => ({ id: p.id })), }); } } catch { /* skip */ } const priceRows = (custPrices[custKey] || []).filter((p) => (p.base_price && Number(p.base_price) > 0) || p.start_date ); for (const price of priceRows) { await apiClient.post(`/table-management/tables/customer_item_prices/add`, { id: crypto.randomUUID(), mapping_id: editCustData.id, customer_id: custKey, item_id: selectedItem.item_number, start_date: price.start_date || null, end_date: price.end_date || null, currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, base_price: price.base_price ? Number(price.base_price) : null, discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, calculated_price: price.calculated_price ? Number(price.calculated_price) : null, }); } } else { // 신규 등록 const mappingRes = await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { id: crypto.randomUUID(), customer_id: custKey, item_id: selectedItem.item_number, customer_item_code: mappingRows[0]?.customer_item_code || "", customer_item_name: mappingRows[0]?.customer_item_name || "", }); const mappingId = mappingRes.data?.data?.id || null; for (let mi = 1; mi < mappingRows.length; mi++) { await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { id: crypto.randomUUID(), customer_id: custKey, item_id: selectedItem.item_number, customer_item_code: mappingRows[mi].customer_item_code || "", customer_item_name: mappingRows[mi].customer_item_name || "", }); } const priceRows = (custPrices[custKey] || []).filter((p) => (p.base_price && Number(p.base_price) > 0) || p.start_date ); for (const price of priceRows) { await apiClient.post(`/table-management/tables/customer_item_prices/add`, { id: crypto.randomUUID(), mapping_id: mappingId || "", customer_id: custKey, item_id: selectedItem.item_number, start_date: price.start_date || null, end_date: price.end_date || null, currency_code: price.currency_code || null, base_price_type: price.base_price_type || null, base_price: price.base_price ? Number(price.base_price) : null, discount_type: price.discount_type || null, discount_value: price.discount_value ? Number(price.discount_value) : null, rounding_type: price.rounding_type || null, rounding_unit_value: price.rounding_unit_value || null, calculated_price: price.calculated_price ? Number(price.calculated_price) : null, }); } } } toast.success(isEditingExisting ? "수정되었습니다." : `${selectedCustsForDetail.length}개 거래처가 추가되었습니다.`); setCustDetailOpen(false); setEditCustData(null); setCustCheckedIds(new Set()); // 우측 새로고침 const sid = selectedItemId; setSelectedItemId(null); setTimeout(() => setSelectedItemId(sid), 50); } catch (err: any) { toast.error(err.response?.data?.message || "저장에 실패했습니다."); } finally { setSaving(false); } }; // 품목 수정 const openEditItem = () => { if (!selectedItem) return; setEditItemForm({ ...selectedItem }); setEditItemOpen(true); }; const handleEditSave = async () => { if (!editItemForm.id) return; setSaving(true); try { await apiClient.put(`/table-management/tables/${ITEM_TABLE}/edit`, { originalData: { id: editItemForm.id }, updatedData: { selling_price: editItemForm.selling_price || null, standard_price: editItemForm.standard_price || null, currency_code: editItemForm.currency_code || null, }, }); toast.success("수정되었습니다."); setEditItemOpen(false); fetchItems(); } catch (err: any) { toast.error(err.response?.data?.message || "수정에 실패했습니다."); } finally { setSaving(false); } }; // 우측: 거래처 매핑 삭제 const handleCustomerMappingDelete = async () => { if (customerCheckedIds.length === 0) return; const ok = await confirm(`선택한 ${customerCheckedIds.length}개 거래처 매핑을 삭제하시겠습니까?`, { description: "관련된 단가 정보도 함께 삭제됩니다.", variant: "destructive", confirmText: "삭제", }); if (!ok) return; try { // 관련 단가 삭제 for (const mappingId of customerCheckedIds) { try { const priceRes = await apiClient.post(`/table-management/tables/customer_item_prices/data`, { page: 1, size: 500, dataFilter: { enabled: true, filters: [{ columnName: "mapping_id", operator: "equals", value: mappingId }] }, autoFilter: true, }); const prices = priceRes.data?.data?.data || priceRes.data?.data?.rows || []; if (prices.length > 0) { await apiClient.delete(`/table-management/tables/customer_item_prices/delete`, { data: prices.map((p: any) => ({ id: p.id })), }); } } catch { /* skip */ } } // 매핑 삭제 await apiClient.delete(`/table-management/tables/${MAPPING_TABLE}/delete`, { data: customerCheckedIds.map((id) => ({ id })), }); toast.success(`${customerCheckedIds.length}개 거래처 매핑이 삭제되었습니다.`); setCustomerCheckedIds([]); const sid = selectedItemId; setSelectedItemId(null); setTimeout(() => setSelectedItemId(sid), 50); } catch { toast.error("삭제에 실패했습니다."); } }; // 엑셀 다운로드 const handleExcelDownload = async () => { if (items.length === 0) return; const data = items.map((i) => ({ 품번: i.item_number, 품명: i.item_name, 규격: i.size, 단위: i.unit, 기준단가: i.standard_price, 판매가격: i.selling_price, 통화: i.currency_code, 상태: i.status, })); await exportToExcel(data, "판매품목정보.xlsx", "판매품목"); toast.success("다운로드 완료"); }; // EDataTable 컬럼 정의 (판매품목) const itemColumns: EDataTableColumn[] = [ { key: "item_number", label: "품번", width: "w-[110px]" }, { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, { key: "size", label: "규격", width: "w-[80px]" }, { key: "unit", label: "단위", width: "w-[60px]" }, { key: "standard_price", label: "기준단가", width: "w-[90px]", align: "right", formatNumber: true }, { key: "selling_price", label: "판매가격", width: "w-[90px]", align: "right", formatNumber: true }, { key: "currency_code", label: "통화", width: "w-[50px]" }, { key: "status", label: "상태", width: "w-[60px]" }, ]; return (
{/* 검색 필터 (DynamicSearchFilter) */}
} /> {/* ── 마스터-디테일 분할 패널 ── */}
{/* 좌측: 판매품목 목록 (마스터) */}
{/* 패널 헤더 */}
판매품목 목록 {itemCount}건
{/* 테이블 영역 */} row.id} loading={itemLoading} emptyMessage="등록된 판매품목이 없어요" selectedId={selectedItemId} onSelect={(id) => setSelectedItemId(id)} onRowDoubleClick={() => openEditItem()} showRowNumber showPagination={false} draggableColumns={false} columnOrderKey="c16-sales-item" />
{/* 우측: 거래처 정보 (디테일) */}
{!selectedItemId ? ( /* 빈 상태 */
품목을 선택해주세요
좌측에서 품목을 선택하면 거래처 정보가 표시돼요
) : ( <> {/* 선택 품목 상세 헤더 */}
{selectedItem?.item_name || "-"} {selectedItem?.item_number || ""}
{/* 거래처별 단가 서브헤더 */}
거래처별 단가 {customerItems.length}건
{/* 거래처 테이블 */}
{customerLoading ? (
) : customerItems.length === 0 ? (
등록된 거래처가 없어요
) : ( 0 && customerCheckedIds.length === customerItems.length} onCheckedChange={(checked) => { if (checked === true) setCustomerCheckedIds(customerItems.map((c) => c.id)); else setCustomerCheckedIds([]); }} /> 거래처코드 거래처명 거래처품번 거래처품명 기준가 단가 통화 {customerItems.map((row) => ( openEditCust(row)} > { if (checked === true) setCustomerCheckedIds((prev) => [...prev, row.id]); else setCustomerCheckedIds((prev) => prev.filter((id) => id !== row.id)); }} /> {row.customer_code} {row.customer_name} {row.customer_item_code} {row.customer_item_name} {formatNum(row.base_price)} {formatNum(row.calculated_price)} {row.currency_code} ))}
)}
)}
{/* ── 품목 수정 모달 ── */} 판매품목 수정 {editItemForm.item_number || ""} — {editItemForm.item_name || ""}
{/* 품목 기본정보 (읽기 전용) */} {[ { key: "item_number", label: "품목코드" }, { key: "item_name", label: "품명" }, { key: "size", label: "규격" }, { key: "unit", label: "단위" }, { key: "material", label: "재질" }, { key: "status", label: "상태" }, ].map((f) => (
))}
{/* 판매 설정 (수정 가능) */}
setEditItemForm((p) => ({ ...p, selling_price: e.target.value }))} placeholder="판매가격을 입력해주세요" className="h-9" />
setEditItemForm((p) => ({ ...p, standard_price: e.target.value }))} placeholder="기준단가를 입력해주세요" className="h-9" />
{/* ── 거래처 검색 및 추가 모달 ── */} 거래처 검색 및 추가 품목에 추가할 거래처를 선택해주세요.
{/* 검색바 */}
setCustSearchKeyword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && searchCustomers()} className="h-9 flex-1" />
{/* 검색 결과 테이블 */}
0 && custCheckedIds.size === custSearchResults.length} onCheckedChange={(checked) => { if (checked === true) setCustCheckedIds(new Set(custSearchResults.map((c) => c.id))); else setCustCheckedIds(new Set()); }} /> 거래처코드 거래처명 거래유형 담당자 {custSearchResults.length === 0 ? ( 검색 결과가 없어요 ) : custSearchResults.map((c) => ( setCustCheckedIds((prev) => { const next = new Set(prev); if (next.has(c.id)) next.delete(c.id); else next.add(c.id); return next; })} > { setCustCheckedIds((prev) => { const next = new Set(prev); if (checked === true) next.add(c.id); else next.delete(c.id); return next; }); }} /> {c.customer_code} {c.customer_name} {c.division} {c.contact_person} ))}
{custCheckedIds.size}개 선택됨
{/* ── 거래처 상세 입력/수정 모달 ── */} 거래처 상세정보 {editCustData ? "수정" : "입력"} — {selectedItem?.item_name || ""} {editCustData ? "거래처 품번/품명과 기간별 단가를 수정해주세요." : "선택한 거래처의 품번/품명과 기간별 단가를 설정해주세요."}
{selectedCustsForDetail.map((cust, idx) => { const custKey = cust.customer_code || cust.id; const mappingRows = custMappings[custKey] || []; const prices = custPrices[custKey] || []; return (
{/* 거래처 헤더 */}
{idx + 1}. {cust.customer_name || custKey} {custKey}
{/* 좌: 거래처 품번/품명 */}
거래처 품번/품명 관리
{mappingRows.length === 0 ? (
입력된 거래처 품번이 없어요
) : mappingRows.map((mRow, mIdx) => (
{mIdx + 1} updateMappingRow(custKey, mRow._id, "customer_item_code", e.target.value)} placeholder="거래처 품번" className="h-8 text-sm flex-1" /> updateMappingRow(custKey, mRow._id, "customer_item_name", e.target.value)} placeholder="거래처 품명" className="h-8 text-sm flex-1" />
))}
{/* 우: 기간별 단가 */}
기간별 단가 설정
{prices.map((price, pIdx) => (
단가 {pIdx + 1} {prices.length > 1 && ( )}
{/* 기간 + 통화 */}
updatePriceRow(custKey, price._id, "start_date", e.target.value)} className="h-8 text-xs flex-1" /> ~ updatePriceRow(custKey, price._id, "end_date", e.target.value)} className="h-8 text-xs flex-1" />
{/* 기준가 + 할인 + 반올림 */}
updatePriceRow(custKey, price._id, "base_price", e.target.value)} className="h-8 text-xs text-right flex-1" placeholder="기준가" />
updatePriceRow(custKey, price._id, "discount_value", e.target.value)} className="h-8 text-xs text-right w-[60px]" placeholder="0" />
{/* 계산 단가 */}
계산 단가: {price.calculated_price ? Number(price.calculated_price).toLocaleString() : "-"}
))}
); })}
{/* 테이블 설정 모달 */} {/* 엑셀 업로드 */} fetchItems()} /> {ConfirmDialogComponent} ); }