"use client"; /** * pop-card-list 설정 패널 (V2 - 이미지 참조 기반 재설계) * * 3개 탭: * [테이블] - 데이터 테이블 선택 * [카드 템플릿] - 헤더/이미지/본문 필드 + 레이아웃 설정 * [데이터 소스] - 조인/필터/정렬/개수 설정 */ import React, { useState, useEffect } from "react"; import { ChevronDown, ChevronRight, Plus, Trash2, Database } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import type { PopCardListConfig, CardListDataSource, CardTemplateConfig, CardHeaderConfig, CardImageConfig, CardBodyConfig, CardFieldBinding, CardColumnJoin, CardColumnFilter, CardSize, CardLayoutMode, FilterOperator, } from "../types"; import { CARD_SIZE_LABELS, CARD_LAYOUT_MODE_LABELS, DEFAULT_CARD_IMAGE, } from "../types"; import { fetchTableList, fetchTableColumns, type TableInfo, type ColumnInfo, } from "../pop-dashboard/utils/dataFetcher"; // ===== Props ===== interface ConfigPanelProps { config: PopCardListConfig | undefined; onUpdate: (config: PopCardListConfig) => void; } // ===== 기본값 ===== const DEFAULT_DATA_SOURCE: CardListDataSource = { tableName: "", }; const DEFAULT_HEADER: CardHeaderConfig = { codeField: undefined, titleField: undefined, }; const DEFAULT_IMAGE: CardImageConfig = { enabled: true, imageColumn: undefined, defaultImage: DEFAULT_CARD_IMAGE, }; const DEFAULT_BODY: CardBodyConfig = { fields: [], }; const DEFAULT_TEMPLATE: CardTemplateConfig = { header: DEFAULT_HEADER, image: DEFAULT_IMAGE, body: DEFAULT_BODY, }; const DEFAULT_CONFIG: PopCardListConfig = { dataSource: DEFAULT_DATA_SOURCE, cardTemplate: DEFAULT_TEMPLATE, layoutMode: "grid", cardsPerRow: 3, cardSize: "medium", }; // ===== 색상 옵션 ===== const COLOR_OPTIONS = [ { value: "__default__", label: "기본" }, { value: "#ef4444", label: "빨간색" }, { value: "#f97316", label: "주황색" }, { value: "#eab308", label: "노란색" }, { value: "#22c55e", label: "초록색" }, { value: "#3b82f6", label: "파란색" }, { value: "#8b5cf6", label: "보라색" }, { value: "#6b7280", label: "회색" }, ]; // ===== 메인 컴포넌트 ===== export function PopCardListConfigPanel({ config, onUpdate }: ConfigPanelProps) { // 3탭 구조: 테이블 선택 → 카드 템플릿 → 데이터 소스 const [activeTab, setActiveTab] = useState<"table" | "template" | "dataSource">( "table" ); // config가 없으면 기본값 사용 const cfg: PopCardListConfig = config || DEFAULT_CONFIG; // config 업데이트 헬퍼 const updateConfig = (partial: Partial) => { onUpdate({ ...cfg, ...partial }); }; // 테이블이 선택되었는지 확인 const hasTable = !!cfg.dataSource?.tableName; return (
{/* 탭 헤더 - 3탭 구조 */}
{/* 탭 내용 */}
{activeTab === "table" && ( )} {activeTab === "template" && ( )} {activeTab === "dataSource" && ( )}
); } // ===== 테이블 선택 탭 ===== function TableSelectTab({ config, onUpdate, }: { config: PopCardListConfig; onUpdate: (partial: Partial) => void; }) { const dataSource = config.dataSource || DEFAULT_DATA_SOURCE; const [tables, setTables] = useState([]); // 테이블 목록 로드 useEffect(() => { fetchTableList().then(setTables); }, []); return (
{/* 테이블 선택 */}

카드 리스트에 표시할 데이터가 있는 테이블을 선택하세요

{/* 선택된 테이블 정보 */} {dataSource.tableName && (

{dataSource.tableName}

선택된 테이블

)}
); } // ===== 데이터 소스 탭 ===== function DataSourceTab({ config, onUpdate, }: { config: PopCardListConfig; onUpdate: (partial: Partial) => void; }) { const dataSource = config.dataSource || DEFAULT_DATA_SOURCE; const [tables, setTables] = useState([]); const [columns, setColumns] = useState([]); // 테이블 목록 로드 useEffect(() => { fetchTableList().then(setTables); }, []); // 테이블 선택 시 컬럼 목록 로드 useEffect(() => { if (dataSource.tableName) { fetchTableColumns(dataSource.tableName).then(setColumns); } else { setColumns([]); } }, [dataSource.tableName]); const updateDataSource = (partial: Partial) => { onUpdate({ dataSource: { ...dataSource, ...partial }, }); }; // 테이블이 선택되지 않은 경우 if (!dataSource.tableName) { return (

