refactor(WEB): 공사관리 리스트 페이지 모바일 필터 마이그레이션

- 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 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-13 18:33:39 +09:00
parent db47a15544
commit ea8d701a8d
16 changed files with 1224 additions and 843 deletions

View File

@@ -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) |

View File

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

View File

@@ -44,11 +44,15 @@ interface TodayIssueSectionProps {
export function TodayIssueSection({ items }: TodayIssueSectionProps) {
const router = useRouter();
const [filter, setFilter] = useState<string>('all');
const [dismissedIds, setDismissedIds] = useState<Set<string>>(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) {
</div>
{/* 우측: 시간 + 버튼 */}
<div className="flex items-center gap-3 shrink-0 ml-4">
<div className="flex items-center gap-3 shrink-0 ml-4" onClick={(e) => e.stopPropagation()}>
<span className="text-xs text-gray-500 whitespace-nowrap">
{item.time}
</span>
{item.needsApproval && (
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
{item.needsApproval ? (
<div className="flex items-center gap-2">
<Button
size="sm"
variant="default"
@@ -137,6 +149,15 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
</Button>
</div>
) : (
<Button
size="sm"
variant="outline"
className="h-7 px-3 text-xs text-gray-600 hover:text-green-600 hover:border-green-600 hover:bg-green-50"
onClick={() => handleDismiss(item)}
>
</Button>
)}
</div>
</div>

View File

@@ -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 = (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-muted-foreground whitespace-nowrap">
{sortedBiddings.length}
</span>
{/* 거래처 필터 (다중선택) */}
<MultiSelectCombobox
options={MOCK_PARTNERS}
value={partnerFilters}
onChange={setPartnerFilters}
placeholder="거래처"
searchPlaceholder="거래처 검색..."
className="w-[120px]"
/>
{/* 입찰자 필터 (다중선택) */}
<MultiSelectCombobox
options={MOCK_BIDDERS}
value={bidderFilters}
onChange={setBidderFilters}
placeholder="입찰자"
searchPlaceholder="입찰자 검색..."
className="w-[120px]"
/>
{/* 상태 필터 */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{BIDDING_STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 정렬 */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[160px]">
<SelectValue placeholder="최신순 (입찰일)" />
</SelectTrigger>
<SelectContent>
{BIDDING_SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
return (
<>
<IntegratedListTemplateV2
@@ -515,7 +526,11 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
icon={FileText}
headerActions={headerActions}
stats={statsCardsData}
tableHeaderActions={tableHeaderActions}
filterConfig={filterConfig}
filterValues={filterValues}
onFilterChange={handleFilterChange}
onFilterReset={handleFilterReset}
filterTitle="입찰 필터"
searchValue={searchValue}
onSearchChange={handleSearchChange}
searchPlaceholder="입찰번호, 거래처, 현장명 검색"

View File

@@ -6,15 +6,8 @@ import { FileText, Clock, CheckCircle, Pencil, Trash2 } 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';
@@ -350,6 +343,95 @@ export default function ContractListClient({
}
}, [selectedItems, loadData]);
// ===== filterConfig 기반 통합 필터 시스템 =====
const filterConfig: FilterFieldConfig[] = useMemo(() => [
{
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 = (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-muted-foreground whitespace-nowrap">
{sortedContracts.length}
</span>
{/* 거래처 필터 */}
<MultiSelectCombobox
options={MOCK_PARTNERS}
value={partnerFilters}
onChange={setPartnerFilters}
placeholder="거래처"
searchPlaceholder="거래처 검색..."
className="w-[120px]"
/>
{/* 계약담당자 필터 */}
<MultiSelectCombobox
options={MOCK_CONTRACT_MANAGERS}
value={contractManagerFilters}
onChange={setContractManagerFilters}
placeholder="계약담당자"
searchPlaceholder="계약담당자 검색..."
className="w-[130px]"
/>
{/* 공사PM 필터 */}
<MultiSelectCombobox
options={MOCK_CONSTRUCTION_PMS}
value={constructionPMFilters}
onChange={setConstructionPMFilters}
placeholder="공사PM"
searchPlaceholder="공사PM 검색..."
className="w-[120px]"
/>
{/* 상태 필터 */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{CONTRACT_STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 정렬 */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[160px]">
<SelectValue placeholder="최신순 (계약일)" />
</SelectTrigger>
<SelectContent>
{CONTRACT_SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
return (
<>
<IntegratedListTemplateV2
@@ -550,7 +565,11 @@ export default function ContractListClient({
icon={FileText}
headerActions={headerActions}
stats={statsCardsData}
tableHeaderActions={tableHeaderActions}
filterConfig={filterConfig}
filterValues={filterValues}
onFilterChange={handleFilterChange}
onFilterReset={handleFilterReset}
filterTitle="계약 필터"
searchValue={searchValue}
onSearchChange={handleSearchChange}
searchPlaceholder="계약번호, 거래처, 현장명 검색"

View File

@@ -6,15 +6,8 @@ import { FileText, FileTextIcon, Clock, FileCheck, 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';
@@ -315,6 +308,81 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
}
}, [selectedItems, loadData]);
// ===== filterConfig 기반 통합 필터 시스템 =====
const filterConfig: FilterFieldConfig[] = useMemo(() => [
{
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 = (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-muted-foreground whitespace-nowrap">
{sortedEstimates.length}
</span>
{/* 거래처 필터 (다중선택) */}
<MultiSelectCombobox
options={MOCK_PARTNERS}
value={partnerFilters}
onChange={setPartnerFilters}
placeholder="거래처"
searchPlaceholder="거래처 검색..."
className="w-[120px]"
/>
{/* 견적자 필터 (다중선택) */}
<MultiSelectCombobox
options={MOCK_ESTIMATORS}
value={estimatorFilters}
onChange={setEstimatorFilters}
placeholder="견적자"
searchPlaceholder="견적자 검색..."
className="w-[120px]"
/>
{/* 상태 필터 */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[110px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{ESTIMATE_STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 정렬 */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="최신순" />
</SelectTrigger>
<SelectContent>
{ESTIMATE_SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
return (
<>
<IntegratedListTemplateV2
@@ -493,7 +504,11 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
icon={FileText}
headerActions={headerActions}
stats={statsCardsData}
tableHeaderActions={tableHeaderActions}
filterConfig={filterConfig}
filterValues={filterValues}
onFilterChange={handleFilterChange}
onFilterReset={handleFilterReset}
filterTitle="견적 필터"
searchValue={searchValue}
onSearchChange={handleSearchChange}
searchPlaceholder="견적번호, 거래처, 현장명 검색"

View File

@@ -6,15 +6,8 @@ import { FileText, Clock, CheckCircle, 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';
@@ -272,6 +265,95 @@ export default function HandoverReportListClient({
[router]
);
// ===== filterConfig 기반 통합 필터 시스템 =====
const filterConfig: FilterFieldConfig[] = useMemo(() => [
{
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 = (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-muted-foreground whitespace-nowrap">
{sortedReports.length}
</span>
{/* 거래처 필터 */}
<MultiSelectCombobox
options={MOCK_PARTNERS}
value={partnerFilters}
onChange={setPartnerFilters}
placeholder="거래처"
searchPlaceholder="거래처 검색..."
className="w-[130px]"
/>
{/* 계약담당자 필터 */}
<MultiSelectCombobox
options={MOCK_CONTRACT_MANAGERS}
value={contractManagerFilters}
onChange={setContractManagerFilters}
placeholder="계약담당자"
searchPlaceholder="계약담당자 검색..."
className="w-[130px]"
/>
{/* 공사PM 필터 */}
<MultiSelectCombobox
options={MOCK_CONSTRUCTION_PMS}
value={constructionPMFilters}
onChange={setConstructionPMFilters}
placeholder="공사PM"
searchPlaceholder="공사PM 검색..."
className="w-[120px]"
/>
{/* 상태 필터 */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{REPORT_STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 정렬 */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="최신순 (계약시작일)" />
</SelectTrigger>
<SelectContent>
{REPORT_SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
return (
<>
<IntegratedListTemplateV2
@@ -464,7 +479,11 @@ export default function HandoverReportListClient({
icon={FileText}
headerActions={headerActions}
stats={statsCardsData}
tableHeaderActions={tableHeaderActions}
filterConfig={filterConfig}
filterValues={filterValues}
onFilterChange={handleFilterChange}
onFilterReset={handleFilterReset}
filterTitle="인수인계보고서 필터"
searchValue={searchValue}
onSearchChange={handleSearchChange}
searchPlaceholder="보고서번호, 거래처, 현장명 검색"

View File

@@ -6,15 +6,8 @@ import { AlertTriangle, Pencil, Plus, Inbox, Clock, CheckCircle, XCircle, Undo2
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 {
@@ -479,116 +472,150 @@ export default function IssueManagementListClient({
},
];
// 테이블 헤더 액션
// 철회 버튼 (선택 시), 거래처(다중), 현장명(다중), 구분(다중), 보고자(다중), 담당자(다중), 중요도(일반), 상태(일반), 정렬
const tableHeaderActions = (
<div className="flex items-center gap-2 flex-wrap justify-end w-full">
{/* 철회 버튼 (선택된 항목이 있을 때만 표시) */}
{selectedItems.size > 0 && (
<Button
variant="outline"
size="sm"
onClick={handleWithdrawClick}
className="text-orange-600 border-orange-300 hover:bg-orange-50"
>
<Undo2 className="mr-2 h-4 w-4" />
({selectedItems.size})
</Button>
)}
// ===== 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. 거래처 필터 (다중선택) */}
<MultiSelectCombobox
options={partnerOptions}
value={partnerFilters}
onChange={setPartnerFilters}
placeholder="거래처"
searchPlaceholder="거래처 검색..."
className="w-[120px]"
/>
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. 현장명 필터 (다중선택) */}
<MultiSelectCombobox
options={siteOptions}
value={siteFilters}
onChange={setSiteFilters}
placeholder="현장명"
searchPlaceholder="현장명 검색..."
className="w-[120px]"
/>
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. 구분 필터 (다중선택) */}
<MultiSelectCombobox
options={categoryOptions}
value={categoryFilters}
onChange={setCategoryFilters}
placeholder="구분"
searchPlaceholder="구분 검색..."
className="w-[100px]"
/>
const handleFilterReset = useCallback(() => {
setPartnerFilters([]);
setSiteFilters([]);
setCategoryFilters([]);
setReporterFilters([]);
setAssigneeFilters([]);
setPriorityFilter('all');
setStatusFilter('all');
setSortBy('latest');
setCurrentPage(1);
}, []);
{/* 4. 보고자 필터 (다중선택) */}
<MultiSelectCombobox
options={reporterOptions}
value={reporterFilters}
onChange={setReporterFilters}
placeholder="보고자"
searchPlaceholder="보고자 검색..."
className="w-[100px]"
/>
{/* 5. 담당자 필터 (다중선택) */}
<MultiSelectCombobox
options={assigneeOptions}
value={assigneeFilters}
onChange={setAssigneeFilters}
placeholder="담당자"
searchPlaceholder="담당자 검색..."
className="w-[100px]"
/>
{/* 6. 중요도 필터 (단일선택) */}
<Select value={priorityFilter} onValueChange={setPriorityFilter}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="중요도" />
</SelectTrigger>
<SelectContent>
{ISSUE_PRIORITY_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 7. 상태 필터 (단일선택) */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{ISSUE_STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 8. 정렬 (단일선택) */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="정렬" />
</SelectTrigger>
<SelectContent>
{ISSUE_SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
// 철회 버튼 (bulkActions용)
const bulkActions = selectedItems.size > 0 ? (
<Button
variant="outline"
size="sm"
onClick={handleWithdrawClick}
className="text-orange-600 border-orange-300 hover:bg-orange-50"
>
<Undo2 className="mr-2 h-4 w-4" />
({selectedItems.size})
</Button>
) : 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="이슈번호, 시공번호, 거래처, 현장, 제목, 보고자, 담당자 검색"

View File

@@ -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 = (
<div className="flex items-center gap-2 flex-wrap">
{/* 총 건수 */}
<span className="text-sm text-muted-foreground whitespace-nowrap">
{sortedItems.length}
</span>
// ===== 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]);
{/* 품목유형 필터 */}
<Select
value={itemTypeFilter}
onValueChange={(v) => {
setItemTypeFilter(v as ItemType | 'all');
setCurrentPage(1);
}}
>
<SelectTrigger className="w-[90px]">
<SelectValue placeholder="품목유형" />
</SelectTrigger>
<SelectContent>
{ITEM_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
const filterValues: FilterValues = useMemo(() => ({
itemType: itemTypeFilter,
category: categoryFilter,
specification: specificationFilter,
orderType: orderTypeFilter,
status: statusFilter,
sortBy: sortBy,
}), [itemTypeFilter, categoryFilter, specificationFilter, orderTypeFilter, statusFilter, sortBy]);
{/* 카테고리 필터 */}
<Select
value={categoryFilter}
onValueChange={(v) => {
setCategoryFilter(v);
setCurrentPage(1);
}}
>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="카테고리" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{categoryOptions.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
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);
}, []);
{/* 규격 필터 */}
<Select
value={specificationFilter}
onValueChange={(v) => {
setSpecificationFilter(v as Specification | 'all');
setCurrentPage(1);
}}
>
<SelectTrigger className="w-[80px]">
<SelectValue placeholder="규격" />
</SelectTrigger>
<SelectContent>
{SPECIFICATION_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 구분 필터 */}
<Select
value={orderTypeFilter}
onValueChange={(v) => {
setOrderTypeFilter(v as OrderType | 'all');
setCurrentPage(1);
}}
>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="구분" />
</SelectTrigger>
<SelectContent>
{ORDER_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 상태 필터 */}
<Select
value={statusFilter}
onValueChange={(v) => {
setStatusFilter(v as ItemStatus | 'all');
setCurrentPage(1);
}}
>
<SelectTrigger className="w-[80px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 정렬 */}
<Select value={sortBy} onValueChange={(v) => setSortBy(v as 'latest' | 'oldest')}>
<SelectTrigger className="w-[90px]">
<SelectValue placeholder="정렬" />
</SelectTrigger>
<SelectContent>
{SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
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="품목명, 품목번호, 카테고리 검색"

View File

@@ -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 = (
<DateRangeSelector
@@ -471,6 +531,11 @@ export default function LaborManagementClient({
},
]}
tableHeaderActions={tableHeaderActions}
filterConfig={filterConfig}
filterValues={filterValues}
onFilterChange={handleFilterChange}
onFilterReset={handleFilterReset}
filterTitle="노임 필터"
searchValue={searchValue}
onSearchChange={handleSearchChange}
searchPlaceholder="노임번호, 구분 검색"

View File

@@ -7,14 +7,7 @@ import { Button } from '@/components/ui/button';
import { TableCell, TableRow, TableHead } 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, StatCard } from '@/components/templates/IntegratedListTemplateV2';
import { IntegratedListTemplateV2, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
import { MobileCard } from '@/components/molecules/MobileCard';
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
import { toast } from 'sonner';
@@ -469,98 +462,112 @@ export default function PricingListClient({
},
];
// 테이블 헤더 액션 (필터 6개)
const tableHeaderActions = (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-muted-foreground whitespace-nowrap">
{sortedPricing.length}
</span>
// ===== 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: '최신순',
},
], []);
{/* 품목유형 필터 */}
<Select value={itemTypeFilter} onValueChange={setItemTypeFilter}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="품목유형" />
</SelectTrigger>
<SelectContent>
{ITEM_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
const filterValues: FilterValues = useMemo(() => ({
itemType: itemTypeFilter,
category: categoryFilter,
spec: specFilter,
division: divisionFilter,
status: statusFilter,
sortBy: sortBy,
}), [itemTypeFilter, categoryFilter, specFilter, divisionFilter, statusFilter, sortBy]);
{/* 카테고리 필터 */}
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="카테고리" />
</SelectTrigger>
<SelectContent>
{CATEGORY_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
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);
}, []);
{/* 규격 필터 */}
<Select value={specFilter} onValueChange={setSpecFilter}>
<SelectTrigger className="w-[90px]">
<SelectValue placeholder="규격" />
</SelectTrigger>
<SelectContent>
{SPEC_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 구분 필터 */}
<Select value={divisionFilter} onValueChange={setDivisionFilter}>
<SelectTrigger className="w-[110px]">
<SelectValue placeholder="구분" />
</SelectTrigger>
<SelectContent>
{DIVISION_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 상태 필터 */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[90px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 정렬 */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[110px]">
<SelectValue placeholder="정렬" />
</SelectTrigger>
<SelectContent>
{SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
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="단가번호, 품목명, 카테고리, 거래처 검색"

View File

@@ -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 = (
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-muted-foreground whitespace-nowrap">
{sortedBriefings.length}
</span>
{/* 거래처 필터 (다중선택) */}
<MultiSelectCombobox
options={MOCK_PARTNERS}
value={partnerFilters}
onChange={setPartnerFilters}
placeholder="거래처"
searchPlaceholder="거래처 검색..."
className="w-[120px]"
/>
{/* 구분 필터 */}
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="구분" />
</SelectTrigger>
<SelectContent>
{TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 참석자 필터 (다중선택) */}
<MultiSelectCombobox
options={MOCK_ATTENDEES}
value={attendeeFilters}
onChange={setAttendeeFilters}
placeholder="참석자"
searchPlaceholder="참석자 검색..."
className="w-[120px]"
/>
{/* 상태 필터 */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 정렬 */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[130px]">
<SelectValue placeholder="최신순" />
</SelectTrigger>
<SelectContent>
{SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
return (
<>
<IntegratedListTemplateV2
@@ -554,7 +566,11 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
icon={Calendar}
headerActions={headerActions}
stats={statsCardsData}
tableHeaderActions={tableHeaderActions}
filterConfig={filterConfig}
filterValues={filterValues}
onFilterChange={handleFilterChange}
onFilterReset={handleFilterReset}
filterTitle="현장설명회 필터"
searchValue={searchValue}
onSearchChange={handleSearchChange}
searchPlaceholder="현장번호, 거래처, 현장명 검색"

View File

@@ -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';
@@ -389,6 +389,66 @@ export default function SiteManagementListClient({
},
];
// ===== 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: 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 = (
<div className="flex items-center gap-2 flex-wrap">
@@ -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="현장번호, 거래처, 현장명, 위치 검색"

View File

@@ -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 = (
<div className="flex items-center gap-2 flex-wrap">
@@ -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="검토번호, 거래처, 현장명, 검토회사 검색"

View File

@@ -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 = (
<div className="flex items-center gap-2 flex-wrap justify-end w-full">
{/* 1. 거래처 필터 (다중선택) */}
<MultiSelectCombobox
options={partnerOptions}
value={partnerFilters}
onChange={setPartnerFilters}
placeholder="거래처"
searchPlaceholder="거래처 검색..."
className="w-[120px]"
/>
// ===== 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. 현장명 필터 (다중선택) */}
<MultiSelectCombobox
options={siteOptions}
value={siteFilters}
onChange={setSiteFilters}
placeholder="현장명"
searchPlaceholder="현장명 검색..."
className="w-[120px]"
/>
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. 구분 필터 (단일선택) */}
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="구분" />
</SelectTrigger>
<SelectContent>
{WORKER_CATEGORY_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
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. 부서 필터 (다중선택) */}
<MultiSelectCombobox
options={departmentOptions}
value={departmentFilters}
onChange={setDepartmentFilters}
placeholder="부서"
searchPlaceholder="부서 검색..."
className="w-[100px]"
/>
{/* 5. 이름 필터 (다중선택) */}
<MultiSelectCombobox
options={nameOptions}
value={nameFilters}
onChange={setNameFilters}
placeholder="이름"
searchPlaceholder="이름 검색..."
className="w-[100px]"
/>
{/* 6. 상태 필터 (단일선택) */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="상태" />
</SelectTrigger>
<SelectContent>
{WORKER_STATUS_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{/* 7. 정렬 (단일선택) */}
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="정렬" />
</SelectTrigger>
<SelectContent>
{WORKER_SORT_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
const handleFilterReset = useCallback(() => {
setPartnerFilters([]);
setSiteFilters([]);
setCategoryFilter('all');
setDepartmentFilters([]);
setNameFilters([]);
setStatusFilter('all');
setSortBy('latest');
setCurrentPage(1);
}, []);
return (
<IntegratedListTemplateV2
@@ -490,7 +514,11 @@ export default function WorkerStatusListClient({
icon={Users}
headerActions={headerActions}
stats={statsCardsData}
tableHeaderActions={tableHeaderActions}
filterConfig={filterConfig}
filterValues={filterValues}
onFilterChange={handleFilterChange}
onFilterReset={handleFilterReset}
filterTitle="작업인력 필터"
searchValue={searchValue}
onSearchChange={handleSearchChange}
searchPlaceholder="거래처, 현장, 부서, 이름, 시공번호 검색"

View File

@@ -193,9 +193,33 @@ export function EmployeeForm({
}
}, [employee, mode]);
// 휴대폰 번호 자동 하이픈 포맷팅
const formatPhoneNumber = (value: string): string => {
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({
<form onSubmit={handleSubmit} className="space-y-6">
{/* 사원 정보 - 프로필 사진 + 기본 정보 */}
<Card>
<CardHeader className="bg-black text-white rounded-t-lg">
<CardTitle className="text-base font-medium"> </CardTitle>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="pt-6">
{/* 기본 정보 필드들 */}
@@ -429,8 +453,8 @@ export function EmployeeForm({
{/* 사원 상세 */}
{(fieldSettings.showProfileImage || fieldSettings.showEmployeeCode || fieldSettings.showGender || fieldSettings.showAddress) && (
<Card>
<CardHeader className="bg-black text-white rounded-t-lg">
<CardTitle className="text-base font-medium"> </CardTitle>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="pt-6 space-y-4">
{/* 프로필 사진 + 사원코드/성별 */}
@@ -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) && (
<Card>
<CardHeader className="bg-black text-white rounded-t-lg">
<CardTitle className="text-base font-medium"> </CardTitle>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="pt-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -770,8 +794,8 @@ export function EmployeeForm({
{/* 사용자 정보 */}
<Card>
<CardHeader className="bg-black text-white rounded-t-lg">
<CardTitle className="text-base font-medium"> </CardTitle>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">