2025-12-19 15:44:38 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* UnifiedList 설정 패널
|
|
|
|
|
|
* 통합 목록 컴포넌트의 세부 설정을 관리합니다.
|
2025-12-23 13:53:22 +09:00
|
|
|
|
* - 현재 화면의 테이블 데이터를 사용
|
|
|
|
|
|
* - 테이블 컬럼 + 엔티티 조인 컬럼 선택 지원
|
2025-12-19 15:44:38 +09:00
|
|
|
|
*/
|
|
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
import React, { useState, useEffect, useMemo } from "react";
|
2025-12-19 15:44:38 +09:00
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2025-12-19 16:40:40 +09:00
|
|
|
|
import { Input } from "@/components/ui/input";
|
2025-12-23 13:53:22 +09:00
|
|
|
|
import { Database, Link2, GripVertical, ChevronDown, ChevronRight } from "lucide-react";
|
2025-12-19 16:40:40 +09:00
|
|
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
2025-12-23 13:53:22 +09:00
|
|
|
|
import { cn } from "@/lib/utils";
|
2025-12-19 15:44:38 +09:00
|
|
|
|
|
|
|
|
|
|
interface UnifiedListConfigPanelProps {
|
|
|
|
|
|
config: Record<string, any>;
|
|
|
|
|
|
onChange: (config: Record<string, any>) => void;
|
2025-12-23 13:53:22 +09:00
|
|
|
|
/** 현재 화면의 테이블명 */
|
|
|
|
|
|
currentTableName?: string;
|
2025-12-19 16:40:40 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ColumnOption {
|
|
|
|
|
|
columnName: string;
|
|
|
|
|
|
displayName: string;
|
2025-12-23 13:53:22 +09:00
|
|
|
|
isJoinColumn?: boolean;
|
|
|
|
|
|
sourceTable?: string;
|
|
|
|
|
|
inputType?: string;
|
2025-12-19 16:40:40 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
|
export const UnifiedListConfigPanel: React.FC<UnifiedListConfigPanelProps> = ({
|
|
|
|
|
|
config,
|
|
|
|
|
|
onChange,
|
2025-12-23 13:53:22 +09:00
|
|
|
|
currentTableName,
|
2025-12-19 15:44:38 +09:00
|
|
|
|
}) => {
|
2025-12-23 13:53:22 +09:00
|
|
|
|
// 컬럼 목록 (테이블 컬럼 + 엔티티 조인 컬럼)
|
2025-12-19 16:40:40 +09:00
|
|
|
|
const [columns, setColumns] = useState<ColumnOption[]>([]);
|
|
|
|
|
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
2025-12-23 13:53:22 +09:00
|
|
|
|
const [expandedJoinSections, setExpandedJoinSections] = useState<Set<string>>(new Set());
|
2025-12-19 16:40:40 +09:00
|
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
|
// 설정 업데이트 핸들러
|
|
|
|
|
|
const updateConfig = (field: string, value: any) => {
|
2025-12-23 13:53:22 +09:00
|
|
|
|
const newConfig = { ...config, [field]: value };
|
|
|
|
|
|
console.log("⚙️ UnifiedListConfigPanel updateConfig:", { field, value, newConfig });
|
|
|
|
|
|
onChange(newConfig);
|
2025-12-19 15:44:38 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
// 테이블명 (현재 화면의 테이블 사용)
|
|
|
|
|
|
const tableName = currentTableName || config.tableName;
|
2025-12-19 16:40:40 +09:00
|
|
|
|
|
2025-12-24 13:54:24 +09:00
|
|
|
|
// 화면의 테이블명을 config에 자동 저장
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (currentTableName && config.tableName !== currentTableName) {
|
|
|
|
|
|
onChange({ ...config, tableName: currentTableName });
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [currentTableName]);
|
|
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
// 테이블 컬럼 및 엔티티 조인 컬럼 로드
|
2025-12-19 16:40:40 +09:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadColumns = async () => {
|
2025-12-23 13:53:22 +09:00
|
|
|
|
if (!tableName) {
|
2025-12-19 16:40:40 +09:00
|
|
|
|
setColumns([]);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-23 13:53:22 +09:00
|
|
|
|
|
2025-12-19 16:40:40 +09:00
|
|
|
|
setLoadingColumns(true);
|
|
|
|
|
|
try {
|
2025-12-23 13:53:22 +09:00
|
|
|
|
// 1. 테이블 컬럼 로드
|
|
|
|
|
|
const columnData = await tableTypeApi.getColumns(tableName);
|
|
|
|
|
|
const baseColumns: ColumnOption[] = columnData.map((c: any) => ({
|
2025-12-19 16:40:40 +09:00
|
|
|
|
columnName: c.columnName || c.column_name,
|
2025-12-23 13:53:22 +09:00
|
|
|
|
displayName: c.displayName || c.columnLabel || c.columnName || c.column_name,
|
|
|
|
|
|
isJoinColumn: false,
|
|
|
|
|
|
inputType: c.inputType || c.input_type || c.webType || c.web_type,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 엔티티 타입 컬럼 찾기 및 조인 컬럼 정보 로드
|
|
|
|
|
|
const entityColumns = columnData.filter((c: any) => (c.inputType || c.input_type) === "entity");
|
|
|
|
|
|
|
|
|
|
|
|
const joinColumnOptions: ColumnOption[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const entityCol of entityColumns) {
|
|
|
|
|
|
const colName = entityCol.columnName || entityCol.column_name;
|
|
|
|
|
|
|
|
|
|
|
|
// referenceTable 우선순위:
|
|
|
|
|
|
// 1. 컬럼의 reference_table 필드
|
|
|
|
|
|
// 2. detailSettings.referenceTable
|
|
|
|
|
|
let referenceTable = entityCol.referenceTable || entityCol.reference_table;
|
|
|
|
|
|
|
|
|
|
|
|
if (!referenceTable) {
|
|
|
|
|
|
let detailSettings = entityCol.detailSettings || entityCol.detail_settings;
|
|
|
|
|
|
if (typeof detailSettings === "string") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
detailSettings = JSON.parse(detailSettings);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
referenceTable = detailSettings?.referenceTable;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (referenceTable) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const refColumnData = await tableTypeApi.getColumns(referenceTable);
|
|
|
|
|
|
|
|
|
|
|
|
refColumnData.forEach((refCol: any) => {
|
|
|
|
|
|
const refColName = refCol.columnName || refCol.column_name;
|
|
|
|
|
|
const refDisplayName = refCol.displayName || refCol.columnLabel || refColName;
|
|
|
|
|
|
|
|
|
|
|
|
joinColumnOptions.push({
|
|
|
|
|
|
columnName: `${colName}.${refColName}`,
|
|
|
|
|
|
displayName: refDisplayName,
|
|
|
|
|
|
isJoinColumn: true,
|
|
|
|
|
|
sourceTable: referenceTable,
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`참조 테이블 ${referenceTable} 컬럼 로드 실패:`, error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setColumns([...baseColumns, ...joinColumnOptions]);
|
2025-12-19 16:40:40 +09:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("컬럼 목록 로드 실패:", error);
|
2025-12-23 13:53:22 +09:00
|
|
|
|
setColumns([]);
|
2025-12-19 16:40:40 +09:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoadingColumns(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-12-23 13:53:22 +09:00
|
|
|
|
|
2025-12-19 16:40:40 +09:00
|
|
|
|
loadColumns();
|
2025-12-23 13:53:22 +09:00
|
|
|
|
}, [tableName]);
|
2025-12-19 16:40:40 +09:00
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
// 컬럼 설정
|
2026-01-05 15:35:19 +09:00
|
|
|
|
const configColumns: Array<{
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
width?: string;
|
|
|
|
|
|
isJoinColumn?: boolean;
|
|
|
|
|
|
inputType?: string;
|
|
|
|
|
|
thousandSeparator?: boolean;
|
|
|
|
|
|
}> = config.columns || [];
|
2025-12-19 15:44:38 +09:00
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
// 컬럼이 추가되었는지 확인
|
|
|
|
|
|
const isColumnAdded = (columnName: string) => {
|
|
|
|
|
|
return configColumns.some((col) => col.key === columnName);
|
2025-12-19 15:44:38 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
// 컬럼 토글 (추가/제거)
|
|
|
|
|
|
const toggleColumn = (column: ColumnOption) => {
|
|
|
|
|
|
if (isColumnAdded(column.columnName)) {
|
|
|
|
|
|
// 제거
|
|
|
|
|
|
const newColumns = configColumns.filter((col) => col.key !== column.columnName);
|
|
|
|
|
|
updateConfig("columns", newColumns);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 추가
|
2026-01-05 15:35:19 +09:00
|
|
|
|
const isNumberType = ["number", "decimal", "integer", "float", "double", "numeric", "currency"].includes(
|
|
|
|
|
|
column.inputType || "",
|
|
|
|
|
|
);
|
2025-12-23 13:53:22 +09:00
|
|
|
|
const newColumn = {
|
|
|
|
|
|
key: column.columnName,
|
|
|
|
|
|
title: column.displayName,
|
|
|
|
|
|
width: "",
|
|
|
|
|
|
isJoinColumn: column.isJoinColumn || false,
|
2026-01-05 15:35:19 +09:00
|
|
|
|
inputType: column.inputType,
|
|
|
|
|
|
thousandSeparator: isNumberType ? true : undefined, // 숫자 타입은 기본적으로 천단위 구분자 사용
|
2025-12-23 13:53:22 +09:00
|
|
|
|
};
|
|
|
|
|
|
updateConfig("columns", [...configColumns, newColumn]);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-05 15:35:19 +09:00
|
|
|
|
// 컬럼 천단위 구분자 토글
|
|
|
|
|
|
const toggleThousandSeparator = (columnKey: string, checked: boolean) => {
|
|
|
|
|
|
const newColumns = configColumns.map((col) => (col.key === columnKey ? { ...col, thousandSeparator: checked } : col));
|
|
|
|
|
|
updateConfig("columns", newColumns);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 숫자 타입 컬럼인지 확인
|
|
|
|
|
|
const isNumberColumn = (columnKey: string) => {
|
|
|
|
|
|
const colInfo = columns.find((c) => c.columnName === columnKey);
|
|
|
|
|
|
const configCol = configColumns.find((c) => c.key === columnKey);
|
|
|
|
|
|
const inputType = configCol?.inputType || colInfo?.inputType || "";
|
|
|
|
|
|
return ["number", "decimal", "integer", "float", "double", "numeric", "currency"].includes(inputType);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
// 컬럼 제목 수정
|
|
|
|
|
|
const updateColumnTitle = (columnKey: string, title: string) => {
|
|
|
|
|
|
const newColumns = configColumns.map((col) => (col.key === columnKey ? { ...col, title } : col));
|
2025-12-19 15:44:38 +09:00
|
|
|
|
updateConfig("columns", newColumns);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
// 그룹별 컬럼 분리
|
|
|
|
|
|
const baseColumns = useMemo(() => columns.filter((col) => !col.isJoinColumn), [columns]);
|
|
|
|
|
|
|
|
|
|
|
|
// 조인 컬럼을 소스 테이블별로 그룹화
|
|
|
|
|
|
const joinColumnsByTable = useMemo(() => {
|
|
|
|
|
|
const grouped: Record<string, ColumnOption[]> = {};
|
|
|
|
|
|
columns
|
|
|
|
|
|
.filter((col) => col.isJoinColumn)
|
|
|
|
|
|
.forEach((col) => {
|
|
|
|
|
|
const table = col.sourceTable || "unknown";
|
|
|
|
|
|
if (!grouped[table]) {
|
|
|
|
|
|
grouped[table] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
grouped[table].push(col);
|
|
|
|
|
|
});
|
|
|
|
|
|
return grouped;
|
|
|
|
|
|
}, [columns]);
|
|
|
|
|
|
|
|
|
|
|
|
// 조인 섹션 토글
|
|
|
|
|
|
const toggleJoinSection = (tableName: string) => {
|
|
|
|
|
|
setExpandedJoinSections((prev) => {
|
|
|
|
|
|
const newSet = new Set(prev);
|
|
|
|
|
|
if (newSet.has(tableName)) {
|
|
|
|
|
|
newSet.delete(tableName);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newSet.add(tableName);
|
|
|
|
|
|
}
|
|
|
|
|
|
return newSet;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{/* 뷰 모드 */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium">표시 방식</Label>
|
2025-12-23 13:53:22 +09:00
|
|
|
|
<Select value={config.viewMode || "table"} onValueChange={(value) => updateConfig("viewMode", value)}>
|
2025-12-19 15:44:38 +09:00
|
|
|
|
<SelectTrigger className="h-8 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="방식 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="table">테이블</SelectItem>
|
|
|
|
|
|
<SelectItem value="card">카드</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
{/* 카드 모드 설정 */}
|
|
|
|
|
|
{config.viewMode === "card" && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<Label className="text-xs font-medium">카드 설정</Label>
|
2025-12-19 15:44:38 +09:00
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
{/* 제목 컬럼 */}
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-muted-foreground text-[10px]">제목 컬럼</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={config.cardConfig?.titleColumn || ""}
|
|
|
|
|
|
onValueChange={(value) => updateConfig("cardConfig", { ...config.cardConfig, titleColumn: value })}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-7 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{configColumns.map((col: any) => (
|
|
|
|
|
|
<SelectItem key={col.key} value={col.key}>
|
|
|
|
|
|
{col.title}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
2025-12-19 15:44:38 +09:00
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
{/* 부제목 컬럼 */}
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-muted-foreground text-[10px]">부제목 컬럼</Label>
|
2025-12-19 16:40:40 +09:00
|
|
|
|
<Select
|
2025-12-23 13:53:22 +09:00
|
|
|
|
value={config.cardConfig?.subtitleColumn || "_none_"}
|
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
|
updateConfig("cardConfig", { ...config.cardConfig, subtitleColumn: value === "_none_" ? "" : value })
|
|
|
|
|
|
}
|
2025-12-19 16:40:40 +09:00
|
|
|
|
>
|
2025-12-23 13:53:22 +09:00
|
|
|
|
<SelectTrigger className="h-7 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="선택 (선택사항)" />
|
2025-12-19 16:40:40 +09:00
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
2025-12-23 13:53:22 +09:00
|
|
|
|
<SelectItem value="_none_">없음</SelectItem>
|
|
|
|
|
|
{configColumns.map((col: any) => (
|
|
|
|
|
|
<SelectItem key={col.key} value={col.key}>
|
|
|
|
|
|
{col.title}
|
2025-12-19 16:40:40 +09:00
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-12-23 13:53:22 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 행당 카드 수 */}
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-muted-foreground text-[10px]">행당 카드 수</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={String(config.cardConfig?.cardsPerRow || 3)}
|
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
|
updateConfig("cardConfig", { ...config.cardConfig, cardsPerRow: parseInt(value) })
|
|
|
|
|
|
}
|
2025-12-19 15:44:38 +09:00
|
|
|
|
>
|
2025-12-23 13:53:22 +09:00
|
|
|
|
<SelectTrigger className="h-7 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="1">1개</SelectItem>
|
|
|
|
|
|
<SelectItem value="2">2개</SelectItem>
|
|
|
|
|
|
<SelectItem value="3">3개</SelectItem>
|
|
|
|
|
|
<SelectItem value="4">4개</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-12-19 15:44:38 +09:00
|
|
|
|
</div>
|
2025-12-23 13:53:22 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
|
|
{/* 컬럼 선택 */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium">표시할 컬럼 선택</Label>
|
|
|
|
|
|
|
|
|
|
|
|
{loadingColumns ? (
|
|
|
|
|
|
<p className="text-muted-foreground py-2 text-xs">컬럼 로딩 중...</p>
|
|
|
|
|
|
) : !tableName ? (
|
|
|
|
|
|
<p className="text-muted-foreground py-2 text-xs">테이블을 선택해주세요</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="max-h-60 space-y-2 overflow-y-auto rounded-md border p-2">
|
|
|
|
|
|
{/* 테이블 컬럼 */}
|
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
|
{baseColumns.map((column) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={column.columnName}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
|
|
|
|
|
|
isColumnAdded(column.columnName) && "bg-primary/10",
|
|
|
|
|
|
)}
|
|
|
|
|
|
onClick={() => toggleColumn(column)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={isColumnAdded(column.columnName)}
|
|
|
|
|
|
onCheckedChange={() => toggleColumn(column)}
|
|
|
|
|
|
className="pointer-events-none h-3.5 w-3.5 flex-shrink-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
|
|
|
|
|
<span className="truncate text-xs">{column.displayName}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 조인 컬럼 (테이블별 그룹) */}
|
|
|
|
|
|
{Object.keys(joinColumnsByTable).length > 0 && (
|
|
|
|
|
|
<div className="mt-2 border-t pt-2">
|
|
|
|
|
|
<div className="text-muted-foreground mb-1 flex items-center gap-1 text-[10px] font-medium">
|
|
|
|
|
|
<Link2 className="h-3 w-3 text-blue-500" />
|
|
|
|
|
|
엔티티 조인 컬럼
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{Object.entries(joinColumnsByTable).map(([refTable, refColumns]) => (
|
|
|
|
|
|
<div key={refTable} className="mb-1">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="hover:bg-muted/30 flex cursor-pointer items-center gap-1 rounded px-1 py-0.5"
|
|
|
|
|
|
onClick={() => toggleJoinSection(refTable)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{expandedJoinSections.has(refTable) ? (
|
|
|
|
|
|
<ChevronDown className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ChevronRight className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span className="truncate text-[10px] font-medium text-blue-600">{refTable}</span>
|
|
|
|
|
|
<span className="text-muted-foreground text-[10px]">({refColumns.length})</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{expandedJoinSections.has(refTable) && (
|
|
|
|
|
|
<div className="ml-3 space-y-0.5">
|
|
|
|
|
|
{refColumns.map((column) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={column.columnName}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
|
|
|
|
|
|
isColumnAdded(column.columnName) && "bg-blue-50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
onClick={() => toggleColumn(column)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={isColumnAdded(column.columnName)}
|
|
|
|
|
|
onCheckedChange={() => toggleColumn(column)}
|
|
|
|
|
|
className="pointer-events-none h-3.5 w-3.5 flex-shrink-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="min-w-0 flex-1 truncate text-xs">{column.displayName}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-12-19 15:44:38 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-23 13:53:22 +09:00
|
|
|
|
{/* 선택된 컬럼 상세 설정 */}
|
|
|
|
|
|
{configColumns.length > 0 && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium">선택된 컬럼 ({configColumns.length}개)</Label>
|
|
|
|
|
|
<div className="max-h-40 space-y-1 overflow-y-auto">
|
|
|
|
|
|
{configColumns.map((column, index) => {
|
|
|
|
|
|
const colInfo = columns.find((c) => c.columnName === column.key);
|
2026-01-05 15:35:19 +09:00
|
|
|
|
const showThousandSeparator = isNumberColumn(column.key);
|
2025-12-23 13:53:22 +09:00
|
|
|
|
return (
|
2026-01-05 15:35:19 +09:00
|
|
|
|
<div key={column.key} className="bg-muted/30 space-y-1.5 rounded-md border p-2">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab" />
|
|
|
|
|
|
{column.isJoinColumn ? (
|
|
|
|
|
|
<Link2 className="h-3 w-3 flex-shrink-0 text-blue-500" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={column.title}
|
|
|
|
|
|
onChange={(e) => updateColumnTitle(column.key, e.target.value)}
|
|
|
|
|
|
placeholder="제목"
|
|
|
|
|
|
className="h-6 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => toggleColumn(colInfo || { columnName: column.key, displayName: column.title })}
|
|
|
|
|
|
className="text-destructive h-6 w-6 p-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/* 숫자 컬럼인 경우 천단위 구분자 옵션 표시 */}
|
|
|
|
|
|
{showThousandSeparator && (
|
|
|
|
|
|
<div className="ml-5 flex items-center gap-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id={`thousand-${column.key}`}
|
|
|
|
|
|
checked={column.thousandSeparator !== false}
|
|
|
|
|
|
onCheckedChange={(checked) => toggleThousandSeparator(column.key, !!checked)}
|
|
|
|
|
|
className="h-3 w-3"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<label htmlFor={`thousand-${column.key}`} className="text-muted-foreground text-[10px]">
|
|
|
|
|
|
천단위 구분자
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2025-12-23 13:53:22 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
|
<Separator />
|
|
|
|
|
|
|
2026-01-05 15:30:57 +09:00
|
|
|
|
{/* 페이지네이션 설정 */}
|
2025-12-19 15:44:38 +09:00
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="pagination"
|
|
|
|
|
|
checked={config.pagination !== false}
|
|
|
|
|
|
onCheckedChange={(checked) => updateConfig("pagination", checked)}
|
|
|
|
|
|
/>
|
2026-01-05 15:30:57 +09:00
|
|
|
|
<label htmlFor="pagination" className="text-xs font-medium">
|
2025-12-23 13:53:22 +09:00
|
|
|
|
페이지네이션
|
|
|
|
|
|
</label>
|
2025-12-19 15:44:38 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-05 15:30:57 +09:00
|
|
|
|
{config.pagination !== false && (
|
2025-12-23 13:53:22 +09:00
|
|
|
|
<div className="space-y-2">
|
2026-01-05 15:30:57 +09:00
|
|
|
|
<Label className="text-xs">페이지당 행 수</Label>
|
2025-12-23 13:53:22 +09:00
|
|
|
|
<Select
|
|
|
|
|
|
value={String(config.pageSize || 10)}
|
|
|
|
|
|
onValueChange={(value) => updateConfig("pageSize", Number(value))}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="5">5개</SelectItem>
|
|
|
|
|
|
<SelectItem value="10">10개</SelectItem>
|
|
|
|
|
|
<SelectItem value="20">20개</SelectItem>
|
|
|
|
|
|
<SelectItem value="50">50개</SelectItem>
|
|
|
|
|
|
<SelectItem value="100">100개</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
2026-01-05 15:30:57 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-12-19 15:44:38 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
UnifiedListConfigPanel.displayName = "UnifiedListConfigPanel";
|
|
|
|
|
|
|
|
|
|
|
|
export default UnifiedListConfigPanel;
|