feat(WEB): UniversalListPage 전체 마이그레이션 및 코드 정리
- UniversalListPage/IntegratedListTemplateV2 컴포넌트 기능 개선 - 회계, HR, 건설, 고객센터, 결재, 설정 등 전체 리스트 컴포넌트 마이그레이션 - 테스트 페이지 및 미사용 API 라우트 정리 (board-test, order-management-test 등) - 미들웨어 토큰 갱신 로직 개선 - AuthenticatedLayout 구조 개선 - claudedocs 문서 업데이트 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,11 +23,12 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { TableRow, TableCell } from '@/components/ui/table';
|
||||
import {
|
||||
IntegratedListTemplateV2,
|
||||
UniversalListPage,
|
||||
type UniversalListConfig,
|
||||
type TabOption,
|
||||
type TableColumn,
|
||||
type StatCard,
|
||||
} from '@/components/templates/IntegratedListTemplateV2';
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
|
||||
import type { PricingListItem, ItemType } from './types';
|
||||
import { ITEM_TYPE_LABELS, ITEM_TYPE_COLORS } from './types';
|
||||
@@ -43,11 +44,25 @@ export function PricingListClient({
|
||||
const [data] = useState<PricingListItem[]>(initialData);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 20;
|
||||
|
||||
// 필터링된 데이터
|
||||
// 탭 필터 함수 (UniversalListPage 내부에서 사용)
|
||||
const tabFilter = (item: PricingListItem, tab: string) => {
|
||||
if (tab === 'all') return true;
|
||||
return item.itemType === tab;
|
||||
};
|
||||
|
||||
// 검색 필터 함수 (UniversalListPage 내부에서 사용)
|
||||
const searchFilter = (item: PricingListItem, search: string) => {
|
||||
const searchLower = search.toLowerCase();
|
||||
return (
|
||||
item.itemCode.toLowerCase().includes(searchLower) ||
|
||||
item.itemName.toLowerCase().includes(searchLower) ||
|
||||
(item.specification?.toLowerCase().includes(searchLower) ?? false)
|
||||
);
|
||||
};
|
||||
|
||||
// 탭별 데이터 수 계산 (통계용)
|
||||
const filteredData = useMemo(() => {
|
||||
let result = [...data];
|
||||
|
||||
@@ -69,12 +84,6 @@ export function PricingListClient({
|
||||
return result;
|
||||
}, [data, activeTab, searchTerm]);
|
||||
|
||||
// 페이지네이션된 데이터
|
||||
const paginatedData = useMemo(() => {
|
||||
const start = (currentPage - 1) * pageSize;
|
||||
return filteredData.slice(start, start + pageSize);
|
||||
}, [filteredData, currentPage, pageSize]);
|
||||
|
||||
// 통계 계산
|
||||
const totalStats = useMemo(() => {
|
||||
const totalAll = data.length;
|
||||
@@ -166,27 +175,6 @@ export function PricingListClient({
|
||||
console.log('이력 조회:', item.id);
|
||||
};
|
||||
|
||||
// 체크박스 전체 선택/해제
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedItems.size === paginatedData.length && paginatedData.length > 0) {
|
||||
setSelectedItems(new Set());
|
||||
} else {
|
||||
const allIds = new Set(paginatedData.map((item) => item.id));
|
||||
setSelectedItems(allIds);
|
||||
}
|
||||
};
|
||||
|
||||
// 개별 체크박스 선택/해제
|
||||
const toggleSelection = (itemId: string) => {
|
||||
const newSelected = new Set(selectedItems);
|
||||
if (newSelected.has(itemId)) {
|
||||
newSelected.delete(itemId);
|
||||
} else {
|
||||
newSelected.add(itemId);
|
||||
}
|
||||
setSelectedItems(newSelected);
|
||||
};
|
||||
|
||||
// 탭 옵션
|
||||
const tabs: TabOption[] = [
|
||||
{ value: 'all', label: '전체', count: totalStats.totalAll, color: 'gray' },
|
||||
@@ -223,15 +211,20 @@ export function PricingListClient({
|
||||
];
|
||||
|
||||
// 테이블 행 렌더링
|
||||
const renderTableRow = (item: PricingListItem, index: number, globalIndex: number) => {
|
||||
const isSelected = selectedItems.has(item.id);
|
||||
const renderTableRow = (
|
||||
item: PricingListItem,
|
||||
index: number,
|
||||
globalIndex: number,
|
||||
handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void }
|
||||
) => {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
|
||||
return (
|
||||
<TableRow key={item.id} className="hover:bg-muted/50">
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => toggleSelection(item.id)}
|
||||
onCheckedChange={onToggle}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-center">
|
||||
@@ -316,9 +309,9 @@ export function PricingListClient({
|
||||
item: PricingListItem,
|
||||
index: number,
|
||||
globalIndex: number,
|
||||
isSelected: boolean,
|
||||
onToggle: () => void
|
||||
handlers: { isSelected: boolean; onToggle: () => void; onRowClick?: () => void }
|
||||
) => {
|
||||
const { isSelected, onToggle } = handlers;
|
||||
return (
|
||||
<ListMobileCard
|
||||
key={item.id}
|
||||
@@ -395,8 +388,8 @@ export function PricingListClient({
|
||||
);
|
||||
};
|
||||
|
||||
// 헤더 액션
|
||||
const headerActions = (
|
||||
// 헤더 액션 (함수로 정의)
|
||||
const headerActions = () => (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
@@ -410,39 +403,44 @@ export function PricingListClient({
|
||||
</Button>
|
||||
);
|
||||
|
||||
// 페이지네이션
|
||||
const totalPages = Math.ceil(filteredData.length / pageSize);
|
||||
// UniversalListPage 설정
|
||||
const pricingConfig: UniversalListConfig<PricingListItem> = {
|
||||
title: '단가 목록',
|
||||
description: '품목별 매입단가, 판매단가 및 마진을 관리합니다',
|
||||
icon: DollarSign,
|
||||
basePath: '/sales/pricing-management',
|
||||
idField: 'id',
|
||||
|
||||
actions: {
|
||||
getList: async () => ({
|
||||
success: true,
|
||||
data: data,
|
||||
totalCount: data.length,
|
||||
}),
|
||||
},
|
||||
|
||||
columns: tableColumns,
|
||||
headerActions,
|
||||
stats,
|
||||
tabs,
|
||||
|
||||
searchPlaceholder: '품목코드, 품목명, 규격 검색...',
|
||||
itemsPerPage: pageSize,
|
||||
clientSideFiltering: true,
|
||||
tabFilter,
|
||||
searchFilter,
|
||||
|
||||
renderTableRow,
|
||||
renderMobileCard,
|
||||
};
|
||||
|
||||
return (
|
||||
<IntegratedListTemplateV2<PricingListItem>
|
||||
title="단가 목록"
|
||||
description="품목별 매입단가, 판매단가 및 마진을 관리합니다"
|
||||
icon={DollarSign}
|
||||
headerActions={headerActions}
|
||||
stats={stats}
|
||||
searchValue={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
searchPlaceholder="품목코드, 품목명, 규격 검색..."
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
<UniversalListPage<PricingListItem>
|
||||
config={pricingConfig}
|
||||
initialData={data}
|
||||
initialTotalCount={data.length}
|
||||
onTabChange={setActiveTab}
|
||||
tableColumns={tableColumns}
|
||||
data={paginatedData}
|
||||
totalCount={filteredData.length}
|
||||
allData={filteredData}
|
||||
selectedItems={selectedItems}
|
||||
onToggleSelection={toggleSelection}
|
||||
onToggleSelectAll={toggleSelectAll}
|
||||
getItemId={(item) => item.id}
|
||||
renderTableRow={renderTableRow}
|
||||
renderMobileCard={renderMobileCard}
|
||||
pagination={{
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalItems: filteredData.length,
|
||||
itemsPerPage: pageSize,
|
||||
onPageChange: setCurrentPage,
|
||||
}}
|
||||
onSearchChange={setSearchTerm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user