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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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, // 수주 품목에서 가져옴
|
||||
|
||||
@@ -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="전체"
|
||||
|
||||
Reference in New Issue
Block a user