diff --git a/claudedocs/[IMPL-2026-01-13] mobile-filter-migration-checklist.md b/claudedocs/[IMPL-2026-01-13] mobile-filter-migration-checklist.md index 725dfb81..978500fd 100644 --- a/claudedocs/[IMPL-2026-01-13] mobile-filter-migration-checklist.md +++ b/claudedocs/[IMPL-2026-01-13] mobile-filter-migration-checklist.md @@ -16,29 +16,29 @@ --- -## πŸ—οΈ 건섀 도메인 (12개) +## πŸ—οΈ 건섀 도메인 (12개) βœ… μ™„λ£Œ ### μž…μ°°κ΄€λ¦¬ -- [ ] ν˜„μž₯μ„€λͺ…νšŒκ΄€λ¦¬ (`SiteBriefingListClient.tsx`) -- [ ] 견적관리 (`EstimateListClient.tsx`) -- [ ] μž…μ°°κ΄€λ¦¬ (`BiddingListClient.tsx`) +- [x] ν˜„μž₯μ„€λͺ…νšŒκ΄€λ¦¬ (`SiteBriefingListClient.tsx`) +- [x] 견적관리 (`EstimateListClient.tsx`) +- [x] μž…μ°°κ΄€λ¦¬ (`BiddingListClient.tsx`) ### 계약관리 -- [ ] 계약관리 (`ContractListClient.tsx`) -- [ ] μΈμˆ˜μΈκ³„λ³΄κ³ μ„œ (`HandoverReportListClient.tsx`) +- [x] 계약관리 (`ContractListClient.tsx`) +- [x] μΈμˆ˜μΈκ³„λ³΄κ³ μ„œ (`HandoverReportListClient.tsx`) ### λ°œμ£Όκ΄€λ¦¬ -- [ ] ν˜„μž₯관리 (`SiteManagementListClient.tsx`) -- [ ] ꡬ쑰검토관리 (`StructureReviewListClient.tsx`) +- [x] ν˜„μž₯관리 (`SiteManagementListClient.tsx`) +- [x] ꡬ쑰검토관리 (`StructureReviewListClient.tsx`) ### 곡사관리 -- [ ] μ΄μŠˆκ΄€λ¦¬ (`IssueManagementListClient.tsx`) -- [ ] μž‘μ—…μΈλ ₯ν˜„ν™© (`WorkerStatusListClient.tsx`) +- [x] μ΄μŠˆκ΄€λ¦¬ (`IssueManagementListClient.tsx`) +- [x] μž‘μ—…μΈλ ₯ν˜„ν™© (`WorkerStatusListClient.tsx`) ### 기쀀정보 -- [ ] ν’ˆλͺ©κ΄€λ¦¬ (`ItemManagementClient.tsx`) -- [ ] 단가관리 (`PricingListClient.tsx`) -- [ ] λ…Έμž„κ΄€λ¦¬ (`LaborManagementClient.tsx`) +- [x] ν’ˆλͺ©κ΄€λ¦¬ (`ItemManagementClient.tsx`) +- [x] 단가관리 (`PricingListClient.tsx`) +- [x] λ…Έμž„κ΄€λ¦¬ (`LaborManagementClient.tsx`) --- @@ -114,15 +114,15 @@ | 도메인 | μ™„λ£Œ | 전체 | μ§„ν–‰λ₯  | |--------|------|------|--------| -| 건섀 (μ™„λ£Œ) | 6 | 6 | 100% | -| 건섀 (λ―Έμ™„λ£Œ) | 0 | 12 | 0% | +| 건섀 (κΈ°μ™„λ£Œ) | 6 | 6 | 100% | +| 건섀 (λ§ˆμ΄κ·Έλ ˆμ΄μ…˜) | 12 | 12 | 100% βœ… | | HR | 0 | 5 | 0% | | νšŒκ³„ | 0 | 11 | 0% | | 생산/자재/ν’ˆμ§ˆ/좜고 | 0 | 6 | 0% | | μ „μžκ²°μž¬ | 0 | 3 | 0% | | μ„€μ • | 0 | 4 | 0% | | 기타 | 0 | 9 | 0% | -| **총계** | **6** | **56** | **11%** | +| **총계** | **18** | **56** | **32%** | --- @@ -178,3 +178,4 @@ const handleFilterReset = useCallback(() => { |------|----------| | 2026-01-13 | 체크리슀트 λ¬Έμ„œ 생성, MobileFilter 슀크둀 버그 μˆ˜μ • | | 2026-01-13 | μ‹œκ³΅κ΄€λ¦¬ mobileFilterSlot β†’ filterConfig λ°©μ‹μœΌλ‘œ λ³€κ²½, ν˜‘λ ₯업체관리 filterConfig 적용 | +| 2026-01-13 | 건섀 도메인 12개 파일 λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ™„λ£Œ (SiteBriefing, Estimate, Bidding, Contract, HandoverReport, SiteManagement, StructureReview, IssueManagement, WorkerStatus, ItemManagement, Pricing, LaborManagement) | diff --git a/src/app/[locale]/(protected)/hr/employee-management/new/page.tsx b/src/app/[locale]/(protected)/hr/employee-management/new/page.tsx index fcb8261b..fe9df044 100644 --- a/src/app/[locale]/(protected)/hr/employee-management/new/page.tsx +++ b/src/app/[locale]/(protected)/hr/employee-management/new/page.tsx @@ -1,22 +1,28 @@ 'use client'; -import { useRouter } from 'next/navigation'; +import { useRouter, useParams } from 'next/navigation'; +import { toast } from 'sonner'; import { EmployeeForm } from '@/components/hr/EmployeeManagement/EmployeeForm'; import { createEmployee } from '@/components/hr/EmployeeManagement/actions'; import type { EmployeeFormData } from '@/components/hr/EmployeeManagement/types'; export default function EmployeeNewPage() { const router = useRouter(); + const params = useParams(); + const locale = params.locale as string || 'ko'; const handleSave = async (data: EmployeeFormData) => { try { const result = await createEmployee(data); if (result.success) { - router.push('/ko/hr/employee-management'); + toast.success('사원이 λ“±λ‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + router.push(`/${locale}/hr/employee-management`); } else { + toast.error(result.error || '사원 등둝에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); console.error('[EmployeeNewPage] Create failed:', result.error); } } catch (error) { + toast.error('μ„œλ²„ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.'); console.error('[EmployeeNewPage] Create error:', error); } }; diff --git a/src/components/business/CEODashboard/sections/TodayIssueSection.tsx b/src/components/business/CEODashboard/sections/TodayIssueSection.tsx index 3628a66c..042a1734 100644 --- a/src/components/business/CEODashboard/sections/TodayIssueSection.tsx +++ b/src/components/business/CEODashboard/sections/TodayIssueSection.tsx @@ -44,11 +44,15 @@ interface TodayIssueSectionProps { export function TodayIssueSection({ items }: TodayIssueSectionProps) { const router = useRouter(); const [filter, setFilter] = useState('all'); + const [dismissedIds, setDismissedIds] = useState>(new Set()); + + // ν™•μΈλ˜μ§€ μ•Šμ€ μ•„μ΄ν…œλ§Œ 필터링 + const activeItems = items.filter((item) => !dismissedIds.has(item.id)); // ν•„ν„°λ§λœ μ•„μ΄ν…œ const filteredItems = filter === 'all' - ? items - : items.filter((item) => item.badge === filter); + ? activeItems + : activeItems.filter((item) => item.badge === filter); // μ•„μ΄ν…œ 클릭 const handleItemClick = (item: TodayIssueListItem) => { @@ -57,13 +61,21 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) { } }; + // 확인 λ²„νŠΌ 클릭 (λͺ©λ‘μ—μ„œ 제거) + const handleDismiss = (item: TodayIssueListItem) => { + setDismissedIds((prev) => new Set(prev).add(item.id)); + toast.success(`"${item.content}" 확인 μ™„λ£Œ`); + }; + // 승인 λ²„νŠΌ 클릭 const handleApprove = (item: TodayIssueListItem) => { + setDismissedIds((prev) => new Set(prev).add(item.id)); toast.success(`"${item.content}" 승인 μ²˜λ¦¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`); }; // 반렀 λ²„νŠΌ 클릭 const handleReject = (item: TodayIssueListItem) => { + setDismissedIds((prev) => new Set(prev).add(item.id)); toast.error(`"${item.content}" 반렀 μ²˜λ¦¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.`); }; @@ -114,12 +126,12 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) { {/* 우츑: μ‹œκ°„ + λ²„νŠΌ */} -
+
e.stopPropagation()}> {item.time} - {item.needsApproval && ( -
e.stopPropagation()}> + {item.needsApproval ? ( +
+ ) : ( + )}
diff --git a/src/components/business/construction/bidding/BiddingListClient.tsx b/src/components/business/construction/bidding/BiddingListClient.tsx index 9ed91fc0..77abedae 100644 --- a/src/components/business/construction/bidding/BiddingListClient.tsx +++ b/src/components/business/construction/bidding/BiddingListClient.tsx @@ -6,15 +6,8 @@ import { FileText, Clock, Trophy, Pencil } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { MultiSelectCombobox, MultiSelectOption } from '@/components/ui/multi-select-combobox'; -import { IntegratedListTemplateV2, TableColumn, StatCard } from '@/components/templates/IntegratedListTemplateV2'; +import { MultiSelectOption } from '@/components/ui/multi-select-combobox'; +import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2'; import { MobileCard } from '@/components/molecules/MobileCard'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { toast } from 'sonner'; @@ -335,6 +328,81 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi } }, [selectedItems, loadData]); + // ===== filterConfig 기반 톡합 ν•„ν„° μ‹œμŠ€ν…œ ===== + const filterConfig: FilterFieldConfig[] = useMemo(() => [ + { + key: 'partner', + label: '거래처', + type: 'multi', + options: MOCK_PARTNERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'bidder', + label: 'μž…μ°°μž', + type: 'multi', + options: MOCK_BIDDERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: BIDDING_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: BIDDING_SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ (μž…μ°°μΌ)', + }, + ], []); + + const filterValues: FilterValues = useMemo(() => ({ + partner: partnerFilters, + bidder: bidderFilters, + status: statusFilter, + sortBy: sortBy, + }), [partnerFilters, bidderFilters, statusFilter, sortBy]); + + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'partner': + setPartnerFilters(value as string[]); + break; + case 'bidder': + setBidderFilters(value as string[]); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); + + const handleFilterReset = useCallback(() => { + setPartnerFilters([]); + setBidderFilters([]); + setStatusFilter('all'); + setSortBy('biddingDateDesc'); + setCurrentPage(1); + }, []); + // ν…Œμ΄λΈ” ν–‰ λ Œλ”λ§ const renderTableRow = useCallback( (bidding: Bidding, index: number, globalIndex: number) => { @@ -450,63 +518,6 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi }, ]; - // ν…Œμ΄λΈ” 헀더 μ•‘μ…˜ (총 건수 + ν•„ν„° 4개: 거래처, μž…μ°°μž, μƒνƒœ, μ •λ ¬) - const tableHeaderActions = ( -
- - 총 {sortedBiddings.length}건 - - - {/* 거래처 ν•„ν„° (닀쀑선택) */} - - - {/* μž…μ°°μž ν•„ν„° (닀쀑선택) */} - - - {/* μƒνƒœ ν•„ν„° */} - - - {/* μ •λ ¬ */} - -
- ); - return ( <> [ + { + key: 'partner', + label: '거래처', + type: 'multi', + options: MOCK_PARTNERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'contractManager', + label: 'κ³„μ•½λ‹΄λ‹Ήμž', + type: 'multi', + options: MOCK_CONTRACT_MANAGERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'constructionPM', + label: '곡사PM', + type: 'multi', + options: MOCK_CONSTRUCTION_PMS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: CONTRACT_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: CONTRACT_SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ (계약일)', + }, + ], []); + + const filterValues: FilterValues = useMemo(() => ({ + partner: partnerFilters, + contractManager: contractManagerFilters, + constructionPM: constructionPMFilters, + status: statusFilter, + sortBy: sortBy, + }), [partnerFilters, contractManagerFilters, constructionPMFilters, statusFilter, sortBy]); + + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'partner': + setPartnerFilters(value as string[]); + break; + case 'contractManager': + setContractManagerFilters(value as string[]); + break; + case 'constructionPM': + setConstructionPMFilters(value as string[]); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); + + const handleFilterReset = useCallback(() => { + setPartnerFilters([]); + setContractManagerFilters([]); + setConstructionPMFilters([]); + setStatusFilter('all'); + setSortBy('contractDateDesc'); + setCurrentPage(1); + }, []); + // ν…Œμ΄λΈ” ν–‰ λ Œλ”λ§ // μˆœμ„œ: μ²΄ν¬λ°•μŠ€, 번호, κ³„μ•½λ²ˆν˜Έ, 거래처, ν˜„μž₯λͺ…, κ³„μ•½λ‹΄λ‹Ήμž, 곡사PM, 총 κ°œμ†Œ, κ³„μ•½κΈˆμ•‘, 계약기간, μƒνƒœ, μž‘μ—… const renderTableRow = useCallback( @@ -475,73 +557,6 @@ export default function ContractListClient({ }, ]; - // ν…Œμ΄λΈ” 헀더 μ•‘μ…˜ (총 건수 + ν•„ν„°λ“€) - const tableHeaderActions = ( -
- - 총 {sortedContracts.length}건 - - - {/* 거래처 ν•„ν„° */} - - - {/* κ³„μ•½λ‹΄λ‹Ήμž ν•„ν„° */} - - - {/* 곡사PM ν•„ν„° */} - - - {/* μƒνƒœ ν•„ν„° */} - - - {/* μ •λ ¬ */} - -
- ); - return ( <> [ + { + key: 'partner', + label: '거래처', + type: 'multi', + options: MOCK_PARTNERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'estimator', + label: '견적자', + type: 'multi', + options: MOCK_ESTIMATORS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: ESTIMATE_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: ESTIMATE_SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ', + }, + ], []); + + const filterValues: FilterValues = useMemo(() => ({ + partner: partnerFilters, + estimator: estimatorFilters, + status: statusFilter, + sortBy: sortBy, + }), [partnerFilters, estimatorFilters, statusFilter, sortBy]); + + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'partner': + setPartnerFilters(value as string[]); + break; + case 'estimator': + setEstimatorFilters(value as string[]); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); + + const handleFilterReset = useCallback(() => { + setPartnerFilters([]); + setEstimatorFilters([]); + setStatusFilter('all'); + setSortBy('latest'); + setCurrentPage(1); + }, []); + // ν…Œμ΄λΈ” ν–‰ λ Œλ”λ§ const renderTableRow = useCallback( (estimate: Estimate, index: number, globalIndex: number) => { @@ -428,63 +496,6 @@ export default function EstimateListClient({ initialData = [], initialStats }: E }, ]; - // ν…Œμ΄λΈ” 헀더 μ•‘μ…˜ (총 건수 + ν•„ν„°λ“€) - const tableHeaderActions = ( -
- - 총 {sortedEstimates.length}건 - - - {/* 거래처 ν•„ν„° (닀쀑선택) */} - - - {/* 견적자 ν•„ν„° (닀쀑선택) */} - - - {/* μƒνƒœ ν•„ν„° */} - - - {/* μ •λ ¬ */} - -
- ); - return ( <> [ + { + key: 'partner', + label: '거래처', + type: 'multi', + options: MOCK_PARTNERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'contractManager', + label: 'κ³„μ•½λ‹΄λ‹Ήμž', + type: 'multi', + options: MOCK_CONTRACT_MANAGERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'constructionPM', + label: '곡사PM', + type: 'multi', + options: MOCK_CONSTRUCTION_PMS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: REPORT_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: REPORT_SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ (κ³„μ•½μ‹œμž‘μΌ)', + }, + ], []); + + const filterValues: FilterValues = useMemo(() => ({ + partner: partnerFilters, + contractManager: contractManagerFilters, + constructionPM: constructionPMFilters, + status: statusFilter, + sortBy: sortBy, + }), [partnerFilters, contractManagerFilters, constructionPMFilters, statusFilter, sortBy]); + + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'partner': + setPartnerFilters(value as string[]); + break; + case 'contractManager': + setContractManagerFilters(value as string[]); + break; + case 'constructionPM': + setConstructionPMFilters(value as string[]); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); + + const handleFilterReset = useCallback(() => { + setPartnerFilters([]); + setContractManagerFilters([]); + setConstructionPMFilters([]); + setStatusFilter('all'); + setSortBy('contractDateDesc'); + setCurrentPage(1); + }, []); + // ν…Œμ΄λΈ” ν–‰ λ Œλ”λ§ const renderTableRow = useCallback( (report: HandoverReport, index: number, globalIndex: number) => { @@ -389,73 +471,6 @@ export default function HandoverReportListClient({ }, ]; - // ν…Œμ΄λΈ” 헀더 μ•‘μ…˜ - const tableHeaderActions = ( -
- - 총 {sortedReports.length}건 - - - {/* 거래처 ν•„ν„° */} - - - {/* κ³„μ•½λ‹΄λ‹Ήμž ν•„ν„° */} - - - {/* 곡사PM ν•„ν„° */} - - - {/* μƒνƒœ ν•„ν„° */} - - - {/* μ •λ ¬ */} - -
- ); - return ( <> - {/* 철회 λ²„νŠΌ (μ„ νƒλœ ν•­λͺ©μ΄ μžˆμ„ λ•Œλ§Œ ν‘œμ‹œ) */} - {selectedItems.size > 0 && ( - - )} + // ===== filterConfig 기반 톡합 ν•„ν„° μ‹œμŠ€ν…œ ===== + const filterConfig: FilterFieldConfig[] = useMemo(() => [ + { + key: 'partner', + label: '거래처', + type: 'multi', + options: partnerOptions.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'site', + label: 'ν˜„μž₯λͺ…', + type: 'multi', + options: siteOptions.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'category', + label: 'ꡬ뢄', + type: 'multi', + options: categoryOptions.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'reporter', + label: '보고자', + type: 'multi', + options: reporterOptions.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'assignee', + label: 'λ‹΄λ‹Ήμž', + type: 'multi', + options: assigneeOptions.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'priority', + label: 'μ€‘μš”λ„', + type: 'single', + options: ISSUE_PRIORITY_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: ISSUE_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: ISSUE_SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ', + }, + ], [partnerOptions, siteOptions, categoryOptions, reporterOptions, assigneeOptions]); - {/* 1. 거래처 ν•„ν„° (닀쀑선택) */} - + const filterValues: FilterValues = useMemo(() => ({ + partner: partnerFilters, + site: siteFilters, + category: categoryFilters, + reporter: reporterFilters, + assignee: assigneeFilters, + priority: priorityFilter, + status: statusFilter, + sortBy: sortBy, + }), [partnerFilters, siteFilters, categoryFilters, reporterFilters, assigneeFilters, priorityFilter, statusFilter, sortBy]); - {/* 2. ν˜„μž₯λͺ… ν•„ν„° (닀쀑선택) */} - + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'partner': + setPartnerFilters(value as string[]); + break; + case 'site': + setSiteFilters(value as string[]); + break; + case 'category': + setCategoryFilters(value as string[]); + break; + case 'reporter': + setReporterFilters(value as string[]); + break; + case 'assignee': + setAssigneeFilters(value as string[]); + break; + case 'priority': + setPriorityFilter(value as string); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); - {/* 3. ꡬ뢄 ν•„ν„° (닀쀑선택) */} - + const handleFilterReset = useCallback(() => { + setPartnerFilters([]); + setSiteFilters([]); + setCategoryFilters([]); + setReporterFilters([]); + setAssigneeFilters([]); + setPriorityFilter('all'); + setStatusFilter('all'); + setSortBy('latest'); + setCurrentPage(1); + }, []); - {/* 4. 보고자 ν•„ν„° (닀쀑선택) */} - - - {/* 5. λ‹΄λ‹Ήμž ν•„ν„° (닀쀑선택) */} - - - {/* 6. μ€‘μš”λ„ ν•„ν„° (단일선택) */} - - - {/* 7. μƒνƒœ ν•„ν„° (단일선택) */} - - - {/* 8. μ •λ ¬ (단일선택) */} - -
- ); + // 철회 λ²„νŠΌ (bulkActions용) + const bulkActions = selectedItems.size > 0 ? ( + + ) : null; return ( <> @@ -598,7 +625,12 @@ export default function IssueManagementListClient({ icon={AlertTriangle} headerActions={headerActions} stats={statsCardsData} - tableHeaderActions={tableHeaderActions} + filterConfig={filterConfig} + filterValues={filterValues} + onFilterChange={handleFilterChange} + onFilterReset={handleFilterReset} + filterTitle="이슈 ν•„ν„°" + bulkActions={bulkActions} searchValue={searchValue} onSearchChange={handleSearchChange} searchPlaceholder="이슈번호, μ‹œκ³΅λ²ˆν˜Έ, 거래처, ν˜„μž₯, 제λͺ©, 보고자, λ‹΄λ‹Ήμž 검색" diff --git a/src/components/business/construction/item-management/ItemManagementClient.tsx b/src/components/business/construction/item-management/ItemManagementClient.tsx index 33b17f7b..7ff9c19c 100644 --- a/src/components/business/construction/item-management/ItemManagementClient.tsx +++ b/src/components/business/construction/item-management/ItemManagementClient.tsx @@ -8,14 +8,7 @@ import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { IntegratedListTemplateV2, TableColumn } from '@/components/templates/IntegratedListTemplateV2'; +import { IntegratedListTemplateV2, TableColumn, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2'; import { MobileCard } from '@/components/molecules/MobileCard'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { toast } from 'sonner'; @@ -427,130 +420,112 @@ export default function ItemManagementClient({ /> ); - // ν…Œμ΄λΈ” 헀더 μ•‘μ…˜ (6개 ν•„ν„°) - const tableHeaderActions = ( -
- {/* 총 건수 */} - - 총 {sortedItems.length}건 - + // ===== filterConfig 기반 톡합 ν•„ν„° μ‹œμŠ€ν…œ ===== + const filterConfig: FilterFieldConfig[] = useMemo(() => [ + { + key: 'itemType', + label: 'ν’ˆλͺ©μœ ν˜•', + type: 'single', + options: ITEM_TYPE_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'category', + label: 'μΉ΄ν…Œκ³ λ¦¬', + type: 'single', + options: categoryOptions.map(c => ({ + value: c.id, + label: c.name, + })), + allOptionLabel: '전체', + }, + { + key: 'specification', + label: '규격', + type: 'single', + options: SPECIFICATION_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'orderType', + label: 'ꡬ뢄', + type: 'single', + options: ORDER_TYPE_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ', + }, + ], [categoryOptions]); - {/* ν’ˆλͺ©μœ ν˜• ν•„ν„° */} - + const filterValues: FilterValues = useMemo(() => ({ + itemType: itemTypeFilter, + category: categoryFilter, + specification: specificationFilter, + orderType: orderTypeFilter, + status: statusFilter, + sortBy: sortBy, + }), [itemTypeFilter, categoryFilter, specificationFilter, orderTypeFilter, statusFilter, sortBy]); - {/* μΉ΄ν…Œκ³ λ¦¬ ν•„ν„° */} - + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'itemType': + setItemTypeFilter(value as ItemType | 'all'); + break; + case 'category': + setCategoryFilter(value as string); + break; + case 'specification': + setSpecificationFilter(value as Specification | 'all'); + break; + case 'orderType': + setOrderTypeFilter(value as OrderType | 'all'); + break; + case 'status': + setStatusFilter(value as ItemStatus | 'all'); + break; + case 'sortBy': + setSortBy(value as 'latest' | 'oldest'); + break; + } + setCurrentPage(1); + }, []); - {/* 규격 ν•„ν„° */} - - - {/* ꡬ뢄 ν•„ν„° */} - - - {/* μƒνƒœ ν•„ν„° */} - - - {/* μ •λ ¬ */} - -
- ); + const handleFilterReset = useCallback(() => { + setItemTypeFilter('all'); + setCategoryFilter('all'); + setSpecificationFilter('all'); + setOrderTypeFilter('all'); + setStatusFilter('all'); + setSortBy('latest'); + setCurrentPage(1); + }, []); return ( <> @@ -573,7 +548,11 @@ export default function ItemManagementClient({ iconColor: 'text-green-500', }, ]} - tableHeaderActions={tableHeaderActions} + filterConfig={filterConfig} + filterValues={filterValues} + onFilterChange={handleFilterChange} + onFilterReset={handleFilterReset} + filterTitle="ν’ˆλͺ© ν•„ν„°" searchValue={searchValue} onSearchChange={handleSearchChange} searchPlaceholder="ν’ˆλͺ©λͺ…, ν’ˆλͺ©λ²ˆν˜Έ, μΉ΄ν…Œκ³ λ¦¬ 검색" diff --git a/src/components/business/construction/labor-management/LaborManagementClient.tsx b/src/components/business/construction/labor-management/LaborManagementClient.tsx index 533b29f5..d59a9c8f 100644 --- a/src/components/business/construction/labor-management/LaborManagementClient.tsx +++ b/src/components/business/construction/labor-management/LaborManagementClient.tsx @@ -15,7 +15,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { IntegratedListTemplateV2, TableColumn } from '@/components/templates/IntegratedListTemplateV2'; +import { IntegratedListTemplateV2, TableColumn, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2'; import { MobileCard } from '@/components/molecules/MobileCard'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { toast } from 'sonner'; @@ -369,6 +369,66 @@ export default function LaborManagementClient({ [handleRowClick] ); + // ===== filterConfig 기반 톡합 ν•„ν„° μ‹œμŠ€ν…œ ===== + const filterConfig: FilterFieldConfig[] = useMemo(() => [ + { + key: 'category', + label: 'ꡬ뢄', + type: 'single', + options: CATEGORY_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ', + }, + ], []); + + const filterValues: FilterValues = useMemo(() => ({ + category: categoryFilter, + status: statusFilter, + sortBy: sortBy, + }), [categoryFilter, statusFilter, sortBy]); + + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'category': + setCategoryFilter(value as LaborCategory | 'all'); + break; + case 'status': + setStatusFilter(value as LaborStatus | 'all'); + break; + case 'sortBy': + setSortBy(value as SortOrder); + break; + } + setCurrentPage(1); + }, []); + + const handleFilterReset = useCallback(() => { + setCategoryFilter('all'); + setStatusFilter('all'); + setSortBy('μ΅œμ‹ μˆœ'); + setCurrentPage(1); + }, []); + // 헀더 μ•‘μ…˜ (λ‚ μ§œμ„ νƒ + 등둝 λ²„νŠΌ) const headerActions = ( - - 총 {sortedPricing.length}건 - + // ===== filterConfig 기반 톡합 ν•„ν„° μ‹œμŠ€ν…œ ===== + const filterConfig: FilterFieldConfig[] = useMemo(() => [ + { + key: 'itemType', + label: 'ν’ˆλͺ©μœ ν˜•', + type: 'single', + options: ITEM_TYPE_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'category', + label: 'μΉ΄ν…Œκ³ λ¦¬', + type: 'single', + options: CATEGORY_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'spec', + label: '규격', + type: 'single', + options: SPEC_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'division', + label: 'ꡬ뢄', + type: 'single', + options: DIVISION_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ', + }, + ], []); - {/* ν’ˆλͺ©μœ ν˜• ν•„ν„° */} - + const filterValues: FilterValues = useMemo(() => ({ + itemType: itemTypeFilter, + category: categoryFilter, + spec: specFilter, + division: divisionFilter, + status: statusFilter, + sortBy: sortBy, + }), [itemTypeFilter, categoryFilter, specFilter, divisionFilter, statusFilter, sortBy]); - {/* μΉ΄ν…Œκ³ λ¦¬ ν•„ν„° */} - + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'itemType': + setItemTypeFilter(value as string); + break; + case 'category': + setCategoryFilter(value as string); + break; + case 'spec': + setSpecFilter(value as string); + break; + case 'division': + setDivisionFilter(value as string); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); - {/* 규격 ν•„ν„° */} - - - {/* ꡬ뢄 ν•„ν„° */} - - - {/* μƒνƒœ ν•„ν„° */} - - - {/* μ •λ ¬ */} - - - ); + const handleFilterReset = useCallback(() => { + setItemTypeFilter('all'); + setCategoryFilter('all'); + setSpecFilter('all'); + setDivisionFilter('all'); + setStatusFilter('all'); + setSortBy('latest'); + setCurrentPage(1); + }, []); // 동적 컬럼으둜 인해 tableColumnsλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šκ³  μ»€μŠ€ν…€ 헀더 μ‚¬μš© const emptyTableColumns: { key: string; label: string; className?: string }[] = []; @@ -573,7 +580,11 @@ export default function PricingListClient({ icon={DollarSign} headerActions={headerActions} stats={statsCardsData} - tableHeaderActions={tableHeaderActions} + filterConfig={filterConfig} + filterValues={filterValues} + onFilterChange={handleFilterChange} + onFilterReset={handleFilterReset} + filterTitle="단가 ν•„ν„°" searchValue={searchValue} onSearchChange={handleSearchChange} searchPlaceholder="λ‹¨κ°€λ²ˆν˜Έ, ν’ˆλͺ©λͺ…, μΉ΄ν…Œκ³ λ¦¬, 거래처 검색" diff --git a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx index 99efcf9e..37fddaca 100644 --- a/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx +++ b/src/components/business/construction/site-briefings/SiteBriefingListClient.tsx @@ -6,15 +6,8 @@ import { Calendar, CalendarDays, CalendarCheck, CalendarClock, Plus, Pencil, Tra import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { MultiSelectCombobox, MultiSelectOption } from '@/components/ui/multi-select-combobox'; -import { IntegratedListTemplateV2, TableColumn, StatCard } from '@/components/templates/IntegratedListTemplateV2'; +import { MultiSelectOption } from '@/components/ui/multi-select-combobox'; +import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2'; import { MobileCard } from '@/components/molecules/MobileCard'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { toast } from 'sonner'; @@ -344,6 +337,96 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin } }, [selectedItems, loadData]); + // ===== filterConfig 기반 톡합 ν•„ν„° μ‹œμŠ€ν…œ ===== + const filterConfig: FilterFieldConfig[] = useMemo(() => [ + { + key: 'partner', + label: '거래처', + type: 'multi', + options: MOCK_PARTNERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'type', + label: 'ꡬ뢄', + type: 'single', + options: TYPE_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'attendee', + label: 'μ°Έμ„μž', + type: 'multi', + options: MOCK_ATTENDEES.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ', + }, + ], []); + + const filterValues: FilterValues = useMemo(() => ({ + partner: partnerFilters, + type: typeFilter, + attendee: attendeeFilters, + status: statusFilter, + sortBy: sortBy, + }), [partnerFilters, typeFilter, attendeeFilters, statusFilter, sortBy]); + + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'partner': + setPartnerFilters(value as string[]); + break; + case 'type': + setTypeFilter(value as string); + break; + case 'attendee': + setAttendeeFilters(value as string[]); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); + + const handleFilterReset = useCallback(() => { + setPartnerFilters([]); + setTypeFilter('all'); + setAttendeeFilters([]); + setStatusFilter('all'); + setSortBy('latest'); + setCurrentPage(1); + }, []); + // ν…Œμ΄λΈ” ν–‰ λ Œλ”λ§ const renderTableRow = useCallback( (briefing: SiteBriefing, index: number, globalIndex: number) => { @@ -475,77 +558,6 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin }, ]; - // ν…Œμ΄λΈ” 헀더 μ•‘μ…˜ (총 건수 + ν•„ν„°λ“€) - const tableHeaderActions = ( -
- - 총 {sortedBriefings.length}건 - - - {/* 거래처 ν•„ν„° (닀쀑선택) */} - - - {/* ꡬ뢄 ν•„ν„° */} - - - {/* μ°Έμ„μž ν•„ν„° (닀쀑선택) */} - - - {/* μƒνƒœ ν•„ν„° */} - - - {/* μ •λ ¬ */} - -
- ); - return ( <> [ + { + key: 'partner', + label: '거래처', + type: 'multi', + options: MOCK_PARTNERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: SITE_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: SITE_SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ', + }, + ], []); + + const filterValues: FilterValues = useMemo(() => ({ + partner: partnerFilters, + status: statusFilter, + sortBy: sortBy, + }), [partnerFilters, statusFilter, sortBy]); + + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'partner': + setPartnerFilters(value as string[]); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); + + const handleFilterReset = useCallback(() => { + setPartnerFilters([]); + setStatusFilter('all'); + setSortBy('latest'); + setCurrentPage(1); + }, []); + // ν…Œμ΄λΈ” 헀더 μ•‘μ…˜ (총 건수 + ν•„ν„°λ“€) const tableHeaderActions = (
@@ -445,6 +505,11 @@ export default function SiteManagementListClient({ headerActions={headerActions} stats={statsCardsData} tableHeaderActions={tableHeaderActions} + filterConfig={filterConfig} + filterValues={filterValues} + onFilterChange={handleFilterChange} + onFilterReset={handleFilterReset} + filterTitle="ν˜„μž₯ ν•„ν„°" searchValue={searchValue} onSearchChange={handleSearchChange} searchPlaceholder="ν˜„μž₯번호, 거래처, ν˜„μž₯λͺ…, μœ„μΉ˜ 검색" diff --git a/src/components/business/construction/structure-review/StructureReviewListClient.tsx b/src/components/business/construction/structure-review/StructureReviewListClient.tsx index 811c8a28..7be7b1a9 100644 --- a/src/components/business/construction/structure-review/StructureReviewListClient.tsx +++ b/src/components/business/construction/structure-review/StructureReviewListClient.tsx @@ -14,7 +14,7 @@ import { SelectValue, } from '@/components/ui/select'; import { MultiSelectCombobox, MultiSelectOption } from '@/components/ui/multi-select-combobox'; -import { IntegratedListTemplateV2, TableColumn, StatCard } from '@/components/templates/IntegratedListTemplateV2'; +import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2'; import { MobileCard } from '@/components/molecules/MobileCard'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { toast } from 'sonner'; @@ -422,6 +422,66 @@ export default function StructureReviewListClient({ }, ]; + // ===== filterConfig 기반 톡합 ν•„ν„° μ‹œμŠ€ν…œ ===== + const filterConfig: FilterFieldConfig[] = useMemo(() => [ + { + key: 'partner', + label: '거래처', + type: 'multi', + options: MOCK_PARTNERS.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: STRUCTURE_REVIEW_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: STRUCTURE_REVIEW_SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ', + }, + ], []); + + const filterValues: FilterValues = useMemo(() => ({ + partner: partnerFilters, + status: statusFilter, + sortBy: sortBy, + }), [partnerFilters, statusFilter, sortBy]); + + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'partner': + setPartnerFilters(value as string[]); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); + + const handleFilterReset = useCallback(() => { + setPartnerFilters([]); + setStatusFilter('all'); + setSortBy('latest'); + setCurrentPage(1); + }, []); + // ν…Œμ΄λΈ” 헀더 μ•‘μ…˜ const tableHeaderActions = (
@@ -478,6 +538,11 @@ export default function StructureReviewListClient({ headerActions={headerActions} stats={statsCardsData} tableHeaderActions={tableHeaderActions} + filterConfig={filterConfig} + filterValues={filterValues} + onFilterChange={handleFilterChange} + onFilterReset={handleFilterReset} + filterTitle="ꡬ쑰검토 ν•„ν„°" searchValue={searchValue} onSearchChange={handleSearchChange} searchPlaceholder="κ²€ν† λ²ˆν˜Έ, 거래처, ν˜„μž₯λͺ…, κ²€ν† νšŒμ‚¬ 검색" diff --git a/src/components/business/construction/worker-status/WorkerStatusListClient.tsx b/src/components/business/construction/worker-status/WorkerStatusListClient.tsx index 1f0cb7ab..d78b1f2f 100644 --- a/src/components/business/construction/worker-status/WorkerStatusListClient.tsx +++ b/src/components/business/construction/worker-status/WorkerStatusListClient.tsx @@ -6,15 +6,8 @@ import { Users, Eye, FileText, Clock, CheckCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { MultiSelectCombobox, MultiSelectOption } from '@/components/ui/multi-select-combobox'; -import { IntegratedListTemplateV2, TableColumn, StatCard } from '@/components/templates/IntegratedListTemplateV2'; +import { MultiSelectOption } from '@/components/ui/multi-select-combobox'; +import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2'; import { MobileCard } from '@/components/molecules/MobileCard'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { toast } from 'sonner'; @@ -396,92 +389,123 @@ export default function WorkerStatusListClient({ }, ]; - // ν…Œμ΄λΈ” 헀더 μ•‘μ…˜ (7개 ν•„ν„°) - const tableHeaderActions = ( -
- {/* 1. 거래처 ν•„ν„° (닀쀑선택) */} - + // ===== filterConfig 기반 톡합 ν•„ν„° μ‹œμŠ€ν…œ ===== + const filterConfig: FilterFieldConfig[] = useMemo(() => [ + { + key: 'partner', + label: '거래처', + type: 'multi', + options: partnerOptions.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'site', + label: 'ν˜„μž₯λͺ…', + type: 'multi', + options: siteOptions.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'category', + label: 'ꡬ뢄', + type: 'single', + options: WORKER_CATEGORY_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'department', + label: 'λΆ€μ„œ', + type: 'multi', + options: departmentOptions.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'name', + label: '이름', + type: 'multi', + options: nameOptions.map(o => ({ + value: o.value, + label: o.label, + })), + }, + { + key: 'status', + label: 'μƒνƒœ', + type: 'single', + options: WORKER_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: '전체', + }, + { + key: 'sortBy', + label: 'μ •λ ¬', + type: 'single', + options: WORKER_SORT_OPTIONS.map(o => ({ + value: o.value, + label: o.label, + })), + allOptionLabel: 'μ΅œμ‹ μˆœ', + }, + ], [partnerOptions, siteOptions, departmentOptions, nameOptions]); - {/* 2. ν˜„μž₯λͺ… ν•„ν„° (닀쀑선택) */} - + const filterValues: FilterValues = useMemo(() => ({ + partner: partnerFilters, + site: siteFilters, + category: categoryFilter, + department: departmentFilters, + name: nameFilters, + status: statusFilter, + sortBy: sortBy, + }), [partnerFilters, siteFilters, categoryFilter, departmentFilters, nameFilters, statusFilter, sortBy]); - {/* 3. ꡬ뢄 ν•„ν„° (단일선택) */} - + const handleFilterChange = useCallback((key: string, value: string | string[]) => { + switch (key) { + case 'partner': + setPartnerFilters(value as string[]); + break; + case 'site': + setSiteFilters(value as string[]); + break; + case 'category': + setCategoryFilter(value as string); + break; + case 'department': + setDepartmentFilters(value as string[]); + break; + case 'name': + setNameFilters(value as string[]); + break; + case 'status': + setStatusFilter(value as string); + break; + case 'sortBy': + setSortBy(value as string); + break; + } + setCurrentPage(1); + }, []); - {/* 4. λΆ€μ„œ ν•„ν„° (닀쀑선택) */} - - - {/* 5. 이름 ν•„ν„° (닀쀑선택) */} - - - {/* 6. μƒνƒœ ν•„ν„° (단일선택) */} - - - {/* 7. μ •λ ¬ (단일선택) */} - -
- ); + const handleFilterReset = useCallback(() => { + setPartnerFilters([]); + setSiteFilters([]); + setCategoryFilter('all'); + setDepartmentFilters([]); + setNameFilters([]); + setStatusFilter('all'); + setSortBy('latest'); + setCurrentPage(1); + }, []); return ( { + const numbers = value.replace(/[^0-9]/g, ''); + if (numbers.length <= 3) return numbers; + if (numbers.length <= 7) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`; + return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7, 11)}`; + }; + + // μ£Όλ―Όλ“±λ‘λ²ˆν˜Έ μžλ™ ν•˜μ΄ν”ˆ ν¬λ§·νŒ… + const formatResidentNumber = (value: string): string => { + const numbers = value.replace(/[^0-9]/g, ''); + if (numbers.length <= 6) return numbers; + return `${numbers.slice(0, 6)}-${numbers.slice(6, 13)}`; + }; + // μž…λ ₯ λ³€κ²½ ν•Έλ“€λŸ¬ const handleChange = (field: keyof EmployeeFormData, value: unknown) => { - setFormData(prev => ({ ...prev, [field]: value })); + let formattedValue = value; + + // μžλ™ ν•˜μ΄ν”ˆ 적용 + if (field === 'phone' && typeof value === 'string') { + formattedValue = formatPhoneNumber(value); + } else if (field === 'residentNumber' && typeof value === 'string') { + formattedValue = formatResidentNumber(value); + } + + setFormData(prev => ({ ...prev, [field]: formattedValue })); // μ—λŸ¬ μ΄ˆκΈ°ν™” if (errors[field as keyof ValidationErrors]) { setErrors(prev => ({ ...prev, [field]: undefined })); @@ -233,8 +257,8 @@ export function EmployeeForm({ if (mode === 'create') { if (!formData.password) { newErrors.password = 'λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.'; - } else if (formData.password.length < 6) { - newErrors.password = 'λΉ„λ°€λ²ˆν˜ΈλŠ” 6자 이상이어야 ν•©λ‹ˆλ‹€.'; + } else if (formData.password.length < 8) { + newErrors.password = 'λΉ„λ°€λ²ˆν˜ΈλŠ” 8자 이상이어야 ν•©λ‹ˆλ‹€.'; } if (formData.password !== formData.confirmPassword) { @@ -331,8 +355,8 @@ export function EmployeeForm({
{/* 사원 정보 - ν”„λ‘œν•„ 사진 + κΈ°λ³Έ 정보 */} - - 사원 정보 + + 사원 정보 {/* κΈ°λ³Έ 정보 ν•„λ“œλ“€ */} @@ -429,8 +453,8 @@ export function EmployeeForm({ {/* 사원 상세 */} {(fieldSettings.showProfileImage || fieldSettings.showEmployeeCode || fieldSettings.showGender || fieldSettings.showAddress) && ( - - 사원 상세 + + 사원 상세 {/* ν”„λ‘œν•„ 사진 + μ‚¬μ›μ½”λ“œ/성별 */} @@ -559,8 +583,8 @@ export function EmployeeForm({ {/* 인사 정보 */} {(fieldSettings.showHireDate || fieldSettings.showEmploymentType || fieldSettings.showRank || fieldSettings.showStatus || fieldSettings.showDepartment || fieldSettings.showPosition || fieldSettings.showClockInLocation || fieldSettings.showClockOutLocation || fieldSettings.showResignationDate || fieldSettings.showResignationReason) && ( - - 인사 정보 + + 인사 정보
@@ -770,8 +794,8 @@ export function EmployeeForm({ {/* μ‚¬μš©μž 정보 */} - - μ‚¬μš©μž 정보 + + μ‚¬μš©μž 정보