리피터 입력폼 수정
This commit is contained in:
parent
97675458d7
commit
5948799a29
|
|
@ -541,6 +541,11 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
const response = await dynamicFormApi.saveData(saveData);
|
||||
|
||||
if (response.success) {
|
||||
// 리피터 데이터 저장 이벤트 발생 (UnifiedRepeater 컴포넌트가 리스닝)
|
||||
window.dispatchEvent(new CustomEvent("repeaterSave", {
|
||||
detail: { parentId: response.data?.id || formData.id }
|
||||
}));
|
||||
|
||||
toast.success("데이터가 성공적으로 저장되었습니다.");
|
||||
} else {
|
||||
toast.error(response.message || "저장에 실패했습니다.");
|
||||
|
|
|
|||
|
|
@ -16,17 +16,23 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
|
|||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Plus, Trash2, GripVertical, Search, X, Check } from "lucide-react";
|
||||
import { Plus, Trash2, GripVertical, Search, X, Check, Calendar } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
UnifiedRepeaterConfig,
|
||||
UnifiedRepeaterProps,
|
||||
RepeaterButtonConfig,
|
||||
ButtonActionType,
|
||||
RepeaterColumnConfig,
|
||||
DEFAULT_REPEATER_CONFIG,
|
||||
} from "@/types/unified-repeater";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
||||
import { format } from "date-fns";
|
||||
import { ko } from "date-fns/locale";
|
||||
|
||||
// 모달 크기 매핑
|
||||
const MODAL_SIZE_MAP = {
|
||||
|
|
@ -37,6 +43,14 @@ const MODAL_SIZE_MAP = {
|
|||
full: "max-w-[95vw]",
|
||||
};
|
||||
|
||||
// 🆕 전역 UnifiedRepeater 등록 (buttonActions에서 사용)
|
||||
// 리피터가 있으면 메인 폼은 리피터 데이터에 병합되어 저장되므로 별도 저장 불필요
|
||||
declare global {
|
||||
interface Window {
|
||||
__unifiedRepeaterInstances?: Set<string>;
|
||||
}
|
||||
}
|
||||
|
||||
export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
||||
config: propConfig,
|
||||
parentId,
|
||||
|
|
@ -75,11 +89,38 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
// 상태 - 버튼 모드
|
||||
const [codeButtons, setCodeButtons] = useState<{ label: string; value: string; variant?: string }[]>([]);
|
||||
|
||||
// 상태 - 컬럼별 공통코드 옵션 (code 타입용)
|
||||
const [columnCodeOptions, setColumnCodeOptions] = useState<Record<string, { label: string; value: string }[]>>({});
|
||||
|
||||
// 상태 - 엔티티 표시 정보 캐시 (FK값 → 표시 데이터)
|
||||
const [entityDisplayCache, setEntityDisplayCache] = useState<Record<string | number, Record<string, any>>>({});
|
||||
|
||||
// 상태 - 소스 테이블 컬럼 정보 (라벨 매핑용)
|
||||
const [sourceTableColumnMap, setSourceTableColumnMap] = useState<Record<string, string>>({});
|
||||
|
||||
// 상태 - 현재 테이블 컬럼 정보 (inputType 매핑용)
|
||||
const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState<Record<string, { inputType: string; detailSettings?: any }>>({});
|
||||
|
||||
// 🆕 전역 등록 - 리피터가 있으면 메인 폼 단독 저장 건너뛰기
|
||||
useEffect(() => {
|
||||
const tableName = config.dataSource?.tableName;
|
||||
if (!tableName) return;
|
||||
|
||||
// 전역 Set 초기화
|
||||
if (!window.__unifiedRepeaterInstances) {
|
||||
window.__unifiedRepeaterInstances = new Set();
|
||||
}
|
||||
|
||||
// 등록
|
||||
window.__unifiedRepeaterInstances.add(tableName);
|
||||
console.log("📦 UnifiedRepeater 등록:", tableName, Array.from(window.__unifiedRepeaterInstances));
|
||||
|
||||
return () => {
|
||||
// 해제
|
||||
window.__unifiedRepeaterInstances?.delete(tableName);
|
||||
console.log("📦 UnifiedRepeater 해제:", tableName, Array.from(window.__unifiedRepeaterInstances || []));
|
||||
};
|
||||
}, [config.dataSource?.tableName]);
|
||||
|
||||
// 외부 데이터 변경 시 동기화
|
||||
useEffect(() => {
|
||||
|
|
@ -88,6 +129,138 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
}
|
||||
}, [initialData]);
|
||||
|
||||
// 저장 이벤트 리스너 - 상위에서 저장 버튼 클릭 시 리피터 데이터도 저장
|
||||
// 🔧 메인 폼 데이터 + 리피터 행 데이터를 병합해서 저장
|
||||
useEffect(() => {
|
||||
const handleSaveEvent = async (event: CustomEvent) => {
|
||||
const tableName = config.dataSource?.tableName;
|
||||
const eventParentId = event.detail?.parentId;
|
||||
const mainFormData = event.detail?.mainFormData || {}; // 🆕 메인 폼 데이터
|
||||
|
||||
if (!tableName || data.length === 0) {
|
||||
console.log("📦 UnifiedRepeater 저장 스킵:", { tableName, dataCount: data.length, hasTable: !!tableName });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("📦 UnifiedRepeater 저장 이벤트 수신:", {
|
||||
tableName,
|
||||
dataCount: data.length,
|
||||
eventParentId,
|
||||
propsParentId: parentId,
|
||||
mainFormDataKeys: Object.keys(mainFormData),
|
||||
referenceKey: config.dataSource?.referenceKey
|
||||
});
|
||||
|
||||
try {
|
||||
// 새 데이터 삽입 (메인 폼 데이터 + 리피터 행 데이터 병합)
|
||||
console.log("🔄 UnifiedRepeater 데이터 삽입 시작:", { dataCount: data.length, mainFormData });
|
||||
|
||||
// 🆕 테이블에 존재하는 컬럼 목록 조회 (존재하지 않는 컬럼 필터링용)
|
||||
let validColumns: Set<string> = new Set();
|
||||
try {
|
||||
const columnsResponse = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
||||
const columns = columnsResponse.data?.data?.columns || columnsResponse.data?.columns || columnsResponse.data || [];
|
||||
validColumns = new Set(columns.map((col: any) => col.columnName || col.column_name || col.name));
|
||||
console.log("📋 테이블 유효 컬럼:", Array.from(validColumns));
|
||||
} catch {
|
||||
console.warn("⚠️ 테이블 컬럼 정보 조회 실패 - 모든 필드 저장 시도");
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
console.log(`🔄 [${i + 1}/${data.length}] 리피터 행 데이터:`, row);
|
||||
|
||||
// _display_ 필드와 임시 필드 제거
|
||||
const cleanRow = Object.fromEntries(
|
||||
Object.entries(row).filter(([key]) => !key.startsWith("_"))
|
||||
);
|
||||
|
||||
// 🆕 메인 폼 데이터 + 리피터 행 데이터 병합
|
||||
// 리피터 행 데이터가 우선 (같은 키가 있으면 덮어씀)
|
||||
// 메인 폼에서 제외할 필드: id (각 행은 새 레코드)
|
||||
const { id: _mainId, ...mainFormDataWithoutId } = mainFormData;
|
||||
const mergedData = {
|
||||
...mainFormDataWithoutId, // 메인 폼 데이터 (id 제외)
|
||||
...cleanRow, // 리피터 행 데이터 (우선)
|
||||
};
|
||||
|
||||
// 🆕 테이블에 존재하지 않는 컬럼 제거
|
||||
const filteredData: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(mergedData)) {
|
||||
// validColumns가 비어있으면 (조회 실패) 모든 필드 포함
|
||||
// validColumns가 있으면 해당 컬럼만 포함
|
||||
if (validColumns.size === 0 || validColumns.has(key)) {
|
||||
filteredData[key] = value;
|
||||
} else {
|
||||
console.log(`🗑️ [${i + 1}/${data.length}] 필터링된 컬럼 (테이블에 없음): ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📝 [${i + 1}/${data.length}] 최종 저장 데이터:`, JSON.stringify(filteredData, null, 2));
|
||||
|
||||
try {
|
||||
// /add 엔드포인트 사용 (INSERT)
|
||||
const saveResponse = await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData);
|
||||
console.log(`✅ [${i + 1}/${data.length}] 저장 성공:`, saveResponse.data);
|
||||
} catch (saveError) {
|
||||
console.error(`❌ [${i + 1}/${data.length}] 저장 실패:`, saveError);
|
||||
throw saveError;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ UnifiedRepeater 전체 데이터 저장 완료:", data.length, "건");
|
||||
} catch (error) {
|
||||
console.error("❌ UnifiedRepeater 저장 실패:", error);
|
||||
throw error; // 상위에서 에러 처리
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("repeaterSave" as any, handleSaveEvent);
|
||||
return () => {
|
||||
window.removeEventListener("repeaterSave" as any, handleSaveEvent);
|
||||
};
|
||||
}, [data, config.dataSource?.tableName, config.dataSource?.referenceKey, parentId]);
|
||||
|
||||
// 현재 테이블 컬럼 정보 로드 (inputType 매핑용)
|
||||
useEffect(() => {
|
||||
const loadCurrentTableColumnInfo = async () => {
|
||||
const tableName = config.dataSource?.tableName;
|
||||
if (!tableName) return;
|
||||
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
||||
const columns = response.data?.data?.columns || response.data?.columns || response.data || [];
|
||||
|
||||
const colInfo: Record<string, { inputType: string; detailSettings?: any }> = {};
|
||||
if (Array.isArray(columns)) {
|
||||
columns.forEach((col: any) => {
|
||||
const colName = col.columnName || col.column_name;
|
||||
let detailSettings = col.detailSettings || col.detail_settings;
|
||||
if (typeof detailSettings === "string") {
|
||||
try {
|
||||
detailSettings = JSON.parse(detailSettings);
|
||||
} catch (e) {
|
||||
detailSettings = null;
|
||||
}
|
||||
}
|
||||
if (colName) {
|
||||
colInfo[colName] = {
|
||||
inputType: col.inputType || col.input_type || "text",
|
||||
detailSettings,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
setCurrentTableColumnInfo(colInfo);
|
||||
console.log("현재 테이블 컬럼 정보 로드:", tableName, colInfo);
|
||||
} catch (error) {
|
||||
console.error("현재 테이블 컬럼 정보 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
loadCurrentTableColumnInfo();
|
||||
}, [config.dataSource?.tableName]);
|
||||
|
||||
// 소스 테이블 컬럼 정보 로드 (라벨 매핑용)
|
||||
useEffect(() => {
|
||||
const loadSourceTableColumns = async () => {
|
||||
|
|
@ -157,6 +330,59 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
loadCodeButtons();
|
||||
}, [config.button?.sourceType, config.button?.commonCode]);
|
||||
|
||||
// 컬럼별 공통코드 옵션 로드 (code 타입 컬럼용)
|
||||
useEffect(() => {
|
||||
const loadColumnCodeOptions = async () => {
|
||||
// config.columns와 currentTableColumnInfo를 함께 확인하여 code 타입 컬럼 찾기
|
||||
const codeColumnsToLoad: { key: string; codeGroup: string }[] = [];
|
||||
|
||||
config.columns.forEach((col) => {
|
||||
// 저장된 설정에서 codeGroup 확인
|
||||
let codeGroup = col.detailSettings?.codeGroup;
|
||||
let inputType = col.inputType;
|
||||
|
||||
// 저장된 정보가 없으면 현재 테이블 정보에서 확인
|
||||
if (!inputType || inputType === "text") {
|
||||
const tableColInfo = currentTableColumnInfo[col.key];
|
||||
if (tableColInfo) {
|
||||
inputType = tableColInfo.inputType;
|
||||
if (!codeGroup && tableColInfo.detailSettings?.codeGroup) {
|
||||
codeGroup = tableColInfo.detailSettings.codeGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inputType === "code" && codeGroup) {
|
||||
codeColumnsToLoad.push({ key: col.key, codeGroup });
|
||||
}
|
||||
});
|
||||
|
||||
if (codeColumnsToLoad.length === 0) return;
|
||||
|
||||
const newOptions: Record<string, { label: string; value: string }[]> = {};
|
||||
|
||||
await Promise.all(
|
||||
codeColumnsToLoad.map(async ({ key, codeGroup }) => {
|
||||
try {
|
||||
const response = await commonCodeApi.codes.getList(codeGroup);
|
||||
if (response.success && response.data) {
|
||||
newOptions[key] = response.data.map((code) => ({
|
||||
label: code.codeName,
|
||||
value: code.codeValue,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`공통코드 로드 실패 (${codeGroup}):`, error);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
setColumnCodeOptions((prev) => ({ ...prev, ...newOptions }));
|
||||
};
|
||||
|
||||
loadColumnCodeOptions();
|
||||
}, [config.columns, currentTableColumnInfo]);
|
||||
|
||||
// 소스 테이블 검색 (modal 모드)
|
||||
const searchSourceTable = useCallback(async () => {
|
||||
const sourceTable = config.dataSource?.sourceTable;
|
||||
|
|
@ -491,6 +717,105 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
return entityDisplayCache[fkValue] || null;
|
||||
};
|
||||
|
||||
// inputType별 입력 컴포넌트 렌더링
|
||||
const renderColumnInput = (col: RepeaterColumnConfig, value: any, onChange: (value: any) => void) => {
|
||||
// 저장된 inputType이 없거나 "text"이면 현재 테이블 정보에서 조회
|
||||
let inputType = col.inputType;
|
||||
let detailSettings = col.detailSettings;
|
||||
|
||||
if (!inputType || inputType === "text") {
|
||||
const tableColInfo = currentTableColumnInfo[col.key];
|
||||
if (tableColInfo) {
|
||||
inputType = tableColInfo.inputType;
|
||||
detailSettings = tableColInfo.detailSettings || detailSettings;
|
||||
}
|
||||
}
|
||||
inputType = inputType || "text";
|
||||
|
||||
const commonClasses = "h-8 text-sm min-w-[80px] w-full";
|
||||
|
||||
console.log("renderColumnInput:", { key: col.key, inputType, value, detailSettings });
|
||||
|
||||
switch (inputType) {
|
||||
case "number":
|
||||
case "decimal":
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={commonClasses}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
step={inputType === "decimal" ? "0.01" : "1"}
|
||||
/>
|
||||
);
|
||||
|
||||
case "date":
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(commonClasses, "justify-start text-left font-normal", !value && "text-muted-foreground")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Calendar className="mr-2 h-4 w-4" />
|
||||
{value ? format(new Date(value), "yyyy-MM-dd", { locale: ko }) : "날짜 선택"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<CalendarComponent
|
||||
mode="single"
|
||||
selected={value ? new Date(value) : undefined}
|
||||
onSelect={(date) => onChange(date ? format(date, "yyyy-MM-dd") : "")}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
case "code":
|
||||
const codeOptions = columnCodeOptions[col.key] || [];
|
||||
return (
|
||||
<Select value={value || ""} onValueChange={onChange}>
|
||||
<SelectTrigger className={commonClasses} onClick={(e) => e.stopPropagation()}>
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{codeOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
case "boolean":
|
||||
case "checkbox":
|
||||
return (
|
||||
<div className="flex items-center justify-center" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={value === true || value === "Y" || value === "1"}
|
||||
onCheckedChange={(checked) => onChange(checked ? "Y" : "N")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "text":
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={commonClasses}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 테이블 렌더링
|
||||
const renderTable = () => {
|
||||
if (config.renderMode === "button") return null;
|
||||
|
|
@ -598,19 +923,17 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|||
.map((col) => (
|
||||
<TableCell key={col.key} className="py-2 min-w-[100px]">
|
||||
{editingRow === index && col.editable !== false ? (
|
||||
<Input
|
||||
value={editedData[col.key] || ""}
|
||||
onChange={(e) =>
|
||||
setEditedData((prev) => ({
|
||||
...prev,
|
||||
[col.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="h-7 text-xs min-w-[80px] w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
renderColumnInput(
|
||||
col,
|
||||
editedData[col.key],
|
||||
(value) => setEditedData((prev) => ({ ...prev, [col.key]: value }))
|
||||
)
|
||||
) : (
|
||||
<span className="text-sm">{row[col.key] || "-"}</span>
|
||||
<span className="text-sm">
|
||||
{col.inputType === "code" && columnCodeOptions[col.key]
|
||||
? columnCodeOptions[col.key].find((opt) => opt.value === row[col.key])?.label || row[col.key] || "-"
|
||||
: row[col.key] || "-"}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ interface ColumnOption {
|
|||
columnName: string;
|
||||
displayName: string;
|
||||
inputType?: string;
|
||||
detailSettings?: {
|
||||
codeGroup?: string;
|
||||
referenceTable?: string;
|
||||
referenceColumn?: string;
|
||||
displayColumn?: string;
|
||||
format?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EntityColumnOption {
|
||||
|
|
@ -194,27 +201,34 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
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") {
|
||||
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 referenceTable = detailSettings?.referenceTable || c.referenceTable;
|
||||
const referenceColumn = detailSettings?.referenceColumn || c.referenceColumn || "id";
|
||||
const displayColumn = detailSettings?.displayColumn || c.displayColumn;
|
||||
|
|
@ -335,12 +349,21 @@ export const UnifiedRepeaterConfigPanel: React.FC<UnifiedRepeaterConfigPanelProp
|
|||
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] });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -398,10 +398,12 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
<UnifiedRepeater
|
||||
config={{
|
||||
renderMode: config.renderMode || "inline",
|
||||
dataSource: config.dataSource || {
|
||||
tableName: "",
|
||||
foreignKey: "",
|
||||
referenceKey: "",
|
||||
dataSource: {
|
||||
tableName: config.dataSource?.tableName || props.tableName || "",
|
||||
foreignKey: config.dataSource?.foreignKey || "",
|
||||
referenceKey: config.dataSource?.referenceKey || "",
|
||||
sourceTable: config.dataSource?.sourceTable,
|
||||
displayColumn: config.dataSource?.displayColumn,
|
||||
},
|
||||
columns: config.columns || [],
|
||||
modal: config.modal,
|
||||
|
|
|
|||
|
|
@ -706,7 +706,12 @@ export class ButtonActionExecutor {
|
|||
if (!response.ok) {
|
||||
throw new Error(`저장 실패: ${response.statusText}`);
|
||||
}
|
||||
} else if (tableName && screenId) {
|
||||
}
|
||||
|
||||
// saveResult를 상위 스코프에서 정의 (repeaterSave 이벤트에서 사용)
|
||||
let saveResult: { success: boolean; data?: any; message?: string } | undefined;
|
||||
|
||||
if (tableName && screenId) {
|
||||
// DB에서 실제 기본키 조회하여 INSERT/UPDATE 자동 판단
|
||||
const primaryKeyResult = await DynamicFormApi.getTablePrimaryKeys(tableName);
|
||||
|
||||
|
|
@ -732,8 +737,6 @@ export class ButtonActionExecutor {
|
|||
primaryKeys,
|
||||
});
|
||||
|
||||
let saveResult;
|
||||
|
||||
if (isUpdate) {
|
||||
// UPDATE 처리 - 부분 업데이트 사용 (원본 데이터가 있는 경우)
|
||||
console.log("🔄 UPDATE 모드로 저장:", {
|
||||
|
|
@ -1088,19 +1091,28 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
// 메인 저장 건너뛰기 조건: RepeatScreenModal 또는 RepeaterFieldGroup에서 같은 테이블 처리
|
||||
// 🆕 UnifiedRepeater 전역 등록 확인
|
||||
// @ts-ignore - window에 동적 속성 사용
|
||||
const unifiedRepeaterTables = Array.from(window.__unifiedRepeaterInstances || []);
|
||||
|
||||
// 메인 저장 건너뛰기 조건:
|
||||
// 1. RepeatScreenModal 또는 RepeaterFieldGroup에서 같은 테이블 처리
|
||||
// 2. UnifiedRepeater가 같은 테이블에 존재 (리피터 데이터에 메인 폼 데이터 병합되어 저장됨)
|
||||
const shouldSkipMainSave =
|
||||
repeatScreenModalTables.includes(tableName) || repeaterFieldGroupTables.includes(tableName);
|
||||
repeatScreenModalTables.includes(tableName) ||
|
||||
repeaterFieldGroupTables.includes(tableName) ||
|
||||
unifiedRepeaterTables.includes(tableName);
|
||||
|
||||
if (shouldSkipMainSave) {
|
||||
console.log(
|
||||
`⏭️ [handleSave] ${tableName} 메인 저장 건너뜀 (RepeaterFieldGroup/RepeatScreenModal에서 처리)`,
|
||||
`⏭️ [handleSave] ${tableName} 메인 저장 건너뜀`,
|
||||
{
|
||||
repeatScreenModalTables,
|
||||
repeaterFieldGroupTables,
|
||||
unifiedRepeaterTables,
|
||||
},
|
||||
);
|
||||
saveResult = { success: true, message: "RepeaterFieldGroup/RepeatScreenModal에서 처리" };
|
||||
saveResult = { success: true, message: "RepeaterFieldGroup/RepeatScreenModal/UnifiedRepeater에서 처리" };
|
||||
} else {
|
||||
saveResult = await DynamicFormApi.saveFormData({
|
||||
screenId,
|
||||
|
|
@ -1229,6 +1241,40 @@ export class ButtonActionExecutor {
|
|||
// 저장 성공 후 이벤트 발생
|
||||
window.dispatchEvent(new CustomEvent("closeEditModal")); // EditModal 닫기
|
||||
window.dispatchEvent(new CustomEvent("saveSuccessInModal")); // ScreenModal 연속 등록 모드 처리
|
||||
|
||||
// UnifiedRepeater 저장 이벤트 발생 (메인 폼 데이터 + 리피터 데이터 병합 저장)
|
||||
// 🔧 formData를 리피터에 전달하여 각 행에 병합 저장
|
||||
const savedId = saveResult?.data?.id || saveResult?.data?.data?.id || formData.id || context.formData?.id;
|
||||
|
||||
// 메인 폼 데이터 구성 (사용자 정보 포함)
|
||||
const mainFormData = {
|
||||
...formData,
|
||||
writer: formData.writer || context.userId,
|
||||
created_by: context.userId,
|
||||
updated_by: context.userId,
|
||||
company_code: formData.company_code || context.companyCode,
|
||||
};
|
||||
|
||||
// _numberingRuleId 등 메타 필드 제거
|
||||
for (const key of Object.keys(mainFormData)) {
|
||||
if (key.endsWith("_numberingRuleId") || key.startsWith("_")) {
|
||||
delete mainFormData[key];
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🔗 [handleSave] repeaterSave 이벤트 발생:", {
|
||||
savedId,
|
||||
tableName: context.tableName,
|
||||
mainFormDataKeys: Object.keys(mainFormData),
|
||||
saveResultData: saveResult?.data
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent("repeaterSave", {
|
||||
detail: {
|
||||
parentId: savedId,
|
||||
tableName: context.tableName,
|
||||
mainFormData, // 🆕 메인 폼 데이터 전달
|
||||
}
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ export interface RepeaterColumnConfig {
|
|||
editable?: boolean; // 편집 가능 여부 (inline 모드)
|
||||
isJoinColumn?: boolean;
|
||||
sourceTable?: string;
|
||||
// 입력 타입 (테이블 타입 관리의 inputType을 따름)
|
||||
inputType?: string; // text, number, date, code, entity 등
|
||||
// 입력 타입별 상세 설정
|
||||
detailSettings?: {
|
||||
codeGroup?: string; // code 타입용
|
||||
referenceTable?: string; // entity 타입용
|
||||
referenceColumn?: string;
|
||||
displayColumn?: string;
|
||||
format?: string; // date, number 타입용
|
||||
};
|
||||
}
|
||||
|
||||
// 버튼 설정 (수동 모드)
|
||||
|
|
|
|||
Loading…
Reference in New Issue