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:
byeongcheolryu
2026-01-16 15:19:09 +09:00
parent 8639eee5df
commit ad493bcea6
90 changed files with 19864 additions and 20305 deletions

View File

@@ -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}
/>
);
}