ERP-node/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx

1071 lines
47 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,
} from "lucide-react";
import { tableTypeApi } from "@/lib/api/screen";
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 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 [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]);
// 설정 업데이트 헬퍼
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],
);
// 현재 화면 테이블 컬럼 로드 + 엔티티 컬럼 감지
useEffect(() => {
const loadCurrentTableColumns = async () => {
if (!currentTableName) {
setCurrentTableColumns([]);
setEntityColumns([]);
return;
}
setLoadingColumns(true);
try {
const columnData = await tableTypeApi.getColumns(currentTableName);
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();
}, [currentTableName]);
// 소스(엔티티) 테이블 컬럼 로드 (모달 모드일 때)
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) => updateConfig({ renderMode: value as any })}>
<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>
{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" />
({currentTableName || "미선택"}) -
</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;