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 })
|
getSales({ perPage: 100 })
|
||||||
.then(result => {
|
.then(result => {
|
||||||
setData(result.data);
|
if (result.success) {
|
||||||
setPagination(result.pagination);
|
setData(result.data);
|
||||||
|
setPagination(result.pagination);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, [mode]);
|
}, [mode]);
|
||||||
|
|
||||||
|
// mode=new일 때 등록 화면 표시
|
||||||
|
if (mode === 'new') {
|
||||||
|
return <SalesDetail mode="new" />;
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-[400px]">
|
<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 (
|
return (
|
||||||
<SalesManagement
|
<SalesManagement
|
||||||
initialData={data}
|
initialData={data}
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ export function SalesDetail({ mode, salesId }: SalesDetailProps) {
|
|||||||
<SelectValue placeholder="거래처명 ▼" />
|
<SelectValue placeholder="거래처명 ▼" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{clients.map((client) => (
|
{clients.filter(c => c.id !== '').map((client) => (
|
||||||
<SelectItem key={client.id} value={client.id}>
|
<SelectItem key={client.id} value={client.id}>
|
||||||
{client.name}
|
{client.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* - deleteConfirmMessage로 삭제 다이얼로그 처리
|
* - deleteConfirmMessage로 삭제 다이얼로그 처리
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useMemo, useCallback } from 'react';
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
@@ -62,7 +62,7 @@ import {
|
|||||||
ISSUANCE_FILTER_OPTIONS,
|
ISSUANCE_FILTER_OPTIONS,
|
||||||
ACCOUNT_SUBJECT_SELECTOR_OPTIONS,
|
ACCOUNT_SUBJECT_SELECTOR_OPTIONS,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { deleteSale, toggleSaleIssuance } from './actions';
|
import { getSales, deleteSale, toggleSaleIssuance } from './actions';
|
||||||
|
|
||||||
// ===== 테이블 컬럼 정의 =====
|
// ===== 테이블 컬럼 정의 =====
|
||||||
const tableColumns = [
|
const tableColumns = [
|
||||||
@@ -95,7 +95,10 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
|||||||
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
// ===== 외부 상태 (UniversalListPage 외부에서 관리) =====
|
||||||
const [startDate, setStartDate] = useState('2025-01-01');
|
const [startDate, setStartDate] = useState('2025-01-01');
|
||||||
const [endDate, setEndDate] = useState('2025-12-31');
|
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('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
// 통합 필터 상태 (filterConfig 사용)
|
// 통합 필터 상태 (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) => {
|
const handleRowClick = useCallback((item: SalesRecord) => {
|
||||||
router.push(`/ko/accounting/sales/${item.id}?mode=view`);
|
router.push(`/ko/accounting/sales/${item.id}?mode=view`);
|
||||||
@@ -254,11 +296,13 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
|||||||
// API 액션
|
// API 액션
|
||||||
actions: {
|
actions: {
|
||||||
getList: async () => {
|
getList: async () => {
|
||||||
return {
|
const result = await getSales({ perPage: 100, page: currentPage });
|
||||||
success: true,
|
if (result.success) {
|
||||||
data: salesData,
|
setSalesData(result.data);
|
||||||
totalCount: salesData.length,
|
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) => {
|
deleteItem: async (id: string) => {
|
||||||
const result = await deleteSale(id);
|
const result = await deleteSale(id);
|
||||||
@@ -500,6 +544,7 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
salesData,
|
salesData,
|
||||||
|
currentPage,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
stats,
|
stats,
|
||||||
@@ -518,7 +563,17 @@ export function SalesManagement({ initialData, initialPagination }: SalesManagem
|
|||||||
|
|
||||||
return (
|
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}>
|
<Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export function transformApiToFrontend(apiData: SaleApiData): SalesRecord {
|
|||||||
salesNo: apiData.sale_number,
|
salesNo: apiData.sale_number,
|
||||||
salesDate: apiData.sale_date,
|
salesDate: apiData.sale_date,
|
||||||
vendorId: String(apiData.client_id),
|
vendorId: String(apiData.client_id),
|
||||||
vendorName: apiData.client?.name ?? '',
|
vendorName: apiData.client?.name || '(거래처 미지정)',
|
||||||
salesType: 'other', // API에 없음, 기본값
|
salesType: 'other', // API에 없음, 기본값
|
||||||
accountSubject: 'other', // API에 없음, 기본값
|
accountSubject: 'other', // API에 없음, 기본값
|
||||||
items, // 수주 품목에서 가져옴
|
items, // 수주 품목에서 가져옴
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export function MobileFilter({
|
|||||||
<SelectItem value="all">
|
<SelectItem value="all">
|
||||||
{field.allOptionLabel || '전체'}
|
{field.allOptionLabel || '전체'}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{field.options.map((option) => (
|
{field.options.filter(opt => opt.value !== '').map((option) => (
|
||||||
<SelectItem key={option.value} value={option.value}>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -299,10 +299,12 @@ export function MobileFilter({
|
|||||||
) : (
|
) : (
|
||||||
// 다중선택: MultiSelectCombobox
|
// 다중선택: MultiSelectCombobox
|
||||||
<MultiSelectCombobox
|
<MultiSelectCombobox
|
||||||
options={field.options.map((opt) => ({
|
options={field.options
|
||||||
value: opt.value,
|
.filter(opt => opt.value !== '')
|
||||||
label: opt.label,
|
.map((opt) => ({
|
||||||
}))}
|
value: opt.value,
|
||||||
|
label: opt.label,
|
||||||
|
}))}
|
||||||
value={(values[field.key] as string[]) || []}
|
value={(values[field.key] as string[]) || []}
|
||||||
onChange={(value) => onChange(field.key, value)}
|
onChange={(value) => onChange(field.key, value)}
|
||||||
placeholder="전체"
|
placeholder="전체"
|
||||||
|
|||||||
Reference in New Issue
Block a user