feat(repeat-screen-modal): 연동 저장, 자동 채번, SUM_EXT 참조 제한 기능 추가
- SyncSaveConfig: 모달 저장 시 다른 테이블에 집계 값 동기화 기능 - RowNumberingConfig: 행 추가 시 채번 규칙 적용하여 자동 번호 생성 - externalTableRefs: SUM_EXT 함수가 참조할 외부 테이블 제한 기능 - triggerRepeatScreenModalSave: 외부에서 저장 트리거 가능한 이벤트 리스너 - TableColumnConfig.hidden: 테이블 컬럼 숨김 기능 (데이터는 유지, 화면만 숨김) - beforeFormSave: FK 자동 채우기 및 _isNew 행 포함 로직 개선
This commit is contained in:
@@ -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">
|
||||
"추가" 버튼 클릭 시 지정한 컬럼에 자동으로 번호를 생성합니다.
|
||||
(옵션설정 > 코드설정에서 등록한 채번 규칙 사용)
|
||||
</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">
|
||||
등록된 채번 규칙이 없습니다. 옵션설정 > 코드설정에서 추가하세요.
|
||||
</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">
|
||||
|
||||
Reference in New Issue
Block a user