feat(WEB): 공사관리 시스템 및 CEO 대시보드 기능 확장
- 공사현장관리: 프로젝트 상세, 공정관리, 칸반보드 구현 - 이슈관리: 현장 이슈 등록/조회 기능 추가 - 근로자현황: 일별 근로자 출역 현황 페이지 추가 - 유틸리티관리: 현장 유틸리티 관리 페이지 추가 - 기성청구: 기성청구 관리 페이지 추가 - CEO 대시보드: 현황판(StatusBoardSection) 추가, 설정 다이얼로그 개선 - 발주관리: 모바일 필터 적용, 리스트 UI 개선 - 공용 컴포넌트: MobileFilter, IntegratedListTemplateV2 개선, CalendarHeader 반응형 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,13 @@ import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -23,6 +30,8 @@ import { StatCards } from "@/components/organisms/StatCards";
|
||||
import { SearchFilter } from "@/components/organisms/SearchFilter";
|
||||
import { ScreenVersionHistory } from "@/components/organisms/ScreenVersionHistory";
|
||||
import { TabChip } from "@/components/atoms/TabChip";
|
||||
import { MultiSelectCombobox } from "@/components/ui/multi-select-combobox";
|
||||
import { MobileFilter, FilterFieldConfig, FilterValues } from "@/components/molecules/MobileFilter";
|
||||
|
||||
/**
|
||||
* 기본 통합 목록_버젼2
|
||||
@@ -119,6 +128,18 @@ export interface IntegratedListTemplateV2Props<T = any> {
|
||||
// 테이블 헤더 액션 (탭 옆에 표시될 셀렉트박스 등)
|
||||
tableHeaderActions?: ReactNode;
|
||||
|
||||
// 모바일/카드 뷰용 필터 슬롯 (xl 미만에서 카드 목록 위에 표시)
|
||||
mobileFilterSlot?: ReactNode;
|
||||
|
||||
// ===== 새로운 통합 필터 시스템 (선택적 사용) =====
|
||||
// filterConfig를 전달하면 PC는 인라인, 모바일은 바텀시트로 자동 분기
|
||||
// 기존 tableHeaderActions, mobileFilterSlot과 함께 사용 가능
|
||||
filterConfig?: FilterFieldConfig[];
|
||||
filterValues?: FilterValues;
|
||||
onFilterChange?: (key: string, value: string | string[]) => void;
|
||||
onFilterReset?: () => void;
|
||||
filterTitle?: string; // 모바일 필터 바텀시트 제목 (기본: "검색 필터")
|
||||
|
||||
// 테이블 앞에 표시될 컨텐츠 (계정과목명 + 저장 버튼 등)
|
||||
beforeTableContent?: ReactNode;
|
||||
|
||||
@@ -184,6 +205,12 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
tableHeaderActions,
|
||||
mobileFilterSlot,
|
||||
filterConfig,
|
||||
filterValues,
|
||||
onFilterChange,
|
||||
onFilterReset,
|
||||
filterTitle = "검색 필터",
|
||||
beforeTableContent,
|
||||
tableColumns,
|
||||
tableTitle,
|
||||
@@ -214,6 +241,75 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.itemsPerPage;
|
||||
const allSelected = selectedItems.size === data.length && data.length > 0;
|
||||
|
||||
// ===== filterConfig 기반 자동 필터 렌더링 =====
|
||||
// PC용 인라인 필터 (xl 이상에서 표시)
|
||||
const renderAutoFilters = () => {
|
||||
if (!filterConfig || !filterValues || !onFilterChange) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{filterConfig.map((field) => {
|
||||
if (field.type === 'single') {
|
||||
// 단일선택: Select
|
||||
return (
|
||||
<Select
|
||||
key={field.key}
|
||||
value={(filterValues[field.key] as string) || 'all'}
|
||||
onValueChange={(value) => onFilterChange(field.key, value)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder={field.allOptionLabel || field.label} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{field.allOptionLabel || '전체'}
|
||||
</SelectItem>
|
||||
{field.options.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
} else {
|
||||
// 다중선택: MultiSelectCombobox
|
||||
return (
|
||||
<MultiSelectCombobox
|
||||
key={field.key}
|
||||
options={field.options.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
}))}
|
||||
value={(filterValues[field.key] as string[]) || []}
|
||||
onChange={(value) => onFilterChange(field.key, value)}
|
||||
placeholder={field.label}
|
||||
searchPlaceholder={`${field.label} 검색...`}
|
||||
className="w-[140px]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 모바일용 바텀시트 필터 (xl 미만에서 표시)
|
||||
const renderAutoMobileFilter = () => {
|
||||
if (!filterConfig || !filterValues || !onFilterChange || !onFilterReset) return null;
|
||||
|
||||
return (
|
||||
<MobileFilter
|
||||
fields={filterConfig}
|
||||
values={filterValues}
|
||||
onChange={onFilterChange}
|
||||
onReset={onFilterReset}
|
||||
buttonLabel="필터"
|
||||
title={filterTitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 일괄삭제 확인 핸들러
|
||||
const handleBulkDeleteClick = () => {
|
||||
setShowDeleteDialog(true);
|
||||
@@ -316,7 +412,9 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
{selectedItems.size}개 항목 선택됨
|
||||
</span>
|
||||
)}
|
||||
{/* 테이블 헤더 액션 (필터/정렬 셀렉트박스 등) */}
|
||||
{/* filterConfig 기반 자동 필터 (PC) */}
|
||||
{renderAutoFilters()}
|
||||
{/* 테이블 헤더 액션 (필터/정렬 셀렉트박스 등) - 기존 방식 */}
|
||||
{tableHeaderActions}
|
||||
{selectedItems.size >= 1 && onBulkDelete && (
|
||||
<Button
|
||||
@@ -351,6 +449,16 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 모바일/카드 뷰용 필터 - filterConfig 자동 생성 또는 기존 mobileFilterSlot */}
|
||||
{(filterConfig || mobileFilterSlot) && (
|
||||
<div className="xl:hidden mb-4">
|
||||
{/* filterConfig가 있으면 자동 생성된 MobileFilter 사용 */}
|
||||
{renderAutoMobileFilter()}
|
||||
{/* 기존 방식: mobileFilterSlot 직접 전달 */}
|
||||
{mobileFilterSlot}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 모바일/태블릿/소형 노트북 (~1279px) 카드 뷰 */}
|
||||
<div className="xl:hidden space-y-4 md:space-y-0 md:grid md:grid-cols-2 md:gap-4 lg:grid-cols-3">
|
||||
{(allData && allData.length > 0 ? allData : data).length === 0 ? (
|
||||
@@ -554,4 +662,7 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
</AlertDialog>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 필터 관련 타입 재export (다른 페이지에서 사용 가능)
|
||||
export type { FilterFieldConfig, FilterValues } from "@/components/molecules/MobileFilter";
|
||||
Reference in New Issue
Block a user