"use client"; import React, { useState, useEffect, useMemo } from "react"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Loader2, Save, X, Layers, Table as TableIcon } from "lucide-react"; import { RepeatScreenModalProps, CardData, CardColumnConfig, GroupedCardData, CardRowData, AggregationConfig, TableColumnConfig, CardContentRowConfig, AggregationDisplayConfig, } from "./types"; import { ComponentRendererProps } from "@/types/component"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; export interface RepeatScreenModalComponentProps extends ComponentRendererProps { config?: RepeatScreenModalProps; groupedData?: Record[]; // EditModal에서 전달하는 그룹 데이터 } export function RepeatScreenModalComponent({ component, isDesignMode = false, formData, onFormDataChange, config, className, groupedData: propsGroupedData, // EditModal에서 전달받는 그룹 데이터 ...props }: RepeatScreenModalComponentProps) { // props에서도 groupedData를 추출 (DynamicWebTypeRenderer에서 전달될 수 있음) const groupedData = propsGroupedData || (props as any).groupedData; const componentConfig = { ...config, ...component?.config, }; // 설정 값 추출 const dataSource = componentConfig?.dataSource; const saveMode = componentConfig?.saveMode || "all"; const cardSpacing = componentConfig?.cardSpacing || "24px"; const showCardBorder = componentConfig?.showCardBorder ?? true; const showCardTitle = componentConfig?.showCardTitle ?? true; const cardTitle = componentConfig?.cardTitle || "카드 {index}"; const grouping = componentConfig?.grouping; // 🆕 v3: 자유 레이아웃 const contentRows = componentConfig?.contentRows || []; // (레거시 호환) const cardLayout = componentConfig?.cardLayout || []; const cardMode = componentConfig?.cardMode || "simple"; const tableLayout = componentConfig?.tableLayout; // 상태 const [rawData, setRawData] = useState([]); // 원본 데이터 const [cardsData, setCardsData] = useState([]); // simple 모드용 const [groupedCardsData, setGroupedCardsData] = useState([]); // withTable 모드용 const [isLoading, setIsLoading] = useState(false); const [loadError, setLoadError] = useState(null); const [isSaving, setIsSaving] = useState(false); // 초기 데이터 로드 useEffect(() => { const loadInitialData = async () => { console.log("[RepeatScreenModal] 데이터 로드 시작"); console.log("[RepeatScreenModal] groupedData (from EditModal):", groupedData); console.log("[RepeatScreenModal] formData:", formData); console.log("[RepeatScreenModal] dataSource:", dataSource); setIsLoading(true); setLoadError(null); try { let loadedData: any[] = []; // 🆕 우선순위 1: EditModal에서 전달받은 groupedData 사용 if (groupedData && groupedData.length > 0) { console.log("[RepeatScreenModal] groupedData 사용:", groupedData.length, "건"); loadedData = groupedData; } // 우선순위 2: API 호출 else if (dataSource && dataSource.sourceTable) { // 필터 조건 생성 const filters: Record = {}; // formData에서 선택된 행 ID 가져오기 let selectedIds: any[] = []; if (formData) { // 1. 명시적으로 설정된 filterField 확인 if (dataSource.filterField) { const filterValue = formData[dataSource.filterField]; if (filterValue) { selectedIds = Array.isArray(filterValue) ? filterValue : [filterValue]; } } // 2. 일반적인 선택 필드 확인 (fallback) if (selectedIds.length === 0) { const commonFields = ['selectedRows', 'selectedIds', 'checkedRows', 'checkedIds', 'ids']; for (const field of commonFields) { if (formData[field]) { const value = formData[field]; selectedIds = Array.isArray(value) ? value : [value]; console.log(`[RepeatScreenModal] ${field}에서 선택된 ID 발견:`, selectedIds); break; } } } // 3. formData에 id가 있으면 단일 행 if (selectedIds.length === 0 && formData.id) { selectedIds = [formData.id]; console.log("[RepeatScreenModal] formData.id 사용:", selectedIds); } } console.log("[RepeatScreenModal] 최종 선택된 ID:", selectedIds); // 선택된 ID가 있으면 필터 적용 if (selectedIds.length > 0) { filters.id = selectedIds; } else { console.warn("[RepeatScreenModal] 선택된 데이터가 없습니다."); setRawData([]); setCardsData([]); setGroupedCardsData([]); setIsLoading(false); return; } console.log("[RepeatScreenModal] API 필터:", filters); // API 호출 const response = await apiClient.post(`/table-management/tables/${dataSource.sourceTable}/data`, { search: filters, page: 1, size: 1000, }); if (response.data.success && response.data.data?.data) { loadedData = response.data.data.data; } } else { console.log("[RepeatScreenModal] 데이터 소스 없음"); setRawData([]); setCardsData([]); setGroupedCardsData([]); setIsLoading(false); return; } console.log("[RepeatScreenModal] 로드된 데이터:", loadedData.length, "건"); if (loadedData.length === 0) { setRawData([]); setCardsData([]); setGroupedCardsData([]); setIsLoading(false); return; } setRawData(loadedData); // 🆕 v3: contentRows가 있으면 새로운 방식 사용 const useNewLayout = contentRows && contentRows.length > 0; // 그룹핑 모드 확인 (groupByField가 없어도 enabled면 그룹핑 모드로 처리) const useGrouping = grouping?.enabled; if (useGrouping) { // 그룹핑 모드 const grouped = processGroupedData(loadedData, grouping); setGroupedCardsData(grouped); } else { // 단순 모드: 각 행이 하나의 카드 const initialCards: CardData[] = await Promise.all( loadedData.map(async (row: any, index: number) => ({ _cardId: `card-${index}-${Date.now()}`, _originalData: { ...row }, _isDirty: false, ...(await loadCardData(row)), })) ); setCardsData(initialCards); } } catch (error: any) { console.error("데이터 로드 실패:", error); setLoadError(error.message || "데이터 로드 중 오류가 발생했습니다."); } finally { setIsLoading(false); } }; loadInitialData(); }, [dataSource, formData, groupedData, contentRows, grouping?.enabled, grouping?.groupByField]); // 그룹화된 데이터 처리 const processGroupedData = (data: any[], groupingConfig: typeof grouping): GroupedCardData[] => { if (!groupingConfig?.enabled) { return []; } const groupByField = groupingConfig.groupByField; const groupMap = new Map(); // groupByField가 없으면 각 행을 개별 그룹으로 처리 if (!groupByField) { // 각 행이 하나의 카드 (그룹) data.forEach((row, index) => { const groupKey = `row-${index}`; groupMap.set(groupKey, [row]); }); } else { // 그룹별로 데이터 분류 data.forEach((row) => { const groupKey = String(row[groupByField] || ""); if (!groupMap.has(groupKey)) { groupMap.set(groupKey, []); } groupMap.get(groupKey)!.push(row); }); } // GroupedCardData 생성 const result: GroupedCardData[] = []; let cardIndex = 0; groupMap.forEach((rows, groupKey) => { // 집계 계산 const aggregations: Record = {}; if (groupingConfig.aggregations) { groupingConfig.aggregations.forEach((agg) => { aggregations[agg.resultField] = calculateAggregation(rows, agg); }); } // 행 데이터 생성 const cardRows: CardRowData[] = rows.map((row, idx) => ({ _rowId: `row-${cardIndex}-${idx}-${Date.now()}`, _originalData: { ...row }, _isDirty: false, ...row, })); result.push({ _cardId: `grouped-card-${cardIndex}-${Date.now()}`, _groupKey: groupKey, _groupField: groupByField || "", _aggregations: aggregations, _rows: cardRows, _representativeData: rows[0] || {}, }); cardIndex++; }); return result; }; // 집계 계산 const calculateAggregation = (rows: any[], agg: AggregationConfig): number => { const values = rows.map((row) => Number(row[agg.sourceField]) || 0); switch (agg.type) { case "sum": return values.reduce((a, b) => a + b, 0); case "count": return values.length; case "avg": return values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; case "min": return values.length > 0 ? Math.min(...values) : 0; case "max": return values.length > 0 ? Math.max(...values) : 0; default: return 0; } }; // 카드 데이터 로드 (소스 설정에 따라) const loadCardData = async (originalData: any): Promise> => { const cardData: Record = {}; // 🆕 v3: contentRows 사용 if (contentRows && contentRows.length > 0) { for (const contentRow of contentRows) { // 헤더/필드 타입의 컬럼 처리 if ((contentRow.type === "header" || contentRow.type === "fields") && contentRow.columns) { for (const col of contentRow.columns) { if (col.sourceConfig) { if (col.sourceConfig.type === "direct") { cardData[col.field] = originalData[col.sourceConfig.sourceColumn || col.field]; } else if (col.sourceConfig.type === "join" && col.sourceConfig.joinTable) { cardData[col.field] = null; // 조인은 나중에 일괄 처리 } else if (col.sourceConfig.type === "manual") { cardData[col.field] = null; } } else { // sourceConfig가 없으면 원본 데이터에서 직접 가져옴 cardData[col.field] = originalData[col.field]; } } } // 테이블 타입의 컬럼 처리 if (contentRow.type === "table" && contentRow.tableColumns) { for (const col of contentRow.tableColumns) { cardData[col.field] = originalData[col.field]; } } } } else { // 레거시: cardLayout 사용 for (const row of cardLayout) { for (const col of row.columns) { if (col.sourceConfig) { if (col.sourceConfig.type === "direct") { cardData[col.field] = originalData[col.sourceConfig.sourceColumn || col.field]; } else if (col.sourceConfig.type === "join" && col.sourceConfig.joinTable) { cardData[col.field] = null; // 조인은 나중에 일괄 처리 } else if (col.sourceConfig.type === "manual") { cardData[col.field] = null; } } else { cardData[col.field] = originalData[col.field]; } } } } return cardData; }; // Simple 모드: 카드 데이터 변경 const handleCardDataChange = (cardId: string, field: string, value: any) => { setCardsData((prev) => prev.map((card) => (card._cardId === cardId ? { ...card, [field]: value, _isDirty: true } : card)) ); }; // WithTable 모드: 행 데이터 변경 const handleRowDataChange = (cardId: string, rowId: string, field: string, value: any) => { setGroupedCardsData((prev) => prev.map((card) => { if (card._cardId !== cardId) return card; const updatedRows = card._rows.map((row) => row._rowId === rowId ? { ...row, [field]: value, _isDirty: true } : row ); // 집계값 재계산 const newAggregations: Record = {}; if (grouping?.aggregations) { grouping.aggregations.forEach((agg) => { newAggregations[agg.resultField] = calculateAggregation(updatedRows, agg); }); } return { ...card, _rows: updatedRows, _aggregations: newAggregations, }; }) ); }; // 카드 제목 생성 const getCardTitle = (data: Record, index: number): string => { let title = cardTitle; title = title.replace("{index}", String(index + 1)); const matches = title.match(/\{(\w+)\}/g); if (matches) { matches.forEach((match) => { const field = match.slice(1, -1); const value = data[field] || ""; title = title.replace(match, String(value)); }); } return title; }; // 전체 저장 const handleSaveAll = async () => { setIsSaving(true); try { if (cardMode === "withTable") { await saveGroupedData(); } else { await saveSimpleData(); } alert("저장되었습니다."); } catch (error: any) { console.error("저장 실패:", error); alert(`저장 중 오류가 발생했습니다: ${error.message}`); } finally { setIsSaving(false); } }; // Simple 모드 저장 const saveSimpleData = async () => { const dirtyCards = cardsData.filter((card) => card._isDirty); if (dirtyCards.length === 0) { alert("변경된 데이터가 없습니다."); return; } const groupedData: Record = {}; for (const card of dirtyCards) { for (const row of cardLayout) { for (const col of row.columns) { if (col.targetConfig && col.targetConfig.saveEnabled !== false) { const targetTable = col.targetConfig.targetTable; const targetColumn = col.targetConfig.targetColumn; const value = card[col.field]; if (!groupedData[targetTable]) { groupedData[targetTable] = []; } let existingRow = groupedData[targetTable].find((r) => r._cardId === card._cardId); if (!existingRow) { existingRow = { _cardId: card._cardId, _originalData: card._originalData, }; groupedData[targetTable].push(existingRow); } existingRow[targetColumn] = value; } } } } await saveToTables(groupedData); setCardsData((prev) => prev.map((card) => ({ ...card, _isDirty: false }))); }; // WithTable 모드 저장 const saveGroupedData = async () => { const dirtyCards = groupedCardsData.filter((card) => card._rows.some((row) => row._isDirty)); if (dirtyCards.length === 0) { alert("변경된 데이터가 없습니다."); return; } const groupedData: Record = {}; for (const card of dirtyCards) { const dirtyRows = card._rows.filter((row) => row._isDirty); for (const row of dirtyRows) { // 테이블 컬럼에서 저장 대상 추출 if (tableLayout?.tableColumns) { for (const col of tableLayout.tableColumns) { if (col.editable && col.targetConfig && col.targetConfig.saveEnabled !== false) { const targetTable = col.targetConfig.targetTable; const targetColumn = col.targetConfig.targetColumn; const value = row[col.field]; if (!groupedData[targetTable]) { groupedData[targetTable] = []; } let existingRow = groupedData[targetTable].find((r) => r._rowId === row._rowId); if (!existingRow) { existingRow = { _rowId: row._rowId, _originalData: row._originalData, }; groupedData[targetTable].push(existingRow); } existingRow[targetColumn] = value; } } } } } await saveToTables(groupedData); setGroupedCardsData((prev) => prev.map((card) => ({ ...card, _rows: card._rows.map((row) => ({ ...row, _isDirty: false })), })) ); }; // 테이블별 저장 const saveToTables = async (groupedData: Record) => { const savePromises = Object.entries(groupedData).map(async ([tableName, rows]) => { return Promise.all( rows.map(async (row) => { const { _cardId, _rowId, _originalData, ...dataToSave } = row; const id = _originalData?.id; if (id) { await apiClient.put(`/table-management/tables/${tableName}/data/${id}`, dataToSave); } else { await apiClient.post(`/table-management/tables/${tableName}/data`, dataToSave); } }) ); }); await Promise.all(savePromises); }; // 수정 여부 확인 const hasDirtyData = useMemo(() => { if (cardMode === "withTable") { return groupedCardsData.some((card) => card._rows.some((row) => row._isDirty)); } return cardsData.some((c) => c._isDirty); }, [cardMode, cardsData, groupedCardsData]); // 디자인 모드 렌더링 if (isDesignMode) { // 행 타입별 개수 계산 const rowTypeCounts = { header: contentRows.filter((r) => r.type === "header").length, aggregation: contentRows.filter((r) => r.type === "aggregation").length, table: contentRows.filter((r) => r.type === "table").length, fields: contentRows.filter((r) => r.type === "fields").length, }; return (
{/* 아이콘 */}
{/* 제목 */}
Repeat Screen Modal
반복 화면 모달
v3 자유 레이아웃
{/* 행 구성 정보 */}
{contentRows.length > 0 ? ( <> {rowTypeCounts.header > 0 && ( 헤더 {rowTypeCounts.header}개 )} {rowTypeCounts.aggregation > 0 && ( 집계 {rowTypeCounts.aggregation}개 )} {rowTypeCounts.table > 0 && ( 테이블 {rowTypeCounts.table}개 )} {rowTypeCounts.fields > 0 && ( 필드 {rowTypeCounts.fields}개 )} ) : ( 행 없음 )}
{/* 통계 정보 */}
{contentRows.length}
행 (Rows)
{grouping?.aggregations?.length || 0}
집계 설정
{dataSource?.sourceTable ? 1 : 0}
데이터 소스
{/* 데이터 소스 정보 */} {dataSource?.sourceTable && (
소스 테이블: {dataSource.sourceTable} {dataSource.filterField && (필터: {dataSource.filterField})}
)} {/* 그룹핑 정보 */} {grouping?.enabled && (
그룹핑: {grouping.groupByField}
)} {/* 카드 제목 정보 */} {showCardTitle && cardTitle && (
카드 제목: {cardTitle}
)} {/* 설정 안내 */}
오른쪽 패널에서 행을 추가하고 타입(헤더/집계/테이블/필드)을 선택하세요
); } // 로딩 상태 if (isLoading) { return (
데이터를 불러오는 중...
); } // 오류 상태 if (loadError) { return (
데이터 로드 실패

