diff --git a/frontend/app/(main)/sales/sales-item/page.tsx b/frontend/app/(main)/sales/sales-item/page.tsx new file mode 100644 index 00000000..e2b8a6eb --- /dev/null +++ b/frontend/app/(main)/sales/sales-item/page.tsx @@ -0,0 +1,504 @@ +"use client"; + +/** + * 판매품목정보 — 하드코딩 페이지 + * + * 좌측: 판매품목 목록 (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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { Plus, Trash2, Save, Loader2, FileSpreadsheet, Download, Pencil, Package, Users, Search } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; +import { useAuth } from "@/hooks/useAuth"; +import { toast } from "sonner"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; +import { useConfirmDialog } from "@/components/common/ConfirmDialog"; +import { FullscreenDialog } from "@/components/common/FullscreenDialog"; +import { ExcelUploadModal } from "@/components/common/ExcelUploadModal"; +import { exportToExcel } from "@/lib/utils/excelExport"; + +const ITEM_TABLE = "item_info"; +const MAPPING_TABLE = "customer_item_mapping"; +const CUSTOMER_TABLE = "customer_mng"; + +// 좌측: 판매품목 컬럼 +const LEFT_COLUMNS: DataGridColumn[] = [ + { key: "item_number", label: "품번", width: "w-[110px]" }, + { key: "item_name", label: "품명", minWidth: "min-w-[130px]" }, + { key: "size", label: "규격", width: "w-[90px]" }, + { key: "unit", label: "단위", width: "w-[60px]" }, + { key: "standard_price", label: "기준단가", width: "w-[90px]", formatNumber: true, align: "right" }, + { key: "selling_price", label: "판매가격", width: "w-[90px]", formatNumber: true, align: "right" }, + { key: "currency_code", label: "통화", width: "w-[50px]" }, + { key: "status", label: "상태", width: "w-[60px]" }, +]; + +// 우측: 거래처 정보 컬럼 +const RIGHT_COLUMNS: DataGridColumn[] = [ + { key: "customer_code", label: "거래처코드", width: "w-[110px]" }, + { key: "customer_name", label: "거래처명", minWidth: "min-w-[120px]" }, + { key: "customer_item_code", label: "거래처품번", width: "w-[100px]" }, + { key: "customer_item_name", label: "거래처품명", width: "w-[100px]" }, + { key: "base_price", label: "기준가", width: "w-[80px]", formatNumber: true, align: "right" }, + { key: "calculated_price", label: "단가", width: "w-[80px]", formatNumber: true, align: "right" }, + { key: "currency_code", label: "통화", width: "w-[50px]" }, +]; + +export default function SalesItemPage() { + const { user } = useAuth(); + const { confirm, ConfirmDialogComponent } = useConfirmDialog(); + + // 좌측: 품목 + const [items, setItems] = useState([]); + const [itemLoading, setItemLoading] = useState(false); + const [itemCount, setItemCount] = useState(0); + const [searchFilters, setSearchFilters] = useState([]); + const [selectedItemId, setSelectedItemId] = useState(null); + + // 우측: 거래처 + const [customerItems, setCustomerItems] = useState([]); + const [customerLoading, setCustomerLoading] = useState(false); + + // 카테고리 + const [categoryOptions, setCategoryOptions] = 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); + + // 카테고리 로드 + useEffect(() => { + const load = async () => { + const optMap: Record = {}; + const flatten = (vals: any[]): { code: string; label: string }[] => { + const result: { code: string; label: string }[] = []; + for (const v of vals) { + result.push({ code: v.valueCode, label: v.valueLabel }); + 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); + }; + 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 = searchFilters.map((f) => ({ 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]); + + useEffect(() => { fetchItems(); }, [fetchItems]); + + // 선택된 품목 + const selectedItem = items.find((i) => i.id === selectedItemId); + + // 우측: 거래처 목록 조회 + useEffect(() => { + if (!selectedItem?.item_number) { setCustomerItems([]); return; } + const itemKey = selectedItem.item_number; + const fetchCustomerItems = async () => { + setCustomerLoading(true); + try { + // 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 || []; + + // 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 */ } + } + + setCustomerItems(mappings.map((m: any) => ({ + ...m, + customer_code: m.customer_id, + customer_name: custMap[m.customer_id]?.customer_name || "", + }))); + } catch (err) { + console.error("거래처 조회 실패:", err); + } finally { + setCustomerLoading(false); + } + }; + fetchCustomerItems(); + }, [selectedItem?.item_number]); + + // 거래처 검색 + 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 addSelectedCustomers = async () => { + const selected = custSearchResults.filter((c) => custCheckedIds.has(c.id)); + if (selected.length === 0 || !selectedItem) return; + try { + for (const cust of selected) { + await apiClient.post(`/table-management/tables/${MAPPING_TABLE}/add`, { + customer_id: cust.customer_code, + item_id: selectedItem.item_number, + }); + } + toast.success(`${selected.length}개 거래처가 추가되었습니다.`); + setCustCheckedIds(new Set()); + setCustSelectOpen(false); + // 우측 새로고침 + const sid = selectedItemId; + setSelectedItemId(null); + setTimeout(() => setSelectedItemId(sid), 50); + } catch (err: any) { + toast.error(err.response?.data?.message || "거래처 추가에 실패했습니다."); + } + }; + + // 품목 수정 + 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 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("다운로드 완료"); + }; + + return ( +
+ {/* 검색 */} + + + +
+ } + /> + + {/* 분할 패널 */} +
+ + {/* 좌측: 판매품목 */} + +
+
+
+ 판매품목 목록 + {itemCount}건 +
+
+ +
+
+ openEditItem()} + emptyMessage="등록된 판매품목이 없습니다" + /> +
+
+ + + + {/* 우측: 거래처 정보 */} + +
+
+
+ 거래처 정보 + {selectedItem && {selectedItem.item_name}} +
+ +
+ {!selectedItemId ? ( +
+ 좌측에서 품목을 선택하세요 +
+ ) : ( + + )} +
+
+
+
+ + {/* 품목 수정 모달 */} + + + + + } + > +
+ {/* 품목 기본정보 (읽기 전용) */} + {[ + { 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} + onChange={(e) => { + if (e.target.checked) 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; + })}> + + {c.customer_code} + {c.customer_name} + {c.division} + {c.contact_person} + + ))} + +
+
+ +
+ {custCheckedIds.size}개 선택됨 +
+ + +
+
+
+
+
+ + {/* 엑셀 업로드 */} + fetchItems()} + /> + + {ConfirmDialogComponent} +
+ ); +}