Files
sam-react-prod/src/components/customer-center/EventManagement/EventList.tsx
byeongcheolryu ad493bcea6 feat(WEB): UniversalListPage 전체 마이그레이션 및 코드 정리
- UniversalListPage/IntegratedListTemplateV2 컴포넌트 기능 개선
- 회계, HR, 건설, 고객센터, 결재, 설정 등 전체 리스트 컴포넌트 마이그레이션
- 테스트 페이지 및 미사용 API 라우트 정리 (board-test, order-management-test 등)
- 미들웨어 토큰 갱신 로직 개선
- AuthenticatedLayout 구조 개선
- claudedocs 문서 업데이트

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 15:19:09 +09:00

260 lines
8.6 KiB
TypeScript

'use client';
/**
* 이벤트 목록 - UniversalListPage 마이그레이션
*
* 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환
* - 탭: 진행중인 이벤트 / 종료된 이벤트
* - 날짜 범위 선택 + 기간 프리셋 버튼
* - 정렬 filterConfig (PC: 인라인, 모바일: 바텀시트)
*/
import { useState, useMemo, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { Calendar } from 'lucide-react';
import { format } from 'date-fns';
import { TableCell, TableRow } from '@/components/ui/table';
import { Card, CardContent } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import {
UniversalListPage,
type UniversalListConfig,
type SelectionHandlers,
type RowClickHandlers,
type TabOption,
} from '@/components/templates/UniversalListPage';
import {
type Event,
type SortOption,
SORT_OPTIONS,
transformPostToEvent,
} from './types';
import { getPosts, deletePost } from '../shared/actions';
export function EventList() {
const router = useRouter();
// ===== 날짜 범위 상태 (외부 관리) =====
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
// ===== 오늘 날짜 기준 진행중/종료 판별 =====
const today = format(new Date(), 'yyyy-MM-dd');
// ===== 행 클릭 핸들러 =====
const handleRowClick = useCallback(
(item: Event) => {
router.push(`/ko/customer-center/events/${item.id}`);
},
[router]
);
// ===== 탭 카운트 계산 함수 =====
const computeTabs = useCallback(
(data: Event[]): TabOption[] => {
const ongoing = data.filter((item) => item.endDate >= today).length;
const ended = data.filter((item) => item.endDate < today).length;
return [
{ value: 'ongoing', label: '진행중인 이벤트', count: ongoing, color: 'blue' },
{ value: 'ended', label: '종료된 이벤트', count: ended, color: 'gray' },
];
},
[today]
);
// ===== UniversalListPage Config =====
const config: UniversalListConfig<Event> = useMemo(
() => ({
// 페이지 기본 정보
title: '이벤트',
description: '이벤트를 확인합니다.',
icon: Calendar,
basePath: '/customer-center/events',
// ID 추출
idField: 'id',
// API 액션
actions: {
getList: async () => {
const result = await getPosts('events', { per_page: 100 });
if (result.success && result.data) {
const transformed = result.data.data.map(transformPostToEvent);
return { success: true, data: transformed, totalCount: transformed.length };
}
return { success: false, error: result.error };
},
deleteItem: async (id: string) => {
const result = await deletePost('events', id);
return { success: result.success, error: result.error };
},
},
// 테이블 컬럼
columns: [
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
{ key: 'title', label: '제목', className: 'min-w-[200px]' },
{ key: 'author', label: '작성자', className: 'w-[100px] text-center' },
{ key: 'period', label: '기간', className: 'w-[200px] text-center' },
{ key: 'viewCount', label: '조회수', className: 'w-[80px] text-center' },
],
// 클라이언트 사이드 필터링
clientSideFiltering: true,
itemsPerPage: 10,
// 탭 설정 (동적 카운트)
tabs: [
{ value: 'ongoing', label: '진행중인 이벤트', count: 0, color: 'blue' },
{ value: 'ended', label: '종료된 이벤트', count: 0, color: 'gray' },
],
defaultTab: 'ongoing',
// 탭 필터
tabFilter: (item, activeTab) => {
if (activeTab === 'ongoing') {
return item.endDate >= today;
}
return item.endDate < today;
},
// 검색 필터
searchPlaceholder: '제목, 작성자로 검색...',
searchFilter: (item, searchValue) => {
const searchLower = searchValue.toLowerCase();
return (
item.title.toLowerCase().includes(searchLower) ||
item.author.toLowerCase().includes(searchLower)
);
},
// 커스텀 필터 (날짜)
customFilterFn: (items, filterValues) => {
if (!startDate || !endDate) return items;
return items.filter((item) => {
// 이벤트 기간이 선택한 기간과 겹치는지 확인
return item.startDate <= endDate && item.endDate >= startDate;
});
},
// 커스텀 정렬 (filterValues에서 정렬 옵션 참조)
customSortFn: (items, filterValues) => {
const sorted = [...items];
const sortOption = (filterValues.sort as SortOption) || 'latest';
switch (sortOption) {
case 'latest':
sorted.sort((a, b) => b.startDate.localeCompare(a.startDate));
break;
case 'oldest':
sorted.sort((a, b) => a.startDate.localeCompare(b.startDate));
break;
case 'views':
sorted.sort((a, b) => b.viewCount - a.viewCount);
break;
}
return sorted;
},
// 필터 설정 (PC: 인라인, 모바일: 바텀시트)
filterConfig: [
{
key: 'sort',
label: '정렬',
type: 'single',
options: SORT_OPTIONS.map((opt) => ({ value: opt.value, label: opt.label })),
},
],
initialFilters: { sort: 'latest' },
// 공통 헤더 옵션: 날짜 선택기 (왼쪽)
dateRangeSelector: {
enabled: true,
startDate,
endDate,
onStartDateChange: setStartDate,
onEndDateChange: setEndDate,
},
// 테이블 헤더 액션 (총 건수만 표시)
tableHeaderActions: ({ totalCount }) => (
<span className="text-sm text-muted-foreground"> {totalCount}</span>
),
// 테이블 행 렌더링
renderTableRow: (
item: Event,
index: number,
globalIndex: number,
handlers: SelectionHandlers & RowClickHandlers<Event>
) => {
return (
<TableRow
key={item.id}
className="hover:bg-muted/50 cursor-pointer"
onClick={() => handleRowClick(item)}
>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={handlers.isSelected}
onCheckedChange={handlers.onToggle}
/>
</TableCell>
<TableCell className="text-center">{globalIndex}</TableCell>
<TableCell>{item.title}</TableCell>
<TableCell className="text-center">{item.author}</TableCell>
<TableCell className="text-center">
{item.startDate} ~ {item.endDate}
</TableCell>
<TableCell className="text-center">{item.viewCount}</TableCell>
</TableRow>
);
},
// 모바일 카드 렌더링
renderMobileCard: (
item: Event,
index: number,
globalIndex: number,
handlers: SelectionHandlers & RowClickHandlers<Event>
) => {
return (
<Card
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleRowClick(item)}
>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={handlers.isSelected}
onCheckedChange={handlers.onToggle}
/>
</div>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">#{globalIndex}</span>
<span className="text-sm text-muted-foreground"> {item.viewCount}</span>
</div>
<h3 className="font-medium">{item.title}</h3>
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground">
<span>{item.author}</span>
<span>|</span>
<span>
{item.startDate} ~ {item.endDate}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
);
},
}),
[startDate, endDate, handleRowClick, today]
);
return <UniversalListPage config={config} />;
}
export default EventList;