fix(WEB): 매출관리 MobileFilter 빈 값 crash 수정 및 페이지 안정성 강화

- 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 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-14 22:17:38 +09:00
parent e4b25e2648
commit 95c9686597
5 changed files with 82 additions and 23 deletions

View File

@@ -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 <SalesDetail mode="new" />;
}
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
@@ -44,11 +51,6 @@ export default function SalesPage() {
);
}
// mode=new일 때 등록 화면 표시
if (mode === 'new') {
return <SalesDetail mode="new" />;
}
return (
<SalesManagement
initialData={data}

View File

@@ -310,7 +310,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
<SelectValue placeholder="거래처명 ▼" />
</SelectTrigger>
<SelectContent>
{clients.map((client) => (
{clients.filter(c => c.id !== '').map((client) => (
<SelectItem key={client.id} value={client.id}>
{client.name}
</SelectItem>

View File

@@ -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<SalesRecord[]>(initialData);
const [salesData, setSalesData] = useState<SalesRecord[]>(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 (
<>
<UniversalListPage config={config} initialData={salesData} />
<UniversalListPage
config={config}
initialData={salesData}
externalPagination={{
currentPage: pagination.currentPage,
totalPages: pagination.lastPage,
totalItems: pagination.total,
itemsPerPage: pagination.perPage,
onPageChange: handlePageChange,
}}
/>
{/* 계정과목명 저장 확인 다이얼로그 */}
<Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>

View File

@@ -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, // 수주 품목에서 가져옴

View File

@@ -289,7 +289,7 @@ export function MobileFilter({
<SelectItem value="all">
{field.allOptionLabel || '전체'}
</SelectItem>
{field.options.map((option) => (
{field.options.filter(opt => opt.value !== '').map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
@@ -299,10 +299,12 @@ export function MobileFilter({
) : (
// 다중선택: MultiSelectCombobox
<MultiSelectCombobox
options={field.options.map((opt) => ({
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="전체"