{loadError}

); } // 🆕 v3: 자유 레이아웃 렌더링 (contentRows 사용) const useNewLayout = contentRows && contentRows.length > 0; const useGrouping = grouping?.enabled; // 그룹핑 모드 렌더링 if (useGrouping) { return (
{groupedCardsData.map((card, cardIndex) => ( r._isDirty) && "border-primary shadow-lg" )} > {/* 카드 제목 (선택사항) */} {showCardTitle && ( {getCardTitle(card._representativeData, cardIndex)} {card._rows.some((r) => r._isDirty) && ( 수정됨 )} )} {/* 🆕 v3: contentRows 기반 렌더링 */} {useNewLayout ? ( contentRows.map((contentRow, rowIndex) => (
{renderContentRow(contentRow, card, grouping?.aggregations || [], handleRowDataChange)}
)) ) : ( // 레거시: tableLayout 사용 <> {tableLayout?.headerRows && tableLayout.headerRows.length > 0 && (
{tableLayout.headerRows.map((row, rowIndex) => (
{row.columns.map((col, colIndex) => (
{renderHeaderColumn(col, card, grouping?.aggregations || [])}
))}
))}
)} {tableLayout?.tableColumns && tableLayout.tableColumns.length > 0 && (
{tableLayout.tableColumns.map((col) => ( {col.label} ))} {card._rows.map((row) => ( {tableLayout.tableColumns.map((col) => ( {renderTableCell(col, row, (value) => handleRowDataChange(card._cardId, row._rowId, col.field, value) )} ))} ))}
)} )}
))}
{/* 저장 버튼 */} {groupedCardsData.length > 0 && (
)} {/* 데이터 없음 */} {groupedCardsData.length === 0 && !isLoading && (
표시할 데이터가 없습니다.
)}
); } // 단순 모드 렌더링 (그룹핑 없음) return (
{cardsData.map((card, cardIndex) => ( {/* 카드 제목 (선택사항) */} {showCardTitle && ( {getCardTitle(card, cardIndex)} {card._isDirty && (수정됨)} )} {/* 🆕 v3: contentRows 기반 렌더링 */} {useNewLayout ? ( contentRows.map((contentRow, rowIndex) => (
{renderSimpleContentRow(contentRow, card, (value, field) => handleCardDataChange(card._cardId, field, value) )}
)) ) : ( // 레거시: cardLayout 사용 cardLayout.map((row, rowIndex) => (
{row.columns.map((col, colIndex) => (
{renderColumn(col, card, (value) => handleCardDataChange(card._cardId, col.field, value))}
))}
)) )}
))}
{/* 저장 버튼 */} {cardsData.length > 0 && (
)} {/* 데이터 없음 */} {cardsData.length === 0 && !isLoading && (
표시할 데이터가 없습니다.
)}
); } // 🆕 v3: contentRow 렌더링 (그룹핑 모드) function renderContentRow( contentRow: CardContentRowConfig, card: GroupedCardData, aggregations: AggregationConfig[], onRowDataChange: (cardId: string, rowId: string, field: string, value: any) => void ) { switch (contentRow.type) { case "header": case "fields": // contentRow에서 직접 columns 가져오기 (v3 구조) const headerColumns = contentRow.columns || []; if (headerColumns.length === 0) { return (
헤더 컬럼이 설정되지 않았습니다.
); } return (
{headerColumns.map((col, colIndex) => (
{renderHeaderColumn(col, card, aggregations)}
))}
); case "aggregation": // contentRow에서 직접 aggregationFields 가져오기 (v3 구조) const aggFields = contentRow.aggregationFields || []; if (aggFields.length === 0) { return (
집계 필드가 설정되지 않았습니다. (레이아웃 탭에서 집계 필드를 추가하세요)
); } return (
{aggFields.map((aggField, fieldIndex) => { // 집계 결과에서 값 가져오기 (aggregationResultField 사용) const value = card._aggregations?.[aggField.aggregationResultField] || 0; return (
{aggField.label || aggField.aggregationResultField}
{typeof value === "number" ? value.toLocaleString() : value || "-"}
); })}
); case "table": // contentRow에서 직접 tableColumns 가져오기 (v3 구조) const tableColumns = contentRow.tableColumns || []; if (tableColumns.length === 0) { return (
테이블 컬럼이 설정되지 않았습니다. (레이아웃 탭에서 테이블 컬럼을 추가하세요)
); } return (
{contentRow.tableTitle && (
{contentRow.tableTitle}
)} {contentRow.showTableHeader !== false && ( {tableColumns.map((col) => ( {col.label} ))} )} {card._rows.map((row) => ( {tableColumns.map((col) => ( {renderTableCell(col, row, (value) => onRowDataChange(card._cardId, row._rowId, col.field, value) )} ))} ))}
); default: return null; } } // 🆕 v3: contentRow 렌더링 (단순 모드) function renderSimpleContentRow( contentRow: CardContentRowConfig, card: CardData, onChange: (value: any, field: string) => void ) { switch (contentRow.type) { case "header": case "fields": return (
{(contentRow.columns || []).map((col, colIndex) => (
{renderColumn(col, card, (value) => onChange(value, col.field))}
))}
); case "aggregation": // 단순 모드에서도 집계 표시 (단일 카드 기준) // contentRow에서 직접 aggregationFields 가져오기 (v3 구조) const aggFields = contentRow.aggregationFields || []; if (aggFields.length === 0) { return (
집계 필드가 설정되지 않았습니다.
); } return (
{aggFields.map((aggField, fieldIndex) => { // 단순 모드에서는 카드 데이터에서 직접 값을 가져옴 (aggregationResultField 사용) const value = card[aggField.aggregationResultField] || card._originalData?.[aggField.aggregationResultField]; return (
{aggField.label || aggField.aggregationResultField}
{typeof value === "number" ? value.toLocaleString() : value || "-"}
); })}
); case "table": // 단순 모드에서도 테이블 표시 (단일 행) // contentRow에서 직접 tableColumns 가져오기 (v3 구조) const tableColumns = contentRow.tableColumns || []; if (tableColumns.length === 0) { return (
테이블 컬럼이 설정되지 않았습니다.
); } return (
{contentRow.tableTitle && (
{contentRow.tableTitle}
)} {contentRow.showTableHeader !== false && ( {tableColumns.map((col) => ( {col.label} ))} )} {/* 단순 모드: 카드 자체가 하나의 행 */} {tableColumns.map((col) => ( {renderSimpleTableCell(col, card, (value) => onChange(value, col.field))} ))}
); default: return null; } } // 단순 모드 테이블 셀 렌더링 function renderSimpleTableCell( col: TableColumnConfig, card: CardData, onChange: (value: any) => void ) { const value = card[col.field] || card._originalData?.[col.field]; if (!col.editable) { // 읽기 전용 if (col.type === "number") { return typeof value === "number" ? value.toLocaleString() : value || "-"; } return value || "-"; } // 편집 가능 switch (col.type) { case "number": return ( onChange(parseFloat(e.target.value) || 0)} className="h-8 text-sm" /> ); case "date": return ( onChange(e.target.value)} className="h-8 text-sm" /> ); case "select": return ( ); default: return ( onChange(e.target.value)} className="h-8 text-sm" /> ); } } // 배경색 클래스 변환 function getBackgroundClass(color: string): string { const colorMap: Record = { blue: "bg-blue-50 dark:bg-blue-950", green: "bg-green-50 dark:bg-green-950", purple: "bg-purple-50 dark:bg-purple-950", orange: "bg-orange-50 dark:bg-orange-950", }; return colorMap[color] || ""; } // 헤더 컬럼 렌더링 (집계값 포함) function renderHeaderColumn( col: CardColumnConfig, card: GroupedCardData, aggregations: AggregationConfig[] ) { let value: any; // 집계값 타입이면 집계 결과에서 가져옴 if (col.type === "aggregation" && col.aggregationField) { value = card._aggregations[col.aggregationField]; const aggConfig = aggregations.find((a) => a.resultField === col.aggregationField); return (
{typeof value === "number" ? value.toLocaleString() : value || "-"} {aggConfig && ({aggConfig.type})}
); } // 일반 필드는 대표 데이터에서 가져옴 value = card._representativeData[col.field]; return (
{value || "-"}
); } // 테이블 셀 렌더링 function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (value: any) => void) { const value = row[col.field]; // Badge 타입 if (col.type === "badge") { const badgeColor = col.badgeColorMap?.[value] || col.badgeVariant || "default"; return {value || "-"}; } // 읽기 전용 if (!col.editable) { if (col.type === "number") { return {typeof value === "number" ? value.toLocaleString() : value || "-"}; } return {value || "-"}; } // 편집 가능 switch (col.type) { case "text": return ( onChange(e.target.value)} className="h-8 text-sm" /> ); case "number": return ( onChange(Number(e.target.value) || 0)} className="h-8 text-sm text-right" /> ); case "date": return ( onChange(e.target.value)} className="h-8 text-sm" /> ); default: return {value || "-"}; } } // 컬럼 렌더링 함수 (Simple 모드) function renderColumn(col: CardColumnConfig, card: CardData, onChange: (value: any) => void) { const value = card[col.field]; const isReadOnly = !col.editable; return (
{isReadOnly && (
{value || "-"}
)} {!isReadOnly && ( <> {col.type === "text" && ( onChange(e.target.value)} placeholder={col.placeholder} className="h-10 text-sm" /> )} {col.type === "number" && ( onChange(e.target.value)} placeholder={col.placeholder} className="h-10 text-sm" /> )} {col.type === "date" && ( onChange(e.target.value)} className="h-10 text-sm" /> )} {col.type === "select" && ( )} {col.type === "textarea" && (