feat(WEB): 공사관리 시스템 및 CEO 대시보드 기능 확장

- 공사현장관리: 프로젝트 상세, 공정관리, 칸반보드 구현
- 이슈관리: 현장 이슈 등록/조회 기능 추가
- 근로자현황: 일별 근로자 출역 현황 페이지 추가
- 유틸리티관리: 현장 유틸리티 관리 페이지 추가
- 기성청구: 기성청구 관리 페이지 추가
- CEO 대시보드: 현황판(StatusBoardSection) 추가, 설정 다이얼로그 개선
- 발주관리: 모바일 필터 적용, 리스트 UI 개선
- 공용 컴포넌트: MobileFilter, IntegratedListTemplateV2 개선, CalendarHeader 반응형 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-13 17:18:29 +09:00
parent d036ce4f42
commit db47a15544
85 changed files with 12940 additions and 499 deletions

View File

@@ -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";