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:
@@ -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) |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="입찰번호, 거래처, 현장명 검색"
|
||||
|
||||
@@ -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="계약번호, 거래처, 현장명 검색"
|
||||
|
||||
@@ -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="견적번호, 거래처, 현장명 검색"
|
||||
|
||||
@@ -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="보고서번호, 거래처, 현장명 검색"
|
||||
|
||||
@@ -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="이슈번호, 시공번호, 거래처, 현장, 제목, 보고자, 담당자 검색"
|
||||
|
||||
@@ -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="품목명, 품목번호, 카테고리 검색"
|
||||
|
||||
@@ -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="노임번호, 구분 검색"
|
||||
|
||||
@@ -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="단가번호, 품목명, 카테고리, 거래처 검색"
|
||||
|
||||
@@ -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="현장번호, 거래처, 현장명 검색"
|
||||
|
||||
@@ -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="현장번호, 거래처, 현장명, 위치 검색"
|
||||
|
||||
@@ -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="검토번호, 거래처, 현장명, 검토회사 검색"
|
||||
|
||||
@@ -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="거래처, 현장, 부서, 이름, 시공번호 검색"
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user