From 1bf91bf043a409d3959fcd632018a234c9cfc4c0 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 26 Mar 2026 09:30:17 +0900 Subject: [PATCH] Merge branch 'mhkim-node' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node --- .../src/controllers/multilangController.ts | 142 ++- backend-node/src/services/multilangService.ts | 20 + backend-node/src/types/multilang.ts | 1 + frontend/app/(main)/sales/claim/page.tsx | 1116 ++++++++--------- frontend/package-lock.json | 42 +- 5 files changed, 696 insertions(+), 625 deletions(-) diff --git a/backend-node/src/controllers/multilangController.ts b/backend-node/src/controllers/multilangController.ts index f14fc3b5..62451708 100644 --- a/backend-node/src/controllers/multilangController.ts +++ b/backend-node/src/controllers/multilangController.ts @@ -191,18 +191,30 @@ export const getLangKeys = async ( ): Promise => { try { const { companyCode, menuCode, keyType, searchText, categoryId } = req.query; + const userCompanyCode = req.user?.companyCode; logger.info("다국어 키 목록 조회 요청", { query: req.query, user: req.user, }); + // company_code 필터링: 비관리자는 자기 회사 + 공통(*) 키만 조회 가능 + let effectiveCompanyCode = companyCode as string; + if (userCompanyCode !== "*") { + // 비관리자가 다른 회사의 데이터를 요청하면 자기 회사로 제한 + if (companyCode && companyCode !== userCompanyCode && companyCode !== "*") { + effectiveCompanyCode = userCompanyCode || ""; + } + } + const multiLangService = new MultiLangService(); const langKeys = await multiLangService.getLangKeys({ - companyCode: companyCode as string, + companyCode: effectiveCompanyCode, menuCode: menuCode as string, keyType: keyType as string, searchText: searchText as string, categoryId: categoryId ? parseInt(categoryId as string, 10) : undefined, + // 비관리자: companyCode 필터가 없으면 자기 회사 + 공통(*) 키만 반환 + userCompanyCode: userCompanyCode, }); const response: ApiResponse = { @@ -235,9 +247,24 @@ export const getLangTexts = async ( ): Promise => { try { const { keyId } = req.params; + const userCompanyCode = req.user?.companyCode; logger.info("다국어 텍스트 조회 요청", { keyId, user: req.user }); const multiLangService = new MultiLangService(); + + // 비관리자: 해당 키가 자기 회사 또는 공통(*) 키인지 검증 + if (userCompanyCode !== "*") { + const keyOwner = await multiLangService.getKeyCompanyCode(parseInt(keyId)); + if (keyOwner && keyOwner !== "*" && keyOwner !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "다른 회사의 다국어 키에 접근할 권한이 없습니다.", + error: { code: "PERMISSION_DENIED" }, + }); + return; + } + } + const langTexts = await multiLangService.getLangTexts(parseInt(keyId)); const response: ApiResponse = { @@ -270,6 +297,7 @@ export const createLangKey = async ( ): Promise => { try { const keyData: CreateLangKeyRequest = req.body; + const userCompanyCode = req.user?.companyCode; logger.info("다국어 키 생성 요청", { keyData, user: req.user }); // 필수 입력값 검증 @@ -285,6 +313,26 @@ export const createLangKey = async ( return; } + // 권한 검사: 공통 키(*)는 최고 관리자만 생성 가능 + if (keyData.companyCode === "*" && userCompanyCode !== "*") { + res.status(403).json({ + success: false, + message: "공통 키는 최고 관리자만 생성할 수 있습니다.", + error: { code: "PERMISSION_DENIED" }, + }); + return; + } + + // 비관리자: 자기 회사 키만 생성 가능 + if (userCompanyCode !== "*" && keyData.companyCode !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "다른 회사의 키를 생성할 권한이 없습니다.", + error: { code: "PERMISSION_DENIED" }, + }); + return; + } + const multiLangService = new MultiLangService(); const keyId = await multiLangService.createLangKey({ ...keyData, @@ -323,10 +371,33 @@ export const updateLangKey = async ( try { const { keyId } = req.params; const keyData: UpdateLangKeyRequest = req.body; + const userCompanyCode = req.user?.companyCode; logger.info("다국어 키 수정 요청", { keyId, keyData, user: req.user }); const multiLangService = new MultiLangService(); + + // 비관리자: 해당 키가 자기 회사 키인지 검증 (공통 키는 수정 불가) + if (userCompanyCode !== "*") { + const keyOwner = await multiLangService.getKeyCompanyCode(parseInt(keyId)); + if (!keyOwner) { + res.status(404).json({ + success: false, + message: "다국어 키를 찾을 수 없습니다.", + error: { code: "KEY_NOT_FOUND" }, + }); + return; + } + if (keyOwner !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "다른 회사의 다국어 키를 수정할 권한이 없습니다.", + error: { code: "PERMISSION_DENIED" }, + }); + return; + } + } + await multiLangService.updateLangKey(parseInt(keyId), { ...keyData, updatedBy: req.user?.userId || "system", @@ -362,9 +433,32 @@ export const deleteLangKey = async ( ): Promise => { try { const { keyId } = req.params; + const userCompanyCode = req.user?.companyCode; logger.info("다국어 키 삭제 요청", { keyId, user: req.user }); const multiLangService = new MultiLangService(); + + // 비관리자: 해당 키가 자기 회사 키인지 검증 (공통 키 삭제 불가) + if (userCompanyCode !== "*") { + const keyOwner = await multiLangService.getKeyCompanyCode(parseInt(keyId)); + if (!keyOwner) { + res.status(404).json({ + success: false, + message: "다국어 키를 찾을 수 없습니다.", + error: { code: "KEY_NOT_FOUND" }, + }); + return; + } + if (keyOwner !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "다른 회사의 다국어 키를 삭제할 권한이 없습니다.", + error: { code: "PERMISSION_DENIED" }, + }); + return; + } + } + await multiLangService.deleteLangKey(parseInt(keyId)); const response: ApiResponse = { @@ -397,9 +491,32 @@ export const toggleLangKey = async ( ): Promise => { try { const { keyId } = req.params; + const userCompanyCode = req.user?.companyCode; logger.info("다국어 키 상태 토글 요청", { keyId, user: req.user }); const multiLangService = new MultiLangService(); + + // 비관리자: 해당 키가 자기 회사 키인지 검증 + if (userCompanyCode !== "*") { + const keyOwner = await multiLangService.getKeyCompanyCode(parseInt(keyId)); + if (!keyOwner) { + res.status(404).json({ + success: false, + message: "다국어 키를 찾을 수 없습니다.", + error: { code: "KEY_NOT_FOUND" }, + }); + return; + } + if (keyOwner !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "다른 회사의 다국어 키를 변경할 권한이 없습니다.", + error: { code: "PERMISSION_DENIED" }, + }); + return; + } + } + const result = await multiLangService.toggleLangKey(parseInt(keyId)); const response: ApiResponse = { @@ -433,6 +550,7 @@ export const saveLangTexts = async ( try { const { keyId } = req.params; const textData: SaveLangTextsRequest = req.body; + const userCompanyCode = req.user?.companyCode; logger.info("다국어 텍스트 저장 요청", { keyId, textData, user: req.user }); @@ -454,6 +572,28 @@ export const saveLangTexts = async ( } const multiLangService = new MultiLangService(); + + // 비관리자: 해당 키가 자기 회사 또는 공통(*) 키인지 검증 + if (userCompanyCode !== "*") { + const keyOwner = await multiLangService.getKeyCompanyCode(parseInt(keyId)); + if (!keyOwner) { + res.status(404).json({ + success: false, + message: "다국어 키를 찾을 수 없습니다.", + error: { code: "KEY_NOT_FOUND" }, + }); + return; + } + if (keyOwner !== userCompanyCode) { + res.status(403).json({ + success: false, + message: "다른 회사의 다국어 텍스트를 수정할 권한이 없습니다.", + error: { code: "PERMISSION_DENIED" }, + }); + return; + } + } + await multiLangService.saveLangTexts(parseInt(keyId), { texts: textData.texts.map((text) => ({ ...text, diff --git a/backend-node/src/services/multilangService.ts b/backend-node/src/services/multilangService.ts index fc765d89..7ca92932 100644 --- a/backend-node/src/services/multilangService.ts +++ b/backend-node/src/services/multilangService.ts @@ -673,6 +673,22 @@ export class MultiLangService { } } + /** + * 키의 소유 회사 코드 조회 (권한 검증용) + */ + async getKeyCompanyCode(keyId: number): Promise { + try { + const result = await queryOne<{ company_code: string }>( + `SELECT company_code FROM multi_lang_key_master WHERE key_id = $1`, + [keyId] + ); + return result?.company_code || null; + } catch (error) { + logger.error("키 소유 회사 코드 조회 실패:", error); + return null; + } + } + /** * 다국어 키 목록 조회 */ @@ -688,6 +704,10 @@ export class MultiLangService { if (params.companyCode) { whereConditions.push(`company_code = $${paramIndex++}`); values.push(params.companyCode); + } else if (params.userCompanyCode && params.userCompanyCode !== "*") { + // 비관리자: companyCode 필터가 없으면 자기 회사 + 공통(*) 키만 반환 + whereConditions.push(`company_code IN ($${paramIndex++}, '*')`); + values.push(params.userCompanyCode); } // 메뉴 코드 필터 diff --git a/backend-node/src/types/multilang.ts b/backend-node/src/types/multilang.ts index c30fdfaa..026810ca 100644 --- a/backend-node/src/types/multilang.ts +++ b/backend-node/src/types/multilang.ts @@ -140,6 +140,7 @@ export interface GetLangKeysParams { includeOverrides?: boolean; page?: number; limit?: number; + userCompanyCode?: string; // 요청 사용자의 회사 코드 (비관리자 필터링용) } export interface GetUserTextParams { diff --git a/frontend/app/(main)/sales/claim/page.tsx b/frontend/app/(main)/sales/claim/page.tsx index 333e8fc6..86ba092d 100644 --- a/frontend/app/(main)/sales/claim/page.tsx +++ b/frontend/app/(main)/sales/claim/page.tsx @@ -18,16 +18,7 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, - DialogDescription, -} from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { @@ -49,11 +40,7 @@ import { CommandList, } from "@/components/ui/command"; import { - Search, Download, - Upload, - Settings, - RotateCcw, Plus, Save, BarChart3, @@ -63,26 +50,42 @@ import { ChevronsUpDown, Loader2, FileSpreadsheet, + Trash2, } 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 { FullscreenDialog } from "@/components/common/FullscreenDialog"; +import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { FormDatePicker } from "@/components/screen/filters/FormDatePicker"; + +// --- 상수 --- +const TABLE_NAME = "claim_mng"; -// --- Types --- type ClaimType = "불량" | "교환" | "반품" | "배송지연" | "기타"; type ClaimStatus = "접수" | "처리중" | "완료" | "취소"; -interface Claim { - claimNo: string; - claimDate: string; - claimType: ClaimType; - claimStatus: ClaimStatus; - customerCode: string; - customerName: string; - managerName: string; - orderNo: string; - claimContent: string; - processContent: string; +interface ClaimRow { + id: number; + claim_no: string; + claim_date: string; + claim_type: string; + claim_status: string; + customer_code: string; + customer_name: string; + manager_name: string; + order_no: string; + claim_content: string; + process_content: string; + company_code?: string; + writer?: string; + created_date?: string; + updated_date?: string; + [key: string]: any; } interface CustomerOption { @@ -96,9 +99,7 @@ interface SalesOrderOption { status: string; } -const initialData: Claim[] = []; - -const getClaimTypeStyle = (type: ClaimType) => { +const getClaimTypeStyle = (type: string) => { switch (type) { case "불량": return "bg-rose-100 text-rose-800 border-rose-200"; @@ -115,7 +116,7 @@ const getClaimTypeStyle = (type: ClaimType) => { } }; -const getClaimStatusStyle = (status: ClaimStatus) => { +const getClaimStatusStyle = (status: string) => { switch (status) { case "접수": return "bg-blue-100 text-blue-800 border-blue-200"; @@ -134,16 +135,16 @@ const CLAIM_TYPES: ClaimType[] = ["불량", "교환", "반품", "배송지연", const CLAIM_STATUSES: ClaimStatus[] = ["접수", "처리중", "완료", "취소"]; export default function ClaimManagementPage() { - const [data, setData] = useState(initialData); + const { user } = useAuth(); + const { confirm, ConfirmDialogComponent } = useConfirmDialog(); + + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [totalCount, setTotalCount] = useState(0); const [selectedClaimNo, setSelectedClaimNo] = useState(null); - // 검색 상태 - const [searchDateFrom, setSearchDateFrom] = useState(""); - const [searchDateTo, setSearchDateTo] = useState(""); - const [searchClaimType, setSearchClaimType] = useState("all"); - const [searchStatus, setSearchStatus] = useState("all"); - const [searchCustomer, setSearchCustomer] = useState(""); - const [searchClaimNo, setSearchClaimNo] = useState(""); + // 검색 필터 (DynamicSearchFilter) + const [searchFilters, setSearchFilters] = useState([]); // 엑셀 업로드 const [excelUploadOpen, setExcelUploadOpen] = useState(false); @@ -151,7 +152,8 @@ export default function ClaimManagementPage() { // 모달 상태 const [isModalOpen, setIsModalOpen] = useState(false); const [isEditMode, setIsEditMode] = useState(false); - const [formData, setFormData] = useState>({}); + const [saving, setSaving] = useState(false); + const [formData, setFormData] = useState>({}); // Combobox 상태 const [customerOpen, setCustomerOpen] = useState(false); @@ -163,10 +165,40 @@ export default function ClaimManagementPage() { const [customersLoading, setCustomersLoading] = useState(false); const [ordersLoading, setOrdersLoading] = useState(false); - useEffect(() => { - }, []); + // --- 데이터 조회 (table-management API + autoFilter로 멀티테넌시 자동 적용) --- + const fetchData = useCallback(async () => { + setLoading(true); + try { + const filters: any[] = searchFilters.map((f) => ({ + columnName: f.columnName, + operator: f.operator, + value: f.value, + })); - // 거래처 목록 조회 + const res = await apiClient.post(`/table-management/tables/${TABLE_NAME}/data`, { + page: 1, + size: 500, + dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, + autoFilter: true, // company_code 자동 필터링 + sort: { columnName: "claim_date", order: "desc" }, + }); + + const rows: ClaimRow[] = res.data?.data?.data || res.data?.data?.rows || []; + setData(rows); + setTotalCount(res.data?.data?.total || rows.length); + } catch (err) { + console.error("클레임 조회 실패:", err); + toast.error("클레임 목록을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }, [searchFilters]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + // 거래처 목록 조회 (autoFilter로 멀티테넌시 적용) const fetchCustomers = useCallback(async (force = false) => { if (!force && customers.length > 0) return; setCustomersLoading(true); @@ -183,8 +215,6 @@ export default function ClaimManagementPage() { customerName: row.customer_name || "", })); setCustomers(list); - } else { - console.warn("거래처 응답 구조 확인:", JSON.stringify(res.data, null, 2)); } } catch (e) { console.error("거래처 목록 조회 실패:", e); @@ -193,7 +223,7 @@ export default function ClaimManagementPage() { } }, [customers.length]); - // 수주 목록 조회 + // 수주 목록 조회 (autoFilter로 멀티테넌시 적용) const fetchSalesOrders = useCallback(async (force = false) => { if (!force && salesOrders.length > 0) return; setOrdersLoading(true); @@ -218,8 +248,6 @@ export default function ClaimManagementPage() { }); } setSalesOrders(list); - } else { - console.warn("수주 응답 구조 확인:", JSON.stringify(res.data, null, 2)); } } catch (e) { console.error("수주 목록 조회 실패:", e); @@ -228,60 +256,26 @@ export default function ClaimManagementPage() { } }, [salesOrders.length]); - const filteredData = useMemo(() => { - return data - .filter((claim) => { - if (searchDateFrom && claim.claimDate < searchDateFrom) return false; - if (searchDateTo && claim.claimDate > searchDateTo) return false; - if (searchClaimType !== "all" && claim.claimType !== searchClaimType) - return false; - if (searchStatus !== "all" && claim.claimStatus !== searchStatus) - return false; - if ( - searchCustomer && - !claim.customerName - .toLowerCase() - .includes(searchCustomer.toLowerCase()) - ) - return false; - if ( - searchClaimNo && - !claim.claimNo.toLowerCase().includes(searchClaimNo.toLowerCase()) - ) - return false; - return true; - }) - .sort((a, b) => b.claimDate.localeCompare(a.claimDate)); - }, [ - data, - searchDateFrom, - searchDateTo, - searchClaimType, - searchStatus, - searchCustomer, - searchClaimNo, - ]); - // 상태별 카운트 const statusCounts = useMemo(() => { const counts = { 접수: 0, 처리중: 0, 완료: 0, 취소: 0 }; data.forEach((claim) => { - if (counts[claim.claimStatus] !== undefined) { - counts[claim.claimStatus]++; + if (counts[claim.claim_status as keyof typeof counts] !== undefined) { + counts[claim.claim_status as keyof typeof counts]++; } }); return counts; }, [data]); + // 클레임번호 자동 생성 const generateClaimNo = useCallback(() => { const year = new Date().getFullYear(); const prefix = `CLM-${year}-`; const existingNumbers = data - .filter((c) => c.claimNo.startsWith(prefix)) - .map((c) => parseInt(c.claimNo.replace(prefix, ""), 10)) + .filter((c) => c.claim_no?.startsWith(prefix)) + .map((c) => parseInt(c.claim_no.replace(prefix, ""), 10)) .filter((n) => !isNaN(n)); - const maxNumber = - existingNumbers.length > 0 ? Math.max(...existingNumbers) : 0; + const maxNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) : 0; return `${prefix}${String(maxNumber + 1).padStart(3, "0")}`; }, [data]); @@ -292,16 +286,16 @@ export default function ClaimManagementPage() { const openRegisterModal = () => { setIsEditMode(false); setFormData({ - claimNo: generateClaimNo(), - claimDate: new Date().toISOString().split("T")[0], - claimType: undefined, - claimStatus: "접수", - customerCode: "", - customerName: "", - managerName: "", - orderNo: "", - claimContent: "", - processContent: "", + claim_no: generateClaimNo(), + claim_date: new Date().toISOString().split("T")[0], + claim_type: undefined, + claim_status: "접수", + customer_code: "", + customer_name: "", + manager_name: "", + order_no: "", + claim_content: "", + process_content: "", }); setIsModalOpen(true); fetchCustomers(true); @@ -309,7 +303,7 @@ export default function ClaimManagementPage() { }; const openEditModal = (claimNo: string) => { - const claim = data.find((c) => c.claimNo === claimNo); + const claim = data.find((c) => c.claim_no === claimNo); if (!claim) return; setIsEditMode(true); setFormData({ ...claim }); @@ -318,55 +312,98 @@ export default function ClaimManagementPage() { fetchSalesOrders(true); }; - const handleFormChange = (field: keyof Claim, value: string) => { + const handleFormChange = (field: string, value: string) => { setFormData((prev) => ({ ...prev, [field]: value })); }; - const handleSave = () => { - if (!formData.claimType || !formData.customerName || !formData.claimContent) { - alert("필수 항목을 모두 입력해주세요."); + // --- 저장 (table-management API, company_code 자동 주입) --- + const handleSave = async () => { + if (!formData.claim_type || !formData.customer_name || !formData.claim_content) { + toast.error("필수 항목을 모두 입력해주세요. (클레임유형, 거래처명, 클레임내용)"); return; } - const claimData: Claim = { - claimNo: formData.claimNo || "", - claimDate: formData.claimDate || new Date().toISOString().split("T")[0], - claimType: formData.claimType as ClaimType, - claimStatus: (formData.claimStatus as ClaimStatus) || "접수", - customerCode: formData.customerCode || "", - customerName: formData.customerName || "", - managerName: formData.managerName || "", - orderNo: formData.orderNo || "", - claimContent: formData.claimContent || "", - processContent: formData.processContent || "", - }; + setSaving(true); + try { + // company_code, writer, created_date 등 시스템 필드는 제외 (백엔드가 자동 주입) + const { id, company_code, writer, created_date, updated_date, created_by, updated_by, ...saveFields } = formData as any; - if (isEditMode) { - setData((prev) => - prev.map((c) => (c.claimNo === claimData.claimNo ? claimData : c)) - ); - } else { - setData((prev) => [claimData, ...prev]); + if (isEditMode && id) { + // 수정 + await apiClient.put(`/table-management/tables/${TABLE_NAME}/edit`, { + originalData: { id }, + updatedData: saveFields, + }); + toast.success("클레임이 수정되었습니다."); + } else { + // 등록 + await apiClient.post(`/table-management/tables/${TABLE_NAME}/add`, saveFields); + toast.success("클레임이 등록되었습니다."); + } + + setIsModalOpen(false); + fetchData(); // 목록 새로고침 + } catch (err: any) { + console.error("클레임 저장 실패:", err); + toast.error(err.response?.data?.message || "저장에 실패했습니다."); + } finally { + setSaving(false); } - - setIsModalOpen(false); - alert("클레임이 저장되었습니다."); }; - const handleResetSearch = () => { - const today = new Date(); - const thirtyDaysAgo = new Date(today); - thirtyDaysAgo.setDate(today.getDate() - 30); - setSearchDateFrom(thirtyDaysAgo.toISOString().split("T")[0]); - setSearchDateTo(today.toISOString().split("T")[0]); - setSearchClaimType("all"); - setSearchStatus("all"); - setSearchCustomer(""); - setSearchClaimNo(""); + // --- 삭제 --- + const handleDelete = async (claimNo: string) => { + const claim = data.find((c) => c.claim_no === claimNo); + if (!claim) return; + + const ok = await confirm(`클레임 ${claimNo}을(를) 삭제하시겠습니까?`, { + variant: "destructive", + confirmText: "삭제", + }); + if (!ok) return; + + try { + await apiClient.delete(`/table-management/tables/${TABLE_NAME}/delete`, { + data: [{ id: claim.id }], + }); + toast.success("클레임이 삭제되었습니다."); + if (selectedClaimNo === claimNo) setSelectedClaimNo(null); + fetchData(); + } catch (err: any) { + console.error("클레임 삭제 실패:", err); + toast.error(err.response?.data?.message || "삭제에 실패했습니다."); + } + }; + + // 엑셀 다운로드 + const handleExcelDownload = async () => { + if (data.length === 0) { + toast.error("다운로드할 데이터가 없습니다."); + return; + } + try { + const exportData = data.map((row) => ({ + 클레임번호: row.claim_no, + 접수일자: row.claim_date, + 클레임유형: row.claim_type, + 처리상태: row.claim_status, + 거래처코드: row.customer_code, + 거래처명: row.customer_name, + 담당자: row.manager_name, + 수주번호: row.order_no, + 클레임내용: row.claim_content, + 처리내용: row.process_content, + })); + await exportToExcel(exportData, "클레임관리.xlsx", "클레임"); + toast.success("엑셀 다운로드 완료"); + } catch (err) { + console.error("엑셀 다운로드 실패:", err); + toast.error("엑셀 다운로드에 실패했습니다."); + } }; const selectedClaim = useMemo( - () => data.find((c) => c.claimNo === selectedClaimNo), + () => data.find((c) => c.claim_no === selectedClaimNo), [data, selectedClaimNo] ); @@ -404,105 +441,13 @@ export default function ClaimManagementPage() { return (
- {/* 검색 섹션 */} - - -
- -
- setSearchDateFrom(e.target.value)} - /> - ~ - setSearchDateTo(e.target.value)} - /> -
-
- -
- - -
- -
- - -
- -
- - setSearchCustomer(e.target.value)} - /> -
- -
- - setSearchClaimNo(e.target.value)} - /> -
- -
- -
- - - - -
- - + {/* 검색 섹션 — DynamicSearchFilter 사용 */} + {/* 메인 분할 레이아웃 */}
@@ -515,13 +460,17 @@ export default function ClaimManagementPage() { 클레임 목록 - {filteredData.length}건 + {totalCount}건 + {loading && }
+ @@ -535,12 +484,8 @@ export default function ClaimManagementPage() { No 클레임번호 접수일자 - - 유형 - - - 상태 - + 유형 + 상태 거래처명 담당자 수주번호 @@ -548,7 +493,7 @@ export default function ClaimManagementPage() { - {filteredData.length === 0 ? ( + {data.length === 0 ? (
- 등록된 클레임이 없습니다 + {loading ? "데이터를 불러오는 중..." : "등록된 클레임이 없습니다"}
) : ( - filteredData.map((claim, idx) => ( + data.map((claim, idx) => ( handleRowClick(claim.claimNo)} - onDoubleClick={() => openEditModal(claim.claimNo)} + onClick={() => handleRowClick(claim.claim_no)} + onDoubleClick={() => openEditModal(claim.claim_no)} > {idx + 1} - {claim.claimNo} + {claim.claim_no} - {claim.claimDate} + {claim.claim_date} - {claim.claimType} + {claim.claim_type} - {claim.claimStatus} + {claim.claim_status} - {claim.customerName} - {claim.managerName || "-"} + {claim.customer_name} + {claim.manager_name || "-"} - {claim.orderNo || "-"} + {claim.order_no || "-"} - {claim.claimContent || "-"} + {claim.claim_content || "-"} )) @@ -652,7 +597,7 @@ export default function ClaimManagementPage() {

- 클레임 상세 - {selectedClaim.claimNo} + 클레임 상세 - {selectedClaim.claim_no}

@@ -661,14 +606,14 @@ export default function ClaimManagementPage() { 클레임번호 - {selectedClaim.claimNo} + {selectedClaim.claim_no}
접수일자 - {selectedClaim.claimDate} + {selectedClaim.claim_date}
@@ -677,10 +622,10 @@ export default function ClaimManagementPage() { - {selectedClaim.claimType} + {selectedClaim.claim_type}
@@ -690,30 +635,30 @@ export default function ClaimManagementPage() { - {selectedClaim.claimStatus} + {selectedClaim.claim_status}
거래처명 - {selectedClaim.customerName} + {selectedClaim.customer_name}
담당자 - {selectedClaim.managerName || "-"} + {selectedClaim.manager_name || "-"}
수주번호 - {selectedClaim.orderNo || "-"} + {selectedClaim.order_no || "-"}
@@ -723,7 +668,7 @@ export default function ClaimManagementPage() { 클레임 내용
- {selectedClaim.claimContent || "-"} + {selectedClaim.claim_content || "-"}
@@ -732,18 +677,28 @@ export default function ClaimManagementPage() { 처리 내용
- {selectedClaim.processContent || "-"} + {selectedClaim.process_content || "-"}
- +
+ + +
) : (
@@ -761,311 +716,15 @@ export default function ClaimManagementPage() {
- {/* 클레임 등록/수정 모달 */} - - - - - {isEditMode ? "클레임 수정" : "클레임 등록"} - - - {isEditMode - ? "클레임 정보를 수정합니다." - : "새로운 클레임을 등록합니다."} - - - -
-
- {/* 왼쪽: 기본 정보 */} -
-

- 클레임 기본 정보 -

- -
- - -
- -
- - - handleFormChange("claimDate", e.target.value) - } - className="h-8 text-xs sm:h-10 sm:text-sm" - /> -
- -
- - -
- -
- - -
- -
- - - - - - - - - - {customersLoading ? ( -
- - 로딩 중... -
- ) : ( - <> - - 거래처를 찾을 수 없습니다. - - - {customers.map((cust) => ( - { - setFormData((prev) => ({ - ...prev, - customerCode: cust.customerCode, - customerName: cust.customerName, - })); - setCustomerOpen(false); - }} - className="text-xs sm:text-sm" - > - -
- {cust.customerName} - - {cust.customerCode} - -
-
- ))} -
- - )} -
-
-
-
-
- -
- - - handleFormChange("managerName", e.target.value) - } - className="h-8 text-xs sm:h-10 sm:text-sm" - /> -
- -
- - - - - - - - - - {ordersLoading ? ( -
- - 로딩 중... -
- ) : ( - <> - - 수주번호를 찾을 수 없습니다. - - - {salesOrders.map((order) => ( - { - setFormData((prev) => ({ - ...prev, - orderNo: order.orderNo, - })); - setOrderOpen(false); - }} - className="text-xs sm:text-sm" - > - -
- {order.orderNo} - - {order.status} - {order.partnerName ? ` | ${order.partnerName}` : ""} - -
-
- ))} -
- - )} -
-
-
-
-
-
- - {/* 오른쪽: 상세 내용 */} -
-

- 클레임 상세 내용 -

- -
- -