먼저 테이블 탭에서 테이블을 선택하세요

); } return (
{/* 현재 선택된 테이블 표시 */}
{dataSource.tableName}
{/* 조인 설정 */} 0 ? `${dataSource.joins.length}개` : "없음" } > {/* 필터 설정 */} 0 ? `${dataSource.filters.length}개` : "없음" } > {/* 정렬 설정 */} {/* 표시 개수 */}
); } // ===== 카드 템플릿 탭 ===== function CardTemplateTab({ config, onUpdate, }: { config: PopCardListConfig; onUpdate: (partial: Partial) => void; }) { const dataSource = config.dataSource || DEFAULT_DATA_SOURCE; const template = config.cardTemplate || DEFAULT_TEMPLATE; const [columns, setColumns] = useState([]); // 테이블 컬럼 로드 useEffect(() => { if (dataSource.tableName) { fetchTableColumns(dataSource.tableName).then(setColumns); } else { setColumns([]); } }, [dataSource.tableName]); const updateTemplate = (partial: Partial) => { onUpdate({ cardTemplate: { ...template, ...partial }, }); }; // 테이블 미선택 시 안내 if (!dataSource.tableName) { return (

테이블을 먼저 선택해주세요

데이터 소스 탭에서 테이블을 선택하세요

); } return (
{/* 헤더 설정 */} updateTemplate({ header })} /> {/* 이미지 설정 */} updateTemplate({ image })} /> {/* 본문 필드 */} updateTemplate({ body })} /> {/* 레이아웃 설정 */}
); } // ===== 접기/펴기 섹션 컴포넌트 ===== function CollapsibleSection({ title, badge, defaultOpen = false, children, }: { title: string; badge?: string; defaultOpen?: boolean; children: React.ReactNode; }) { const [open, setOpen] = useState(defaultOpen); return (
{open &&
{children}
}
); } // ===== 헤더 설정 섹션 ===== function HeaderSettingsSection({ header, columns, onUpdate, }: { header: CardHeaderConfig; columns: ColumnInfo[]; onUpdate: (header: CardHeaderConfig) => void; }) { return (
{/* 코드 필드 */}

카드 헤더 왼쪽에 표시될 코드 (예: ITEM032)

{/* 제목 필드 */}

