From ea8d701a8dd8de1de46465c7353d259d828ee4f6 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Tue, 13 Jan 2026 18:33:39 +0900 Subject: [PATCH] =?UTF-8?q?refactor(WEB):=20=EA=B3=B5=EC=82=AC=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BiddingListClient: MobileFilter 컴포넌트 적용 - ContractListClient: MobileFilter 컴포넌트 적용 - EstimateListClient: MobileFilter 컴포넌트 적용 - HandoverReportListClient: MobileFilter 컴포넌트 적용 - IssueManagementListClient: MobileFilter 컴포넌트 적용 - ItemManagementClient: MobileFilter 컴포넌트 적용 - LaborManagementClient: MobileFilter 컴포넌트 적용 - PricingListClient: MobileFilter 컴포넌트 적용 - SiteBriefingListClient: MobileFilter 컴포넌트 적용 - SiteManagementListClient: MobileFilter 컴포넌트 적용 - StructureReviewListClient: MobileFilter 컴포넌트 적용 - WorkerStatusListClient: MobileFilter 컴포넌트 적용 - TodayIssueSection: CEO 대시보드 이슈 섹션 개선 - EmployeeForm: 사원등록 폼 개선 Co-Authored-By: Claude Opus 4.5 --- ...1-13] mobile-filter-migration-checklist.md | 33 +-- .../hr/employee-management/new/page.tsx | 10 +- .../sections/TodayIssueSection.tsx | 31 +- .../bidding/BiddingListClient.tsx | 149 +++++----- .../contract/ContractListClient.tsx | 173 +++++++----- .../estimates/EstimateListClient.tsx | 149 +++++----- .../HandoverReportListClient.tsx | 173 +++++++----- .../IssueManagementListClient.tsx | 264 ++++++++++-------- .../item-management/ItemManagementClient.tsx | 239 ++++++++-------- .../LaborManagementClient.tsx | 67 ++++- .../pricing-management/PricingListClient.tsx | 207 +++++++------- .../site-briefings/SiteBriefingListClient.tsx | 178 ++++++------ .../SiteManagementListClient.tsx | 67 ++++- .../StructureReviewListClient.tsx | 67 ++++- .../worker-status/WorkerStatusListClient.tsx | 214 ++++++++------ .../hr/EmployeeManagement/EmployeeForm.tsx | 46 ++- 16 files changed, 1224 insertions(+), 843 deletions(-) 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({ {/* 사용자 정보 */} - - 사용자 정보 + + 사용자 정보