1410 lines
61 KiB
TypeScript
1410 lines
61 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* UnifiedRepeater 설정 패널
|
|
*
|
|
* 렌더링 모드별 설정:
|
|
* - inline: 현재 화면 테이블 컬럼 직접 입력
|
|
* - modal: 엔티티 선택 + 추가 입력 (FK 저장 + 추가 컬럼 입력)
|
|
*/
|
|
|
|
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
|
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";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import {
|
|
Database,
|
|
Link2,
|
|
Plus,
|
|
Trash2,
|
|
GripVertical,
|
|
ArrowRight,
|
|
Calculator,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Eye,
|
|
EyeOff,
|
|
Wand2,
|
|
Check,
|
|
ChevronsUpDown,
|
|
} from "lucide-react";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "@/components/ui/command";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { tableTypeApi } from "@/lib/api/screen";
|
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
|
import { getAvailableNumberingRules, getAvailableNumberingRulesForScreen } from "@/lib/api/numberingRule";
|
|
import { NumberingRuleConfig } from "@/types/numbering-rule";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
UnifiedRepeaterConfig,
|
|
RepeaterColumnConfig,
|
|
DEFAULT_REPEATER_CONFIG,
|
|
RENDER_MODE_OPTIONS,
|
|
MODAL_SIZE_OPTIONS,
|
|
} from "@/types/unified-repeater";
|
|
|
|
// 테이블 엔티티 관계 정보
|
|
interface TableRelation {
|
|
tableName: string;
|
|
tableLabel: string;
|
|
foreignKeyColumn: string; // 저장 테이블의 FK 컬럼
|
|
referenceColumn: string; // 마스터 테이블의 PK 컬럼
|
|
}
|
|
|
|
interface UnifiedRepeaterConfigPanelProps {
|
|
config: UnifiedRepeaterConfig;
|
|
onChange: (config: UnifiedRepeaterConfig) => void;
|
|
currentTableName?: string;
|
|
screenTableName?: string;
|
|
tableColumns?: any[];
|
|
menuObjid?: number | string; // 🆕 메뉴 ID (채번 규칙 조회용)
|
|
}
|
|
|
|
interface ColumnOption {
|
|
columnName: string;
|
|
displayName: string;
|
|
inputType?: string;
|
|
detailSettings?: {
|
|
codeGroup?: string;
|
|
referenceTable?: string;
|
|
referenceColumn?: string;
|
|
displayColumn?: string;
|
|
format?: string;
|
|
};
|
|
}
|
|
|
|
interface EntityColumnOption {
|
|
columnName: string;
|
|
displayName: string;
|
|
referenceTable?: string;
|
|
referenceColumn?: string;
|
|
displayColumn?: string;
|
|
}
|
|
|
|
interface CalculationRule {
|
|
id: string;
|
|
targetColumn: string;
|
|
formula: string;
|
|
label?: string;
|
|
}
|
|
|
|
export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProps> = ({
|
|
config: propConfig,
|
|
onChange,
|
|
currentTableName: propCurrentTableName,
|
|
screenTableName,
|
|
menuObjid,
|
|
}) => {
|
|
const currentTableName = screenTableName || propCurrentTableName;
|
|
|
|
// config 안전하게 초기화
|
|
const config: UnifiedRepeaterConfig = useMemo(() => ({
|
|
...DEFAULT_REPEATER_CONFIG,
|
|
...propConfig,
|
|
renderMode: propConfig?.renderMode || DEFAULT_REPEATER_CONFIG.renderMode,
|
|
dataSource: {
|
|
...DEFAULT_REPEATER_CONFIG.dataSource,
|
|
...propConfig?.dataSource,
|
|
},
|
|
columns: propConfig?.columns || [],
|
|
modal: {
|
|
...DEFAULT_REPEATER_CONFIG.modal,
|
|
...propConfig?.modal,
|
|
},
|
|
features: {
|
|
...DEFAULT_REPEATER_CONFIG.features,
|
|
...propConfig?.features,
|
|
},
|
|
}), [propConfig]);
|
|
|
|
// 상태 관리
|
|
const [currentTableColumns, setCurrentTableColumns] = useState<ColumnOption[]>([]); // 현재 테이블 컬럼
|
|
const [entityColumns, setEntityColumns] = useState<EntityColumnOption[]>([]); // 엔티티 타입 컬럼
|
|
const [sourceTableColumns, setSourceTableColumns] = useState<ColumnOption[]>([]); // 소스(엔티티) 테이블 컬럼
|
|
const [calculationRules, setCalculationRules] = useState<CalculationRule[]>([]);
|
|
|
|
const [loadingColumns, setLoadingColumns] = useState(false);
|
|
const [loadingSourceColumns, setLoadingSourceColumns] = useState(false);
|
|
|
|
// 저장 테이블 관련 상태
|
|
const [allTables, setAllTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
|
|
const [loadingTables, setLoadingTables] = useState(false);
|
|
const [relatedTables, setRelatedTables] = useState<TableRelation[]>([]); // 현재 테이블과 연관된 테이블 목록
|
|
const [loadingRelations, setLoadingRelations] = useState(false);
|
|
const [tableComboboxOpen, setTableComboboxOpen] = useState(false); // 테이블 Combobox 열림 상태
|
|
|
|
// 🆕 확장된 컬럼 (상세 설정 표시용)
|
|
const [expandedColumn, setExpandedColumn] = useState<string | null>(null);
|
|
|
|
// 🆕 채번 규칙 목록
|
|
const [numberingRules, setNumberingRules] = useState<NumberingRuleConfig[]>([]);
|
|
const [loadingNumberingRules, setLoadingNumberingRules] = useState(false);
|
|
|
|
// 🆕 대상 메뉴 목록 (채번 규칙 선택용)
|
|
const [parentMenus, setParentMenus] = useState<any[]>([]);
|
|
const [loadingMenus, setLoadingMenus] = useState(false);
|
|
|
|
// 🆕 선택된 메뉴 OBJID (컬럼별로 저장, 한 번 선택하면 공유)
|
|
const [selectedMenuObjid, setSelectedMenuObjid] = useState<number | undefined>(() => {
|
|
// 기존 config에서 저장된 값이 있으면 복원
|
|
const existingAutoFill = config.columns.find(c => c.autoFill?.type === "numbering" && c.autoFill.selectedMenuObjid);
|
|
return existingAutoFill?.autoFill?.selectedMenuObjid || (menuObjid ? Number(menuObjid) : undefined);
|
|
});
|
|
|
|
// 자동 입력 타입 옵션
|
|
const autoFillOptions = [
|
|
{ value: "none", label: "없음" },
|
|
{ value: "currentDate", label: "현재 날짜" },
|
|
{ value: "currentDateTime", label: "현재 날짜+시간" },
|
|
{ value: "sequence", label: "순번 (1, 2, 3...)" },
|
|
{ value: "numbering", label: "채번 규칙" },
|
|
{ value: "fromMainForm", label: "메인 폼에서 복사" },
|
|
{ value: "fixed", label: "고정값" },
|
|
];
|
|
|
|
// 🆕 대상 메뉴 목록 로드 (사용자 메뉴의 레벨 2)
|
|
useEffect(() => {
|
|
const loadMenus = async () => {
|
|
setLoadingMenus(true);
|
|
try {
|
|
const { apiClient } = await import("@/lib/api/client");
|
|
const response = await apiClient.get("/admin/menus");
|
|
|
|
if (response.data.success && response.data.data) {
|
|
const allMenus = response.data.data;
|
|
|
|
// 사용자 메뉴(menu_type='1')의 레벨 2만 필터링
|
|
const level2UserMenus = allMenus.filter((menu: any) =>
|
|
menu.menu_type === '1' && menu.lev === 2
|
|
);
|
|
|
|
setParentMenus(level2UserMenus);
|
|
}
|
|
} catch (error) {
|
|
console.error("부모 메뉴 로드 실패:", error);
|
|
} finally {
|
|
setLoadingMenus(false);
|
|
}
|
|
};
|
|
loadMenus();
|
|
}, []);
|
|
|
|
// 🆕 채번 규칙 로드 (선택된 메뉴 기준)
|
|
useEffect(() => {
|
|
const loadNumberingRules = async () => {
|
|
// 메뉴가 선택되지 않았으면 로드하지 않음
|
|
if (!selectedMenuObjid) {
|
|
setNumberingRules([]);
|
|
return;
|
|
}
|
|
|
|
setLoadingNumberingRules(true);
|
|
try {
|
|
const result = await getAvailableNumberingRules(selectedMenuObjid);
|
|
|
|
if (result?.success && result.data) {
|
|
setNumberingRules(result.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("채번 규칙 로드 실패:", error);
|
|
setNumberingRules([]);
|
|
} finally {
|
|
setLoadingNumberingRules(false);
|
|
}
|
|
};
|
|
loadNumberingRules();
|
|
}, [selectedMenuObjid]);
|
|
|
|
// 전체 테이블 목록 로드
|
|
useEffect(() => {
|
|
const loadTables = async () => {
|
|
setLoadingTables(true);
|
|
try {
|
|
const response = await tableManagementApi.getTableList();
|
|
if (response.success && response.data) {
|
|
setAllTables(response.data.map((t: any) => ({
|
|
tableName: t.tableName || t.table_name,
|
|
displayName: t.displayName || t.table_label || t.tableName || t.table_name,
|
|
})));
|
|
}
|
|
} catch (error) {
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
} finally {
|
|
setLoadingTables(false);
|
|
}
|
|
};
|
|
loadTables();
|
|
}, []);
|
|
|
|
// 현재 테이블과 연관된 테이블 목록 로드 (엔티티 관계 기반)
|
|
useEffect(() => {
|
|
const loadRelatedTables = async () => {
|
|
if (!currentTableName) {
|
|
setRelatedTables([]);
|
|
return;
|
|
}
|
|
|
|
setLoadingRelations(true);
|
|
try {
|
|
// column_labels에서 현재 테이블을 reference_table로 참조하는 테이블 찾기
|
|
const { apiClient } = await import("@/lib/api/client");
|
|
const response = await apiClient.get(`/table-management/columns/${currentTableName}/referenced-by`);
|
|
|
|
if (response.data.success && response.data.data) {
|
|
const relations: TableRelation[] = response.data.data.map((rel: any) => ({
|
|
tableName: rel.tableName || rel.table_name,
|
|
tableLabel: rel.tableLabel || rel.table_label || rel.tableName || rel.table_name,
|
|
foreignKeyColumn: rel.columnName || rel.column_name, // FK 컬럼
|
|
referenceColumn: rel.referenceColumn || rel.reference_column || "id", // PK 컬럼
|
|
}));
|
|
setRelatedTables(relations);
|
|
}
|
|
} catch (error) {
|
|
console.error("연관 테이블 로드 실패:", error);
|
|
setRelatedTables([]);
|
|
} finally {
|
|
setLoadingRelations(false);
|
|
}
|
|
};
|
|
loadRelatedTables();
|
|
}, [currentTableName]);
|
|
|
|
// 설정 업데이트 헬퍼
|
|
const updateConfig = useCallback(
|
|
(updates: Partial<UnifiedRepeaterConfig>) => {
|
|
onChange({ ...config, ...updates });
|
|
},
|
|
[config, onChange],
|
|
);
|
|
|
|
const updateDataSource = useCallback(
|
|
(field: string, value: any) => {
|
|
updateConfig({
|
|
dataSource: { ...config.dataSource, [field]: value },
|
|
});
|
|
},
|
|
[config.dataSource, updateConfig],
|
|
);
|
|
|
|
const updateModal = useCallback(
|
|
(field: string, value: any) => {
|
|
updateConfig({
|
|
modal: { ...config.modal, [field]: value },
|
|
});
|
|
},
|
|
[config.modal, updateConfig],
|
|
);
|
|
|
|
const updateFeatures = useCallback(
|
|
(field: string, value: boolean) => {
|
|
updateConfig({
|
|
features: { ...config.features, [field]: value },
|
|
});
|
|
},
|
|
[config.features, updateConfig],
|
|
);
|
|
|
|
// 저장 테이블 선택 핸들러 - 엔티티 관계에서 FK/PK 자동 설정
|
|
const handleSaveTableSelect = useCallback((tableName: string) => {
|
|
// 빈 값 선택 시 (현재 테이블로 복원)
|
|
if (!tableName || tableName === currentTableName) {
|
|
updateConfig({
|
|
useCustomTable: false,
|
|
mainTableName: undefined,
|
|
foreignKeyColumn: undefined,
|
|
foreignKeySourceColumn: undefined,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 연관 테이블에서 FK 관계 찾기
|
|
const relation = relatedTables.find(r => r.tableName === tableName);
|
|
|
|
if (relation) {
|
|
// 엔티티 관계가 있으면 자동으로 FK/PK 설정
|
|
updateConfig({
|
|
useCustomTable: true,
|
|
mainTableName: tableName,
|
|
foreignKeyColumn: relation.foreignKeyColumn,
|
|
foreignKeySourceColumn: relation.referenceColumn,
|
|
});
|
|
} else {
|
|
// 엔티티 관계가 없으면 직접 입력 필요
|
|
updateConfig({
|
|
useCustomTable: true,
|
|
mainTableName: tableName,
|
|
foreignKeyColumn: undefined,
|
|
foreignKeySourceColumn: "id",
|
|
});
|
|
}
|
|
}, [currentTableName, relatedTables, updateConfig]);
|
|
|
|
// 저장 테이블 컬럼 로드 (저장 테이블이 설정되면 해당 테이블, 아니면 현재 화면 테이블)
|
|
// 실제 저장할 테이블의 컬럼을 보여줘야 함
|
|
const targetTableForColumns = config.useCustomTable && config.mainTableName
|
|
? config.mainTableName
|
|
: currentTableName;
|
|
|
|
useEffect(() => {
|
|
const loadCurrentTableColumns = async () => {
|
|
if (!targetTableForColumns) {
|
|
setCurrentTableColumns([]);
|
|
setEntityColumns([]);
|
|
return;
|
|
}
|
|
|
|
setLoadingColumns(true);
|
|
try {
|
|
const columnData = await tableTypeApi.getColumns(targetTableForColumns);
|
|
const cols: ColumnOption[] = [];
|
|
const entityCols: EntityColumnOption[] = [];
|
|
|
|
for (const c of columnData) {
|
|
// detailSettings 파싱
|
|
let detailSettings: any = null;
|
|
if (c.detailSettings) {
|
|
try {
|
|
detailSettings = typeof c.detailSettings === "string"
|
|
? JSON.parse(c.detailSettings)
|
|
: c.detailSettings;
|
|
} catch (e) {
|
|
console.warn("detailSettings 파싱 실패:", c.detailSettings);
|
|
}
|
|
}
|
|
|
|
const col: ColumnOption = {
|
|
columnName: c.columnName || c.column_name,
|
|
displayName: c.displayName || c.columnLabel || c.columnName || c.column_name,
|
|
inputType: c.inputType || c.input_type,
|
|
detailSettings: detailSettings ? {
|
|
codeGroup: detailSettings.codeGroup,
|
|
referenceTable: detailSettings.referenceTable,
|
|
referenceColumn: detailSettings.referenceColumn,
|
|
displayColumn: detailSettings.displayColumn,
|
|
format: detailSettings.format,
|
|
} : undefined,
|
|
};
|
|
cols.push(col);
|
|
|
|
// 엔티티 타입 컬럼 감지
|
|
if (col.inputType === "entity") {
|
|
const referenceTable = detailSettings?.referenceTable || c.referenceTable;
|
|
const referenceColumn = detailSettings?.referenceColumn || c.referenceColumn || "id";
|
|
const displayColumn = detailSettings?.displayColumn || c.displayColumn;
|
|
|
|
if (referenceTable) {
|
|
entityCols.push({
|
|
columnName: col.columnName,
|
|
displayName: col.displayName,
|
|
referenceTable,
|
|
referenceColumn,
|
|
displayColumn,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
setCurrentTableColumns(cols);
|
|
setEntityColumns(entityCols);
|
|
} catch (error) {
|
|
console.error("저장 테이블 컬럼 로드 실패:", error);
|
|
setCurrentTableColumns([]);
|
|
setEntityColumns([]);
|
|
} finally {
|
|
setLoadingColumns(false);
|
|
}
|
|
};
|
|
loadCurrentTableColumns();
|
|
}, [targetTableForColumns]);
|
|
|
|
// 소스(엔티티) 테이블 컬럼 로드 (모달 모드일 때)
|
|
useEffect(() => {
|
|
const loadSourceTableColumns = async () => {
|
|
const sourceTable = config.dataSource?.sourceTable;
|
|
if (!sourceTable) {
|
|
setSourceTableColumns([]);
|
|
return;
|
|
}
|
|
|
|
setLoadingSourceColumns(true);
|
|
try {
|
|
const columnData = await tableTypeApi.getColumns(sourceTable);
|
|
const cols: ColumnOption[] = columnData.map((c: any) => ({
|
|
columnName: c.columnName || c.column_name,
|
|
displayName: c.displayName || c.columnLabel || c.columnName || c.column_name,
|
|
inputType: c.inputType || c.input_type,
|
|
}));
|
|
setSourceTableColumns(cols);
|
|
} catch (error) {
|
|
console.error("소스 테이블 컬럼 로드 실패:", error);
|
|
setSourceTableColumns([]);
|
|
} finally {
|
|
setLoadingSourceColumns(false);
|
|
}
|
|
};
|
|
|
|
if (config.renderMode === "modal") {
|
|
loadSourceTableColumns();
|
|
}
|
|
}, [config.dataSource?.sourceTable, config.renderMode]);
|
|
|
|
// 컬럼 토글 (현재 테이블 컬럼 - 입력용)
|
|
const toggleInputColumn = (column: ColumnOption) => {
|
|
const existingIndex = config.columns.findIndex((c) => c.key === column.columnName);
|
|
if (existingIndex >= 0) {
|
|
const newColumns = config.columns.filter((c) => c.key !== column.columnName);
|
|
updateConfig({ columns: newColumns });
|
|
} else {
|
|
// 컬럼의 inputType과 detailSettings 정보 포함
|
|
const newColumn: RepeaterColumnConfig = {
|
|
key: column.columnName,
|
|
title: column.displayName,
|
|
width: "auto",
|
|
visible: true,
|
|
editable: true,
|
|
inputType: column.inputType || "text",
|
|
detailSettings: column.detailSettings ? {
|
|
codeGroup: column.detailSettings.codeGroup,
|
|
referenceTable: column.detailSettings.referenceTable,
|
|
referenceColumn: column.detailSettings.referenceColumn,
|
|
displayColumn: column.detailSettings.displayColumn,
|
|
format: column.detailSettings.format,
|
|
} : undefined,
|
|
};
|
|
updateConfig({ columns: [...config.columns, newColumn] });
|
|
}
|
|
};
|
|
|
|
// 🆕 소스 컬럼 토글 - columns 배열에 isSourceDisplay: true로 추가
|
|
const toggleSourceDisplayColumn = (column: ColumnOption) => {
|
|
const exists = config.columns.some((c) => c.key === column.columnName && c.isSourceDisplay);
|
|
|
|
if (exists) {
|
|
// 제거
|
|
updateConfig({ columns: config.columns.filter((c) => c.key !== column.columnName) });
|
|
} else {
|
|
// 추가 (isSourceDisplay: true)
|
|
const newColumn: RepeaterColumnConfig = {
|
|
key: column.columnName,
|
|
title: column.displayName,
|
|
width: "auto",
|
|
visible: true,
|
|
editable: false, // 소스 표시 컬럼은 편집 불가
|
|
isSourceDisplay: true,
|
|
};
|
|
updateConfig({ columns: [...config.columns, newColumn] });
|
|
}
|
|
};
|
|
|
|
const isColumnAdded = (columnName: string) => {
|
|
return config.columns.some((c) => c.key === columnName && !c.isSourceDisplay);
|
|
};
|
|
|
|
const isSourceColumnSelected = (columnName: string) => {
|
|
return config.columns.some((c) => c.key === columnName && c.isSourceDisplay);
|
|
};
|
|
|
|
// 컬럼 속성 업데이트
|
|
const updateColumnProp = (key: string, field: keyof RepeaterColumnConfig, value: any) => {
|
|
const newColumns = config.columns.map((col) => (col.key === key ? { ...col, [field]: value } : col));
|
|
updateConfig({ columns: newColumns });
|
|
};
|
|
|
|
// 계산 규칙 추가
|
|
const addCalculationRule = () => {
|
|
setCalculationRules(prev => [
|
|
...prev,
|
|
{ id: `calc_${Date.now()}`, targetColumn: "", formula: "" }
|
|
]);
|
|
};
|
|
|
|
// 계산 규칙 삭제
|
|
const removeCalculationRule = (id: string) => {
|
|
setCalculationRules(prev => prev.filter(r => r.id !== id));
|
|
};
|
|
|
|
// 계산 규칙 업데이트
|
|
const updateCalculationRule = (id: string, field: keyof CalculationRule, value: string) => {
|
|
setCalculationRules(prev =>
|
|
prev.map(r => r.id === id ? { ...r, [field]: value } : r)
|
|
);
|
|
};
|
|
|
|
// 엔티티 컬럼 선택 시 소스 테이블 자동 설정
|
|
const handleEntityColumnSelect = (columnName: string) => {
|
|
const selectedEntity = entityColumns.find(c => c.columnName === columnName);
|
|
|
|
if (selectedEntity) {
|
|
console.log("엔티티 컬럼 선택:", selectedEntity);
|
|
|
|
// 소스 테이블 컬럼에서 라벨 정보 찾기
|
|
const displayColInfo = sourceTableColumns.find(c => c.columnName === selectedEntity.displayColumn);
|
|
const displayLabel = displayColInfo?.displayName || selectedEntity.displayColumn || "";
|
|
|
|
updateConfig({
|
|
dataSource: {
|
|
...config.dataSource,
|
|
sourceTable: selectedEntity.referenceTable || "",
|
|
foreignKey: selectedEntity.columnName,
|
|
referenceKey: selectedEntity.referenceColumn || "id",
|
|
displayColumn: selectedEntity.displayColumn,
|
|
},
|
|
modal: {
|
|
...config.modal,
|
|
searchFields: selectedEntity.displayColumn ? [selectedEntity.displayColumn] : [],
|
|
// 라벨 포함 형식으로 저장
|
|
sourceDisplayColumns: selectedEntity.displayColumn
|
|
? [{ key: selectedEntity.displayColumn, label: displayLabel }]
|
|
: [],
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
// 모드 여부
|
|
const isInlineMode = config.renderMode === "inline";
|
|
const isModalMode = config.renderMode === "modal";
|
|
|
|
// 엔티티 컬럼 제외한 입력 가능 컬럼 (FK 컬럼 제외)
|
|
const inputableColumns = useMemo(() => {
|
|
const fkColumn = config.dataSource?.foreignKey;
|
|
return currentTableColumns.filter(col =>
|
|
col.columnName !== fkColumn && // FK 컬럼 제외
|
|
col.inputType !== "entity" // 다른 엔티티 컬럼도 제외 (필요시)
|
|
);
|
|
}, [currentTableColumns, config.dataSource?.foreignKey]);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Tabs defaultValue="basic" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="basic" className="text-xs">기본</TabsTrigger>
|
|
<TabsTrigger value="columns" className="text-xs">컬럼</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* 기본 설정 탭 */}
|
|
<TabsContent value="basic" className="mt-4 space-y-4">
|
|
{/* 렌더링 모드 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">렌더링 모드</Label>
|
|
<Select
|
|
value={config.renderMode}
|
|
onValueChange={(value) => {
|
|
const newMode = value as any;
|
|
const currentMode = config.renderMode;
|
|
|
|
// 모달 → 인라인 모드로 변경 시: isSourceDisplay 컬럼 제거 및 모달 설정 초기화
|
|
if (currentMode === "modal" && newMode === "inline") {
|
|
const filteredColumns = config.columns.filter((col) => !col.isSourceDisplay);
|
|
updateConfig({
|
|
renderMode: newMode,
|
|
columns: filteredColumns,
|
|
dataSource: {
|
|
...config.dataSource,
|
|
sourceTable: undefined,
|
|
foreignKey: undefined,
|
|
referenceKey: undefined,
|
|
displayColumn: undefined,
|
|
},
|
|
modal: {
|
|
...config.modal,
|
|
searchFields: [],
|
|
sourceDisplayColumns: [],
|
|
},
|
|
});
|
|
} else {
|
|
updateConfig({ renderMode: newMode });
|
|
}
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="모드 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{RENDER_MODE_OPTIONS.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
<div className="flex flex-col">
|
|
<span>{opt.label}</span>
|
|
<span className="text-[10px] text-gray-400">
|
|
{opt.value === "inline" && "현재 테이블 컬럼 직접 입력"}
|
|
{opt.value === "modal" && "엔티티 선택 후 추가 정보 입력"}
|
|
{opt.value === "button" && "버튼으로 관련 화면 열기"}
|
|
</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 저장 대상 테이블 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">저장 테이블</Label>
|
|
|
|
{/* 현재 선택된 테이블 표시 (기존 테이블 UI와 동일한 스타일) */}
|
|
<div className={cn(
|
|
"rounded-lg border p-3",
|
|
config.useCustomTable && config.mainTableName
|
|
? "border-orange-300 bg-orange-50"
|
|
: "border-blue-300 bg-blue-50"
|
|
)}>
|
|
<div className="flex items-center gap-2">
|
|
<Database className={cn(
|
|
"h-4 w-4",
|
|
config.useCustomTable && config.mainTableName
|
|
? "text-orange-600"
|
|
: "text-blue-600"
|
|
)} />
|
|
<div className="flex-1">
|
|
<p className={cn(
|
|
"text-sm font-medium",
|
|
config.useCustomTable && config.mainTableName
|
|
? "text-orange-700"
|
|
: "text-blue-700"
|
|
)}>
|
|
{config.useCustomTable && config.mainTableName
|
|
? (allTables.find(t => t.tableName === config.mainTableName)?.displayName || config.mainTableName)
|
|
: (currentTableName || "미설정")
|
|
}
|
|
</p>
|
|
{config.useCustomTable && config.mainTableName && config.foreignKeyColumn && (
|
|
<p className="text-[10px] text-orange-600 mt-0.5">
|
|
FK: {config.foreignKeyColumn} → {currentTableName}.{config.foreignKeySourceColumn || "id"}
|
|
</p>
|
|
)}
|
|
{!config.useCustomTable && currentTableName && (
|
|
<p className="text-[10px] text-blue-600 mt-0.5">화면 메인 테이블</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 테이블 변경 Combobox */}
|
|
<Popover open={tableComboboxOpen} onOpenChange={setTableComboboxOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
role="combobox"
|
|
aria-expanded={tableComboboxOpen}
|
|
disabled={loadingTables || loadingRelations}
|
|
className="h-8 w-full justify-between text-xs"
|
|
>
|
|
{loadingTables ? "로딩 중..." : "다른 테이블 선택..."}
|
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
className="p-0"
|
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
|
align="start"
|
|
>
|
|
<Command>
|
|
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
|
<CommandList className="max-h-60">
|
|
<CommandEmpty className="text-xs py-3 text-center">
|
|
테이블을 찾을 수 없습니다.
|
|
</CommandEmpty>
|
|
|
|
{/* 현재 테이블 (기본) */}
|
|
{currentTableName && (
|
|
<CommandGroup heading="기본">
|
|
<CommandItem
|
|
value={currentTableName}
|
|
onSelect={() => {
|
|
handleSaveTableSelect(currentTableName);
|
|
setTableComboboxOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
!config.useCustomTable || !config.mainTableName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<Database className="mr-2 h-3 w-3 text-blue-500" />
|
|
<span>{currentTableName}</span>
|
|
<span className="ml-1 text-[10px] text-muted-foreground">(기본)</span>
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
)}
|
|
|
|
{/* 연관 테이블 (엔티티 관계) */}
|
|
{relatedTables.length > 0 && (
|
|
<CommandGroup heading="연관 테이블 (FK 자동 설정)">
|
|
{relatedTables.map((rel) => (
|
|
<CommandItem
|
|
key={rel.tableName}
|
|
value={`${rel.tableName} ${rel.tableLabel}`}
|
|
onSelect={() => {
|
|
handleSaveTableSelect(rel.tableName);
|
|
setTableComboboxOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.mainTableName === rel.tableName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<Link2 className="mr-2 h-3 w-3 text-orange-500" />
|
|
<span>{rel.tableLabel}</span>
|
|
<span className="ml-1 text-[10px] text-muted-foreground">
|
|
({rel.foreignKeyColumn})
|
|
</span>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
)}
|
|
|
|
{/* 전체 테이블 목록 */}
|
|
<CommandGroup heading="전체 테이블 (FK 직접 입력)">
|
|
{allTables
|
|
.filter(t => t.tableName !== currentTableName && !relatedTables.some(r => r.tableName === t.tableName))
|
|
.map((table) => (
|
|
<CommandItem
|
|
key={table.tableName}
|
|
value={`${table.tableName} ${table.displayName}`}
|
|
onSelect={() => {
|
|
handleSaveTableSelect(table.tableName);
|
|
setTableComboboxOpen(false);
|
|
}}
|
|
className="text-xs"
|
|
>
|
|
<Check
|
|
className={cn(
|
|
"mr-2 h-3 w-3",
|
|
config.mainTableName === table.tableName ? "opacity-100" : "opacity-0"
|
|
)}
|
|
/>
|
|
<Database className="mr-2 h-3 w-3 text-gray-400" />
|
|
<span>{table.displayName}</span>
|
|
</CommandItem>
|
|
))
|
|
}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
|
|
{/* FK 직접 입력 (연관 테이블이 아닌 경우만) */}
|
|
{config.useCustomTable && config.mainTableName &&
|
|
!relatedTables.some(r => r.tableName === config.mainTableName) && (
|
|
<div className="space-y-2 rounded border border-amber-200 bg-amber-50 p-2">
|
|
<p className="text-[10px] text-amber-700">
|
|
엔티티 관계가 설정되지 않은 테이블입니다. FK 컬럼을 직접 입력하세요.
|
|
</p>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">FK 컬럼</Label>
|
|
<Input
|
|
value={config.foreignKeyColumn || ""}
|
|
onChange={(e) => updateConfig({ foreignKeyColumn: e.target.value })}
|
|
placeholder="예: master_id"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px]">PK 컬럼</Label>
|
|
<Input
|
|
value={config.foreignKeySourceColumn || "id"}
|
|
onChange={(e) => updateConfig({ foreignKeySourceColumn: e.target.value })}
|
|
placeholder="id"
|
|
className="h-7 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 현재 화면 정보 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">현재 화면</Label>
|
|
{currentTableName ? (
|
|
<div className="rounded border border-blue-200 bg-blue-50 p-2">
|
|
<p className="text-xs text-blue-700 font-medium">{currentTableName}</p>
|
|
<p className="text-[10px] text-blue-500">
|
|
컬럼 {currentTableColumns.length}개 / 엔티티 {entityColumns.length}개
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="rounded border border-amber-200 bg-amber-50 p-2">
|
|
<p className="text-[10px] text-amber-600">화면에 테이블을 먼저 설정해주세요</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 모달 모드: 엔티티 컬럼 선택 */}
|
|
{isModalMode && (
|
|
<>
|
|
<Separator />
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">엔티티 선택</Label>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
모달에서 검색할 엔티티를 선택하세요 (FK만 저장됨)
|
|
</p>
|
|
|
|
{entityColumns.length > 0 ? (
|
|
<Select
|
|
value={config.dataSource?.foreignKey || ""}
|
|
onValueChange={handleEntityColumnSelect}
|
|
disabled={!currentTableName}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue placeholder="엔티티 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{entityColumns.map((col) => (
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
<div className="flex items-center gap-2">
|
|
<Link2 className="h-3 w-3 text-blue-500" />
|
|
<span>{col.displayName}</span>
|
|
<ArrowRight className="h-3 w-3 text-gray-400" />
|
|
<span className="text-gray-500">{col.referenceTable}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : (
|
|
<div className="rounded border border-gray-200 bg-gray-50 p-2">
|
|
<p className="text-[10px] text-gray-500">
|
|
{loadingColumns ? "로딩 중..." : "엔티티 타입 컬럼이 없습니다"}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* 선택된 엔티티 정보 */}
|
|
{config.dataSource?.sourceTable && (
|
|
<div className="rounded border border-green-200 bg-green-50 p-2 space-y-1">
|
|
<p className="text-xs text-green-700 font-medium">선택된 엔티티</p>
|
|
<div className="text-[10px] text-green-600">
|
|
<p>검색 테이블: {config.dataSource.sourceTable}</p>
|
|
<p>저장 컬럼: {config.dataSource.foreignKey} (FK)</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<Separator />
|
|
|
|
{/* 기능 옵션 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium">기능 옵션</Label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="showAddButton"
|
|
checked={config.features?.showAddButton ?? true}
|
|
onCheckedChange={(checked) => updateFeatures("showAddButton", !!checked)}
|
|
/>
|
|
<label htmlFor="showAddButton" className="text-xs">추가 버튼</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="showDeleteButton"
|
|
checked={config.features?.showDeleteButton ?? true}
|
|
onCheckedChange={(checked) => updateFeatures("showDeleteButton", !!checked)}
|
|
/>
|
|
<label htmlFor="showDeleteButton" className="text-xs">삭제 버튼</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="inlineEdit"
|
|
checked={config.features?.inlineEdit ?? false}
|
|
onCheckedChange={(checked) => updateFeatures("inlineEdit", !!checked)}
|
|
/>
|
|
<label htmlFor="inlineEdit" className="text-xs">인라인 편집</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="multiSelect"
|
|
checked={config.features?.multiSelect ?? true}
|
|
onCheckedChange={(checked) => updateFeatures("multiSelect", !!checked)}
|
|
/>
|
|
<label htmlFor="multiSelect" className="text-xs">다중 선택</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="showRowNumber"
|
|
checked={config.features?.showRowNumber ?? false}
|
|
onCheckedChange={(checked) => updateFeatures("showRowNumber", !!checked)}
|
|
/>
|
|
<label htmlFor="showRowNumber" className="text-xs">행 번호</label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="selectable"
|
|
checked={config.features?.selectable ?? false}
|
|
onCheckedChange={(checked) => updateFeatures("selectable", !!checked)}
|
|
/>
|
|
<label htmlFor="selectable" className="text-xs">행 선택</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* 컬럼 설정 탭 - 🆕 통합 컬럼 선택 */}
|
|
<TabsContent value="columns" className="mt-4 space-y-4">
|
|
{/* 통합 컬럼 선택 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">컬럼 선택</Label>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
{isModalMode
|
|
? "표시할 컬럼과 입력 컬럼을 선택하세요. 아이콘으로 표시/입력 구분"
|
|
: "입력받을 컬럼을 선택하세요"
|
|
}
|
|
</p>
|
|
|
|
{/* 모달 모드: 소스 테이블 컬럼 (표시용) */}
|
|
{isModalMode && config.dataSource?.sourceTable && (
|
|
<>
|
|
<div className="text-[10px] font-medium text-blue-600 mt-2 mb-1 flex items-center gap-1">
|
|
<Link2 className="h-3 w-3" />
|
|
소스 테이블 ({config.dataSource.sourceTable}) - 표시용
|
|
</div>
|
|
{loadingSourceColumns ? (
|
|
<p className="text-muted-foreground py-2 text-xs">로딩 중...</p>
|
|
) : sourceTableColumns.length === 0 ? (
|
|
<p className="text-muted-foreground py-2 text-xs">컬럼 정보가 없습니다</p>
|
|
) : (
|
|
<div className="max-h-28 space-y-0.5 overflow-y-auto rounded-md border border-blue-200 bg-blue-50/30 p-2">
|
|
{sourceTableColumns.map((column) => (
|
|
<div
|
|
key={`source-${column.columnName}`}
|
|
className={cn(
|
|
"hover:bg-blue-100/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1",
|
|
isSourceColumnSelected(column.columnName) && "bg-blue-100",
|
|
)}
|
|
onClick={() => toggleSourceDisplayColumn(column)}
|
|
>
|
|
<Checkbox
|
|
checked={isSourceColumnSelected(column.columnName)}
|
|
onCheckedChange={() => toggleSourceDisplayColumn(column)}
|
|
className="pointer-events-none h-3.5 w-3.5"
|
|
/>
|
|
<Link2 className="text-blue-500 h-3 w-3 flex-shrink-0" />
|
|
<span className="truncate text-xs">{column.displayName}</span>
|
|
<span className="text-[10px] text-blue-400 ml-auto">표시</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* 저장 테이블 컬럼 (입력용) */}
|
|
<div className="text-[10px] font-medium text-gray-600 mt-3 mb-1 flex items-center gap-1">
|
|
<Database className="h-3 w-3" />
|
|
저장 테이블 ({targetTableForColumns || "미선택"}) - 입력용
|
|
</div>
|
|
{loadingColumns ? (
|
|
<p className="text-muted-foreground py-2 text-xs">로딩 중...</p>
|
|
) : inputableColumns.length === 0 ? (
|
|
<p className="text-muted-foreground py-2 text-xs">
|
|
컬럼 정보가 없습니다
|
|
</p>
|
|
) : (
|
|
<div className="max-h-36 space-y-0.5 overflow-y-auto rounded-md border p-2">
|
|
{inputableColumns.map((column) => (
|
|
<div
|
|
key={`input-${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={() => toggleInputColumn(column)}
|
|
>
|
|
<Checkbox
|
|
checked={isColumnAdded(column.columnName)}
|
|
onCheckedChange={() => toggleInputColumn(column)}
|
|
className="pointer-events-none h-3.5 w-3.5"
|
|
/>
|
|
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
|
<span className="truncate text-xs">{column.displayName}</span>
|
|
<span className="text-[10px] text-gray-400 ml-auto">{column.inputType}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 선택된 컬럼 상세 설정 - 🆕 모든 컬럼 통합, 순서 변경 가능 */}
|
|
{config.columns.length > 0 && (
|
|
<>
|
|
<Separator />
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">
|
|
선택된 컬럼 ({config.columns.length}개)
|
|
<span className="text-muted-foreground ml-2 font-normal">드래그로 순서 변경</span>
|
|
</Label>
|
|
<div className="max-h-48 space-y-1 overflow-y-auto">
|
|
{config.columns.map((col, index) => (
|
|
<div key={col.key} className="space-y-1">
|
|
{/* 컬럼 헤더 (드래그 가능) */}
|
|
<div
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-md border p-2",
|
|
col.isSourceDisplay ? "border-blue-200 bg-blue-50/50" : "border-gray-200 bg-muted/30",
|
|
col.hidden && "opacity-50",
|
|
)}
|
|
draggable
|
|
onDragStart={(e) => {
|
|
e.dataTransfer.setData("columnIndex", String(index));
|
|
}}
|
|
onDragOver={(e) => e.preventDefault()}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
const fromIndex = parseInt(e.dataTransfer.getData("columnIndex"), 10);
|
|
if (fromIndex !== index) {
|
|
const newColumns = [...config.columns];
|
|
const [movedCol] = newColumns.splice(fromIndex, 1);
|
|
newColumns.splice(index, 0, movedCol);
|
|
updateConfig({ columns: newColumns });
|
|
}
|
|
}}
|
|
>
|
|
<GripVertical className="text-muted-foreground h-3 w-3 cursor-grab flex-shrink-0" />
|
|
|
|
{/* 확장/축소 버튼 (입력 컬럼만) */}
|
|
{!col.isSourceDisplay && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setExpandedColumn(expandedColumn === col.key ? null : col.key)}
|
|
className="p-0.5 hover:bg-gray-200 rounded"
|
|
>
|
|
{expandedColumn === col.key ? (
|
|
<ChevronDown className="h-3 w-3 text-gray-500" />
|
|
) : (
|
|
<ChevronRight className="h-3 w-3 text-gray-500" />
|
|
)}
|
|
</button>
|
|
)}
|
|
|
|
{col.isSourceDisplay ? (
|
|
<Link2 className="text-blue-500 h-3 w-3 flex-shrink-0" title="소스 표시 (읽기 전용)" />
|
|
) : (
|
|
<Database className="text-muted-foreground h-3 w-3 flex-shrink-0" />
|
|
)}
|
|
|
|
<Input
|
|
value={col.title}
|
|
onChange={(e) => updateColumnProp(col.key, "title", e.target.value)}
|
|
placeholder="제목"
|
|
className="h-6 flex-1 text-xs"
|
|
/>
|
|
|
|
{/* 히든 토글 (입력 컬럼만) */}
|
|
{!col.isSourceDisplay && (
|
|
<button
|
|
type="button"
|
|
onClick={() => updateColumnProp(col.key, "hidden", !col.hidden)}
|
|
className={cn(
|
|
"p-1 rounded hover:bg-gray-200",
|
|
col.hidden ? "text-gray-400" : "text-gray-600",
|
|
)}
|
|
title={col.hidden ? "히든 (저장만 됨)" : "표시됨"}
|
|
>
|
|
{col.hidden ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
|
</button>
|
|
)}
|
|
|
|
{/* 자동입력 표시 아이콘 */}
|
|
{!col.isSourceDisplay && col.autoFill?.type && col.autoFill.type !== "none" && (
|
|
<Wand2 className="h-3 w-3 text-purple-500 flex-shrink-0" title="자동 입력" />
|
|
)}
|
|
|
|
{/* 편집 가능 체크박스 */}
|
|
{!col.isSourceDisplay && (
|
|
<Checkbox
|
|
checked={col.editable ?? true}
|
|
onCheckedChange={(checked) => updateColumnProp(col.key, "editable", !!checked)}
|
|
title="편집 가능"
|
|
/>
|
|
)}
|
|
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
if (col.isSourceDisplay) {
|
|
toggleSourceDisplayColumn({ columnName: col.key, displayName: col.title });
|
|
} else {
|
|
toggleInputColumn({ columnName: col.key, displayName: col.title });
|
|
}
|
|
}}
|
|
className="text-destructive h-6 w-6 p-0"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 확장된 상세 설정 (입력 컬럼만) */}
|
|
{!col.isSourceDisplay && expandedColumn === col.key && (
|
|
<div className="ml-6 space-y-2 rounded-md border border-dashed border-gray-300 bg-gray-50 p-2">
|
|
{/* 자동 입력 설정 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">자동 입력</Label>
|
|
<Select
|
|
value={col.autoFill?.type || "none"}
|
|
onValueChange={(value) => {
|
|
const autoFill = value === "none" ? undefined : { type: value as any };
|
|
updateColumnProp(col.key, "autoFill", autoFill);
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{autoFillOptions.map((opt) => (
|
|
<SelectItem key={opt.value} value={opt.value} className="text-xs">
|
|
{opt.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 채번 규칙 선택 */}
|
|
{col.autoFill?.type === "numbering" && (
|
|
<div className="space-y-2">
|
|
{/* 대상 메뉴 선택 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">
|
|
대상 메뉴 선택 <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Select
|
|
value={selectedMenuObjid?.toString() || ""}
|
|
onValueChange={(value) => {
|
|
const menuObjidNum = parseInt(value);
|
|
setSelectedMenuObjid(menuObjidNum);
|
|
|
|
// 컬럼 설정에도 저장하여 유지
|
|
updateColumnProp(col.key, "autoFill", {
|
|
...col.autoFill,
|
|
selectedMenuObjid: menuObjidNum,
|
|
numberingRuleId: undefined, // 메뉴 변경 시 규칙 초기화
|
|
});
|
|
}}
|
|
disabled={loadingMenus}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue placeholder={loadingMenus ? "메뉴 로딩 중..." : "채번규칙을 사용할 메뉴 선택"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{parentMenus.length === 0 ? (
|
|
<SelectItem value="no-menus" disabled>
|
|
사용 가능한 메뉴가 없습니다
|
|
</SelectItem>
|
|
) : (
|
|
parentMenus.map((menu) => (
|
|
<SelectItem key={menu.objid} value={menu.objid.toString()}>
|
|
{menu.menu_name_kor}
|
|
</SelectItem>
|
|
))
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
이 입력 필드가 어느 메뉴에 속할지 선택하세요 (해당 메뉴의 채번규칙이 적용됩니다)
|
|
</p>
|
|
</div>
|
|
|
|
{/* 채번 규칙 선택 (메뉴 선택 후) */}
|
|
{selectedMenuObjid ? (
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">
|
|
채번 규칙 선택 <span className="text-destructive">*</span>
|
|
</Label>
|
|
{loadingNumberingRules ? (
|
|
<p className="text-[10px] text-gray-400">규칙 로딩 중...</p>
|
|
) : numberingRules.length === 0 ? (
|
|
<div className="rounded-md border border-amber-200 bg-amber-50 p-2 text-[10px] text-amber-800">
|
|
선택된 메뉴에 사용 가능한 채번 규칙이 없습니다
|
|
</div>
|
|
) : (
|
|
<Select
|
|
value={col.autoFill?.numberingRuleId || ""}
|
|
onValueChange={(value) => updateColumnProp(col.key, "autoFill", {
|
|
...col.autoFill,
|
|
selectedMenuObjid,
|
|
numberingRuleId: value,
|
|
})}
|
|
>
|
|
<SelectTrigger className="h-7 text-xs">
|
|
<SelectValue placeholder="채번 규칙 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{numberingRules.map((rule) => (
|
|
<SelectItem key={rule.ruleId} value={rule.ruleId} className="text-xs">
|
|
{rule.ruleName}
|
|
{rule.description && (
|
|
<span className="text-muted-foreground ml-2">
|
|
- {rule.description}
|
|
</span>
|
|
)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
{col.autoFill?.numberingRuleId && (
|
|
<p className="text-[10px] text-green-600">
|
|
저장 시 채번 API를 통해 자동 생성됩니다.
|
|
</p>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="rounded-md border border-amber-200 bg-amber-50 p-2 text-[10px] text-amber-800">
|
|
먼저 대상 메뉴를 선택하세요
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 메인 폼에서 복사 설정 */}
|
|
{col.autoFill?.type === "fromMainForm" && (
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">복사할 필드명</Label>
|
|
<Input
|
|
value={col.autoFill?.sourceField || ""}
|
|
onChange={(e) => updateColumnProp(col.key, "autoFill", {
|
|
...col.autoFill,
|
|
sourceField: e.target.value,
|
|
})}
|
|
placeholder="order_no"
|
|
className="h-6 text-xs"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* 고정값 설정 */}
|
|
{col.autoFill?.type === "fixed" && (
|
|
<div className="space-y-1">
|
|
<Label className="text-[10px] text-gray-600">고정값</Label>
|
|
<Input
|
|
value={String(col.autoFill?.fixedValue || "")}
|
|
onChange={(e) => updateColumnProp(col.key, "autoFill", {
|
|
...col.autoFill,
|
|
fixedValue: e.target.value,
|
|
})}
|
|
placeholder="고정값 입력"
|
|
className="h-6 text-xs"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 계산 규칙 */}
|
|
{(isModalMode || isInlineMode) && config.columns.length > 0 && (
|
|
<>
|
|
<Separator />
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-medium">계산 규칙</Label>
|
|
<Button type="button" variant="outline" size="sm" onClick={addCalculationRule} className="h-6 text-xs">
|
|
<Calculator className="mr-1 h-3 w-3" />
|
|
추가
|
|
</Button>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
예: 금액 = 수량 * 단가
|
|
</p>
|
|
|
|
<div className="max-h-32 space-y-2 overflow-y-auto">
|
|
{calculationRules.map((rule) => (
|
|
<div key={rule.id} className="flex items-center gap-2 rounded border p-2">
|
|
<Select
|
|
value={rule.targetColumn}
|
|
onValueChange={(value) => updateCalculationRule(rule.id, "targetColumn", value)}
|
|
>
|
|
<SelectTrigger className="h-7 w-24 text-xs">
|
|
<SelectValue placeholder="결과" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{config.columns.map((col) => (
|
|
<SelectItem key={col.key} value={col.key}>
|
|
{col.title}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<span className="text-xs">=</span>
|
|
|
|
<Input
|
|
value={rule.formula}
|
|
onChange={(e) => updateCalculationRule(rule.id, "formula", e.target.value)}
|
|
placeholder="quantity * unit_price"
|
|
className="h-7 flex-1 text-xs"
|
|
/>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeCalculationRule(rule.id)}
|
|
className="h-7 w-7 p-0 text-destructive"
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
|
|
{calculationRules.length === 0 && (
|
|
<p className="text-muted-foreground py-2 text-center text-xs">
|
|
계산 규칙이 없습니다
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</TabsContent>
|
|
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
UnifiedRepeaterConfigPanel.displayName = "UnifiedRepeaterConfigPanel";
|
|
|
|
export default UnifiedRepeaterConfigPanel;
|