Files
sam-react-prod/src/components/customer-center/InquiryManagement/InquiryList.tsx
byeongcheolryu c6b605200d feat: 신규 페이지 구현 및 HR/설정 기능 개선
신규 페이지:
- 회계관리: 거래처, 예상비용, 청구서, 발주서
- 게시판: 공지사항, 자료실, 커뮤니티
- 고객센터: 문의/FAQ
- 설정: 계정, 알림, 출퇴근, 팝업, 구독, 결제내역
- 리포트 (차트 시각화)
- 개발자 테스트 URL 페이지

기능 개선:
- HR 직원관리/휴가관리/카드관리 강화
- IntegratedListTemplateV2 확장
- AuthenticatedLayout 패딩 표준화
- 로그인 페이지 UI 개선

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 19:12:34 +09:00

336 lines
11 KiB
TypeScript

'use client';
/**
* 1:1 문의 목록 컴포넌트
*
* 디자인 스펙:
* - 페이지 타이틀: 1:1 문의
* - 페이지 설명: 1:1 문의를 등록하고 답변을 확인합니다.
* - 날짜 범위 선택 + 기간 프리셋 버튼
* - 문의 등록 버튼
* - 검색창
* - 테이블 필터 3개: 상담분류, 상태, 정렬
* - 테이블 컬럼: No., 상담분류, 제목, 상태, 등록일
*/
import { useState, useMemo, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { MessageSquare, Plus } 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 { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
IntegratedListTemplateV2,
type TableColumn,
type PaginationConfig,
} from '@/components/templates/IntegratedListTemplateV2';
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
import {
type Inquiry,
type InquiryCategory,
type InquiryStatus,
type SortOption,
CATEGORY_FILTER_OPTIONS,
INQUIRY_STATUSES,
SORT_OPTIONS,
INQUIRY_CATEGORY_LABELS,
INQUIRY_STATUS_LABELS,
MOCK_INQUIRIES,
} from './types';
const ITEMS_PER_PAGE = 10;
export function InquiryList() {
const router = useRouter();
// ===== 상태 관리 =====
const [inquiries] = useState<Inquiry[]>(MOCK_INQUIRIES);
const [searchValue, setSearchValue] = useState('');
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const [currentPage, setCurrentPage] = useState(1);
// 필터 상태
const [categoryFilter, setCategoryFilter] = useState<InquiryCategory | 'all'>('all');
const [statusFilter, setStatusFilter] = useState<InquiryStatus | 'all'>('all');
const [sortOption, setSortOption] = useState<SortOption>('latest');
// 날짜 범위
const [startDate, setStartDate] = useState(format(new Date(), 'yyyy-MM-dd'));
const [endDate, setEndDate] = useState(format(new Date(), 'yyyy-MM-dd'));
// ===== 필터링 및 정렬된 데이터 =====
const filteredData = useMemo(() => {
let result = [...inquiries];
// 상담분류 필터
if (categoryFilter !== 'all') {
result = result.filter((item) => item.category === categoryFilter);
}
// 상태 필터
if (statusFilter !== 'all') {
result = result.filter((item) => item.status === statusFilter);
}
// 날짜 필터
if (startDate && endDate) {
result = result.filter((item) => {
const itemDate = format(new Date(item.createdAt), 'yyyy-MM-dd');
return itemDate >= startDate && itemDate <= endDate;
});
}
// 검색 필터
if (searchValue) {
const searchLower = searchValue.toLowerCase();
result = result.filter(
(item) =>
item.title.toLowerCase().includes(searchLower) ||
item.authorName.toLowerCase().includes(searchLower)
);
}
// 정렬
if (sortOption === 'latest') {
result.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
} else {
result.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
}
return result;
}, [inquiries, categoryFilter, statusFilter, startDate, endDate, searchValue, sortOption]);
// ===== 페이지네이션 =====
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
return filteredData.slice(startIndex, startIndex + ITEMS_PER_PAGE);
}, [filteredData, currentPage]);
const totalPages = Math.ceil(filteredData.length / ITEMS_PER_PAGE);
// ===== 핸들러 =====
const handleToggleSelection = useCallback((id: string) => {
setSelectedItems((prev) => {
const newSet = new Set(prev);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}
return newSet;
});
}, []);
const handleToggleSelectAll = useCallback(() => {
if (selectedItems.size === paginatedData.length) {
setSelectedItems(new Set());
} else {
setSelectedItems(new Set(paginatedData.map((item) => item.id)));
}
}, [selectedItems.size, paginatedData]);
const handleRowClick = useCallback(
(item: Inquiry) => {
router.push(`/ko/customer-center/inquiries/${item.id}`);
},
[router]
);
const handleCreate = useCallback(() => {
router.push('/ko/customer-center/inquiries/create');
}, [router]);
// ===== 상태 Badge 색상 =====
const getStatusBadge = (status: InquiryStatus) => {
if (status === 'waiting') {
return <Badge variant="secondary" className="bg-yellow-100 text-yellow-700">{INQUIRY_STATUS_LABELS[status]}</Badge>;
}
return <Badge variant="secondary" className="bg-green-100 text-green-700">{INQUIRY_STATUS_LABELS[status]}</Badge>;
};
// ===== 테이블 컬럼 =====
const tableColumns: TableColumn[] = [
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
{ key: 'category', label: '상담분류', className: 'w-[120px]' },
{ key: 'title', label: '제목', className: 'min-w-[200px]' },
{ key: 'status', label: '상태', className: 'w-[100px] text-center' },
{ key: 'createdAt', label: '등록일', className: 'w-[120px] text-center' },
];
// ===== 테이블 행 렌더링 =====
const renderTableRow = useCallback(
(item: Inquiry, index: number, globalIndex: number) => {
const isSelected = selectedItems.has(item.id);
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={isSelected}
onCheckedChange={() => handleToggleSelection(item.id)}
/>
</TableCell>
<TableCell className="text-center">{globalIndex}</TableCell>
<TableCell>
<Badge variant="outline">{INQUIRY_CATEGORY_LABELS[item.category]}</Badge>
</TableCell>
<TableCell>{item.title}</TableCell>
<TableCell className="text-center">{getStatusBadge(item.status)}</TableCell>
<TableCell className="text-center">
{format(new Date(item.createdAt), 'yyyy-MM-dd')}
</TableCell>
</TableRow>
);
},
[selectedItems, handleRowClick, handleToggleSelection]
);
// ===== 모바일 카드 렌더링 =====
const renderMobileCard = useCallback(
(
item: Inquiry,
index: number,
globalIndex: number,
isSelected: boolean,
onToggle: () => void
) => {
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={isSelected} onCheckedChange={onToggle} />
</div>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">#{globalIndex}</span>
<Badge variant="outline">{INQUIRY_CATEGORY_LABELS[item.category]}</Badge>
</div>
{getStatusBadge(item.status)}
</div>
<h3 className="font-medium">{item.title}</h3>
<div className="text-sm text-muted-foreground">
{format(new Date(item.createdAt), 'yyyy-MM-dd')}
</div>
</div>
</div>
</CardContent>
</Card>
);
},
[handleRowClick]
);
// ===== 페이지네이션 설정 =====
const pagination: PaginationConfig = {
currentPage,
totalPages,
totalItems: filteredData.length,
itemsPerPage: ITEMS_PER_PAGE,
onPageChange: setCurrentPage,
};
return (
<IntegratedListTemplateV2
title="1:1 문의"
description="1:1 문의를 등록하고 답변을 확인합니다."
icon={MessageSquare}
headerActions={
<div className="flex items-center gap-2 flex-wrap">
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
/>
<Button onClick={handleCreate}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
}
searchValue={searchValue}
onSearchChange={setSearchValue}
searchPlaceholder="제목, 작성자로 검색..."
tableHeaderActions={
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-muted-foreground whitespace-nowrap">
{filteredData.length}
</span>
<Select
value={categoryFilter}
onValueChange={(v) => setCategoryFilter(v as InquiryCategory | 'all')}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="상담분류" />
</SelectTrigger>
<SelectContent>
{CATEGORY_FILTER_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={statusFilter}
onValueChange={(v) => setStatusFilter(v as InquiryStatus | 'all')}
>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{INQUIRY_STATUSES.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={sortOption} onValueChange={(v) => setSortOption(v as SortOption)}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="정렬" />
</SelectTrigger>
<SelectContent>
{SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
}
tableColumns={tableColumns}
data={paginatedData}
allData={filteredData}
selectedItems={selectedItems}
onToggleSelection={handleToggleSelection}
onToggleSelectAll={handleToggleSelectAll}
getItemId={(item) => item.id}
renderTableRow={renderTableRow}
renderMobileCard={renderMobileCard}
pagination={pagination}
/>
);
}
export default InquiryList;