From 95c9686597dd1405543f8f737fb98cf6526fbdca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 14 Feb 2026 22:17:38 +0900 Subject: [PATCH] =?UTF-8?q?fix(WEB):=20=EB=A7=A4=EC=B6=9C=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20MobileFilter=20=EB=B9=88=20=EA=B0=92=20crash=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=95=88=EC=A0=95=EC=84=B1=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MobileFilter: 단일선택/다중선택 모두 빈 value 필터링 추가 (crash 근본 원인) - types.ts: vendorName 빈 문자열 → '(거래처 미지정)' 기본값으로 방어 - SalesDetail: 거래처 Select에 빈 id 필터링 추가 - page.tsx: mode=new 분기를 로딩 전으로 이동 (불필요한 API 호출 방지) - index.tsx: getSales API 직접 호출, 페이지네이션, 자동 로드 추가 Co-Authored-By: Claude Opus 4.6 --- .../(protected)/accounting/sales/page.tsx | 16 ++-- .../SalesManagement/SalesDetail.tsx | 2 +- .../accounting/SalesManagement/index.tsx | 73 ++++++++++++++++--- .../accounting/SalesManagement/types.ts | 2 +- src/components/molecules/MobileFilter.tsx | 12 +-- 5 files changed, 82 insertions(+), 23 deletions(-) diff --git a/src/app/[locale]/(protected)/accounting/sales/page.tsx b/src/app/[locale]/(protected)/accounting/sales/page.tsx index 3d313c0c..6ba000e9 100644 --- a/src/app/[locale]/(protected)/accounting/sales/page.tsx +++ b/src/app/[locale]/(protected)/accounting/sales/page.tsx @@ -30,12 +30,19 @@ export default function SalesPage() { getSales({ perPage: 100 }) .then(result => { - setData(result.data); - setPagination(result.pagination); + if (result.success) { + setData(result.data); + setPagination(result.pagination); + } }) .finally(() => setIsLoading(false)); }, [mode]); + // mode=new일 때 등록 화면 표시 + if (mode === 'new') { + return ; + } + if (isLoading) { return (
@@ -44,11 +51,6 @@ export default function SalesPage() { ); } - // mode=new일 때 등록 화면 표시 - if (mode === 'new') { - return ; - } - return ( - {clients.map((client) => ( + {clients.filter(c => c.id !== '').map((client) => ( {client.name} diff --git a/src/components/accounting/SalesManagement/index.tsx b/src/components/accounting/SalesManagement/index.tsx index a03a8371..8ab7f236 100644 --- a/src/components/accounting/SalesManagement/index.tsx +++ b/src/components/accounting/SalesManagement/index.tsx @@ -14,7 +14,7 @@ * - deleteConfirmMessage로 삭제 다이얼로그 처리 */ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; import { @@ -62,7 +62,7 @@ import { ISSUANCE_FILTER_OPTIONS, ACCOUNT_SUBJECT_SELECTOR_OPTIONS, } from './types'; -import { deleteSale, toggleSaleIssuance } from './actions'; +import { getSales, deleteSale, toggleSaleIssuance } from './actions'; // ===== 테이블 컬럼 정의 ===== const tableColumns = [ @@ -95,7 +95,10 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [startDate, setStartDate] = useState('2025-01-01'); const [endDate, setEndDate] = useState('2025-12-31'); - const [salesData, setSalesData] = useState(initialData); + const [salesData, setSalesData] = useState(initialData || []); + const [pagination, setPagination] = useState(initialPagination); + const [currentPage, setCurrentPage] = useState(initialPagination.currentPage); + const [isLoading, setIsLoading] = useState(false); const [searchQuery, setSearchQuery] = useState(''); // 통합 필터 상태 (filterConfig 사용) @@ -178,6 +181,45 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem }); }, []); + // ===== API 데이터 로드 ===== + const loadData = useCallback(async (page: number = 1) => { + setIsLoading(true); + try { + const result = await getSales({ + search: searchQuery || undefined, + startDate, + endDate, + perPage: 100, + page, + }); + + if (result.success) { + setSalesData(result.data); + setPagination(result.pagination); + setCurrentPage(result.pagination.currentPage); + } else { + toast.error(result.error || '데이터를 불러오는데 실패했습니다.'); + } + } catch { + toast.error('데이터를 불러오는데 실패했습니다.'); + } finally { + setIsLoading(false); + } + }, [searchQuery, startDate, endDate]); + + // initialData가 비어있으면 자동 로드 + useEffect(() => { + if ((!initialData || initialData.length === 0) && !isLoading) { + loadData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // ===== 페이지 변경 ===== + const handlePageChange = useCallback((page: number) => { + loadData(page); + }, [loadData]); + // ===== 핸들러 ===== const handleRowClick = useCallback((item: SalesRecord) => { router.push(`/ko/accounting/sales/${item.id}?mode=view`); @@ -254,11 +296,13 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem // API 액션 actions: { getList: async () => { - return { - success: true, - data: salesData, - totalCount: salesData.length, - }; + const result = await getSales({ perPage: 100, page: currentPage }); + if (result.success) { + setSalesData(result.data); + setPagination(result.pagination); + return { success: true, data: result.data, totalCount: result.pagination.total }; + } + return { success: true, data: salesData, totalCount: salesData.length }; }, deleteItem: async (id: string) => { const result = await deleteSale(id); @@ -500,6 +544,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem }), [ salesData, + currentPage, startDate, endDate, stats, @@ -518,7 +563,17 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem return ( <> - + {/* 계정과목명 저장 확인 다이얼로그 */} diff --git a/src/components/accounting/SalesManagement/types.ts b/src/components/accounting/SalesManagement/types.ts index cb9bb8fa..7bf28962 100644 --- a/src/components/accounting/SalesManagement/types.ts +++ b/src/components/accounting/SalesManagement/types.ts @@ -230,7 +230,7 @@ export function transformApiToFrontend(apiData: SaleApiData): SalesRecord { salesNo: apiData.sale_number, salesDate: apiData.sale_date, vendorId: String(apiData.client_id), - vendorName: apiData.client?.name ?? '', + vendorName: apiData.client?.name || '(거래처 미지정)', salesType: 'other', // API에 없음, 기본값 accountSubject: 'other', // API에 없음, 기본값 items, // 수주 품목에서 가져옴 diff --git a/src/components/molecules/MobileFilter.tsx b/src/components/molecules/MobileFilter.tsx index d9b49268..e6fdfedb 100644 --- a/src/components/molecules/MobileFilter.tsx +++ b/src/components/molecules/MobileFilter.tsx @@ -289,7 +289,7 @@ export function MobileFilter({ {field.allOptionLabel || '전체'} - {field.options.map((option) => ( + {field.options.filter(opt => opt.value !== '').map((option) => ( {option.label} @@ -299,10 +299,12 @@ export function MobileFilter({ ) : ( // 다중선택: MultiSelectCombobox ({ - value: opt.value, - label: opt.label, - }))} + options={field.options + .filter(opt => opt.value !== '') + .map((opt) => ({ + value: opt.value, + label: opt.label, + }))} value={(values[field.key] as string[]) || []} onChange={(value) => onChange(field.key, value)} placeholder="전체"