카드 헤더 오른쪽에 표시될 제목 (예: 너트 M10)

); } // ===== 이미지 설정 섹션 ===== function ImageSettingsSection({ image, columns, onUpdate, }: { image: CardImageConfig; columns: ColumnInfo[]; onUpdate: (image: CardImageConfig) => void; }) { return (
{/* 이미지 사용 여부 */}
onUpdate({ ...image, enabled: checked })} />
{image.enabled && ( <> {/* 기본 이미지 미리보기 */}
기본 이미지 미리보기
기본 이미지
{/* 기본 이미지 URL */}
onUpdate({ ...image, defaultImage: e.target.value || DEFAULT_CARD_IMAGE, }) } placeholder="이미지 URL 입력" className="mt-1 h-7 text-xs" />

이미지가 없는 항목에 표시될 기본 이미지

{/* 이미지 컬럼 선택 */}

DB에서 이미지 URL을 가져올 컬럼. URL이 없으면 기본 이미지 사용

)}
); } // ===== 본문 필드 섹션 ===== function BodyFieldsSection({ body, columns, onUpdate, }: { body: CardBodyConfig; columns: ColumnInfo[]; onUpdate: (body: CardBodyConfig) => void; }) { const fields = body.fields || []; // 필드 추가 const addField = () => { const newField: CardFieldBinding = { id: `field-${Date.now()}`, columnName: "", label: "", textColor: undefined, }; onUpdate({ fields: [...fields, newField] }); }; // 필드 업데이트 const updateField = (index: number, updated: CardFieldBinding) => { const newFields = [...fields]; newFields[index] = updated; onUpdate({ fields: newFields }); }; // 필드 삭제 const deleteField = (index: number) => { const newFields = fields.filter((_, i) => i !== index); onUpdate({ fields: newFields }); }; // 필드 순서 이동 const moveField = (index: number, direction: "up" | "down") => { const newIndex = direction === "up" ? index - 1 : index + 1; if (newIndex < 0 || newIndex >= fields.length) return; const newFields = [...fields]; [newFields[index], newFields[newIndex]] = [ newFields[newIndex], newFields[index], ]; onUpdate({ fields: newFields }); }; return (
{/* 필드 목록 */} {fields.length === 0 ? (

본문에 표시할 필드를 추가하세요

) : (
{fields.map((field, index) => ( updateField(index, updated)} onDelete={() => deleteField(index)} onMove={(dir) => moveField(index, dir)} /> ))}
)} {/* 필드 추가 버튼 */}
); } // ===== 필드 편집기 ===== function FieldEditor({ field, index, columns, totalCount, onUpdate, onDelete, onMove, }: { field: CardFieldBinding; index: number; columns: ColumnInfo[]; totalCount: number; onUpdate: (field: CardFieldBinding) => void; onDelete: () => void; onMove: (direction: "up" | "down") => void; }) { return (
{/* 순서 이동 버튼 */}
{/* 필드 설정 */}
{/* 라벨 */}
onUpdate({ ...field, label: e.target.value })} placeholder="예: 발주일" className="mt-1 h-7 text-xs" />
{/* 컬럼 */}
{/* 텍스트 색상 */}
{/* 삭제 버튼 */}
); } // ===== 레이아웃 설정 섹션 ===== function LayoutSettingsSection({ config, onUpdate, }: { config: PopCardListConfig; onUpdate: (partial: Partial) => void; }) { const isGridMode = config.layoutMode === "grid"; return (
{/* 카드 크기 */}
{(["small", "medium", "large"] as CardSize[]).map((size) => ( ))}
{/* 배치 방식 */}
{(["grid", "horizontal", "vertical"] as CardLayoutMode[]).map( (mode) => ( ) )}
{/* 격자 배치일 때만 한 줄 카드 수 표시 */} {isGridMode && (
)}
); } // ===== 조인 설정 섹션 ===== function JoinSettingsSection({ dataSource, tables, onUpdate, }: { dataSource: CardListDataSource; tables: TableInfo[]; onUpdate: (partial: Partial) => void; }) { const joins = dataSource.joins || []; const [sourceColumns, setSourceColumns] = useState([]); const [targetColumnsMap, setTargetColumnsMap] = useState< Record >({}); // 소스 테이블 컬럼 로드 useEffect(() => { if (dataSource.tableName) { fetchTableColumns(dataSource.tableName).then(setSourceColumns); } }, [dataSource.tableName]); // 조인 추가 const addJoin = () => { const newJoin: CardColumnJoin = { targetTable: "", joinType: "LEFT", sourceColumn: "", targetColumn: "", }; onUpdate({ joins: [...joins, newJoin] }); }; // 조인 업데이트 const updateJoin = (index: number, updated: CardColumnJoin) => { const newJoins = [...joins]; newJoins[index] = updated; onUpdate({ joins: newJoins }); // 대상 테이블 컬럼 로드 if (updated.targetTable && !targetColumnsMap[updated.targetTable]) { fetchTableColumns(updated.targetTable).then((cols) => { setTargetColumnsMap((prev) => ({ ...prev, [updated.targetTable]: cols, })); }); } }; // 조인 삭제 const deleteJoin = (index: number) => { const newJoins = joins.filter((_, i) => i !== index); onUpdate({ joins: newJoins.length > 0 ? newJoins : undefined }); }; return (
{joins.length === 0 ? (

다른 테이블과 조인하여 추가 컬럼을 사용할 수 있습니다

) : (
{joins.map((join, index) => (
조인 {index + 1}
{/* 조인 타입 */} {/* 대상 테이블 */} {/* ON 조건 */} {join.targetTable && (
=
)}
))}
)}
); } // ===== 필터 설정 섹션 ===== function FilterSettingsSection({ dataSource, columns, onUpdate, }: { dataSource: CardListDataSource; columns: ColumnInfo[]; onUpdate: (partial: Partial) => void; }) { const filters = dataSource.filters || []; const operators: { value: FilterOperator; label: string }[] = [ { value: "=", label: "=" }, { value: "!=", label: "!=" }, { value: ">", label: ">" }, { value: ">=", label: ">=" }, { value: "<", label: "<" }, { value: "<=", label: "<=" }, { value: "like", label: "LIKE" }, ]; // 필터 추가 const addFilter = () => { const newFilter: CardColumnFilter = { column: "", operator: "=", value: "", }; onUpdate({ filters: [...filters, newFilter] }); }; // 필터 업데이트 const updateFilter = (index: number, updated: CardColumnFilter) => { const newFilters = [...filters]; newFilters[index] = updated; onUpdate({ filters: newFilters }); }; // 필터 삭제 const deleteFilter = (index: number) => { const newFilters = filters.filter((_, i) => i !== index); onUpdate({ filters: newFilters.length > 0 ? newFilters : undefined }); }; return (
{filters.length === 0 ? (

필터 조건을 추가하여 데이터를 필터링할 수 있습니다

) : (
{filters.map((filter, index) => (
updateFilter(index, { ...filter, value: e.target.value }) } placeholder="값" className="h-7 flex-1 text-xs" />
))}
)}
); } // ===== 정렬 설정 섹션 ===== function SortSettingsSection({ dataSource, columns, onUpdate, }: { dataSource: CardListDataSource; columns: ColumnInfo[]; onUpdate: (partial: Partial) => void; }) { const sort = dataSource.sort; return (
{/* 정렬 사용 여부 */}
{sort && (
{/* 정렬 컬럼 */}
{/* 정렬 방향 */}
)}
); } // ===== 표시 개수 설정 섹션 ===== function LimitSettingsSection({ dataSource, onUpdate, }: { dataSource: CardListDataSource; onUpdate: (partial: Partial) => void; }) { const limit = dataSource.limit || { mode: "all" as const }; const isLimited = limit.mode === "limited"; return (
{/* 모드 선택 */}
{isLimited && (
onUpdate({ limit: { mode: "limited", count: parseInt(e.target.value, 10) || 10, }, }) } className="mt-1 h-7 text-xs" />
)}
); }