ERP-node/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel...

3008 lines
141 KiB
TypeScript
Raw Normal View History

2025-10-15 17:25:38 +09:00
"use client";
import React, { useState, useMemo, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Checkbox } from "@/components/ui/checkbox";
2025-10-15 17:25:38 +09:00
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Slider } from "@/components/ui/slider";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
2025-10-15 17:25:38 +09:00
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
// Accordion 제거 - 단순 섹션으로 변경
import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown } from "lucide-react";
2025-10-15 17:25:38 +09:00
import { cn } from "@/lib/utils";
import { SplitPanelLayoutConfig } from "./types";
import { TableInfo, ColumnInfo } from "@/types/screen";
import { tableTypeApi } from "@/lib/api/screen";
import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel";
2025-10-15 17:25:38 +09:00
interface SplitPanelLayoutConfigPanelProps {
config: SplitPanelLayoutConfig;
onChange: (config: SplitPanelLayoutConfig) => void;
tables?: TableInfo[]; // 전체 테이블 목록 (선택적)
screenTableName?: string; // 현재 화면의 테이블명 (좌측 패널에서 사용)
menuObjid?: number; // 🆕 메뉴 OBJID (카테고리 값 조회 시 필요)
2025-10-15 17:25:38 +09:00
}
/**
*
*/
const GroupByColumnsSelector: React.FC<{
tableName?: string;
selectedColumns: string[];
onChange: (columns: string[]) => void;
}> = ({ tableName, selectedColumns, onChange }) => {
const [columns, setColumns] = useState<any[]>([]); // ColumnTypeInfo 타입
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!tableName) {
setColumns([]);
return;
}
const loadColumns = async () => {
setLoading(true);
try {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getColumnList(tableName);
if (response.success && response.data && response.data.columns) {
setColumns(response.data.columns);
}
} catch (error) {
console.error("컬럼 정보 로드 실패:", error);
} finally {
setLoading(false);
}
};
loadColumns();
}, [tableName]);
const toggleColumn = (columnName: string) => {
const newSelection = selectedColumns.includes(columnName)
? selectedColumns.filter((c) => c !== columnName)
: [...selectedColumns, columnName];
onChange(newSelection);
};
if (!tableName) {
return (
<div className="rounded-md border border-dashed p-3">
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground text-center text-xs"> </p>
</div>
);
}
return (
<div>
<Label className="text-xs"> </Label>
{loading ? (
<div className="rounded-md border p-3">
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground text-center text-xs"> ...</p>
</div>
) : columns.length === 0 ? (
<div className="rounded-md border border-dashed p-3">
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground text-center text-xs"> </p>
</div>
) : (
2025-12-11 11:37:40 +09:00
<div className="max-h-[200px] space-y-1 overflow-y-auto rounded-md border p-3">
{columns.map((col) => (
<div key={col.columnName} className="flex items-center gap-2">
<Checkbox
id={`groupby-${col.columnName}`}
checked={selectedColumns.includes(col.columnName)}
onCheckedChange={() => toggleColumn(col.columnName)}
/>
2025-12-11 11:37:40 +09:00
<label htmlFor={`groupby-${col.columnName}`} className="flex-1 cursor-pointer text-xs">
{col.columnLabel || col.columnName}
<span className="text-muted-foreground ml-1">({col.columnName})</span>
</label>
</div>
))}
</div>
)}
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground mt-1 text-[10px]">
: {selectedColumns.length > 0 ? selectedColumns.join(", ") : "없음"}
<br />
</p>
</div>
);
};
/**
* Combobox
*/
const ScreenSelector: React.FC<{
value?: number;
onChange: (screenId?: number) => void;
}> = ({ value, onChange }) => {
const [open, setOpen] = useState(false);
const [screens, setScreens] = useState<Array<{ screenId: number; screenName: string; screenCode: string }>>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const loadScreens = async () => {
setLoading(true);
try {
const { screenApi } = await import("@/lib/api/screen");
const response = await screenApi.getScreens({ page: 1, size: 1000 });
2025-12-11 11:37:40 +09:00
setScreens(
response.data.map((s) => ({ screenId: s.screenId, screenName: s.screenName, screenCode: s.screenCode })),
);
} catch (error) {
console.error("화면 목록 로드 실패:", error);
} finally {
setLoading(false);
}
};
loadScreens();
}, []);
const selectedScreen = screens.find((s) => s.screenId === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="h-8 w-full justify-between text-xs"
disabled={loading}
>
{loading ? "로딩 중..." : selectedScreen ? selectedScreen.screenName : "화면 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
2025-12-11 11:37:40 +09:00
<PopoverContent className="w-[400px] p-0" align="start">
<Command>
<CommandInput placeholder="화면 검색..." className="text-xs" />
<CommandList>
2025-12-11 11:37:40 +09:00
<CommandEmpty className="py-6 text-center text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[300px] overflow-auto">
{screens.map((screen) => (
<CommandItem
key={screen.screenId}
value={`${screen.screenName.toLowerCase()} ${screen.screenCode.toLowerCase()} ${screen.screenId}`}
onSelect={() => {
onChange(screen.screenId === value ? undefined : screen.screenId);
setOpen(false);
}}
className="text-xs"
>
2025-12-11 11:37:40 +09:00
<Check className={cn("mr-2 h-4 w-4", value === screen.screenId ? "opacity-100" : "opacity-0")} />
<div className="flex flex-col">
<span className="font-medium">{screen.screenName}</span>
2025-12-11 11:37:40 +09:00
<span className="text-muted-foreground text-[10px]">{screen.screenCode}</span>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
2025-10-15 17:25:38 +09:00
/**
* SplitPanelLayout
*/
export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelProps> = ({
config,
onChange,
tables = [], // 기본값 빈 배열 (현재 화면 테이블만)
2025-10-15 17:25:38 +09:00
screenTableName, // 현재 화면의 테이블명
menuObjid, // 🆕 메뉴 OBJID
2025-10-15 17:25:38 +09:00
}) => {
const [rightTableOpen, setRightTableOpen] = useState(false);
const [leftColumnOpen, setLeftColumnOpen] = useState(false);
const [rightColumnOpen, setRightColumnOpen] = useState(false);
const [loadedTableColumns, setLoadedTableColumns] = useState<Record<string, ColumnInfo[]>>({});
const [loadingColumns, setLoadingColumns] = useState<Record<string, boolean>>({});
const [allTables, setAllTables] = useState<any[]>([]); // 조인 모드용 전체 테이블 목록
// 엔티티 참조 테이블 컬럼
type EntityRefTable = { tableName: string; columns: ColumnInfo[] };
const [entityReferenceTables, setEntityReferenceTables] = useState<Record<string, EntityRefTable[]>>({});
2025-12-11 11:37:40 +09:00
// 🆕 입력 필드용 로컬 상태
const [isUserEditing, setIsUserEditing] = useState(false);
const [localTitles, setLocalTitles] = useState({
left: config.leftPanel?.title || "",
right: config.rightPanel?.title || "",
});
// 관계 타입
const relationshipType = config.rightPanel?.relation?.type || "detail";
2025-12-11 11:37:40 +09:00
// config 변경 시 로컬 타이틀 동기화 (사용자가 입력 중이 아닐 때만)
useEffect(() => {
if (!isUserEditing) {
setLocalTitles({
left: config.leftPanel?.title || "",
right: config.rightPanel?.title || "",
});
}
}, [config.leftPanel?.title, config.rightPanel?.title, isUserEditing]);
// 조인 모드일 때만 전체 테이블 목록 로드
useEffect(() => {
if (relationshipType === "join") {
const loadAllTables = async () => {
try {
const { tableManagementApi } = await import("@/lib/api/tableManagement");
const response = await tableManagementApi.getTableList();
if (response.success && response.data) {
console.log("✅ 분할패널 조인 모드: 전체 테이블 목록 로드", response.data.length, "개");
setAllTables(response.data);
}
} catch (error) {
console.error("❌ 전체 테이블 목록 로드 실패:", error);
}
};
loadAllTables();
} else {
// 상세 모드일 때는 기본 테이블만 사용
setAllTables([]);
}
}, [relationshipType]);
2025-10-15 17:25:38 +09:00
// screenTableName이 변경되면 좌측 패널 테이블을 항상 화면 테이블로 설정
2025-10-15 17:25:38 +09:00
useEffect(() => {
if (screenTableName) {
// 좌측 패널은 항상 현재 화면의 테이블 사용
2025-10-15 17:25:38 +09:00
if (config.leftPanel?.tableName !== screenTableName) {
updateLeftPanel({ tableName: screenTableName });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenTableName]);
// 좌측 패널 테이블 컬럼 로드 완료 시 PK 자동 추가
useEffect(() => {
const leftTableName = config.leftPanel?.tableName || screenTableName;
if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showAdd) {
const currentAddModalColumns = config.leftPanel?.addModalColumns || [];
const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns);
2025-12-11 11:37:40 +09:00
// PK가 추가되었으면 업데이트
if (updatedColumns.length !== currentAddModalColumns.length) {
console.log(`🔄 좌측 패널: PK 컬럼 자동 추가 (${leftTableName})`);
updateLeftPanel({ addModalColumns: updatedColumns });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showAdd]);
2025-11-07 15:21:44 +09:00
// 좌측 패널 하위 항목 추가 모달 PK 자동 추가
useEffect(() => {
const leftTableName = config.leftPanel?.tableName || screenTableName;
if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showItemAddButton) {
const currentAddModalColumns = config.leftPanel?.itemAddConfig?.addModalColumns || [];
const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns);
2025-12-11 11:37:40 +09:00
2025-11-07 15:21:44 +09:00
// PK가 추가되었으면 업데이트
if (updatedColumns.length !== currentAddModalColumns.length) {
console.log(`🔄 좌측 패널 하위 항목 추가: PK 컬럼 자동 추가 (${leftTableName})`);
2025-12-11 11:37:40 +09:00
updateLeftPanel({
2025-11-07 15:21:44 +09:00
itemAddConfig: {
...config.leftPanel?.itemAddConfig,
addModalColumns: updatedColumns,
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
2025-12-11 11:37:40 +09:00
},
2025-11-07 15:21:44 +09:00
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showItemAddButton]);
// 우측 패널 테이블 컬럼 로드 완료 시 PK 자동 추가
useEffect(() => {
const rightTableName = config.rightPanel?.tableName;
if (rightTableName && loadedTableColumns[rightTableName] && config.rightPanel?.showAdd) {
const currentAddModalColumns = config.rightPanel?.addModalColumns || [];
const updatedColumns = ensurePrimaryKeysInAddModal(rightTableName, currentAddModalColumns);
2025-12-11 11:37:40 +09:00
// PK가 추가되었으면 업데이트
if (updatedColumns.length !== currentAddModalColumns.length) {
console.log(`🔄 우측 패널: PK 컬럼 자동 추가 (${rightTableName})`);
updateRightPanel({ addModalColumns: updatedColumns });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.rightPanel?.tableName, loadedTableColumns, config.rightPanel?.showAdd]);
// 테이블 컬럼 로드 함수
const loadTableColumns = async (tableName: string) => {
if (loadedTableColumns[tableName] || loadingColumns[tableName]) {
return; // 이미 로드되었거나 로딩 중
}
setLoadingColumns((prev) => ({ ...prev, [tableName]: true }));
try {
const columnsResponse = await tableTypeApi.getColumns(tableName);
console.log(`📊 테이블 ${tableName} 컬럼 응답:`, columnsResponse);
const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => ({
tableName: col.tableName || tableName,
columnName: col.columnName || col.column_name,
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
dataType: col.dataType || col.data_type || col.dbType,
webType: col.webType || col.web_type,
input_type: col.inputType || col.input_type,
widgetType: col.widgetType || col.widget_type || col.webType || col.web_type,
isNullable: col.isNullable || col.is_nullable,
required: col.required !== undefined ? col.required : col.isNullable === "NO" || col.is_nullable === "NO",
columnDefault: col.columnDefault || col.column_default,
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
isPrimaryKey: col.isPrimaryKey || false, // PK 여부 추가
codeCategory: col.codeCategory || col.code_category,
codeValue: col.codeValue || col.code_value,
referenceTable: col.referenceTable || col.reference_table, // 🆕 참조 테이블
referenceColumn: col.referenceColumn || col.reference_column, // 🆕 참조 컬럼
displayColumn: col.displayColumn || col.display_column, // 🆕 표시 컬럼
}));
console.log(`✅ 테이블 ${tableName} 컬럼 ${columns.length}개 로드됨:`, columns);
setLoadedTableColumns((prev) => ({ ...prev, [tableName]: columns }));
2025-12-11 11:37:40 +09:00
// 🆕 엔티티 타입 컬럼의 참조 테이블 컬럼도 로드
await loadEntityReferenceColumns(tableName, columns);
} catch (error) {
console.error(`테이블 ${tableName} 컬럼 로드 실패:`, error);
setLoadedTableColumns((prev) => ({ ...prev, [tableName]: [] }));
} finally {
setLoadingColumns((prev) => ({ ...prev, [tableName]: false }));
}
};
2025-12-11 11:37:40 +09:00
// 🆕 엔티티 참조 테이블의 컬럼 로드
const loadEntityReferenceColumns = async (sourceTableName: string, columns: ColumnInfo[]) => {
const entityColumns = columns.filter(
2025-12-11 11:37:40 +09:00
(col) => (col.input_type === "entity" || col.webType === "entity") && col.referenceTable,
);
2025-12-11 11:37:40 +09:00
if (entityColumns.length === 0) {
return;
}
2025-12-11 11:37:40 +09:00
console.log(
`🔗 테이블 ${sourceTableName}의 엔티티 참조 ${entityColumns.length}개 발견:`,
entityColumns.map((c) => `${c.columnName} -> ${c.referenceTable}`),
);
2025-12-11 11:37:40 +09:00
const referenceTableData: Array<{ tableName: string; columns: ColumnInfo[] }> = [];
// 각 참조 테이블의 컬럼 로드
for (const entityCol of entityColumns) {
const refTableName = entityCol.referenceTable!;
2025-12-11 11:37:40 +09:00
// 이미 로드했으면 스킵
2025-12-11 11:37:40 +09:00
if (referenceTableData.some((t) => t.tableName === refTableName)) continue;
try {
const refColumnsResponse = await tableTypeApi.getColumns(refTableName);
const refColumns: ColumnInfo[] = (refColumnsResponse || []).map((col: any) => ({
tableName: col.tableName || refTableName,
columnName: col.columnName || col.column_name,
columnLabel: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
dataType: col.dataType || col.data_type || col.dbType,
input_type: col.inputType || col.input_type,
}));
2025-12-11 11:37:40 +09:00
referenceTableData.push({ tableName: refTableName, columns: refColumns });
console.log(` ✅ 참조 테이블 ${refTableName} 컬럼 ${refColumns.length}개 로드됨`);
} catch (error) {
console.error(` ❌ 참조 테이블 ${refTableName} 컬럼 로드 실패:`, error);
}
}
2025-12-11 11:37:40 +09:00
// 참조 테이블 정보 저장
2025-12-11 11:37:40 +09:00
setEntityReferenceTables((prev) => ({
...prev,
2025-12-11 11:37:40 +09:00
[sourceTableName]: referenceTableData,
}));
2025-12-11 11:37:40 +09:00
console.log(`✅ [엔티티 참조] ${sourceTableName}의 참조 테이블 저장 완료:`, {
sourceTableName,
referenceTableCount: referenceTableData.length,
2025-12-11 11:37:40 +09:00
referenceTables: referenceTableData.map((t) => `${t.tableName}(${t.columns.length}개)`),
});
};
// 좌측/우측 테이블이 변경되면 해당 테이블의 컬럼 로드
useEffect(() => {
if (config.leftPanel?.tableName) {
loadTableColumns(config.leftPanel.tableName);
}
}, [config.leftPanel?.tableName]);
useEffect(() => {
if (config.rightPanel?.tableName) {
loadTableColumns(config.rightPanel.tableName);
}
}, [config.rightPanel?.tableName]);
2025-10-15 17:25:38 +09:00
console.log("🔧 SplitPanelLayoutConfigPanel 렌더링");
console.log(" - config:", config);
console.log(" - tables:", tables);
console.log(" - tablesCount:", tables.length);
console.log(" - screenTableName:", screenTableName);
console.log(" - leftTable:", config.leftPanel?.tableName);
console.log(" - rightTable:", config.rightPanel?.tableName);
const updateConfig = (updates: Partial<SplitPanelLayoutConfig>) => {
const newConfig = { ...config, ...updates };
console.log("🔄 Config 업데이트:", newConfig);
onChange(newConfig);
};
// PK 컬럼을 추가 모달에 자동으로 포함시키는 함수
const ensurePrimaryKeysInAddModal = (
tableName: string,
2025-12-11 11:37:40 +09:00
existingColumns: Array<{ name: string; label: string; required?: boolean }> = [],
) => {
const tableColumns = loadedTableColumns[tableName];
if (!tableColumns) {
console.warn(`⚠️ 테이블 ${tableName}의 컬럼 정보가 로드되지 않음`);
return existingColumns;
}
// PK 컬럼 찾기
const pkColumns = tableColumns.filter((col) => col.isPrimaryKey);
2025-12-11 11:37:40 +09:00
console.log(
`🔑 테이블 ${tableName}의 PK 컬럼:`,
pkColumns.map((c) => c.columnName),
);
// 자동으로 처리되는 컬럼 (백엔드에서 자동 추가)
2025-12-11 11:37:40 +09:00
const autoHandledColumns = ["company_code", "company_name"];
// 기존 컬럼 이름 목록
const existingColumnNames = existingColumns.map((col) => col.name);
// PK 컬럼을 맨 앞에 추가 (이미 있거나 자동 처리되는 컬럼은 제외)
const pkColumnsToAdd = pkColumns
.filter((col) => !existingColumnNames.includes(col.columnName))
.filter((col) => !autoHandledColumns.includes(col.columnName)) // 자동 처리 컬럼 제외
.map((col) => ({
name: col.columnName,
label: col.columnLabel || col.columnName,
required: true, // PK는 항상 필수
}));
if (pkColumnsToAdd.length > 0) {
2025-12-11 11:37:40 +09:00
console.log(
`✅ PK 컬럼 ${pkColumnsToAdd.length}개 자동 추가:`,
pkColumnsToAdd.map((c) => c.name),
);
}
return [...pkColumnsToAdd, ...existingColumns];
};
2025-10-15 17:25:38 +09:00
const updateLeftPanel = (updates: Partial<SplitPanelLayoutConfig["leftPanel"]>) => {
const newConfig = {
...config,
leftPanel: { ...config.leftPanel, ...updates },
};
console.log("🔄 Left Panel 업데이트:", newConfig);
onChange(newConfig);
};
const updateRightPanel = (updates: Partial<SplitPanelLayoutConfig["rightPanel"]>) => {
const newConfig = {
...config,
rightPanel: { ...config.rightPanel, ...updates },
};
console.log("🔄 Right Panel 업데이트:", newConfig);
onChange(newConfig);
};
// 좌측 테이블명
const leftTableName = config.leftPanel?.tableName || screenTableName || "";
2025-12-11 11:37:40 +09:00
// 좌측 테이블 컬럼 (로드된 컬럼 사용)
2025-10-15 17:25:38 +09:00
const leftTableColumns = useMemo(() => {
return leftTableName ? loadedTableColumns[leftTableName] || [] : [];
}, [loadedTableColumns, leftTableName]);
2025-10-15 17:25:38 +09:00
// 우측 테이블명
const rightTableName = config.rightPanel?.tableName || "";
2025-12-11 11:37:40 +09:00
// 우측 테이블 컬럼 (로드된 컬럼 사용)
2025-10-15 17:25:38 +09:00
const rightTableColumns = useMemo(() => {
return rightTableName ? loadedTableColumns[rightTableName] || [] : [];
}, [loadedTableColumns, rightTableName]);
2025-10-15 17:25:38 +09:00
// 테이블 데이터 로딩 상태 확인
if (!tables || tables.length === 0) {
return (
<div className="rounded-lg border p-4">
<p className="text-sm font-medium"> .</p>
<p className="mt-1 text-xs text-gray-600">
2025-10-15 17:25:38 +09:00
.
</p>
</div>
);
}
// 조인 모드에서 우측 테이블 선택 시 사용할 테이블 목록
const availableRightTables = relationshipType === "join" ? allTables : tables;
console.log("📊 분할패널 테이블 목록 상태:");
console.log(" - relationshipType:", relationshipType);
console.log(" - allTables:", allTables.length, "개");
console.log(" - availableRightTables:", availableRightTables.length, "개");
2025-10-15 17:25:38 +09:00
return (
<div className="space-y-4">
{/* 관계 타입 선택 */}
<div className="space-y-3">
<h3 className="text-sm font-semibold"> </h3>
2025-10-15 17:25:38 +09:00
<Select
value={relationshipType}
onValueChange={(value: "join" | "detail") => {
2025-10-15 17:25:38 +09:00
// 상세 모드로 변경 시 우측 테이블을 현재 화면 테이블로 설정
if (value === "detail" && screenTableName) {
updateRightPanel({
relation: { ...config.rightPanel?.relation, type: value },
tableName: screenTableName,
});
} else {
updateRightPanel({
relation: { ...config.rightPanel?.relation, type: value },
});
}
}}
>
<SelectTrigger className="bg-white">
<SelectValue placeholder="관계 타입 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="detail">
<div className="flex flex-col">
<span className="font-medium"> (DETAIL)</span>
<span className="text-xs text-gray-500"> ( )</span>
</div>
</SelectItem>
<SelectItem value="join">
<div className="flex flex-col">
<span className="font-medium"> (JOIN)</span>
<span className="text-xs text-gray-500"> ( )</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 좌측 패널 설정 */}
2025-12-11 11:37:40 +09:00
<div className="mt-4 space-y-4 border-t pt-4">
<h3 className="text-sm font-semibold"> ()</h3>
2025-10-15 17:25:38 +09:00
<div className="space-y-2">
<Label> </Label>
<Input
value={localTitles.left}
onChange={(e) => {
setIsUserEditing(true);
2025-12-11 11:37:40 +09:00
setLocalTitles((prev) => ({ ...prev, left: e.target.value }));
}}
onBlur={() => {
setIsUserEditing(false);
updateLeftPanel({ title: localTitles.left });
}}
2025-10-15 17:25:38 +09:00
placeholder="좌측 패널 제목"
/>
</div>
<div className="space-y-2">
<Label> (px)</Label>
<Input
type="number"
value={config.leftPanel?.panelHeaderHeight || 48}
onChange={(e) => updateLeftPanel({ panelHeaderHeight: parseInt(e.target.value) || 48 })}
placeholder="48"
min={32}
max={120}
/>
<p className="text-xs text-muted-foreground"> (기본: 48px)</p>
</div>
2025-10-15 17:25:38 +09:00
<div className="space-y-2">
<Label> ( )</Label>
2025-10-15 17:25:38 +09:00
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3">
<p className="text-sm font-medium text-gray-900">
{config.leftPanel?.tableName || screenTableName || "테이블이 지정되지 않음"}
</p>
2025-10-15 17:25:38 +09:00
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
</div>
<div className="space-y-2">
<Label> </Label>
<Select
value={config.leftPanel?.displayMode || "list"}
onValueChange={(value: "list" | "table") => updateLeftPanel({ displayMode: value })}
>
<SelectTrigger className="bg-white">
<SelectValue placeholder="표시 모드 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="list">
<div className="flex flex-col">
<span className="font-medium"> (LIST)</span>
<span className="text-xs text-gray-500"> ()</span>
</div>
</SelectItem>
<SelectItem value="table">
<div className="flex flex-col">
<span className="font-medium"> (TABLE)</span>
<span className="text-xs text-gray-500"> </span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
2025-10-15 17:25:38 +09:00
<div className="flex items-center justify-between">
<Label> </Label>
<Switch
checked={config.leftPanel?.showSearch ?? true}
onCheckedChange={(checked) => updateLeftPanel({ showSearch: checked })}
/>
</div>
<div className="flex items-center justify-between">
<Label> </Label>
<Switch
checked={config.leftPanel?.showAdd ?? false}
onCheckedChange={(checked) => updateLeftPanel({ showAdd: checked })}
/>
</div>
2025-11-07 15:21:44 +09:00
<div className="flex items-center justify-between">
<Label> + </Label>
<Switch
checked={config.leftPanel?.showItemAddButton ?? false}
onCheckedChange={(checked) => updateLeftPanel({ showItemAddButton: checked })}
/>
</div>
{/* 항목별 + 버튼 설정 (하위 항목 추가) */}
{config.leftPanel?.showItemAddButton && (
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
<Label className="text-sm font-semibold"> </Label>
<p className="text-xs text-gray-600">
+ (: 부서 )
</p>
{/* 현재 항목의 값을 가져올 컬럼 (sourceColumn) */}
<div>
<Label className="text-xs"> ID </Label>
2025-12-11 11:37:40 +09:00
<p className="mb-2 text-[10px] text-gray-500"> (: dept_code)</p>
2025-11-07 15:21:44 +09:00
<Popover>
<PopoverTrigger asChild>
2025-12-11 11:37:40 +09:00
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
2025-11-07 15:21:44 +09:00
{config.leftPanel?.itemAddConfig?.sourceColumn || "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{leftTableColumns
2025-12-11 11:37:40 +09:00
.filter((column) => !["company_code", "company_name"].includes(column.columnName))
2025-11-07 15:21:44 +09:00
.map((column) => (
2025-12-11 11:37:40 +09:00
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
updateLeftPanel({
itemAddConfig: {
...config.leftPanel?.itemAddConfig,
sourceColumn: value,
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
},
});
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
config.leftPanel?.itemAddConfig?.sourceColumn === column.columnName
? "opacity-100"
: "opacity-0",
)}
/>
{column.columnLabel || column.columnName}
<span className="ml-2 text-[10px] text-gray-500">({column.columnName})</span>
</CommandItem>
))}
2025-11-07 15:21:44 +09:00
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 상위 항목 ID를 저장할 컬럼 (parentColumn) */}
<div>
<Label className="text-xs"> </Label>
<p className="mb-2 text-[10px] text-gray-500">
ID를 (: parent_dept_code)
</p>
<Popover>
<PopoverTrigger asChild>
2025-12-11 11:37:40 +09:00
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
2025-11-07 15:21:44 +09:00
{config.leftPanel?.itemAddConfig?.parentColumn || "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{leftTableColumns
2025-12-11 11:37:40 +09:00
.filter((column) => !["company_code", "company_name"].includes(column.columnName))
2025-11-07 15:21:44 +09:00
.map((column) => (
2025-12-11 11:37:40 +09:00
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
updateLeftPanel({
itemAddConfig: {
...config.leftPanel?.itemAddConfig,
parentColumn: value,
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
},
});
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
config.leftPanel?.itemAddConfig?.parentColumn === column.columnName
? "opacity-100"
: "opacity-0",
)}
/>
{column.columnLabel || column.columnName}
<span className="ml-2 text-[10px] text-gray-500">({column.columnName})</span>
</CommandItem>
))}
2025-11-07 15:21:44 +09:00
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
{/* 하위 항목 추가 모달 컬럼 설정 */}
<div className="space-y-2 rounded border border-blue-300 bg-white p-2">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => {
const currentColumns = config.leftPanel?.itemAddConfig?.addModalColumns || [];
2025-12-11 11:37:40 +09:00
const newColumns = [...currentColumns, { name: "", label: "", required: false }];
updateLeftPanel({
2025-11-07 15:21:44 +09:00
itemAddConfig: {
...config.leftPanel?.itemAddConfig,
addModalColumns: newColumns,
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
2025-12-11 11:37:40 +09:00
},
2025-11-07 15:21:44 +09:00
});
}}
className="h-6 text-[10px]"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
2025-12-11 11:37:40 +09:00
<p className="text-[10px] text-gray-600"> </p>
2025-11-07 15:21:44 +09:00
<div className="space-y-2">
{(config.leftPanel?.itemAddConfig?.addModalColumns || []).length === 0 ? (
<div className="rounded-md border border-dashed border-gray-300 bg-gray-50 p-2 text-center">
<p className="text-[10px] text-gray-500"> </p>
</div>
) : (
(config.leftPanel?.itemAddConfig?.addModalColumns || []).map((col, index) => {
2025-12-11 11:37:40 +09:00
const column = leftTableColumns.find((c) => c.columnName === col.name);
2025-11-07 15:21:44 +09:00
const isPK = column?.isPrimaryKey || false;
return (
<div
key={index}
className={cn(
"flex items-center gap-2 rounded-md border p-2",
2025-12-11 11:37:40 +09:00
isPK ? "border-yellow-300 bg-yellow-50" : "bg-white",
2025-11-07 15:21:44 +09:00
)}
>
{isPK && (
<span className="rounded bg-yellow-200 px-1.5 py-0.5 text-[10px] font-semibold text-yellow-700">
PK
</span>
)}
<div className="flex-1">
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={isPK}
className="h-7 w-full justify-between text-[10px]"
>
{col.name || "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{leftTableColumns
2025-12-11 11:37:40 +09:00
.filter((column) => !["company_code", "company_name"].includes(column.columnName))
2025-11-07 15:21:44 +09:00
.map((column) => (
2025-12-11 11:37:40 +09:00
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
const newColumns = [
...(config.leftPanel?.itemAddConfig?.addModalColumns || []),
];
newColumns[index] = {
...newColumns[index],
name: value,
label: column.columnLabel || value,
};
updateLeftPanel({
itemAddConfig: {
...config.leftPanel?.itemAddConfig,
addModalColumns: newColumns,
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
},
});
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
col.name === column.columnName ? "opacity-100" : "opacity-0",
)}
/>
{column.columnLabel || column.columnName}
<span className="ml-2 text-[10px] text-gray-500">({column.columnName})</span>
</CommandItem>
))}
2025-11-07 15:21:44 +09:00
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center gap-1">
<label className="flex cursor-pointer items-center gap-1 text-[10px] text-gray-600">
<input
type="checkbox"
checked={col.required ?? false}
disabled={isPK}
onChange={(e) => {
const newColumns = [...(config.leftPanel?.itemAddConfig?.addModalColumns || [])];
newColumns[index] = {
...newColumns[index],
required: e.target.checked,
};
2025-12-11 11:37:40 +09:00
updateLeftPanel({
2025-11-07 15:21:44 +09:00
itemAddConfig: {
...config.leftPanel?.itemAddConfig,
addModalColumns: newColumns,
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
2025-12-11 11:37:40 +09:00
},
2025-11-07 15:21:44 +09:00
});
}}
className="h-3 w-3"
/>
</label>
</div>
<Button
size="sm"
variant="ghost"
disabled={isPK}
onClick={() => {
const newColumns = (config.leftPanel?.itemAddConfig?.addModalColumns || []).filter(
2025-12-11 11:37:40 +09:00
(_, i) => i !== index,
2025-11-07 15:21:44 +09:00
);
2025-12-11 11:37:40 +09:00
updateLeftPanel({
2025-11-07 15:21:44 +09:00
itemAddConfig: {
...config.leftPanel?.itemAddConfig,
addModalColumns: newColumns,
parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "",
sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "",
2025-12-11 11:37:40 +09:00
},
2025-11-07 15:21:44 +09:00
});
}}
className="h-7 w-7 p-0"
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
>
<X className="h-3 w-3" />
</Button>
</div>
);
})
)}
</div>
</div>
</div>
)}
{/* 좌측 패널 표시 컬럼 설정 */}
<div className="space-y-3 rounded-lg border border-green-200 bg-green-50 p-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => {
const currentColumns = config.leftPanel?.columns || [];
2025-12-11 11:37:40 +09:00
const newColumns = [...currentColumns, { name: "", label: "", width: 100 }];
updateLeftPanel({ columns: newColumns });
}}
className="h-7 text-xs"
disabled={!config.leftPanel?.tableName && !screenTableName}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<p className="text-xs text-gray-600">
. .
</p>
{/* 선택된 컬럼 목록 */}
<div className="space-y-2">
{(config.leftPanel?.columns || []).length === 0 ? (
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
<p className="text-xs text-gray-500"> </p>
2025-12-11 11:37:40 +09:00
<p className="mt-1 text-[10px] text-gray-400"> </p>
</div>
) : (
(config.leftPanel?.columns || []).map((col, index) => {
const isTableMode = config.leftPanel?.displayMode === "table";
2025-12-11 11:37:40 +09:00
return (
2025-12-11 11:37:40 +09:00
<div key={index} className="space-y-2 rounded-md border bg-white p-2">
<div className="flex items-center gap-2">
{/* 순서 변경 버튼 */}
<div className="flex flex-col gap-0.5">
<Button
size="sm"
variant="ghost"
onClick={() => {
if (index === 0) return;
const newColumns = [...(config.leftPanel?.columns || [])];
[newColumns[index - 1], newColumns[index]] = [newColumns[index], newColumns[index - 1]];
updateLeftPanel({ columns: newColumns });
}}
disabled={index === 0}
className="h-4 w-6 p-0"
title="위로 이동"
>
<ArrowUp className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
const columns = config.leftPanel?.columns || [];
if (index === columns.length - 1) return;
const newColumns = [...columns];
[newColumns[index], newColumns[index + 1]] = [newColumns[index + 1], newColumns[index]];
updateLeftPanel({ columns: newColumns });
}}
disabled={index === (config.leftPanel?.columns || []).length - 1}
className="h-4 w-6 p-0"
title="아래로 이동"
>
<ArrowDown className="h-3 w-3" />
</Button>
</div>
<div className="flex-1">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
{col.label || col.name || "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandEmpty className="text-xs"> .</CommandEmpty>
<div className="max-h-[300px] overflow-auto">
{/* 기본 테이블 컬럼 */}
<CommandGroup heading={leftTableName ? `📋 ${leftTableName} 컬럼` : "📋 기본 컬럼"}>
{leftTableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
const newColumns = [...(config.leftPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
name: value,
label: column.columnLabel || value,
};
updateLeftPanel({ columns: newColumns });
// Popover 닫기
document.body.click();
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
col.name === column.columnName ? "opacity-100" : "opacity-0",
)}
/>
{column.columnLabel || column.columnName}
<span className="ml-2 text-[10px] text-gray-500">({column.columnName})</span>
</CommandItem>
))}
</CommandGroup>
{/* 🆕 엔티티 참조 테이블 컬럼 */}
{leftTableName &&
entityReferenceTables[leftTableName]?.map((refTable) => (
<CommandGroup
key={refTable.tableName}
heading={`🔗 ${refTable.tableName} (엔티티)`}
>
{refTable.columns.map((column) => {
const fullColumnName = `${refTable.tableName}.${column.columnName}`;
return (
<CommandItem
key={fullColumnName}
value={fullColumnName}
onSelect={(value) => {
const newColumns = [...(config.leftPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
name: value,
label: column.columnLabel || column.columnName,
};
updateLeftPanel({ columns: newColumns });
// Popover 닫기
document.body.click();
}}
className="pl-6 text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
col.name === fullColumnName ? "opacity-100" : "opacity-0",
)}
/>
{column.columnLabel || column.columnName}
<span className="ml-2 text-[10px] text-gray-500">
({column.columnName})
</span>
</CommandItem>
);
})}
</CommandGroup>
))}
</div>
</Command>
</PopoverContent>
</Popover>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => {
2025-12-11 11:37:40 +09:00
const newColumns = (config.leftPanel?.columns || []).filter((_, i) => i !== index);
updateLeftPanel({ columns: newColumns });
}}
2025-12-11 11:37:40 +09:00
className="h-8 w-8 p-0"
>
2025-12-11 11:37:40 +09:00
<X className="h-3 w-3" />
</Button>
</div>
2025-12-11 11:37:40 +09:00
{/* 테이블 모드 전용 옵션 */}
{isTableMode && (
<div className="grid grid-cols-3 gap-2 pt-1">
<div className="space-y-1">
<Label className="text-[10px] text-gray-600"> (px)</Label>
<Input
type="number"
min="50"
value={col.width || 100}
onChange={(e) => {
const newColumns = [...(config.leftPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
2025-12-11 11:37:40 +09:00
width: parseInt(e.target.value) || 100,
};
updateLeftPanel({ columns: newColumns });
}}
2025-12-11 11:37:40 +09:00
className="h-7 text-xs"
/>
2025-12-11 11:37:40 +09:00
</div>
<div className="space-y-1">
<Label className="text-[10px] text-gray-600"></Label>
<Select
value={col.align || "left"}
onValueChange={(value: "left" | "center" | "right") => {
const newColumns = [...(config.leftPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
align: value,
};
updateLeftPanel({ columns: newColumns });
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left"></SelectItem>
<SelectItem value="center"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<label className="flex h-7 cursor-pointer items-center gap-1 text-[10px]">
<input
type="checkbox"
checked={col.sortable ?? false}
onChange={(e) => {
const newColumns = [...(config.leftPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
sortable: e.target.checked,
};
updateLeftPanel({ columns: newColumns });
}}
className="h-3 w-3"
/>
</label>
</div>
</div>
2025-12-11 11:37:40 +09:00
)}
{/* 🆕 날짜 타입 포맷 설정 (좌측) */}
{(() => {
const column = leftTableColumns.find((c) => c.columnName === col.name);
const dbDateTypes = [
"date",
"timestamp",
"timestamptz",
"timestamp with time zone",
"timestamp without time zone",
"time",
"timetz",
];
const inputDateTypes = ["date", "datetime", "time"];
const isDate =
column &&
(dbDateTypes.includes(column.dataType?.toLowerCase() || "") ||
inputDateTypes.includes(column.input_type?.toLowerCase() || "") ||
inputDateTypes.includes(column.webType?.toLowerCase() || ""));
if (!isDate) return null;
return (
<div className="space-y-2 border-t pt-2">
<Label className="text-[10px] text-gray-600"> </Label>
<Select
value={col.format?.dateFormat || "YYYY-MM-DD"}
onValueChange={(value) => {
const newColumns = [...(config.leftPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
format: { ...newColumns[index].format, type: "date", dateFormat: value },
};
updateLeftPanel({ columns: newColumns });
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="날짜 형식" />
</SelectTrigger>
<SelectContent>
<SelectItem value="YYYY-MM-DD">2024-01-15</SelectItem>
<SelectItem value="YYYY.MM.DD">2024.01.15</SelectItem>
<SelectItem value="YYYY/MM/DD">2024/01/15</SelectItem>
<SelectItem value="YYYY-MM-DD HH:mm">2024-01-15 14:30</SelectItem>
<SelectItem value="relative"> </SelectItem>
</SelectContent>
</Select>
</div>
);
})()}
{/* 🆕 숫자 타입 포맷 설정 (좌측) */}
{(() => {
const column = leftTableColumns.find((c) => c.columnName === col.name);
const dbNumericTypes = [
"numeric",
"decimal",
"integer",
"bigint",
"double precision",
"real",
"smallint",
"int4",
"int8",
"float4",
"float8",
];
const inputNumericTypes = ["number", "decimal", "currency", "integer"];
const isNumeric =
column &&
(dbNumericTypes.includes(column.dataType?.toLowerCase() || "") ||
inputNumericTypes.includes(column.input_type?.toLowerCase() || "") ||
inputNumericTypes.includes(column.webType?.toLowerCase() || ""));
if (!isNumeric) return null;
return (
<div className="space-y-2 border-t pt-2">
<Label className="text-[10px] text-gray-600"> </Label>
<div className="grid grid-cols-2 gap-2">
<label className="flex cursor-pointer items-center gap-1 text-[10px]">
<input
type="checkbox"
checked={col.format?.thousandSeparator ?? false}
onChange={(e) => {
const newColumns = [...(config.leftPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
format: {
...newColumns[index].format,
type: "number",
thousandSeparator: e.target.checked,
},
};
updateLeftPanel({ columns: newColumns });
}}
className="h-3 w-3"
/>
(,)
</label>
<div className="flex items-center gap-1">
<Label className="text-[10px]">:</Label>
<Input
type="number"
min="0"
max="10"
value={col.format?.decimalPlaces ?? ""}
onChange={(e) => {
const newColumns = [...(config.leftPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
format: {
...newColumns[index].format,
type: "number",
decimalPlaces: e.target.value ? parseInt(e.target.value) : undefined,
},
};
updateLeftPanel({ columns: newColumns });
}}
className="h-6 w-12 text-xs"
/>
</div>
</div>
</div>
);
})()}
</div>
);
})
)}
</div>
</div>
{/* 좌측 패널 추가 모달 컬럼 설정 */}
{config.leftPanel?.showAdd && (
<div className="space-y-3 rounded-lg border border-purple-200 bg-purple-50 p-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => {
const currentColumns = config.leftPanel?.addModalColumns || [];
2025-12-11 11:37:40 +09:00
const newColumns = [...currentColumns, { name: "", label: "", required: false }];
updateLeftPanel({ addModalColumns: newColumns });
}}
className="h-7 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
2025-12-11 11:37:40 +09:00
<p className="text-xs text-gray-600"> </p>
<div className="space-y-2">
{(config.leftPanel?.addModalColumns || []).length === 0 ? (
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
<p className="text-xs text-gray-500"> </p>
</div>
) : (
(config.leftPanel?.addModalColumns || []).map((col, index) => {
// 현재 컬럼이 PK인지 확인
2025-12-11 11:37:40 +09:00
const column = leftTableColumns.find((c) => c.columnName === col.name);
const isPK = column?.isPrimaryKey || false;
2025-12-11 11:37:40 +09:00
return (
2025-12-11 11:37:40 +09:00
<div
key={index}
className={cn(
"flex items-center gap-2 rounded-md border p-2",
isPK ? "border-yellow-300 bg-yellow-50" : "bg-white",
)}
>
2025-12-11 11:37:40 +09:00
{isPK && (
<span className="rounded bg-yellow-200 px-1.5 py-0.5 text-[10px] font-semibold text-yellow-700">
PK
</span>
)}
<div className="flex-1">
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={isPK}
className="h-8 w-full justify-between text-xs"
>
{col.name || "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{leftTableColumns
.filter((column) => !["company_code", "company_name"].includes(column.columnName))
.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
const newColumns = [...(config.leftPanel?.addModalColumns || [])];
newColumns[index] = {
...newColumns[index],
name: value,
label: column.columnLabel || value,
};
updateLeftPanel({ addModalColumns: newColumns });
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
col.name === column.columnName ? "opacity-100" : "opacity-0",
)}
/>
{column.columnLabel || column.columnName}
<span className="ml-2 text-[10px] text-gray-500">({column.columnName})</span>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center gap-1">
<label className="flex cursor-pointer items-center gap-1 text-xs text-gray-600">
<input
type="checkbox"
checked={col.required ?? false}
disabled={isPK}
onChange={(e) => {
const newColumns = [...(config.leftPanel?.addModalColumns || [])];
newColumns[index] = {
...newColumns[index],
required: e.target.checked,
};
updateLeftPanel({ addModalColumns: newColumns });
}}
className="h-3 w-3"
/>
</label>
</div>
<Button
size="sm"
variant="ghost"
disabled={isPK}
onClick={() => {
const newColumns = (config.leftPanel?.addModalColumns || []).filter((_, i) => i !== index);
updateLeftPanel({ addModalColumns: newColumns });
}}
className="h-8 w-8 p-0"
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
>
<X className="h-3 w-3" />
</Button>
</div>
);
})
)}
</div>
</div>
)}
</div>
{/* 좌측 패널 데이터 필터링 */}
<div className="mt-4 space-y-4 border-t pt-4">
<h3 className="text-sm font-semibold"> </h3>
<p className="text-muted-foreground text-xs"> </p>
<DataFilterConfigPanel
tableName={config.leftPanel?.tableName || screenTableName}
columns={leftTableColumns.map(
(col) =>
({
columnName: col.columnName,
columnLabel: col.columnLabel || col.columnName,
dataType: col.dataType || "text",
input_type: (col as any).input_type,
}) as any,
)}
config={config.leftPanel?.dataFilter}
onConfigChange={(dataFilter) => updateLeftPanel({ dataFilter })}
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
/>
</div>
{/* 우측 패널 설정 */}
<div className="mt-4 space-y-4 border-t pt-4">
<h3 className="text-sm font-semibold"> ({relationshipType === "detail" ? "상세" : "조인"})</h3>
<div className="space-y-2">
<Label> </Label>
<Input
value={localTitles.right}
onChange={(e) => {
setIsUserEditing(true);
setLocalTitles((prev) => ({ ...prev, right: e.target.value }));
}}
onBlur={() => {
setIsUserEditing(false);
updateRightPanel({ title: localTitles.right });
}}
2025-10-15 17:25:38 +09:00
placeholder="우측 패널 제목"
/>
</div>
<div className="space-y-2">
<Label> (px)</Label>
<Input
type="number"
value={config.rightPanel?.panelHeaderHeight || 48}
onChange={(e) => updateRightPanel({ panelHeaderHeight: parseInt(e.target.value) || 48 })}
placeholder="48"
min={32}
max={120}
/>
<p className="text-xs text-muted-foreground"> (기본: 48px)</p>
</div>
2025-10-15 17:25:38 +09:00
{/* 관계 타입에 따라 테이블 선택 UI 변경 */}
{relationshipType === "detail" ? (
// 상세 모드: 좌측과 동일한 테이블 (자동 설정)
2025-10-15 17:25:38 +09:00
<div className="space-y-2">
<Label> ( )</Label>
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3">
<p className="text-sm font-medium text-gray-900">
{config.leftPanel?.tableName || screenTableName || "테이블이 지정되지 않음"}
</p>
2025-10-15 17:25:38 +09:00
<p className="mt-1 text-xs text-gray-500"> </p>
</div>
</div>
) : (
// 조인 모드: 전체 테이블에서 선택 가능
2025-10-15 17:25:38 +09:00
<div className="space-y-2">
<Label> ( )</Label>
<Popover open={rightTableOpen} onOpenChange={setRightTableOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={rightTableOpen}
className="w-full justify-between"
>
{config.rightPanel?.tableName || "테이블을 선택하세요"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="테이블 검색..." />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{availableRightTables.map((table) => (
2025-10-15 17:25:38 +09:00
<CommandItem
key={table.tableName}
value={`${table.displayName || ""} ${table.tableName}`}
onSelect={() => {
updateRightPanel({ tableName: table.tableName });
2025-10-15 17:25:38 +09:00
setRightTableOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.rightPanel?.tableName === table.tableName ? "opacity-100" : "opacity-0",
)}
/>
{table.displayName || table.tableName}
{table.displayName && <span className="ml-2 text-xs text-gray-500">({table.tableName})</span>}
2025-10-15 17:25:38 +09:00
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
)}
<div className="space-y-2">
<Label> </Label>
<Select
value={config.rightPanel?.displayMode || "list"}
onValueChange={(value: "list" | "table") => updateRightPanel({ displayMode: value })}
>
<SelectTrigger className="bg-white">
<SelectValue placeholder="표시 모드 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="list">
<div className="flex flex-col">
<span className="font-medium"> (LIST)</span>
<span className="text-xs text-gray-500"> ()</span>
</div>
</SelectItem>
<SelectItem value="table">
<div className="flex flex-col">
<span className="font-medium"> (TABLE)</span>
<span className="text-xs text-gray-500"> </span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 요약 표시 설정 (LIST 모드에서만) */}
{(config.rightPanel?.displayMode || "list") === "list" && (
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
<Label className="text-sm font-semibold"> </Label>
2025-12-11 11:37:40 +09:00
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
type="number"
min="1"
max="10"
value={config.rightPanel?.summaryColumnCount ?? 3}
onChange={(e) => {
const value = parseInt(e.target.value) || 3;
updateRightPanel({ summaryColumnCount: value });
}}
className="bg-white"
/>
2025-12-11 11:37:40 +09:00
<p className="text-xs text-gray-500"> (기본: 3개)</p>
</div>
<div className="flex items-center justify-between space-x-2">
<div className="flex-1">
<Label className="text-xs"> </Label>
<p className="text-xs text-gray-500"> </p>
</div>
<Checkbox
checked={config.rightPanel?.summaryShowLabel ?? true}
onCheckedChange={(checked) => {
updateRightPanel({ summaryShowLabel: checked as boolean });
}}
/>
</div>
</div>
)}
{/* 컬럼 매핑 - 조인 모드에서만 표시 */}
2025-10-15 17:25:38 +09:00
{relationshipType !== "detail" && (
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
2025-12-11 11:37:40 +09:00
<div className="flex items-center justify-between">
<div>
<Label className="text-sm font-semibold"> ( )</Label>
<p className="text-xs text-gray-600"> </p>
</div>
<Button
size="sm"
variant="ghost"
className="h-6 text-xs"
onClick={() => {
const currentKeys = config.rightPanel?.relation?.keys || [];
// 단일키에서 복합키로 전환 시 기존 값 유지
if (
currentKeys.length === 0 &&
config.rightPanel?.relation?.leftColumn &&
config.rightPanel?.relation?.foreignKey
) {
updateRightPanel({
relation: {
...config.rightPanel?.relation,
keys: [
{
leftColumn: config.rightPanel.relation.leftColumn,
rightColumn: config.rightPanel.relation.foreignKey,
},
{ leftColumn: "", rightColumn: "" },
],
},
});
} else {
updateRightPanel({
relation: {
...config.rightPanel?.relation,
keys: [...currentKeys, { leftColumn: "", rightColumn: "" }],
},
});
}
}}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
2025-10-15 17:25:38 +09:00
2025-12-11 11:37:40 +09:00
<p className="text-[10px] text-blue-600">복합키: 여러 (: item_code + lot_number)</p>
{/* 복합키가 설정된 경우 */}
{(config.rightPanel?.relation?.keys || []).length > 0 ? (
<>
{(config.rightPanel?.relation?.keys || []).map((key, index) => (
<div key={index} className="space-y-2 rounded-md border bg-white p-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium"> {index + 1}</span>
<Button
size="sm"
variant="ghost"
className="text-destructive h-6 w-6 p-0"
onClick={() => {
const newKeys = (config.rightPanel?.relation?.keys || []).filter((_, i) => i !== index);
updateRightPanel({
relation: { ...config.rightPanel?.relation, keys: newKeys },
});
}}
>
<X className="h-3 w-3" />
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<Label className="text-xs"> </Label>
<Select
value={key.leftColumn || ""}
onValueChange={(value) => {
const newKeys = [...(config.rightPanel?.relation?.keys || [])];
newKeys[index] = { ...newKeys[index], leftColumn: value };
2025-10-15 17:25:38 +09:00
updateRightPanel({
2025-12-11 11:37:40 +09:00
relation: { ...config.rightPanel?.relation, keys: newKeys },
2025-10-15 17:25:38 +09:00
});
}}
>
2025-12-11 11:37:40 +09:00
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="좌측 컬럼" />
</SelectTrigger>
<SelectContent>
{leftTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs"> </Label>
<Select
value={key.rightColumn || ""}
onValueChange={(value) => {
const newKeys = [...(config.rightPanel?.relation?.keys || [])];
newKeys[index] = { ...newKeys[index], rightColumn: value };
2025-10-15 17:25:38 +09:00
updateRightPanel({
2025-12-11 11:37:40 +09:00
relation: { ...config.rightPanel?.relation, keys: newKeys },
2025-10-15 17:25:38 +09:00
});
}}
>
2025-12-11 11:37:40 +09:00
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="우측 컬럼" />
</SelectTrigger>
<SelectContent>
{rightTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
))}
</>
) : (
/* 단일키 (하위 호환성) */
<>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Popover open={leftColumnOpen} onOpenChange={setLeftColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={leftColumnOpen}
className="w-full justify-between"
disabled={!config.leftPanel?.tableName}
>
{config.rightPanel?.relation?.leftColumn || "좌측 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{leftTableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
updateRightPanel({
relation: { ...config.rightPanel?.relation, leftColumn: value },
});
setLeftColumnOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.rightPanel?.relation?.leftColumn === column.columnName
? "opacity-100"
: "opacity-0",
)}
/>
{column.columnName}
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center justify-center">
<ArrowRight className="h-4 w-4 text-gray-400" />
</div>
<div className="space-y-2">
<Label className="text-xs"> ()</Label>
<Popover open={rightColumnOpen} onOpenChange={setRightColumnOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={rightColumnOpen}
className="w-full justify-between"
disabled={!config.rightPanel?.tableName}
>
{config.rightPanel?.relation?.foreignKey || "우측 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{rightTableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
updateRightPanel({
relation: { ...config.rightPanel?.relation, foreignKey: value },
});
setRightColumnOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.rightPanel?.relation?.foreignKey === column.columnName
? "opacity-100"
: "opacity-0",
)}
/>
{column.columnName}
<span className="ml-2 text-xs text-gray-500">({column.columnLabel || ""})</span>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
</>
)}
2025-10-15 17:25:38 +09:00
</div>
)}
<div className="flex items-center justify-between">
<Label> </Label>
<Switch
checked={config.rightPanel?.showSearch ?? true}
onCheckedChange={(checked) => updateRightPanel({ showSearch: checked })}
/>
</div>
<div className="flex items-center justify-between">
<Label> </Label>
<Switch
checked={config.rightPanel?.showAdd ?? false}
onCheckedChange={(checked) => updateRightPanel({ showAdd: checked })}
/>
</div>
{/* 우측 패널 표시 컬럼 설정 */}
<div className="space-y-3 rounded-lg border border-green-200 bg-green-50 p-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => {
const currentColumns = config.rightPanel?.columns || [];
2025-12-11 11:37:40 +09:00
const newColumns = [...currentColumns, { name: "", label: "", width: 100 }];
updateRightPanel({ columns: newColumns });
}}
className="h-7 text-xs"
disabled={!config.rightPanel?.tableName}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<p className="text-xs text-gray-600">
. .
</p>
{/* 선택된 컬럼 목록 */}
<div className="space-y-2">
{(config.rightPanel?.columns || []).length === 0 ? (
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
<p className="text-xs text-gray-500"> </p>
2025-12-11 11:37:40 +09:00
<p className="mt-1 text-[10px] text-gray-400"> </p>
</div>
) : (
(config.rightPanel?.columns || []).map((col, index) => {
const isTableMode = config.rightPanel?.displayMode === "table";
2025-12-11 11:37:40 +09:00
return (
2025-12-11 11:37:40 +09:00
<div key={index} className="space-y-2 rounded-md border bg-white p-2">
<div className="flex items-center gap-2">
{/* 순서 변경 버튼 */}
<div className="flex flex-col gap-0.5">
<Button
size="sm"
variant="ghost"
onClick={() => {
if (index === 0) return;
const newColumns = [...(config.rightPanel?.columns || [])];
[newColumns[index - 1], newColumns[index]] = [newColumns[index], newColumns[index - 1]];
updateRightPanel({ columns: newColumns });
}}
disabled={index === 0}
className="h-4 w-6 p-0"
title="위로 이동"
>
<ArrowUp className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
const columns = config.rightPanel?.columns || [];
if (index === columns.length - 1) return;
const newColumns = [...columns];
[newColumns[index], newColumns[index + 1]] = [newColumns[index + 1], newColumns[index]];
updateRightPanel({ columns: newColumns });
}}
disabled={index === (config.rightPanel?.columns || []).length - 1}
className="h-4 w-6 p-0"
title="아래로 이동"
>
<ArrowDown className="h-3 w-3" />
</Button>
</div>
<div className="flex-1">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
{col.label || col.name || "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandEmpty className="text-xs"> .</CommandEmpty>
<div className="max-h-[300px] overflow-auto">
{/* 기본 테이블 컬럼 */}
<CommandGroup heading={rightTableName ? `📋 ${rightTableName} 컬럼` : "📋 기본 컬럼"}>
{rightTableColumns.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
name: value,
label: column.columnLabel || value,
};
updateRightPanel({ columns: newColumns });
// Popover 닫기
document.body.click();
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
col.name === column.columnName ? "opacity-100" : "opacity-0",
)}
/>
{column.columnLabel || column.columnName}
<span className="ml-2 text-[10px] text-gray-500">({column.columnName})</span>
</CommandItem>
))}
</CommandGroup>
{/* 🆕 엔티티 참조 테이블 컬럼 */}
{rightTableName &&
entityReferenceTables[rightTableName]?.map((refTable) => (
<CommandGroup
key={refTable.tableName}
heading={`🔗 ${refTable.tableName} (엔티티)`}
>
{refTable.columns.map((column) => {
const fullColumnName = `${refTable.tableName}.${column.columnName}`;
return (
<CommandItem
key={fullColumnName}
value={fullColumnName}
onSelect={(value) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
name: value,
label: column.columnLabel || column.columnName,
};
updateRightPanel({ columns: newColumns });
// Popover 닫기
document.body.click();
}}
className="pl-6 text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
col.name === fullColumnName ? "opacity-100" : "opacity-0",
)}
/>
{column.columnLabel || column.columnName}
<span className="ml-2 text-[10px] text-gray-500">
({column.columnName})
</span>
</CommandItem>
);
})}
</CommandGroup>
))}
</div>
</Command>
</PopoverContent>
</Popover>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => {
2025-12-11 11:37:40 +09:00
const newColumns = (config.rightPanel?.columns || []).filter((_, i) => i !== index);
updateRightPanel({ columns: newColumns });
}}
2025-12-11 11:37:40 +09:00
className="h-8 w-8 p-0"
>
2025-12-11 11:37:40 +09:00
<X className="h-3 w-3" />
</Button>
</div>
2025-12-11 11:37:40 +09:00
{/* 테이블 모드 전용 옵션 */}
{isTableMode && (
<div className="grid grid-cols-3 gap-2 pt-1">
<div className="space-y-1">
<Label className="text-[10px] text-gray-600"> (px)</Label>
<Input
type="number"
min="50"
value={col.width || 100}
onChange={(e) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
2025-12-11 11:37:40 +09:00
width: parseInt(e.target.value) || 100,
};
updateRightPanel({ columns: newColumns });
}}
2025-12-11 11:37:40 +09:00
className="h-7 text-xs"
/>
2025-12-11 11:37:40 +09:00
</div>
<div className="space-y-1">
<Label className="text-[10px] text-gray-600"></Label>
<Select
value={col.align || "left"}
onValueChange={(value: "left" | "center" | "right") => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
align: value,
};
updateRightPanel({ columns: newColumns });
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left"></SelectItem>
<SelectItem value="center"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<label className="flex h-7 cursor-pointer items-center gap-1 text-[10px]">
<input
type="checkbox"
2025-12-11 11:37:40 +09:00
checked={col.sortable ?? false}
onChange={(e) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
2025-12-11 11:37:40 +09:00
sortable: e.target.checked,
};
updateRightPanel({ columns: newColumns });
}}
className="h-3 w-3"
/>
2025-12-11 11:37:40 +09:00
</label>
</div>
2025-12-11 11:37:40 +09:00
</div>
)}
{/* LIST 모드: 볼드 설정 */}
{!isTableMode && (
<div className="space-y-2 border-t pt-2">
<Label className="text-[10px] text-gray-600"> </Label>
<label className="flex cursor-pointer items-center gap-1.5 text-[10px]">
<input
type="checkbox"
checked={col.bold ?? false}
onChange={(e) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = { ...newColumns[index], bold: e.target.checked };
updateRightPanel({ columns: newColumns });
}}
className="h-3 w-3"
/>
</label>
</div>
)}
{/* 🆕 날짜 타입 포맷 설정 */}
{(() => {
// 컬럼 타입 확인 (DB 타입 + 입력 타입 모두 체크)
const column = rightTableColumns.find((c) => c.columnName === col.name);
const dbDateTypes = [
"date",
"timestamp",
"timestamptz",
"timestamp with time zone",
"timestamp without time zone",
"time",
"timetz",
];
const inputDateTypes = ["date", "datetime", "time"];
const isDate =
column &&
(dbDateTypes.includes(column.dataType?.toLowerCase() || "") ||
inputDateTypes.includes(column.input_type?.toLowerCase() || "") ||
inputDateTypes.includes(column.webType?.toLowerCase() || ""));
if (!isDate) return null;
return (
<div className="space-y-2 border-t pt-2">
<Label className="text-[10px] text-gray-600"> </Label>
<Select
value={col.format?.dateFormat || "YYYY-MM-DD"}
onValueChange={(value) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
format: {
...newColumns[index].format,
type: "date",
dateFormat: value,
},
};
updateRightPanel({ columns: newColumns });
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="날짜 형식 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="YYYY-MM-DD">2024-01-15 (YYYY-MM-DD)</SelectItem>
<SelectItem value="YYYY.MM.DD">2024.01.15 (YYYY.MM.DD)</SelectItem>
<SelectItem value="YYYY/MM/DD">2024/01/15 (YYYY/MM/DD)</SelectItem>
<SelectItem value="MM-DD-YYYY">01-15-2024 (MM-DD-YYYY)</SelectItem>
<SelectItem value="DD-MM-YYYY">15-01-2024 (DD-MM-YYYY)</SelectItem>
<SelectItem value="YYYY-MM-DD HH:mm">2024-01-15 14:30 (+)</SelectItem>
<SelectItem value="YYYY-MM-DD HH:mm:ss">2024-01-15 14:30:45 (++)</SelectItem>
<SelectItem value="relative"> (3 )</SelectItem>
</SelectContent>
</Select>
{/* 미리보기 */}
<div className="rounded bg-gray-100 p-2">
2025-12-11 11:37:40 +09:00
<p className="mb-1 text-[10px] text-gray-600">:</p>
<p className="text-xs font-medium">
2025-12-11 11:37:40 +09:00
{(() => {
const now = new Date();
const format = col.format?.dateFormat || "YYYY-MM-DD";
if (format === "relative") return "3일 전";
return format
.replace("YYYY", String(now.getFullYear()))
.replace("MM", String(now.getMonth() + 1).padStart(2, "0"))
.replace("DD", String(now.getDate()).padStart(2, "0"))
.replace("HH", String(now.getHours()).padStart(2, "0"))
.replace("mm", String(now.getMinutes()).padStart(2, "0"))
.replace("ss", String(now.getSeconds()).padStart(2, "0"));
})()}
</p>
</div>
2025-12-11 11:37:40 +09:00
</div>
);
})()}
{/* 🆕 숫자 타입 포맷 설정 */}
{(() => {
// 컬럼 타입 확인 (DB 타입 + 입력 타입 모두 체크)
const column = rightTableColumns.find((c) => c.columnName === col.name);
const dbNumericTypes = [
"numeric",
"decimal",
"integer",
"bigint",
"double precision",
"real",
"smallint",
"int4",
"int8",
"float4",
"float8",
];
const inputNumericTypes = ["number", "decimal", "currency", "integer"];
const isNumeric =
column &&
(dbNumericTypes.includes(column.dataType?.toLowerCase() || "") ||
inputNumericTypes.includes(column.input_type?.toLowerCase() || "") ||
inputNumericTypes.includes(column.webType?.toLowerCase() || ""));
if (!isNumeric) return null;
return (
<div className="space-y-2 border-t pt-2">
<Label className="text-[10px] text-gray-600"> </Label>
<div className="grid grid-cols-2 gap-2">
{/* 천 단위 구분자 */}
<label className="flex cursor-pointer items-center gap-1.5 text-[10px]">
<input
type="checkbox"
checked={col.format?.thousandSeparator ?? false}
onChange={(e) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
format: {
...newColumns[index].format,
thousandSeparator: e.target.checked,
},
};
updateRightPanel({ columns: newColumns });
}}
className="h-3 w-3"
/>
(,)
</label>
{/* 소수점 자릿수 */}
<div className="space-y-1">
<Label className="text-[10px] text-gray-600"></Label>
<Input
type="number"
min="0"
max="10"
placeholder="0"
value={col.format?.decimalPlaces ?? ""}
onChange={(e) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
format: {
...newColumns[index].format,
decimalPlaces: e.target.value ? parseInt(e.target.value) : undefined,
},
};
updateRightPanel({ columns: newColumns });
}}
className="h-7 text-xs"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2">
{/* 접두사 */}
<div className="space-y-1">
<Label className="text-[10px] text-gray-600"></Label>
<Input
type="text"
placeholder="₩, $, 등"
value={col.format?.prefix ?? ""}
onChange={(e) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
format: {
...newColumns[index].format,
prefix: e.target.value || undefined,
},
};
updateRightPanel({ columns: newColumns });
}}
className="h-7 text-xs"
/>
</div>
{/* 접미사 */}
<div className="space-y-1">
<Label className="text-[10px] text-gray-600"></Label>
<Input
type="text"
placeholder="원, 개, 등"
value={col.format?.suffix ?? ""}
onChange={(e) => {
const newColumns = [...(config.rightPanel?.columns || [])];
newColumns[index] = {
...newColumns[index],
format: {
...newColumns[index].format,
suffix: e.target.value || undefined,
},
};
updateRightPanel({ columns: newColumns });
}}
className="h-7 text-xs"
/>
</div>
</div>
{/* 미리보기 */}
{(col.format?.thousandSeparator ||
col.format?.prefix ||
col.format?.suffix ||
col.format?.decimalPlaces !== undefined) && (
<div className="rounded bg-gray-100 p-2">
<p className="mb-1 text-[10px] text-gray-600">:</p>
<p className="text-xs font-medium">
{col.format?.prefix || ""}
{(1234567.89).toLocaleString("ko-KR", {
minimumFractionDigits: col.format?.decimalPlaces ?? 0,
maximumFractionDigits: col.format?.decimalPlaces ?? 10,
useGrouping: col.format?.thousandSeparator ?? false,
})}
{col.format?.suffix || ""}
</p>
</div>
)}
</div>
);
})()}
</div>
);
})
)}
</div>
</div>
{/* 우측 패널 추가 모달 컬럼 설정 */}
{config.rightPanel?.showAdd && (
<div className="space-y-3 rounded-lg border border-purple-200 bg-purple-50 p-3">
<div className="flex items-center justify-between">
<Label className="text-sm font-semibold"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => {
const currentColumns = config.rightPanel?.addModalColumns || [];
2025-12-11 11:37:40 +09:00
const newColumns = [...currentColumns, { name: "", label: "", required: false }];
updateRightPanel({ addModalColumns: newColumns });
}}
className="h-7 text-xs"
disabled={!config.rightPanel?.tableName}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
2025-12-11 11:37:40 +09:00
<p className="text-xs text-gray-600"> </p>
<div className="space-y-2">
{(config.rightPanel?.addModalColumns || []).length === 0 ? (
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
<p className="text-xs text-gray-500"> </p>
</div>
) : (
(config.rightPanel?.addModalColumns || []).map((col, index) => {
// 현재 컬럼이 PK인지 확인
2025-12-11 11:37:40 +09:00
const column = rightTableColumns.find((c) => c.columnName === col.name);
const isPK = column?.isPrimaryKey || false;
2025-12-11 11:37:40 +09:00
return (
2025-12-11 11:37:40 +09:00
<div
key={index}
className={cn(
"flex items-center gap-2 rounded-md border p-2",
isPK ? "border-yellow-300 bg-yellow-50" : "bg-white",
)}
>
{isPK && (
<span className="rounded bg-yellow-200 px-1.5 py-0.5 text-[10px] font-semibold text-yellow-700">
PK
</span>
)}
<div className="flex-1">
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
disabled={isPK}
className="h-8 w-full justify-between text-xs"
>
{col.name || "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandEmpty className="text-xs"> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{rightTableColumns
.filter((column) => !["company_code", "company_name"].includes(column.columnName))
.map((column) => (
<CommandItem
key={column.columnName}
value={column.columnName}
onSelect={(value) => {
const newColumns = [...(config.rightPanel?.addModalColumns || [])];
newColumns[index] = {
...newColumns[index],
name: value,
label: column.columnLabel || value,
};
updateRightPanel({ addModalColumns: newColumns });
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
col.name === column.columnName ? "opacity-100" : "opacity-0",
)}
/>
{column.columnLabel || column.columnName}
<span className="ml-2 text-[10px] text-gray-500">({column.columnName})</span>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center gap-1">
<label className="flex cursor-pointer items-center gap-1 text-xs text-gray-600">
<input
type="checkbox"
checked={col.required ?? false}
disabled={isPK}
2025-12-11 11:37:40 +09:00
onChange={(e) => {
const newColumns = [...(config.rightPanel?.addModalColumns || [])];
newColumns[index] = {
...newColumns[index],
required: e.target.checked,
};
updateRightPanel({ addModalColumns: newColumns });
}}
className="h-3 w-3"
/>
</label>
</div>
<Button
size="sm"
variant="ghost"
disabled={isPK}
onClick={() => {
const newColumns = (config.rightPanel?.addModalColumns || []).filter((_, i) => i !== index);
updateRightPanel({ addModalColumns: newColumns });
}}
className="h-8 w-8 p-0"
title={isPK ? "PK 컬럼은 삭제할 수 없습니다" : ""}
>
<X className="h-3 w-3" />
</Button>
</div>
2025-12-11 11:37:40 +09:00
);
})
)}
</div>
{/* 중계 테이블 설정 */}
2025-12-11 11:37:40 +09:00
<div className="mt-3 space-y-3 rounded-lg border border-orange-200 bg-orange-50 p-3">
<Label className="text-sm font-semibold"> (N:M )</Label>
2025-12-11 11:37:40 +09:00
<p className="text-xs text-gray-600"> </p>
<div>
<Label className="text-xs text-gray-700"> </Label>
<Input
value={config.rightPanel?.addConfig?.targetTable || ""}
onChange={(e) => {
const addConfig = config.rightPanel?.addConfig || {};
updateRightPanel({
addConfig: {
...addConfig,
targetTable: e.target.value,
},
});
}}
placeholder="예: user_dept"
className="mt-1 h-8 text-xs"
/>
2025-12-11 11:37:40 +09:00
<p className="mt-1 text-[10px] text-gray-500"> </p>
</div>
<div>
<Label className="text-xs text-gray-700"> </Label>
<Input
value={config.rightPanel?.addConfig?.leftPanelColumn || ""}
onChange={(e) => {
const addConfig = config.rightPanel?.addConfig || {};
updateRightPanel({
addConfig: {
...addConfig,
leftPanelColumn: e.target.value,
},
});
}}
placeholder="예: dept_code"
className="mt-1 h-8 text-xs"
/>
2025-12-11 11:37:40 +09:00
<p className="mt-1 text-[10px] text-gray-500"> </p>
</div>
<div>
<Label className="text-xs text-gray-700"> </Label>
<Input
value={config.rightPanel?.addConfig?.targetColumn || ""}
onChange={(e) => {
const addConfig = config.rightPanel?.addConfig || {};
updateRightPanel({
addConfig: {
...addConfig,
targetColumn: e.target.value,
},
});
}}
placeholder="예: dept_code"
className="mt-1 h-8 text-xs"
/>
2025-12-11 11:37:40 +09:00
<p className="mt-1 text-[10px] text-gray-500"> </p>
</div>
<div>
<Label className="text-xs text-gray-700"> (JSON)</Label>
<textarea
value={JSON.stringify(config.rightPanel?.addConfig?.autoFillColumns || {}, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
const addConfig = config.rightPanel?.addConfig || {};
updateRightPanel({
addConfig: {
...addConfig,
autoFillColumns: parsed,
},
});
} catch (err) {
// JSON 파싱 오류는 무시 (입력 중)
}
}}
placeholder='{ "is_primary": false }'
2025-12-11 11:37:40 +09:00
className="border-input mt-1 h-20 w-full rounded-md border bg-white px-3 py-2 font-mono text-xs"
/>
2025-12-11 11:37:40 +09:00
<p className="mt-1 text-[10px] text-gray-500"> (: is_primary: false)</p>
</div>
</div>
</div>
)}
</div>
{/* 우측 패널 데이터 필터링 */}
2025-12-11 11:37:40 +09:00
<div className="mt-4 space-y-4 border-t pt-4">
<h3 className="text-sm font-semibold"> </h3>
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground text-xs"> </p>
<DataFilterConfigPanel
tableName={config.rightPanel?.tableName}
columns={rightTableColumns.map(
(col) =>
({
columnName: col.columnName,
columnLabel: col.columnLabel || col.columnName,
dataType: col.dataType || "text",
input_type: (col as any).input_type,
}) as any,
)}
config={config.rightPanel?.dataFilter}
onConfigChange={(dataFilter) => updateRightPanel({ dataFilter })}
menuObjid={menuObjid} // 🆕 메뉴 OBJID 전달
/>
</div>
{/* 우측 패널 중복 제거 */}
2025-12-11 11:37:40 +09:00
<div className="mt-4 space-y-4 border-t pt-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold"> </h3>
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground text-xs"> </p>
</div>
<Switch
checked={config.rightPanel?.deduplication?.enabled ?? false}
onCheckedChange={(checked) => {
if (checked) {
updateRightPanel({
deduplication: {
enabled: true,
groupByColumn: "",
keepStrategy: "latest",
sortColumn: "start_date",
},
});
} else {
updateRightPanel({ deduplication: undefined });
}
}}
/>
</div>
{config.rightPanel?.deduplication?.enabled && (
2025-12-11 11:37:40 +09:00
<div className="space-y-3 border-l-2 pl-4">
{/* 중복 제거 기준 컬럼 */}
<div>
<Label className="text-xs"> </Label>
<Select
value={config.rightPanel?.deduplication?.groupByColumn || ""}
onValueChange={(value) =>
updateRightPanel({
deduplication: { ...config.rightPanel?.deduplication!, groupByColumn: value },
})
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="기준 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{rightTableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground mt-1 text-[10px]">
</p>
</div>
{/* 유지 전략 */}
<div>
<Label className="text-xs"> </Label>
<Select
value={config.rightPanel?.deduplication?.keepStrategy || "latest"}
onValueChange={(value: any) =>
updateRightPanel({
deduplication: { ...config.rightPanel?.deduplication!, keepStrategy: value },
})
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="latest"> ( )</SelectItem>
<SelectItem value="earliest"> ( )</SelectItem>
<SelectItem value="current_date"> ( )</SelectItem>
<SelectItem value="base_price"> </SelectItem>
</SelectContent>
</Select>
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground mt-1 text-[10px]">
{config.rightPanel?.deduplication?.keepStrategy === "latest" &&
"가장 최근에 추가된 데이터를 표시합니다"}
{config.rightPanel?.deduplication?.keepStrategy === "earliest" &&
"가장 먼저 추가된 데이터를 표시합니다"}
{config.rightPanel?.deduplication?.keepStrategy === "current_date" &&
"오늘 날짜 기준으로 유효한 기간의 데이터를 표시합니다"}
{config.rightPanel?.deduplication?.keepStrategy === "base_price" &&
"기준단가(base_price)로 체크된 데이터를 표시합니다"}
</p>
</div>
{/* 정렬 기준 컬럼 (latest/earliest만) */}
{(config.rightPanel?.deduplication?.keepStrategy === "latest" ||
config.rightPanel?.deduplication?.keepStrategy === "earliest") && (
<div>
<Label className="text-xs"> </Label>
<Select
value={config.rightPanel?.deduplication?.sortColumn || ""}
onValueChange={(value) =>
updateRightPanel({
deduplication: { ...config.rightPanel?.deduplication!, sortColumn: value },
})
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="정렬 기준 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{rightTableColumns
2025-12-11 11:37:40 +09:00
.filter(
(col) =>
col.dataType === "date" || col.dataType === "timestamp" || col.columnName.includes("date"),
)
.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground mt-1 text-[10px]">
/ ( )
</p>
</div>
)}
</div>
)}
</div>
{/* 🆕 우측 패널 수정 버튼 설정 */}
2025-12-11 11:37:40 +09:00
<div className="mt-4 space-y-4 border-t pt-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-semibold"> </h3>
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground text-xs"> </p>
</div>
<Switch
checked={config.rightPanel?.editButton?.enabled ?? true}
onCheckedChange={(checked) => {
updateRightPanel({
editButton: {
enabled: checked,
mode: config.rightPanel?.editButton?.mode || "auto",
buttonLabel: config.rightPanel?.editButton?.buttonLabel,
buttonVariant: config.rightPanel?.editButton?.buttonVariant,
},
});
}}
/>
</div>
{(config.rightPanel?.editButton?.enabled ?? true) && (
2025-12-11 11:37:40 +09:00
<div className="space-y-3 border-l-2 pl-4">
{/* 수정 모드 */}
<div>
<Label className="text-xs"> </Label>
<Select
value={config.rightPanel?.editButton?.mode || "auto"}
onValueChange={(value: "auto" | "modal") =>
updateRightPanel({
editButton: {
...config.rightPanel?.editButton,
mode: value,
enabled: config.rightPanel?.editButton?.enabled ?? true,
},
})
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto"> ()</SelectItem>
<SelectItem value="modal"> </SelectItem>
</SelectContent>
</Select>
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground mt-1 text-[10px]">
{config.rightPanel?.editButton?.mode === "modal"
? "지정한 화면을 모달로 열어 데이터를 수정합니다"
: "현재 위치에서 직접 데이터를 수정합니다"}
</p>
</div>
{/* 모달 화면 선택 (modal 모드일 때만) */}
{config.rightPanel?.editButton?.mode === "modal" && (
<div>
<Label className="text-xs"> </Label>
<ScreenSelector
value={config.rightPanel?.editButton?.modalScreenId}
onChange={(screenId) =>
updateRightPanel({
editButton: {
...config.rightPanel?.editButton!,
modalScreenId: screenId,
},
})
}
/>
2025-12-11 11:37:40 +09:00
<p className="text-muted-foreground mt-1 text-[10px]"> </p>
</div>
)}
{/* 버튼 라벨 */}
<div>
<Label className="text-xs"> </Label>
<Input
value={config.rightPanel?.editButton?.buttonLabel || "수정"}
onChange={(e) =>
updateRightPanel({
editButton: {
...config.rightPanel?.editButton!,
buttonLabel: e.target.value,
enabled: config.rightPanel?.editButton?.enabled ?? true,
mode: config.rightPanel?.editButton?.mode || "auto",
},
})
}
className="h-8 text-xs"
placeholder="수정"
/>
</div>
{/* 버튼 스타일 */}
<div>
<Label className="text-xs"> </Label>
<Select
value={config.rightPanel?.editButton?.buttonVariant || "outline"}
onValueChange={(value: any) =>
updateRightPanel({
editButton: {
...config.rightPanel?.editButton!,
buttonVariant: value,
enabled: config.rightPanel?.editButton?.enabled ?? true,
mode: config.rightPanel?.editButton?.mode || "auto",
},
})
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"> ()</SelectItem>
<SelectItem value="outline"></SelectItem>
<SelectItem value="ghost"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 🆕 그룹핑 기준 컬럼 설정 (modal 모드일 때만 표시) */}
{config.rightPanel?.editButton?.mode === "modal" && (
<GroupByColumnsSelector
tableName={config.rightPanel?.tableName}
selectedColumns={config.rightPanel?.editButton?.groupByColumns || []}
onChange={(columns) => {
updateRightPanel({
editButton: {
...config.rightPanel?.editButton!,
groupByColumns: columns,
enabled: config.rightPanel?.editButton?.enabled ?? true,
mode: config.rightPanel?.editButton?.mode || "auto",
},
});
}}
/>
)}
</div>
)}
</div>
2025-12-11 11:37:40 +09:00
{/* 🆕 우측 패널 삭제 버튼 설정 */}
<div className="space-y-3 rounded-lg border border-red-200 bg-red-50 p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold"> </h3>
</div>
<Switch
checked={config.rightPanel?.deleteButton?.enabled ?? true}
onCheckedChange={(checked) => {
updateRightPanel({
deleteButton: {
enabled: checked,
buttonLabel: config.rightPanel?.deleteButton?.buttonLabel,
buttonVariant: config.rightPanel?.deleteButton?.buttonVariant,
},
});
}}
/>
</div>
{(config.rightPanel?.deleteButton?.enabled ?? true) && (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
{/* 버튼 라벨 */}
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
value={config.rightPanel?.deleteButton?.buttonLabel || ""}
placeholder="삭제"
onChange={(e) => {
updateRightPanel({
deleteButton: {
...config.rightPanel?.deleteButton!,
buttonLabel: e.target.value || undefined,
enabled: config.rightPanel?.deleteButton?.enabled ?? true,
},
});
}}
className="h-8 text-xs"
/>
</div>
2025-10-15 17:25:38 +09:00
2025-12-11 11:37:40 +09:00
{/* 버튼 스타일 */}
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Select
value={config.rightPanel?.deleteButton?.buttonVariant || "ghost"}
onValueChange={(value: "default" | "outline" | "ghost" | "destructive") => {
updateRightPanel({
deleteButton: {
...config.rightPanel?.deleteButton!,
buttonVariant: value,
enabled: config.rightPanel?.deleteButton?.enabled ?? true,
},
});
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ghost">Ghost ()</SelectItem>
<SelectItem value="outline">Outline</SelectItem>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="destructive">Destructive ()</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 삭제 확인 메시지 */}
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
value={config.rightPanel?.deleteButton?.confirmMessage || ""}
placeholder="정말 삭제하시겠습니까?"
onChange={(e) => {
updateRightPanel({
deleteButton: {
...config.rightPanel?.deleteButton!,
confirmMessage: e.target.value || undefined,
enabled: config.rightPanel?.deleteButton?.enabled ?? true,
},
});
}}
className="h-8 text-xs"
/>
</div>
</div>
)}
</div>
{/* 레이아웃 설정 */}
<div className="mt-4 space-y-4 border-t pt-4">
2025-10-15 17:25:38 +09:00
<div className="space-y-2">
<Label> : {config.splitRatio || 30}%</Label>
<Slider
value={[config.splitRatio || 30]}
onValueChange={(value) => updateConfig({ splitRatio: value[0] })}
min={20}
max={80}
step={5}
/>
</div>
<div className="flex items-center justify-between">
<Label> </Label>
<Switch
checked={config.resizable ?? true}
onCheckedChange={(checked) => updateConfig({ resizable: checked })}
/>
</div>
<div className="flex items-center justify-between">
<Label> </Label>
<Switch
checked={config.autoLoad ?? true}
onCheckedChange={(checked) => updateConfig({ autoLoad: checked })}
/>
</div>
</div>
2025-10-15 17:25:38 +09:00
</div>
);
};