"use client"; import { ReactNode, Fragment, useState, RefObject } from "react"; import { LucideIcon, Trash2, Plus } from "lucide-react"; import { DateRangeSelector } from "@/components/molecules/DateRangeSelector"; import { Card, CardContent } from "@/components/ui/card"; 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, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { PageLayout } from "@/components/organisms/PageLayout"; import { PageHeader } from "@/components/organisms/PageHeader"; 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 * * 품목관리 스타일의 완전한 목록 템플릿 * - PageHeader, StatCards, SearchFilter, ScreenVersionHistory * - 탭 기반 필터 (데스크톱: TabsList, 모바일: 커스텀 버튼) * - 체크박스 포함 DataTable (Desktop) * - 체크박스 포함 모바일 카드 (Mobile) * - 페이지네이션 */ export interface TabOption { value: string; label: string; count: number; color?: string; // 모바일 탭 색상 } export interface TableColumn { key: string; label: string; className?: string; hideOnMobile?: boolean; hideOnTablet?: boolean; } export interface PaginationConfig { currentPage: number; totalPages: number; totalItems: number; itemsPerPage: number; onPageChange: (page: number) => void; } export interface StatCard { label: string; value: string | number; icon: LucideIcon; iconColor: string; onClick?: () => void; isActive?: boolean; } export interface VersionHistoryItem { version: string; description: string; modifiedBy: string; modifiedAt: string; } export interface DevMetadata { componentName: string; pagePath: string; description: string; apis?: any[]; dataStructures?: any[]; dbSchema?: any[]; businessLogic?: any[]; } export interface IntegratedListTemplateV2Props { // 페이지 헤더 title: string; description?: string; icon?: LucideIcon; headerActions?: ReactNode; // ===== 공통 헤더 옵션 (달력/등록버튼) ===== /** * 날짜 범위 선택기 (왼쪽 배치) * - enabled: 달력 표시 여부 * - showPresets: 프리셋 버튼 (당해년도, 전전월, 전월, 당월, 어제, 오늘) * - startDate/endDate: 외부 상태 연동 * - onChange: 날짜 변경 콜백 */ dateRangeSelector?: { enabled: boolean; showPresets?: boolean; startDate?: string; endDate?: string; onStartDateChange?: (date: string) => void; onEndDateChange?: (date: string) => void; }; /** * 등록 버튼 (오른쪽 끝 배치) * - label: 버튼 텍스트 (예: '등록', '공정 등록') * - onClick: 클릭 핸들러 * - icon: 아이콘 (기본: Plus) */ createButton?: { label: string; onClick: () => void; icon?: LucideIcon; }; // 탭 콘텐츠 (헤더 액션 아래, 검색 위에 표시되는 커스텀 탭) tabsContent?: ReactNode; // 통계 카드 stats?: StatCard[]; // 경고 배너 (통계 카드와 검색 영역 사이) alertBanner?: ReactNode; // 버전 이력 versionHistory?: VersionHistoryItem[]; versionHistoryTitle?: string; // 검색 및 필터 searchValue?: string; onSearchChange?: (value: string) => void; searchPlaceholder?: string; extraFilters?: ReactNode; // Select, DatePicker 등 추가 필터 hideSearch?: boolean; // 검색창 숨김 여부 // 탭 (품목 유형, 상태 등) - optional tabs?: TabOption[]; activeTab?: string; onTabChange?: (value: string) => void; // 테이블 헤더 액션 (탭 옆에 표시될 셀렉트박스 등) 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; // 테이블 컬럼 tableColumns: TableColumn[]; tableTitle?: string; // "전체 목록 (100개)" 같은 타이틀 // 커스텀 테이블 헤더 렌더링 (동적 컬럼용) renderCustomTableHeader?: () => ReactNode; // 테이블 하단 푸터 (합계 등) tableFooter?: ReactNode; // 데이터 data: T[]; // 데스크톱용 페이지네이션된 데이터 totalCount?: number; // 전체 데이터 개수 (역순 번호 계산용) allData?: T[]; // 모바일 인피니티 스크롤용 전체 필터된 데이터 mobileDisplayCount?: number; // 모바일에서 표시할 개수 onLoadMore?: () => void; // 더 불러오기 콜백 infinityScrollSentinelRef?: RefObject; // 인피니티 스크롤용 sentinel ref // 체크박스 선택 selectedItems: Set; onToggleSelection: (id: string) => void; onToggleSelectAll: () => void; getItemId: (item: T) => string; // 아이템에서 ID 추출 onBulkDelete?: () => void; // 일괄 삭제 핸들러 // 테이블 표시 옵션 showCheckbox?: boolean; // 체크박스 표시 여부 (기본: true) showRowNumber?: boolean; // 번호 컬럼 표시 여부 (기본: true, tableColumns에 번호 포함 시) // 렌더링 함수 renderTableRow: (item: T, index: number, globalIndex: number) => ReactNode; renderMobileCard: (item: T, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void) => ReactNode; // 페이지네이션 pagination: PaginationConfig; // 개발자 메타데이터 devMetadata?: DevMetadata; // 로딩 상태 isLoading?: boolean; } export function IntegratedListTemplateV2({ title, description, icon, headerActions, dateRangeSelector, createButton, tabsContent, stats, alertBanner, versionHistory, versionHistoryTitle = "수정 이력", searchValue, onSearchChange, searchPlaceholder = "검색...", extraFilters, hideSearch = false, tabs, activeTab, onTabChange, tableHeaderActions, mobileFilterSlot, filterConfig, filterValues, onFilterChange, onFilterReset, filterTitle = "검색 필터", beforeTableContent, tableColumns, tableTitle, renderCustomTableHeader, tableFooter, data, totalCount, allData, mobileDisplayCount, onLoadMore, infinityScrollSentinelRef, selectedItems, onToggleSelection, onToggleSelectAll, getItemId, onBulkDelete, showCheckbox = true, // 기본값 true showRowNumber = true, // 기본값 true (번호 컬럼은 renderTableRow에서 처리) renderTableRow, renderMobileCard, pagination, devMetadata, isLoading, }: IntegratedListTemplateV2Props) { const [showDeleteDialog, setShowDeleteDialog] = useState(false); 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 (
{filterConfig.map((field) => { if (field.type === 'single') { // 단일선택: Select return ( ); } else { // 다중선택: MultiSelectCombobox return ( ({ 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]" /> ); } })}
); }; // 모바일용 바텀시트 필터 (xl 미만에서 표시) const renderAutoMobileFilter = () => { if (!filterConfig || !filterValues || !onFilterChange || !onFilterReset) return null; return ( ); }; // 일괄삭제 확인 핸들러 const handleBulkDeleteClick = () => { setShowDeleteDialog(true); }; // 일괄삭제 실행 const handleConfirmDelete = () => { if (onBulkDelete) { onBulkDelete(); } setShowDeleteDialog(false); }; return ( {/* 페이지 헤더 */} {/* 헤더 액션 (달력, 버튼 등) - 타이틀 아래 배치 */} {/* 레이아웃: [달력 (왼쪽)] -------------- [등록 버튼 (오른쪽 끝)] */} {(dateRangeSelector?.enabled || createButton || headerActions) && (
{/* 날짜 범위 선택기 (왼쪽) */} {dateRangeSelector?.enabled && ( )} {/* 레거시 헤더 액션 (기존 호환성 유지) */} {headerActions} {/* 등록 버튼 (오른쪽 끝) */} {createButton && ( )}
)} {/* 커스텀 탭 콘텐츠 (헤더 아래, 검색 위) */} {tabsContent && (
{tabsContent}
)} {/* 통계 카드 - 태블릿/데스크톱 */} {stats && stats.length > 0 && (
)} {/* 경고 배너 (통계 카드와 검색 영역 사이) */} {alertBanner} {/* 버전 이력 */} {versionHistory && versionHistory.length > 0 && ( )} {/* 검색 및 필터 */} {!hideSearch && ( {})} searchPlaceholder={searchPlaceholder} filterButton={false} extraActions={extraFilters} /> )} {/* 테이블 앞 컨텐츠 (계정과목명 + 저장 버튼, 달력 등) */} {beforeTableContent && (
{beforeTableContent}
)} {/* 목록 카드 */} {/* 데스크톱 (1280px+) - TabChip 탭 */}
{tabs && tabs.map((tab) => ( onTabChange?.(tab.value)} color={tab.color as any} /> ))}
{/* 선택된 항목 수 표시 */} {selectedItems.size > 0 && ( {selectedItems.size}개 항목 선택됨 )} {/* 테이블 헤더 액션 (총 N건 등) - 필터 앞에 배치 */} {tableHeaderActions} {/* filterConfig 기반 자동 필터 (PC) */} {renderAutoFilters()} {selectedItems.size >= 1 && onBulkDelete && ( )}
{/* 모바일/태블릿 (~1279px) - TabChip 탭 */} {tabs && tabs.length > 0 && (
{tabs.map((tab) => ( onTabChange?.(tab.value)} color={tab.color as any} /> ))}
)} {/* 탭 컨텐츠 */} {(tabs || [{ value: 'default', label: '', count: 0 }]).map((tab) => ( {/* 모바일/태블릿/소형 노트북 (~1279px) - 선택 삭제 버튼 */} {selectedItems.size >= 2 && onBulkDelete && (
)} {/* 모바일/카드 뷰용 필터 - filterConfig 자동 생성 또는 기존 mobileFilterSlot */} {(filterConfig || mobileFilterSlot) && (
{/* filterConfig가 있으면 자동 생성된 MobileFilter 사용 */} {renderAutoMobileFilter()} {/* 기존 방식: mobileFilterSlot 직접 전달 */} {mobileFilterSlot}
)} {/* 모바일/태블릿/소형 노트북 (~1279px) 카드 뷰 */}
{(allData && allData.length > 0 ? allData : data).length === 0 ? (
검색 결과가 없습니다.
) : ( // 백엔드가 created_at ASC로 정렬해서 보내줌 (오래된 순) (allData || data).map((item, index) => { const itemId = getItemId(item); const isSelected = selectedItems.has(itemId); // 순차 번호: 1번부터 시작 const globalIndex = index + 1; return (
{renderMobileCard( item, index, globalIndex, isSelected, () => onToggleSelection(itemId) )}
); }) )} {/* 인피니티 스크롤 Sentinel */} {infinityScrollSentinelRef && ( {/* 데스크톱 (1280px+) 테이블 뷰 */}
{renderCustomTableHeader ? ( // 커스텀 테이블 헤더 사용 (동적 컬럼용) renderCustomTableHeader() ) : ( // 기본 테이블 헤더 <> {showCheckbox && ( )} {tableColumns.map((column) => { // "actions" 컬럼은 항상 렌더링하되, 선택된 항목이 없을 때는 빈 헤더로 표시 return ( {column.key === "actions" && selectedItems.size === 0 ? "" : column.label} ); })} )} {data.length === 0 ? ( 검색 결과가 없습니다. ) : ( // 백엔드가 created_at ASC로 정렬해서 보내줌 (오래된 순) data.map((item, index) => { const itemId = getItemId(item); // 순차 번호: startIndex 기준으로 1부터 시작 const globalIndex = startIndex + index + 1; return ( {renderTableRow(item, index, globalIndex)} ); }) )} {tableFooter && ( {tableFooter} )}
))} {/* 페이지네이션 - 데스크톱에서만 표시 */}
전체 {pagination.totalItems}개 중 {pagination.totalItems > 0 ? startIndex + 1 : 0}-{Math.min(startIndex + pagination.itemsPerPage, pagination.totalItems)}개 표시
{pagination.totalPages > 1 && (
{Array.from({ length: pagination.totalPages }, (_, i) => i + 1).map((page) => { // 현재 페이지 근처만 표시 if ( page === 1 || page === pagination.totalPages || (page >= pagination.currentPage - 2 && page <= pagination.currentPage + 2) ) { return ( ); } else if ( page === pagination.currentPage - 3 || page === pagination.currentPage + 3 ) { return ...; } return null; })}
)}
{/* 일괄 삭제 확인 다이얼로그 - 단일 삭제와 동일한 디자인 */} ⚠️ 삭제 확인

선택한 {selectedItems.size}개의 항목을 삭제하시겠습니까?

⚠️
주의
삭제된 항목은 복구할 수 없습니다. 관련된 데이터도 함께 삭제될 수 있습니다.
취소 삭제
); } // 필터 관련 타입 재export (다른 페이지에서 사용 가능) export type { FilterFieldConfig, FilterValues } from "@/components/molecules/MobileFilter";