"use client"; /** * GridCellDropZone — 그리드 양식의 데이터 연결 드롭 존 * * 모든 셀에 드롭 가능. 헤더 영역(좌측/상단)은 배경색으로 시각 구분. * 푸터 영역(하단)은 요약 집계 셀로 시각 구분. * - 헤더 영역 드롭 → 고정 라벨(컬럼명)로 설정 * - 데이터 영역 드롭 → 데이터 바인딩(field)으로 설정 * - 푸터 영역 → 요약(집계) 타입 설정 가능 */ import React from "react"; import { useDrop } from "react-dnd"; import { X, Database, Type, Calculator } from "lucide-react"; import { TABLE_COLUMN_DND_TYPE } from "./TableColumnPalette"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import type { GridCell } from "@/types/report"; interface PaletteDragItem { columnName: string; dataType: string; } interface GridCellDropZoneProps { cells: GridCell[]; rowCount: number; colCount: number; colWidths: number[]; rowHeights: number[]; headerRows?: number; headerCols?: number; footerRows?: number; onCellDrop: (row: number, col: number, columnName: string) => void; onHeaderDrop: (row: number, col: number, columnName: string) => void; onCellClear: (row: number, col: number) => void; onFooterCellClick?: (row: number, col: number) => void; } const SUMMARY_LABELS: Record = { SUM: "합계", AVG: "평균", COUNT: "개수", }; // ─── 통합 드롭 셀 ─────────────────────────────────────────────────────────── interface DropCellProps { cell: GridCell; width: number; height: number; isHeader: boolean; isFooter: boolean; onDrop: (columnName: string) => void; onClear: () => void; onFooterClick?: () => void; } function DropCell({ cell, width, height, isHeader, isFooter, onDrop, onClear, onFooterClick }: DropCellProps) { const isField = cell.cellType === "field" && !!cell.field; const isLabel = cell.cellType === "static" && !!cell.value; const hasSummary = !!cell.summaryType && cell.summaryType !== "NONE"; const [{ isOver, canDrop }, drop] = useDrop( () => ({ accept: TABLE_COLUMN_DND_TYPE, drop: (item: PaletteDragItem) => onDrop(item.columnName), canDrop: () => !cell.merged, collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop(), }), }), [cell, onDrop], ); if (cell.merged) return null; const rSpan = cell.rowSpan ?? 1; const cSpan = cell.colSpan ?? 1; let bg = isHeader ? "#f3f4f6" : isFooter ? "#f8fafc" : "white"; if (isOver && canDrop) bg = "#dbeafe"; else if (hasSummary && isFooter) bg = "#eff6ff"; else if (isField) bg = "#eff6ff"; else if (isLabel && isHeader) bg = "#f0fdf4"; const renderContent = () => { if (isFooter && hasSummary) { return (
{SUMMARY_LABELS[cell.summaryType!] || cell.summaryType} {cell.field && ({cell.field})}
); } if (isFooter && !isField && !isLabel) { return ( 클릭하여 설정 ); } if (isField) { return (
{cell.field}
); } if (isLabel) { return (
{cell.value}
); } return ; }; return ( { drop(node); }} rowSpan={rSpan > 1 ? rSpan : undefined} colSpan={cSpan > 1 ? cSpan : undefined} className={`border ${isHeader ? "border-gray-300" : isFooter ? "border-blue-200" : "border-gray-200"}`} style={{ width, height: Math.max(height, 32), minWidth: width, backgroundColor: bg, padding: "3px 6px", verticalAlign: "middle", transition: "background-color 150ms", }} > {renderContent()} ); } // ─── 메인 컴포넌트 ────────────────────────────────────────────────────────── export function GridCellDropZone({ cells, rowCount, colCount, colWidths, rowHeights, headerRows = 1, headerCols = 1, footerRows = 0, onCellDrop, onHeaderDrop, onCellClear, onFooterCellClick, }: GridCellDropZoneProps) { const totalWidth = colWidths.reduce((a, b) => a + b, 0); const findCell = (row: number, col: number) => cells.find((c) => c.row === row && c.col === col); if (cells.length === 0 || rowCount === 0) { return (
레이아웃 탭에서 격자를 먼저 구성하세요.
); } const fieldCount = cells.filter( (c) => (c.cellType === "field" || (c.cellType === "static" && c.value)) && !c.merged, ).length; const totalNonMerged = cells.filter((c) => !c.merged).length; const footerStartRow = rowCount - footerRows; return (
열 격자 배치 {fieldCount}/{totalNonMerged} 배치됨
{colWidths.map((w, i) => ( ))} {Array.from({ length: rowCount }).map((_, r) => { const tds: React.ReactNode[] = []; for (let c = 0; c < colCount; c++) { const cell = findCell(r, c); if (!cell || cell.merged) continue; const cSpan = cell.colSpan ?? 1; const rSpan = cell.rowSpan ?? 1; const w = colWidths.slice(c, c + cSpan).reduce((a, b) => a + b, 0); const h = rowHeights.slice(r, r + rSpan).reduce((a, b) => a + b, 0); const isHeader = r < headerRows || c < headerCols; const isFooter = footerRows > 0 && r >= footerStartRow; const cellNode = ( isHeader ? onHeaderDrop(r, c, columnName) : onCellDrop(r, c, columnName) } onClear={() => onCellClear(r, c)} onFooterClick={isFooter && onFooterCellClick ? () => onFooterCellClick(r, c) : undefined} /> ); if (isFooter) { tds.push( {cellNode}

{cell.summaryType && cell.summaryType !== "NONE" ? `${SUMMARY_LABELS[cell.summaryType] || cell.summaryType}${cell.field ? ` (${cell.field})` : ""}` : "클릭하여 계산 방식을 선택하세요"}

, ); } else { tds.push(cellNode); } } return
{tds}; })}
); }