feat(repeat-screen-modal): 연동 저장, 자동 채번, SUM_EXT 참조 제한 기능 추가

- SyncSaveConfig: 모달 저장 시 다른 테이블에 집계 값 동기화 기능
- RowNumberingConfig: 행 추가 시 채번 규칙 적용하여 자동 번호 생성
- externalTableRefs: SUM_EXT 함수가 참조할 외부 테이블 제한 기능
- triggerRepeatScreenModalSave: 외부에서 저장 트리거 가능한 이벤트 리스너
- TableColumnConfig.hidden: 테이블 컬럼 숨김 기능 (데이터는 유지, 화면만 숨김)
- beforeFormSave: FK 자동 채우기 및 _isNew 행 포함 로직 개선
This commit is contained in:
SeongHyun Kim
2025-12-10 17:13:39 +09:00
parent ae6f022f88
commit 512e1e30d1
6 changed files with 950 additions and 65 deletions

View File

@@ -21,6 +21,8 @@ import {
TableColumnConfig,
CardContentRowConfig,
AggregationDisplayConfig,
SyncSaveConfig,
RowNumberingConfig,
} from "./types";
import { tableManagementApi } from "@/lib/api/tableManagement";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
@@ -738,6 +740,7 @@ function AggregationSettingsModal({
aggregations,
sourceTable,
allTables,
contentRows,
onSave,
}: {
open: boolean;
@@ -745,6 +748,7 @@ function AggregationSettingsModal({
aggregations: AggregationConfig[];
sourceTable: string;
allTables: { tableName: string; displayName?: string }[];
contentRows: CardContentRowConfig[];
onSave: (aggregations: AggregationConfig[]) => void;
}) {
// 로컬 상태로 집계 목록 관리
@@ -852,6 +856,7 @@ function AggregationSettingsModal({
sourceTable={sourceTable}
allTables={allTables}
existingAggregations={localAggregations}
contentRows={contentRows}
onUpdate={(updates) => updateAggregation(index, updates)}
onRemove={() => removeAggregation(index)}
onMove={(direction) => moveAggregation(index, direction)}
@@ -884,6 +889,7 @@ function AggregationConfigItemModal({
sourceTable,
allTables,
existingAggregations,
contentRows,
onUpdate,
onRemove,
onMove,
@@ -894,6 +900,7 @@ function AggregationConfigItemModal({
sourceTable: string;
allTables: { tableName: string; displayName?: string }[];
existingAggregations: AggregationConfig[];
contentRows: CardContentRowConfig[];
onUpdate: (updates: Partial<AggregationConfig>) => void;
onRemove: () => void;
onMove: (direction: "up" | "down") => void;
@@ -1120,6 +1127,15 @@ function AggregationConfigItemModal({
}}
/>
</div>
{/* 🆕 v3.11: SUM_EXT 참조 테이블 선택 */}
{localFormula.includes("_EXT") && (
<ExternalTableRefSelector
contentRows={contentRows}
selectedRefs={agg.externalTableRefs || []}
onUpdate={(refs) => onUpdate({ externalTableRefs: refs })}
/>
)}
</div>
)}
@@ -1280,6 +1296,504 @@ function FormulaColumnAggregator({
);
}
// 🆕 v3.11: SUM_EXT 참조 테이블 선택 컴포넌트
function ExternalTableRefSelector({
contentRows,
selectedRefs,
onUpdate,
}: {
contentRows: CardContentRowConfig[];
selectedRefs: string[];
onUpdate: (refs: string[]) => void;
}) {
// 외부 데이터 소스가 활성화된 테이블 행만 필터링
const tableRowsWithExternalSource = contentRows.filter(
(row) => row.type === "table" && row.tableDataSource?.enabled
);
if (tableRowsWithExternalSource.length === 0) {
return (
<div className="p-3 bg-gray-100 dark:bg-gray-800 rounded-lg">
<p className="text-xs text-muted-foreground">
.
</p>
</div>
);
}
const isAllSelected = selectedRefs.length === 0;
const handleToggleTable = (tableId: string) => {
if (selectedRefs.includes(tableId)) {
// 이미 선택된 경우 제거
const newRefs = selectedRefs.filter((id) => id !== tableId);
onUpdate(newRefs);
} else {
// 선택되지 않은 경우 추가
onUpdate([...selectedRefs, tableId]);
}
};
const handleSelectAll = () => {
onUpdate([]); // 빈 배열 = 모든 테이블 사용
};
return (
<div className="p-3 bg-amber-50 dark:bg-amber-950 rounded-lg border border-amber-200 space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-amber-700 dark:text-amber-300 font-medium">
SUM_EXT ( )
</Label>
<Button
size="sm"
variant={isAllSelected ? "default" : "outline"}
onClick={handleSelectAll}
className="h-6 text-[10px] px-2"
>
</Button>
</div>
<p className="text-[10px] text-amber-600">
SUM_EXT . .
</p>
<div className="space-y-1">
{tableRowsWithExternalSource.map((row) => {
const isSelected = selectedRefs.length === 0 || selectedRefs.includes(row.id);
const tableTitle = row.title || row.tableDataSource?.sourceTable || row.id;
const tableName = row.tableDataSource?.sourceTable || "";
return (
<div
key={row.id}
className={cn(
"flex items-center gap-2 p-2 rounded border cursor-pointer transition-colors",
isSelected
? "bg-amber-100 border-amber-300"
: "bg-white border-gray-200 hover:bg-gray-50"
)}
onClick={() => handleToggleTable(row.id)}
>
<input
type="checkbox"
checked={isSelected}
onChange={() => {}} // onClick에서 처리
className="h-4 w-4 rounded border-gray-300 text-amber-600 focus:ring-amber-500"
/>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium truncate">{tableTitle}</p>
<p className="text-[10px] text-muted-foreground truncate">
: {tableName} | ID: {row.id.slice(-10)}
</p>
</div>
</div>
);
})}
</div>
{selectedRefs.length > 0 && (
<p className="text-[10px] text-amber-700 bg-amber-100 px-2 py-1 rounded">
: {selectedRefs.length} ( )
</p>
)}
</div>
);
}
// 🆕 v3.12: 연동 저장 설정 섹션
function SyncSaveConfigSection({
row,
allTables,
onUpdateRow,
}: {
row: CardContentRowConfig;
allTables: { tableName: string; displayName?: string }[];
onUpdateRow: (updates: Partial<CardContentRowConfig>) => void;
}) {
const syncSaves = row.tableCrud?.syncSaves || [];
const sourceTable = row.tableDataSource?.sourceTable || "";
// 연동 저장 추가
const addSyncSave = () => {
const newSyncSave: SyncSaveConfig = {
id: `sync-${Date.now()}`,
enabled: true,
sourceColumn: "",
aggregationType: "sum",
targetTable: "",
targetColumn: "",
joinKey: {
sourceField: "",
targetField: "id",
},
};
onUpdateRow({
tableCrud: {
...row.tableCrud,
allowCreate: row.tableCrud?.allowCreate || false,
allowUpdate: row.tableCrud?.allowUpdate || false,
allowDelete: row.tableCrud?.allowDelete || false,
syncSaves: [...syncSaves, newSyncSave],
},
});
};
// 연동 저장 삭제
const removeSyncSave = (index: number) => {
const newSyncSaves = [...syncSaves];
newSyncSaves.splice(index, 1);
onUpdateRow({
tableCrud: {
...row.tableCrud,
allowCreate: row.tableCrud?.allowCreate || false,
allowUpdate: row.tableCrud?.allowUpdate || false,
allowDelete: row.tableCrud?.allowDelete || false,
syncSaves: newSyncSaves,
},
});
};
// 연동 저장 업데이트
const updateSyncSave = (index: number, updates: Partial<SyncSaveConfig>) => {
const newSyncSaves = [...syncSaves];
newSyncSaves[index] = { ...newSyncSaves[index], ...updates };
onUpdateRow({
tableCrud: {
...row.tableCrud,
allowCreate: row.tableCrud?.allowCreate || false,
allowUpdate: row.tableCrud?.allowUpdate || false,
allowDelete: row.tableCrud?.allowDelete || false,
syncSaves: newSyncSaves,
},
});
};
return (
<div className="space-y-2 p-2 bg-orange-50 dark:bg-orange-950 rounded-lg border border-orange-200">
<div className="flex items-center justify-between">
<Label className="text-[10px] font-semibold text-orange-700 dark:text-orange-300">
( )
</Label>
<Button
size="sm"
variant="outline"
onClick={addSyncSave}
className="h-5 text-[9px] px-1 bg-orange-100 hover:bg-orange-200 border-orange-300"
>
<Plus className="h-2 w-2 mr-0.5" />
</Button>
</div>
{syncSaves.length === 0 ? (
<p className="text-[9px] text-muted-foreground text-center py-2">
. .
</p>
) : (
<div className="space-y-2">
{syncSaves.map((sync, index) => (
<SyncSaveConfigItem
key={sync.id}
sync={sync}
index={index}
sourceTable={sourceTable}
allTables={allTables}
onUpdate={(updates) => updateSyncSave(index, updates)}
onRemove={() => removeSyncSave(index)}
/>
))}
</div>
)}
</div>
);
}
// 🆕 v3.12: 개별 연동 저장 설정 아이템
function SyncSaveConfigItem({
sync,
index,
sourceTable,
allTables,
onUpdate,
onRemove,
}: {
sync: SyncSaveConfig;
index: number;
sourceTable: string;
allTables: { tableName: string; displayName?: string }[];
onUpdate: (updates: Partial<SyncSaveConfig>) => void;
onRemove: () => void;
}) {
return (
<div className={cn(
"p-2 rounded border space-y-2",
sync.enabled ? "bg-orange-100 border-orange-300" : "bg-gray-100 border-gray-300 opacity-60"
)}>
{/* 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Switch
checked={sync.enabled}
onCheckedChange={(checked) => onUpdate({ enabled: checked })}
className="scale-[0.6]"
/>
<Badge className="text-[8px] px-1 py-0 bg-orange-200 text-orange-800">
{index + 1}
</Badge>
</div>
<Button
size="sm"
variant="ghost"
onClick={onRemove}
className="h-4 w-4 p-0 hover:bg-destructive/10 hover:text-destructive"
>
<X className="h-3 w-3" />
</Button>
</div>
{/* 소스 설정 */}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-0.5">
<Label className="text-[8px] text-muted-foreground"> ({sourceTable})</Label>
<SourceColumnSelector
sourceTable={sourceTable}
value={sync.sourceColumn}
onChange={(value) => onUpdate({ sourceColumn: value })}
placeholder="컬럼 선택"
/>
</div>
<div className="space-y-0.5">
<Label className="text-[8px] text-muted-foreground"> </Label>
<Select
value={sync.aggregationType}
onValueChange={(value) => onUpdate({ aggregationType: value as SyncSaveConfig["aggregationType"] })}
>
<SelectTrigger className="h-6 text-[10px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sum"> (SUM)</SelectItem>
<SelectItem value="count"> (COUNT)</SelectItem>
<SelectItem value="avg"> (AVG)</SelectItem>
<SelectItem value="min"> (MIN)</SelectItem>
<SelectItem value="max"> (MAX)</SelectItem>
<SelectItem value="latest"> (LATEST)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 대상 설정 */}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-0.5">
<Label className="text-[8px] text-muted-foreground"> </Label>
<Select
value={sync.targetTable}
onValueChange={(value) => onUpdate({ targetTable: value })}
>
<SelectTrigger className="h-6 text-[10px]">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{allTables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.displayName || table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-0.5">
<Label className="text-[8px] text-muted-foreground"> </Label>
<SourceColumnSelector
sourceTable={sync.targetTable}
value={sync.targetColumn}
onChange={(value) => onUpdate({ targetColumn: value })}
placeholder="컬럼 선택"
/>
</div>
</div>
{/* 조인 키 설정 */}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-0.5">
<Label className="text-[8px] text-muted-foreground"> ()</Label>
<SourceColumnSelector
sourceTable={sourceTable}
value={sync.joinKey.sourceField}
onChange={(value) => onUpdate({ joinKey: { ...sync.joinKey, sourceField: value } })}
placeholder="예: sales_order_id"
/>
</div>
<div className="space-y-0.5">
<Label className="text-[8px] text-muted-foreground"> ()</Label>
<SourceColumnSelector
sourceTable={sync.targetTable}
value={sync.joinKey.targetField}
onChange={(value) => onUpdate({ joinKey: { ...sync.joinKey, targetField: value } })}
placeholder="예: id"
/>
</div>
</div>
{/* 설정 요약 */}
{sync.sourceColumn && sync.targetTable && sync.targetColumn && (
<p className="text-[8px] text-orange-700 bg-orange-200 px-2 py-1 rounded">
{sourceTable}.{sync.sourceColumn} {sync.aggregationType.toUpperCase()} {" "}
{sync.targetTable}.{sync.targetColumn}
</p>
)}
</div>
);
}
// 🆕 v3.13: 행 추가 시 자동 채번 설정 섹션
function RowNumberingConfigSection({
row,
onUpdateRow,
}: {
row: CardContentRowConfig;
onUpdateRow: (updates: Partial<CardContentRowConfig>) => void;
}) {
const [numberingRules, setNumberingRules] = useState<{ id: string; name: string; code: string }[]>([]);
const [isLoading, setIsLoading] = useState(false);
const rowNumbering = row.tableCrud?.rowNumbering;
const tableColumns = row.tableColumns || [];
// 채번 규칙 목록 로드 (옵션설정 > 코드설정에서 등록된 전체 목록)
useEffect(() => {
const loadNumberingRules = async () => {
setIsLoading(true);
try {
const { getNumberingRules } = await import("@/lib/api/numberingRule");
const response = await getNumberingRules();
if (response.success && response.data) {
setNumberingRules(response.data.map((rule: any, index: number) => ({
id: String(rule.ruleId || rule.id || `rule-${index}`),
name: rule.ruleName || rule.name || "이름 없음",
code: rule.ruleId || rule.code || "",
})));
}
} catch (error) {
console.error("채번 규칙 로드 실패:", error);
setNumberingRules([]);
} finally {
setIsLoading(false);
}
};
loadNumberingRules();
}, []);
// 채번 설정 업데이트
const updateRowNumbering = (updates: Partial<RowNumberingConfig>) => {
const currentNumbering = row.tableCrud?.rowNumbering || {
enabled: false,
targetColumn: "",
numberingRuleId: "",
};
onUpdateRow({
tableCrud: {
...row.tableCrud,
allowCreate: row.tableCrud?.allowCreate || false,
allowUpdate: row.tableCrud?.allowUpdate || false,
allowDelete: row.tableCrud?.allowDelete || false,
rowNumbering: {
...currentNumbering,
...updates,
},
},
});
};
return (
<div className="space-y-2 p-3 bg-purple-50 dark:bg-purple-950 rounded-lg border border-purple-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Switch
checked={rowNumbering?.enabled || false}
onCheckedChange={(checked) => updateRowNumbering({ enabled: checked })}
className="scale-90"
/>
<Label className="text-xs font-semibold text-purple-700 dark:text-purple-300">
</Label>
</div>
</div>
<p className="text-[10px] text-purple-600 leading-tight">
&quot;&quot; .
( &gt; )
</p>
{rowNumbering?.enabled && (
<div className="space-y-3 pt-2 border-t border-purple-200">
{/* 대상 컬럼 선택 */}
<div className="space-y-1">
<Label className="text-[10px] text-purple-700"> *</Label>
<Select
value={rowNumbering.targetColumn || ""}
onValueChange={(value) => updateRowNumbering({ targetColumn: value })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{tableColumns.map((col, index) => (
<SelectItem key={col.id || `col-${index}`} value={col.field} className="text-xs">
{col.label || col.field}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[9px] text-muted-foreground">
( )
</p>
</div>
{/* 채번 규칙 선택 */}
<div className="space-y-1">
<Label className="text-[10px] text-purple-700"> *</Label>
<Select
value={rowNumbering.numberingRuleId || ""}
onValueChange={(value) => updateRowNumbering({ numberingRuleId: value })}
disabled={isLoading}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder={isLoading ? "로딩 중..." : "채번 규칙 선택"} />
</SelectTrigger>
<SelectContent>
{numberingRules.map((rule) => (
<SelectItem key={rule.id} value={rule.id} className="text-xs">
{rule.name} ({rule.code})
</SelectItem>
))}
</SelectContent>
</Select>
{numberingRules.length === 0 && !isLoading && (
<p className="text-[9px] text-amber-600">
. &gt; .
</p>
)}
</div>
{/* 설정 요약 */}
{rowNumbering.targetColumn && rowNumbering.numberingRuleId && (
<div className="text-[10px] text-purple-700 bg-purple-100 px-2 py-1.5 rounded">
"추가" <strong>{rowNumbering.targetColumn}</strong>
</div>
)}
</div>
)}
</div>
);
}
// 🆕 레이아웃 설정 전용 모달
function LayoutSettingsModal({
open,
@@ -2040,6 +2554,78 @@ function LayoutRowConfigModal({
)}
</div>
{/* CRUD 설정 */}
<div className="space-y-2 p-3 bg-green-100/50 dark:bg-green-900/20 rounded border border-green-200 dark:border-green-800">
<Label className="text-xs font-semibold">CRUD </Label>
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2">
<Switch
checked={row.tableCrud?.allowCreate || false}
onCheckedChange={(checked) =>
onUpdateRow({
tableCrud: { ...row.tableCrud, allowCreate: checked, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: row.tableCrud?.allowDelete || false },
})
}
className="scale-90"
/>
<Label className="text-xs"></Label>
</div>
<div className="flex items-center gap-2">
<Switch
checked={row.tableCrud?.allowUpdate || false}
onCheckedChange={(checked) =>
onUpdateRow({
tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: checked, allowDelete: row.tableCrud?.allowDelete || false },
})
}
className="scale-90"
/>
<Label className="text-xs"></Label>
</div>
<div className="flex items-center gap-2">
<Switch
checked={row.tableCrud?.allowDelete || false}
onCheckedChange={(checked) =>
onUpdateRow({
tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: checked },
})
}
className="scale-90"
/>
<Label className="text-xs"></Label>
</div>
</div>
{row.tableCrud?.allowDelete && (
<div className="flex items-center gap-2 pl-2 pt-1 border-t border-green-200">
<Switch
checked={row.tableCrud?.deleteConfirm?.enabled !== false}
onCheckedChange={(checked) =>
onUpdateRow({
tableCrud: { ...row.tableCrud!, deleteConfirm: { enabled: checked } },
})
}
className="scale-75"
/>
<Label className="text-[10px]"> </Label>
</div>
)}
</div>
{/* 🆕 v3.13: 행 추가 시 자동 채번 설정 */}
{row.tableCrud?.allowCreate && (
<RowNumberingConfigSection
row={row}
onUpdateRow={onUpdateRow}
/>
)}
{/* 🆕 v3.12: 연동 저장 설정 */}
<SyncSaveConfigSection
row={row}
allTables={allTables}
onUpdateRow={onUpdateRow}
/>
{/* 테이블 컬럼 목록 */}
<div className="space-y-2">
<div className="flex items-center justify-between">
@@ -2077,7 +2663,7 @@ function LayoutRowConfigModal({
<X className="h-3 w-3" />
</Button>
</div>
<div className="grid grid-cols-4 gap-2">
<div className="grid grid-cols-5 gap-2">
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<SourceColumnSelector
@@ -2124,6 +2710,17 @@ function LayoutRowConfigModal({
<span className="ml-1 text-[10px]">{col.editable ? "예" : "아니오"}</span>
</div>
</div>
<div className="space-y-1">
<Label className="text-[10px]"></Label>
<div className="flex items-center h-6 px-2 border rounded-md bg-background">
<Switch
checked={col.hidden || false}
onCheckedChange={(checked) => onUpdateTableColumn(colIndex, { hidden: checked })}
className="scale-75"
/>
<span className="ml-1 text-[10px]">{col.hidden ? "예" : "아니오"}</span>
</div>
</div>
</div>
</div>
))}
@@ -3188,21 +3785,21 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM
<div className="space-y-2 pt-2 border-t overflow-hidden">
<div className="flex items-center justify-between">
<Label className="text-[10px] font-semibold"> </Label>
<Button
size="sm"
variant="outline"
<Button
size="sm"
variant="outline"
onClick={() => setAggregationModalOpen(true)}
className="h-6 text-[9px] px-2"
>
>
<Layers className="h-3 w-3 mr-1" />
({(localConfig.grouping?.aggregations || []).length})
</Button>
</Button>
</div>
{/* 현재 집계 목록 요약 */}
{(localConfig.grouping?.aggregations || []).length > 0 ? (
<div className="space-y-1 max-h-[120px] overflow-y-auto pr-1">
{(localConfig.grouping?.aggregations || []).map((agg, index) => (
{(localConfig.grouping?.aggregations || []).map((agg, index) => (
<div
key={`agg-summary-${index}`}
className={cn(
@@ -3239,15 +3836,15 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM
<div className="space-y-3 border rounded-lg p-3 bg-card">
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold"> </h3>
<Button
size="sm"
variant="outline"
<Button
size="sm"
variant="outline"
onClick={() => setLayoutModalOpen(true)}
className="h-6 text-[9px] px-2"
>
<Layers className="h-3 w-3 mr-1" />
>
<Layers className="h-3 w-3 mr-1" />
({(localConfig.contentRows || []).length})
</Button>
</Button>
</div>
{/* 현재 레이아웃 요약 */}
@@ -3324,6 +3921,7 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM
aggregations={localConfig.grouping?.aggregations || []}
sourceTable={localConfig.dataSource?.sourceTable || ""}
allTables={allTables}
contentRows={localConfig.contentRows || []}
onSave={(newAggregations) => {
updateGrouping({ aggregations: newAggregations });
}}
@@ -4192,7 +4790,7 @@ function ContentRowConfigSection({
checked={row.tableCrud?.allowCreate || false}
onCheckedChange={(checked) =>
onUpdateRow({
tableCrud: { ...row.tableCrud, allowCreate: checked, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: row.tableCrud?.allowDelete || false, allowSave: row.tableCrud?.allowSave || false },
tableCrud: { ...row.tableCrud, allowCreate: checked, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: row.tableCrud?.allowDelete || false },
})
}
className="scale-[0.5]"
@@ -4204,7 +4802,7 @@ function ContentRowConfigSection({
checked={row.tableCrud?.allowUpdate || false}
onCheckedChange={(checked) =>
onUpdateRow({
tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: checked, allowDelete: row.tableCrud?.allowDelete || false, allowSave: row.tableCrud?.allowSave || false },
tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: checked, allowDelete: row.tableCrud?.allowDelete || false },
})
}
className="scale-[0.5]"
@@ -4216,25 +4814,13 @@ function ContentRowConfigSection({
checked={row.tableCrud?.allowDelete || false}
onCheckedChange={(checked) =>
onUpdateRow({
tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: checked, allowSave: row.tableCrud?.allowSave || false },
tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: checked },
})
}
className="scale-[0.5]"
/>
<Label className="text-[9px]"></Label>
</div>
<div className="flex items-center gap-1">
<Switch
checked={row.tableCrud?.allowSave || false}
onCheckedChange={(checked) =>
onUpdateRow({
tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: row.tableCrud?.allowDelete || false, allowSave: checked },
})
}
className="scale-[0.5]"
/>
<Label className="text-[9px]"></Label>
</div>
</div>
{row.tableCrud?.allowDelete && (
<div className="flex items-center gap-1 pl-2">
@@ -4252,6 +4838,21 @@ function ContentRowConfigSection({
)}
</div>
{/* 🆕 v3.12: 연동 저장 설정 */}
<SyncSaveConfigSection
row={row}
allTables={allTables}
onUpdateRow={onUpdateRow}
/>
{/* 🆕 v3.13: 행 추가 시 자동 채번 설정 */}
{row.tableCrud?.allowCreate && (
<RowNumberingConfigSection
row={row}
onUpdateRow={onUpdateRow}
/>
)}
{/* 테이블 컬럼 목록 */}
<div className="space-y-2 pl-2 border-l-2 border-blue-300">
<div className="flex items-center justify-between">