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`)
|
- [x] 현장설명회관리 (`SiteBriefingListClient.tsx`)
|
||||||
- [ ] 견적관리 (`EstimateListClient.tsx`)
|
- [x] 견적관리 (`EstimateListClient.tsx`)
|
||||||
- [ ] 입찰관리 (`BiddingListClient.tsx`)
|
- [x] 입찰관리 (`BiddingListClient.tsx`)
|
||||||
|
|
||||||
### 계약관리
|
### 계약관리
|
||||||
- [ ] 계약관리 (`ContractListClient.tsx`)
|
- [x] 계약관리 (`ContractListClient.tsx`)
|
||||||
- [ ] 인수인계보고서 (`HandoverReportListClient.tsx`)
|
- [x] 인수인계보고서 (`HandoverReportListClient.tsx`)
|
||||||
|
|
||||||
### 발주관리
|
### 발주관리
|
||||||
- [ ] 현장관리 (`SiteManagementListClient.tsx`)
|
- [x] 현장관리 (`SiteManagementListClient.tsx`)
|
||||||
- [ ] 구조검토관리 (`StructureReviewListClient.tsx`)
|
- [x] 구조검토관리 (`StructureReviewListClient.tsx`)
|
||||||
|
|
||||||
### 공사관리
|
### 공사관리
|
||||||
- [ ] 이슈관리 (`IssueManagementListClient.tsx`)
|
- [x] 이슈관리 (`IssueManagementListClient.tsx`)
|
||||||
- [ ] 작업인력현황 (`WorkerStatusListClient.tsx`)
|
- [x] 작업인력현황 (`WorkerStatusListClient.tsx`)
|
||||||
|
|
||||||
### 기준정보
|
### 기준정보
|
||||||
- [ ] 품목관리 (`ItemManagementClient.tsx`)
|
- [x] 품목관리 (`ItemManagementClient.tsx`)
|
||||||
- [ ] 단가관리 (`PricingListClient.tsx`)
|
- [x] 단가관리 (`PricingListClient.tsx`)
|
||||||
- [ ] 노임관리 (`LaborManagementClient.tsx`)
|
- [x] 노임관리 (`LaborManagementClient.tsx`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -114,15 +114,15 @@
|
|||||||
|
|
||||||
| 도메인 | 완료 | 전체 | 진행률 |
|
| 도메인 | 완료 | 전체 | 진행률 |
|
||||||
|--------|------|------|--------|
|
|--------|------|------|--------|
|
||||||
| 건설 (완료) | 6 | 6 | 100% |
|
| 건설 (기완료) | 6 | 6 | 100% |
|
||||||
| 건설 (미완료) | 0 | 12 | 0% |
|
| 건설 (마이그레이션) | 12 | 12 | 100% ✅ |
|
||||||
| HR | 0 | 5 | 0% |
|
| HR | 0 | 5 | 0% |
|
||||||
| 회계 | 0 | 11 | 0% |
|
| 회계 | 0 | 11 | 0% |
|
||||||
| 생산/자재/품질/출고 | 0 | 6 | 0% |
|
| 생산/자재/품질/출고 | 0 | 6 | 0% |
|
||||||
| 전자결재 | 0 | 3 | 0% |
|
| 전자결재 | 0 | 3 | 0% |
|
||||||
| 설정 | 0 | 4 | 0% |
|
| 설정 | 0 | 4 | 0% |
|
||||||
| 기타 | 0 | 9 | 0% |
|
| 기타 | 0 | 9 | 0% |
|
||||||
| **총계** | **6** | **56** | **11%** |
|
| **총계** | **18** | **56** | **32%** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -178,3 +178,4 @@ const handleFilterReset = useCallback(() => {
|
|||||||
|------|----------|
|
|------|----------|
|
||||||
| 2026-01-13 | 체크리스트 문서 생성, MobileFilter 스크롤 버그 수정 |
|
| 2026-01-13 | 체크리스트 문서 생성, MobileFilter 스크롤 버그 수정 |
|
||||||
| 2026-01-13 | 시공관리 mobileFilterSlot → filterConfig 방식으로 변경, 협력업체관리 filterConfig 적용 |
|
| 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';
|
'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 { EmployeeForm } from '@/components/hr/EmployeeManagement/EmployeeForm';
|
||||||
import { createEmployee } from '@/components/hr/EmployeeManagement/actions';
|
import { createEmployee } from '@/components/hr/EmployeeManagement/actions';
|
||||||
import type { EmployeeFormData } from '@/components/hr/EmployeeManagement/types';
|
import type { EmployeeFormData } from '@/components/hr/EmployeeManagement/types';
|
||||||
|
|
||||||
export default function EmployeeNewPage() {
|
export default function EmployeeNewPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const locale = params.locale as string || 'ko';
|
||||||
|
|
||||||
const handleSave = async (data: EmployeeFormData) => {
|
const handleSave = async (data: EmployeeFormData) => {
|
||||||
try {
|
try {
|
||||||
const result = await createEmployee(data);
|
const result = await createEmployee(data);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
router.push('/ko/hr/employee-management');
|
toast.success('사원이 등록되었습니다.');
|
||||||
|
router.push(`/${locale}/hr/employee-management`);
|
||||||
} else {
|
} else {
|
||||||
|
toast.error(result.error || '사원 등록에 실패했습니다.');
|
||||||
console.error('[EmployeeNewPage] Create failed:', result.error);
|
console.error('[EmployeeNewPage] Create failed:', result.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
toast.error('서버 오류가 발생했습니다.');
|
||||||
console.error('[EmployeeNewPage] Create error:', error);
|
console.error('[EmployeeNewPage] Create error:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,11 +44,15 @@ interface TodayIssueSectionProps {
|
|||||||
export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [filter, setFilter] = useState<string>('all');
|
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'
|
const filteredItems = filter === 'all'
|
||||||
? items
|
? activeItems
|
||||||
: items.filter((item) => item.badge === filter);
|
: activeItems.filter((item) => item.badge === filter);
|
||||||
|
|
||||||
// 아이템 클릭
|
// 아이템 클릭
|
||||||
const handleItemClick = (item: TodayIssueListItem) => {
|
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) => {
|
const handleApprove = (item: TodayIssueListItem) => {
|
||||||
|
setDismissedIds((prev) => new Set(prev).add(item.id));
|
||||||
toast.success(`"${item.content}" 승인 처리되었습니다.`);
|
toast.success(`"${item.content}" 승인 처리되었습니다.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 반려 버튼 클릭
|
// 반려 버튼 클릭
|
||||||
const handleReject = (item: TodayIssueListItem) => {
|
const handleReject = (item: TodayIssueListItem) => {
|
||||||
|
setDismissedIds((prev) => new Set(prev).add(item.id));
|
||||||
toast.error(`"${item.content}" 반려 처리되었습니다.`);
|
toast.error(`"${item.content}" 반려 처리되었습니다.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,12 +126,12 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
|||||||
</div>
|
</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">
|
<span className="text-xs text-gray-500 whitespace-nowrap">
|
||||||
{item.time}
|
{item.time}
|
||||||
</span>
|
</span>
|
||||||
{item.needsApproval && (
|
{item.needsApproval ? (
|
||||||
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
@@ -137,6 +149,15 @@ export function TodayIssueSection({ items }: TodayIssueSectionProps) {
|
|||||||
반려
|
반려
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,15 +6,8 @@ import { FileText, Clock, Trophy, Pencil } from 'lucide-react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
||||||
Select,
|
import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
|
||||||
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 { MobileCard } from '@/components/molecules/MobileCard';
|
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -335,6 +328,81 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
|
|||||||
}
|
}
|
||||||
}, [selectedItems, loadData]);
|
}, [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(
|
const renderTableRow = useCallback(
|
||||||
(bidding: Bidding, index: number, globalIndex: number) => {
|
(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<IntegratedListTemplateV2
|
<IntegratedListTemplateV2
|
||||||
@@ -515,7 +526,11 @@ export default function BiddingListClient({ initialData = [], initialStats }: Bi
|
|||||||
icon={FileText}
|
icon={FileText}
|
||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="입찰 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="입찰번호, 거래처, 현장명 검색"
|
searchPlaceholder="입찰번호, 거래처, 현장명 검색"
|
||||||
|
|||||||
@@ -6,15 +6,8 @@ import { FileText, Clock, CheckCircle, Pencil, Trash2 } from 'lucide-react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
||||||
Select,
|
import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
|
||||||
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 { MobileCard } from '@/components/molecules/MobileCard';
|
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -350,6 +343,95 @@ export default function ContractListClient({
|
|||||||
}
|
}
|
||||||
}, [selectedItems, loadData]);
|
}, [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, 총 개소, 계약금액, 계약기간, 상태, 작업
|
// 순서: 체크박스, 번호, 계약번호, 거래처, 현장명, 계약담당자, 공사PM, 총 개소, 계약금액, 계약기간, 상태, 작업
|
||||||
const renderTableRow = useCallback(
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<IntegratedListTemplateV2
|
<IntegratedListTemplateV2
|
||||||
@@ -550,7 +565,11 @@ export default function ContractListClient({
|
|||||||
icon={FileText}
|
icon={FileText}
|
||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="계약 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="계약번호, 거래처, 현장명 검색"
|
searchPlaceholder="계약번호, 거래처, 현장명 검색"
|
||||||
|
|||||||
@@ -6,15 +6,8 @@ import { FileText, FileTextIcon, Clock, FileCheck, Pencil } from 'lucide-react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
||||||
Select,
|
import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
|
||||||
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 { MobileCard } from '@/components/molecules/MobileCard';
|
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -315,6 +308,81 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
|||||||
}
|
}
|
||||||
}, [selectedItems, loadData]);
|
}, [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(
|
const renderTableRow = useCallback(
|
||||||
(estimate: Estimate, index: number, globalIndex: number) => {
|
(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<IntegratedListTemplateV2
|
<IntegratedListTemplateV2
|
||||||
@@ -493,7 +504,11 @@ export default function EstimateListClient({ initialData = [], initialStats }: E
|
|||||||
icon={FileText}
|
icon={FileText}
|
||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="견적 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="견적번호, 거래처, 현장명 검색"
|
searchPlaceholder="견적번호, 거래처, 현장명 검색"
|
||||||
|
|||||||
@@ -6,15 +6,8 @@ import { FileText, Clock, CheckCircle, Pencil } from 'lucide-react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
||||||
Select,
|
import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
|
||||||
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 { MobileCard } from '@/components/molecules/MobileCard';
|
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -272,6 +265,95 @@ export default function HandoverReportListClient({
|
|||||||
[router]
|
[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(
|
const renderTableRow = useCallback(
|
||||||
(report: HandoverReport, index: number, globalIndex: number) => {
|
(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<IntegratedListTemplateV2
|
<IntegratedListTemplateV2
|
||||||
@@ -464,7 +479,11 @@ export default function HandoverReportListClient({
|
|||||||
icon={FileText}
|
icon={FileText}
|
||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="인수인계보고서 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="보고서번호, 거래처, 현장명 검색"
|
searchPlaceholder="보고서번호, 거래처, 현장명 검색"
|
||||||
|
|||||||
@@ -6,15 +6,8 @@ import { AlertTriangle, Pencil, Plus, Inbox, Clock, CheckCircle, XCircle, Undo2
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
||||||
Select,
|
import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
|
||||||
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 { MobileCard } from '@/components/molecules/MobileCard';
|
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import {
|
import {
|
||||||
@@ -479,116 +472,150 @@ export default function IssueManagementListClient({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 테이블 헤더 액션
|
// ===== filterConfig 기반 통합 필터 시스템 =====
|
||||||
// 철회 버튼 (선택 시), 거래처(다중), 현장명(다중), 구분(다중), 보고자(다중), 담당자(다중), 중요도(일반), 상태(일반), 정렬
|
const filterConfig: FilterFieldConfig[] = useMemo(() => [
|
||||||
const tableHeaderActions = (
|
{
|
||||||
<div className="flex items-center gap-2 flex-wrap justify-end w-full">
|
key: 'partner',
|
||||||
{/* 철회 버튼 (선택된 항목이 있을 때만 표시) */}
|
label: '거래처',
|
||||||
{selectedItems.size > 0 && (
|
type: 'multi',
|
||||||
<Button
|
options: partnerOptions.map(o => ({
|
||||||
variant="outline"
|
value: o.value,
|
||||||
size="sm"
|
label: o.label,
|
||||||
onClick={handleWithdrawClick}
|
})),
|
||||||
className="text-orange-600 border-orange-300 hover:bg-orange-50"
|
},
|
||||||
>
|
{
|
||||||
<Undo2 className="mr-2 h-4 w-4" />
|
key: 'site',
|
||||||
철회 ({selectedItems.size})
|
label: '현장명',
|
||||||
</Button>
|
type: 'multi',
|
||||||
)}
|
options: siteOptions.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
label: '구분',
|
||||||
|
type: 'multi',
|
||||||
|
options: categoryOptions.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'reporter',
|
||||||
|
label: '보고자',
|
||||||
|
type: 'multi',
|
||||||
|
options: reporterOptions.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'assignee',
|
||||||
|
label: '담당자',
|
||||||
|
type: 'multi',
|
||||||
|
options: assigneeOptions.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'priority',
|
||||||
|
label: '중요도',
|
||||||
|
type: 'single',
|
||||||
|
options: ISSUE_PRIORITY_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: '상태',
|
||||||
|
type: 'single',
|
||||||
|
options: ISSUE_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sortBy',
|
||||||
|
label: '정렬',
|
||||||
|
type: 'single',
|
||||||
|
options: ISSUE_SORT_OPTIONS.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '최신순',
|
||||||
|
},
|
||||||
|
], [partnerOptions, siteOptions, categoryOptions, reporterOptions, assigneeOptions]);
|
||||||
|
|
||||||
{/* 1. 거래처 필터 (다중선택) */}
|
const filterValues: FilterValues = useMemo(() => ({
|
||||||
<MultiSelectCombobox
|
partner: partnerFilters,
|
||||||
options={partnerOptions}
|
site: siteFilters,
|
||||||
value={partnerFilters}
|
category: categoryFilters,
|
||||||
onChange={setPartnerFilters}
|
reporter: reporterFilters,
|
||||||
placeholder="거래처"
|
assignee: assigneeFilters,
|
||||||
searchPlaceholder="거래처 검색..."
|
priority: priorityFilter,
|
||||||
className="w-[120px]"
|
status: statusFilter,
|
||||||
/>
|
sortBy: sortBy,
|
||||||
|
}), [partnerFilters, siteFilters, categoryFilters, reporterFilters, assigneeFilters, priorityFilter, statusFilter, sortBy]);
|
||||||
|
|
||||||
{/* 2. 현장명 필터 (다중선택) */}
|
const handleFilterChange = useCallback((key: string, value: string | string[]) => {
|
||||||
<MultiSelectCombobox
|
switch (key) {
|
||||||
options={siteOptions}
|
case 'partner':
|
||||||
value={siteFilters}
|
setPartnerFilters(value as string[]);
|
||||||
onChange={setSiteFilters}
|
break;
|
||||||
placeholder="현장명"
|
case 'site':
|
||||||
searchPlaceholder="현장명 검색..."
|
setSiteFilters(value as string[]);
|
||||||
className="w-[120px]"
|
break;
|
||||||
/>
|
case 'category':
|
||||||
|
setCategoryFilters(value as string[]);
|
||||||
|
break;
|
||||||
|
case 'reporter':
|
||||||
|
setReporterFilters(value as string[]);
|
||||||
|
break;
|
||||||
|
case 'assignee':
|
||||||
|
setAssigneeFilters(value as string[]);
|
||||||
|
break;
|
||||||
|
case 'priority':
|
||||||
|
setPriorityFilter(value as string);
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
setStatusFilter(value as string);
|
||||||
|
break;
|
||||||
|
case 'sortBy':
|
||||||
|
setSortBy(value as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
{/* 3. 구분 필터 (다중선택) */}
|
const handleFilterReset = useCallback(() => {
|
||||||
<MultiSelectCombobox
|
setPartnerFilters([]);
|
||||||
options={categoryOptions}
|
setSiteFilters([]);
|
||||||
value={categoryFilters}
|
setCategoryFilters([]);
|
||||||
onChange={setCategoryFilters}
|
setReporterFilters([]);
|
||||||
placeholder="구분"
|
setAssigneeFilters([]);
|
||||||
searchPlaceholder="구분 검색..."
|
setPriorityFilter('all');
|
||||||
className="w-[100px]"
|
setStatusFilter('all');
|
||||||
/>
|
setSortBy('latest');
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
{/* 4. 보고자 필터 (다중선택) */}
|
// 철회 버튼 (bulkActions용)
|
||||||
<MultiSelectCombobox
|
const bulkActions = selectedItems.size > 0 ? (
|
||||||
options={reporterOptions}
|
<Button
|
||||||
value={reporterFilters}
|
variant="outline"
|
||||||
onChange={setReporterFilters}
|
size="sm"
|
||||||
placeholder="보고자"
|
onClick={handleWithdrawClick}
|
||||||
searchPlaceholder="보고자 검색..."
|
className="text-orange-600 border-orange-300 hover:bg-orange-50"
|
||||||
className="w-[100px]"
|
>
|
||||||
/>
|
<Undo2 className="mr-2 h-4 w-4" />
|
||||||
|
철회 ({selectedItems.size})
|
||||||
{/* 5. 담당자 필터 (다중선택) */}
|
</Button>
|
||||||
<MultiSelectCombobox
|
) : null;
|
||||||
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -598,7 +625,12 @@ export default function IssueManagementListClient({
|
|||||||
icon={AlertTriangle}
|
icon={AlertTriangle}
|
||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="이슈 필터"
|
||||||
|
bulkActions={bulkActions}
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="이슈번호, 시공번호, 거래처, 현장, 제목, 보고자, 담당자 검색"
|
searchPlaceholder="이슈번호, 시공번호, 거래처, 현장, 제목, 보고자, 담당자 검색"
|
||||||
|
|||||||
@@ -8,14 +8,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import { IntegratedListTemplateV2, TableColumn, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { IntegratedListTemplateV2, TableColumn } from '@/components/templates/IntegratedListTemplateV2';
|
|
||||||
import { MobileCard } from '@/components/molecules/MobileCard';
|
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -427,130 +420,112 @@ export default function ItemManagementClient({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 테이블 헤더 액션 (6개 필터)
|
// ===== filterConfig 기반 통합 필터 시스템 =====
|
||||||
const tableHeaderActions = (
|
const filterConfig: FilterFieldConfig[] = useMemo(() => [
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
{
|
||||||
{/* 총 건수 */}
|
key: 'itemType',
|
||||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
label: '품목유형',
|
||||||
총 {sortedItems.length}건
|
type: 'single',
|
||||||
</span>
|
options: ITEM_TYPE_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
label: '카테고리',
|
||||||
|
type: 'single',
|
||||||
|
options: categoryOptions.map(c => ({
|
||||||
|
value: c.id,
|
||||||
|
label: c.name,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'specification',
|
||||||
|
label: '규격',
|
||||||
|
type: 'single',
|
||||||
|
options: SPECIFICATION_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'orderType',
|
||||||
|
label: '구분',
|
||||||
|
type: 'single',
|
||||||
|
options: ORDER_TYPE_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: '상태',
|
||||||
|
type: 'single',
|
||||||
|
options: STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sortBy',
|
||||||
|
label: '정렬',
|
||||||
|
type: 'single',
|
||||||
|
options: SORT_OPTIONS.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '최신순',
|
||||||
|
},
|
||||||
|
], [categoryOptions]);
|
||||||
|
|
||||||
{/* 품목유형 필터 */}
|
const filterValues: FilterValues = useMemo(() => ({
|
||||||
<Select
|
itemType: itemTypeFilter,
|
||||||
value={itemTypeFilter}
|
category: categoryFilter,
|
||||||
onValueChange={(v) => {
|
specification: specificationFilter,
|
||||||
setItemTypeFilter(v as ItemType | 'all');
|
orderType: orderTypeFilter,
|
||||||
setCurrentPage(1);
|
status: statusFilter,
|
||||||
}}
|
sortBy: sortBy,
|
||||||
>
|
}), [itemTypeFilter, categoryFilter, specificationFilter, orderTypeFilter, statusFilter, sortBy]);
|
||||||
<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 handleFilterChange = useCallback((key: string, value: string | string[]) => {
|
||||||
<Select
|
switch (key) {
|
||||||
value={categoryFilter}
|
case 'itemType':
|
||||||
onValueChange={(v) => {
|
setItemTypeFilter(value as ItemType | 'all');
|
||||||
setCategoryFilter(v);
|
break;
|
||||||
setCurrentPage(1);
|
case 'category':
|
||||||
}}
|
setCategoryFilter(value as string);
|
||||||
>
|
break;
|
||||||
<SelectTrigger className="w-[100px]">
|
case 'specification':
|
||||||
<SelectValue placeholder="카테고리" />
|
setSpecificationFilter(value as Specification | 'all');
|
||||||
</SelectTrigger>
|
break;
|
||||||
<SelectContent>
|
case 'orderType':
|
||||||
<SelectItem value="all">전체</SelectItem>
|
setOrderTypeFilter(value as OrderType | 'all');
|
||||||
{categoryOptions.map((category) => (
|
break;
|
||||||
<SelectItem key={category.id} value={category.id}>
|
case 'status':
|
||||||
{category.name}
|
setStatusFilter(value as ItemStatus | 'all');
|
||||||
</SelectItem>
|
break;
|
||||||
))}
|
case 'sortBy':
|
||||||
</SelectContent>
|
setSortBy(value as 'latest' | 'oldest');
|
||||||
</Select>
|
break;
|
||||||
|
}
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
{/* 규격 필터 */}
|
const handleFilterReset = useCallback(() => {
|
||||||
<Select
|
setItemTypeFilter('all');
|
||||||
value={specificationFilter}
|
setCategoryFilter('all');
|
||||||
onValueChange={(v) => {
|
setSpecificationFilter('all');
|
||||||
setSpecificationFilter(v as Specification | 'all');
|
setOrderTypeFilter('all');
|
||||||
setCurrentPage(1);
|
setStatusFilter('all');
|
||||||
}}
|
setSortBy('latest');
|
||||||
>
|
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -573,7 +548,11 @@ export default function ItemManagementClient({
|
|||||||
iconColor: 'text-green-500',
|
iconColor: 'text-green-500',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
tableHeaderActions={tableHeaderActions}
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="품목 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="품목명, 품목번호, 카테고리 검색"
|
searchPlaceholder="품목명, 품목번호, 카테고리 검색"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} 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 { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -369,6 +369,66 @@ export default function LaborManagementClient({
|
|||||||
[handleRowClick]
|
[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 = (
|
const headerActions = (
|
||||||
<DateRangeSelector
|
<DateRangeSelector
|
||||||
@@ -471,6 +531,11 @@ export default function LaborManagementClient({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
tableHeaderActions={tableHeaderActions}
|
tableHeaderActions={tableHeaderActions}
|
||||||
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="노임 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="노임번호, 구분 검색"
|
searchPlaceholder="노임번호, 구분 검색"
|
||||||
|
|||||||
@@ -7,14 +7,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { TableCell, TableRow, TableHead } from '@/components/ui/table';
|
import { TableCell, TableRow, TableHead } from '@/components/ui/table';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import { IntegratedListTemplateV2, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { IntegratedListTemplateV2, StatCard } from '@/components/templates/IntegratedListTemplateV2';
|
|
||||||
import { MobileCard } from '@/components/molecules/MobileCard';
|
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -469,98 +462,112 @@ export default function PricingListClient({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 테이블 헤더 액션 (필터 6개)
|
// ===== filterConfig 기반 통합 필터 시스템 =====
|
||||||
const tableHeaderActions = (
|
const filterConfig: FilterFieldConfig[] = useMemo(() => [
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
{
|
||||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
key: 'itemType',
|
||||||
총 {sortedPricing.length}건
|
label: '품목유형',
|
||||||
</span>
|
type: 'single',
|
||||||
|
options: ITEM_TYPE_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
label: '카테고리',
|
||||||
|
type: 'single',
|
||||||
|
options: CATEGORY_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'spec',
|
||||||
|
label: '규격',
|
||||||
|
type: 'single',
|
||||||
|
options: SPEC_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'division',
|
||||||
|
label: '구분',
|
||||||
|
type: 'single',
|
||||||
|
options: DIVISION_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: '상태',
|
||||||
|
type: 'single',
|
||||||
|
options: STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sortBy',
|
||||||
|
label: '정렬',
|
||||||
|
type: 'single',
|
||||||
|
options: SORT_OPTIONS.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '최신순',
|
||||||
|
},
|
||||||
|
], []);
|
||||||
|
|
||||||
{/* 품목유형 필터 */}
|
const filterValues: FilterValues = useMemo(() => ({
|
||||||
<Select value={itemTypeFilter} onValueChange={setItemTypeFilter}>
|
itemType: itemTypeFilter,
|
||||||
<SelectTrigger className="w-[100px]">
|
category: categoryFilter,
|
||||||
<SelectValue placeholder="품목유형" />
|
spec: specFilter,
|
||||||
</SelectTrigger>
|
division: divisionFilter,
|
||||||
<SelectContent>
|
status: statusFilter,
|
||||||
{ITEM_TYPE_OPTIONS.map((option) => (
|
sortBy: sortBy,
|
||||||
<SelectItem key={option.value} value={option.value}>
|
}), [itemTypeFilter, categoryFilter, specFilter, divisionFilter, statusFilter, sortBy]);
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{/* 카테고리 필터 */}
|
const handleFilterChange = useCallback((key: string, value: string | string[]) => {
|
||||||
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
switch (key) {
|
||||||
<SelectTrigger className="w-[140px]">
|
case 'itemType':
|
||||||
<SelectValue placeholder="카테고리" />
|
setItemTypeFilter(value as string);
|
||||||
</SelectTrigger>
|
break;
|
||||||
<SelectContent>
|
case 'category':
|
||||||
{CATEGORY_OPTIONS.map((option) => (
|
setCategoryFilter(value as string);
|
||||||
<SelectItem key={option.value} value={option.value}>
|
break;
|
||||||
{option.label}
|
case 'spec':
|
||||||
</SelectItem>
|
setSpecFilter(value as string);
|
||||||
))}
|
break;
|
||||||
</SelectContent>
|
case 'division':
|
||||||
</Select>
|
setDivisionFilter(value as string);
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
setStatusFilter(value as string);
|
||||||
|
break;
|
||||||
|
case 'sortBy':
|
||||||
|
setSortBy(value as string);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
{/* 규격 필터 */}
|
const handleFilterReset = useCallback(() => {
|
||||||
<Select value={specFilter} onValueChange={setSpecFilter}>
|
setItemTypeFilter('all');
|
||||||
<SelectTrigger className="w-[90px]">
|
setCategoryFilter('all');
|
||||||
<SelectValue placeholder="규격" />
|
setSpecFilter('all');
|
||||||
</SelectTrigger>
|
setDivisionFilter('all');
|
||||||
<SelectContent>
|
setStatusFilter('all');
|
||||||
{SPEC_OPTIONS.map((option) => (
|
setSortBy('latest');
|
||||||
<SelectItem key={option.value} value={option.value}>
|
setCurrentPage(1);
|
||||||
{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>
|
|
||||||
);
|
|
||||||
|
|
||||||
// 동적 컬럼으로 인해 tableColumns를 사용하지 않고 커스텀 헤더 사용
|
// 동적 컬럼으로 인해 tableColumns를 사용하지 않고 커스텀 헤더 사용
|
||||||
const emptyTableColumns: { key: string; label: string; className?: string }[] = [];
|
const emptyTableColumns: { key: string; label: string; className?: string }[] = [];
|
||||||
@@ -573,7 +580,11 @@ export default function PricingListClient({
|
|||||||
icon={DollarSign}
|
icon={DollarSign}
|
||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="단가 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="단가번호, 품목명, 카테고리, 거래처 검색"
|
searchPlaceholder="단가번호, 품목명, 카테고리, 거래처 검색"
|
||||||
|
|||||||
@@ -6,15 +6,8 @@ import { Calendar, CalendarDays, CalendarCheck, CalendarClock, Plus, Pencil, Tra
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
||||||
Select,
|
import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
|
||||||
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 { MobileCard } from '@/components/molecules/MobileCard';
|
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -344,6 +337,96 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
|
|||||||
}
|
}
|
||||||
}, [selectedItems, loadData]);
|
}, [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(
|
const renderTableRow = useCallback(
|
||||||
(briefing: SiteBriefing, index: number, globalIndex: number) => {
|
(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<IntegratedListTemplateV2
|
<IntegratedListTemplateV2
|
||||||
@@ -554,7 +566,11 @@ export default function SiteBriefingListClient({ initialData = [] }: SiteBriefin
|
|||||||
icon={Calendar}
|
icon={Calendar}
|
||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="현장설명회 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="현장번호, 거래처, 현장명 검색"
|
searchPlaceholder="현장번호, 거래처, 현장명 검색"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { MultiSelectCombobox, MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
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 { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
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 = (
|
const tableHeaderActions = (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
@@ -445,6 +505,11 @@ export default function SiteManagementListClient({
|
|||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
tableHeaderActions={tableHeaderActions}
|
||||||
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="현장 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="현장번호, 거래처, 현장명, 위치 검색"
|
searchPlaceholder="현장번호, 거래처, 현장명, 위치 검색"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { MultiSelectCombobox, MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
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 { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
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 = (
|
const tableHeaderActions = (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
@@ -478,6 +538,11 @@ export default function StructureReviewListClient({
|
|||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
tableHeaderActions={tableHeaderActions}
|
||||||
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="구조검토 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="검토번호, 거래처, 현장명, 검토회사 검색"
|
searchPlaceholder="검토번호, 거래처, 현장명, 검토회사 검색"
|
||||||
|
|||||||
@@ -6,15 +6,8 @@ import { Users, Eye, FileText, Clock, CheckCircle } from 'lucide-react';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { TableCell, TableRow } from '@/components/ui/table';
|
import { TableCell, TableRow } from '@/components/ui/table';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {
|
import { MultiSelectOption } from '@/components/ui/multi-select-combobox';
|
||||||
Select,
|
import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2';
|
||||||
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 { MobileCard } from '@/components/molecules/MobileCard';
|
import { MobileCard } from '@/components/molecules/MobileCard';
|
||||||
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -396,92 +389,123 @@ export default function WorkerStatusListClient({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// 테이블 헤더 액션 (7개 필터)
|
// ===== filterConfig 기반 통합 필터 시스템 =====
|
||||||
const tableHeaderActions = (
|
const filterConfig: FilterFieldConfig[] = useMemo(() => [
|
||||||
<div className="flex items-center gap-2 flex-wrap justify-end w-full">
|
{
|
||||||
{/* 1. 거래처 필터 (다중선택) */}
|
key: 'partner',
|
||||||
<MultiSelectCombobox
|
label: '거래처',
|
||||||
options={partnerOptions}
|
type: 'multi',
|
||||||
value={partnerFilters}
|
options: partnerOptions.map(o => ({
|
||||||
onChange={setPartnerFilters}
|
value: o.value,
|
||||||
placeholder="거래처"
|
label: o.label,
|
||||||
searchPlaceholder="거래처 검색..."
|
})),
|
||||||
className="w-[120px]"
|
},
|
||||||
/>
|
{
|
||||||
|
key: 'site',
|
||||||
|
label: '현장명',
|
||||||
|
type: 'multi',
|
||||||
|
options: siteOptions.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'category',
|
||||||
|
label: '구분',
|
||||||
|
type: 'single',
|
||||||
|
options: WORKER_CATEGORY_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'department',
|
||||||
|
label: '부서',
|
||||||
|
type: 'multi',
|
||||||
|
options: departmentOptions.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
label: '이름',
|
||||||
|
type: 'multi',
|
||||||
|
options: nameOptions.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: '상태',
|
||||||
|
type: 'single',
|
||||||
|
options: WORKER_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '전체',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sortBy',
|
||||||
|
label: '정렬',
|
||||||
|
type: 'single',
|
||||||
|
options: WORKER_SORT_OPTIONS.map(o => ({
|
||||||
|
value: o.value,
|
||||||
|
label: o.label,
|
||||||
|
})),
|
||||||
|
allOptionLabel: '최신순',
|
||||||
|
},
|
||||||
|
], [partnerOptions, siteOptions, departmentOptions, nameOptions]);
|
||||||
|
|
||||||
{/* 2. 현장명 필터 (다중선택) */}
|
const filterValues: FilterValues = useMemo(() => ({
|
||||||
<MultiSelectCombobox
|
partner: partnerFilters,
|
||||||
options={siteOptions}
|
site: siteFilters,
|
||||||
value={siteFilters}
|
category: categoryFilter,
|
||||||
onChange={setSiteFilters}
|
department: departmentFilters,
|
||||||
placeholder="현장명"
|
name: nameFilters,
|
||||||
searchPlaceholder="현장명 검색..."
|
status: statusFilter,
|
||||||
className="w-[120px]"
|
sortBy: sortBy,
|
||||||
/>
|
}), [partnerFilters, siteFilters, categoryFilter, departmentFilters, nameFilters, statusFilter, sortBy]);
|
||||||
|
|
||||||
{/* 3. 구분 필터 (단일선택) */}
|
const handleFilterChange = useCallback((key: string, value: string | string[]) => {
|
||||||
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
|
switch (key) {
|
||||||
<SelectTrigger className="w-[100px]">
|
case 'partner':
|
||||||
<SelectValue placeholder="구분" />
|
setPartnerFilters(value as string[]);
|
||||||
</SelectTrigger>
|
break;
|
||||||
<SelectContent>
|
case 'site':
|
||||||
{WORKER_CATEGORY_OPTIONS.map((option) => (
|
setSiteFilters(value as string[]);
|
||||||
<SelectItem key={option.value} value={option.value}>
|
break;
|
||||||
{option.label}
|
case 'category':
|
||||||
</SelectItem>
|
setCategoryFilter(value as string);
|
||||||
))}
|
break;
|
||||||
</SelectContent>
|
case 'department':
|
||||||
</Select>
|
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. 부서 필터 (다중선택) */}
|
const handleFilterReset = useCallback(() => {
|
||||||
<MultiSelectCombobox
|
setPartnerFilters([]);
|
||||||
options={departmentOptions}
|
setSiteFilters([]);
|
||||||
value={departmentFilters}
|
setCategoryFilter('all');
|
||||||
onChange={setDepartmentFilters}
|
setDepartmentFilters([]);
|
||||||
placeholder="부서"
|
setNameFilters([]);
|
||||||
searchPlaceholder="부서 검색..."
|
setStatusFilter('all');
|
||||||
className="w-[100px]"
|
setSortBy('latest');
|
||||||
/>
|
setCurrentPage(1);
|
||||||
|
}, []);
|
||||||
{/* 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>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntegratedListTemplateV2
|
<IntegratedListTemplateV2
|
||||||
@@ -490,7 +514,11 @@ export default function WorkerStatusListClient({
|
|||||||
icon={Users}
|
icon={Users}
|
||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
stats={statsCardsData}
|
stats={statsCardsData}
|
||||||
tableHeaderActions={tableHeaderActions}
|
filterConfig={filterConfig}
|
||||||
|
filterValues={filterValues}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
onFilterReset={handleFilterReset}
|
||||||
|
filterTitle="작업인력 필터"
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
searchPlaceholder="거래처, 현장, 부서, 이름, 시공번호 검색"
|
searchPlaceholder="거래처, 현장, 부서, 이름, 시공번호 검색"
|
||||||
|
|||||||
@@ -193,9 +193,33 @@ export function EmployeeForm({
|
|||||||
}
|
}
|
||||||
}, [employee, mode]);
|
}, [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) => {
|
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]) {
|
if (errors[field as keyof ValidationErrors]) {
|
||||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||||||
@@ -233,8 +257,8 @@ export function EmployeeForm({
|
|||||||
if (mode === 'create') {
|
if (mode === 'create') {
|
||||||
if (!formData.password) {
|
if (!formData.password) {
|
||||||
newErrors.password = '비밀번호를 입력해주세요.';
|
newErrors.password = '비밀번호를 입력해주세요.';
|
||||||
} else if (formData.password.length < 6) {
|
} else if (formData.password.length < 8) {
|
||||||
newErrors.password = '비밀번호는 6자 이상이어야 합니다.';
|
newErrors.password = '비밀번호는 8자 이상이어야 합니다.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.password !== formData.confirmPassword) {
|
if (formData.password !== formData.confirmPassword) {
|
||||||
@@ -331,8 +355,8 @@ export function EmployeeForm({
|
|||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* 사원 정보 - 프로필 사진 + 기본 정보 */}
|
{/* 사원 정보 - 프로필 사진 + 기본 정보 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="bg-black text-white rounded-t-lg">
|
<CardHeader>
|
||||||
<CardTitle className="text-base font-medium">사원 정보</CardTitle>
|
<CardTitle className="text-base">사원 정보</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
{/* 기본 정보 필드들 */}
|
{/* 기본 정보 필드들 */}
|
||||||
@@ -429,8 +453,8 @@ export function EmployeeForm({
|
|||||||
{/* 사원 상세 */}
|
{/* 사원 상세 */}
|
||||||
{(fieldSettings.showProfileImage || fieldSettings.showEmployeeCode || fieldSettings.showGender || fieldSettings.showAddress) && (
|
{(fieldSettings.showProfileImage || fieldSettings.showEmployeeCode || fieldSettings.showGender || fieldSettings.showAddress) && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="bg-black text-white rounded-t-lg">
|
<CardHeader>
|
||||||
<CardTitle className="text-base font-medium">사원 상세</CardTitle>
|
<CardTitle className="text-base">사원 상세</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-6 space-y-4">
|
<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) && (
|
{(fieldSettings.showHireDate || fieldSettings.showEmploymentType || fieldSettings.showRank || fieldSettings.showStatus || fieldSettings.showDepartment || fieldSettings.showPosition || fieldSettings.showClockInLocation || fieldSettings.showClockOutLocation || fieldSettings.showResignationDate || fieldSettings.showResignationReason) && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="bg-black text-white rounded-t-lg">
|
<CardHeader>
|
||||||
<CardTitle className="text-base font-medium">인사 정보</CardTitle>
|
<CardTitle className="text-base">인사 정보</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-6 space-y-4">
|
<CardContent className="pt-6 space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
@@ -770,8 +794,8 @@ export function EmployeeForm({
|
|||||||
|
|
||||||
{/* 사용자 정보 */}
|
{/* 사용자 정보 */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="bg-black text-white rounded-t-lg">
|
<CardHeader>
|
||||||
<CardTitle className="text-base font-medium">사용자 정보</CardTitle>
|
<CardTitle className="text-base">사용자 정보</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user