ix: 부모-자식 모달 데이터 전달 문제 해결 및 미사용 multiRowSave 기능 제거

InteractiveScreenViewerDynamic: 생성 모드에서 formData를 initialData로 전달하도록 수정
UniversalFormModal: saveMultipleRows 함수 및 multiRowSave 관련 코드 전체 제거
types/config: MultiRowSaveConfig 인터페이스 및 기본값 제거
FieldDetailSettingsModal: receiveFromParent UI 옵션 제거
SaveSettingsModal: 저장 모드 설명 개선
DB: multiRowSave.enabled=true인 화면 3개 설정 수정
This commit is contained in:
SeongHyun Kim 2026-01-07 17:42:40 +09:00
parent 42d1a3fc5e
commit 6f4c9b7fdd
9 changed files with 40 additions and 229 deletions

View File

@ -365,7 +365,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
isInteractive={true} isInteractive={true}
formData={formData} formData={formData}
originalData={originalData || undefined} originalData={originalData || undefined}
initialData={originalData || undefined} // 🆕 조건부 컨테이너 등에서 initialData로 전달 initialData={(originalData && Object.keys(originalData).length > 0) ? originalData : formData} // 🆕 originalData가 있으면 사용, 없으면 formData 사용 (생성 모드에서 부모 데이터 전달)
onFormDataChange={handleFormDataChange} onFormDataChange={handleFormDataChange}
screenId={screenInfo?.id} screenId={screenInfo?.id}
tableName={screenInfo?.tableName} tableName={screenInfo?.tableName}

View File

@ -654,7 +654,6 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
}, },
}); });
window.dispatchEvent(event); window.dispatchEvent(event);
console.log("[SplitPanelLayout2] 우측 추가 모달 열기");
}, [ }, [
config.rightPanel?.addModalScreenId, config.rightPanel?.addModalScreenId,
config.rightPanel?.addButtonLabel, config.rightPanel?.addButtonLabel,

View File

@ -139,6 +139,7 @@ export function UniversalFormModalComponent({
}: UniversalFormModalComponentProps & { _initialData?: any; _originalData?: any; _groupedData?: any }) { }: UniversalFormModalComponentProps & { _initialData?: any; _originalData?: any; _groupedData?: any }) {
// initialData 우선순위: 직접 전달된 prop > DynamicComponentRenderer에서 전달된 prop // initialData 우선순위: 직접 전달된 prop > DynamicComponentRenderer에서 전달된 prop
const initialData = propInitialData || _initialData; const initialData = propInitialData || _initialData;
// 설정 병합 // 설정 병합
const config: UniversalFormModalConfig = useMemo(() => { const config: UniversalFormModalConfig = useMemo(() => {
const componentConfig = component?.config || {}; const componentConfig = component?.config || {};
@ -155,11 +156,6 @@ export function UniversalFormModalComponent({
...defaultConfig.saveConfig, ...defaultConfig.saveConfig,
...propConfig?.saveConfig, ...propConfig?.saveConfig,
...componentConfig.saveConfig, ...componentConfig.saveConfig,
multiRowSave: {
...defaultConfig.saveConfig.multiRowSave,
...propConfig?.saveConfig?.multiRowSave,
...componentConfig.saveConfig?.multiRowSave,
},
afterSave: { afterSave: {
...defaultConfig.saveConfig.afterSave, ...defaultConfig.saveConfig.afterSave,
...propConfig?.saveConfig?.afterSave, ...propConfig?.saveConfig?.afterSave,
@ -1504,118 +1500,6 @@ export function UniversalFormModalComponent({
formData, formData,
]); ]);
// 다중 행 저장 (겸직 등)
const saveMultipleRows = useCallback(async () => {
const { multiRowSave } = config.saveConfig;
if (!multiRowSave) return;
let { commonFields = [], repeatSectionId = "" } = multiRowSave;
const { typeColumn, mainTypeValue, subTypeValue, mainSectionFields = [] } = multiRowSave;
// 공통 필드가 설정되지 않은 경우, 기본정보 섹션의 모든 필드를 공통 필드로 사용
if (commonFields.length === 0) {
const nonRepeatableSections = config.sections.filter((s) => !s.repeatable);
commonFields = nonRepeatableSections.flatMap((s) => (s.fields || []).map((f) => f.columnName));
}
// 반복 섹션 ID가 설정되지 않은 경우, 첫 번째 반복 섹션 사용
if (!repeatSectionId) {
const repeatableSection = config.sections.find((s) => s.repeatable);
if (repeatableSection) {
repeatSectionId = repeatableSection.id;
}
}
// 반복 섹션 데이터
const repeatItems = repeatSections[repeatSectionId] || [];
// 저장할 행들 생성
const rowsToSave: any[] = [];
// 공통 데이터 (모든 행에 적용)
const commonData: any = {};
commonFields.forEach((fieldName) => {
if (formData[fieldName] !== undefined) {
commonData[fieldName] = formData[fieldName];
}
});
// 메인 섹션 필드 데이터 (메인 행에만 적용되는 부서/직급 등)
const mainSectionData: any = {};
mainSectionFields.forEach((fieldName) => {
if (formData[fieldName] !== undefined) {
mainSectionData[fieldName] = formData[fieldName];
}
});
// 메인 행 (공통 데이터 + 메인 섹션 필드)
const mainRow: any = { ...commonData, ...mainSectionData };
if (typeColumn) {
mainRow[typeColumn] = mainTypeValue || "main";
}
rowsToSave.push(mainRow);
// 반복 섹션 행들 (공통 데이터 + 반복 섹션 필드)
for (const item of repeatItems) {
const subRow: any = { ...commonData };
// 반복 섹션의 필드 값 추가
const repeatSection = config.sections.find((s) => s.id === repeatSectionId);
(repeatSection?.fields || []).forEach((field) => {
if (item[field.columnName] !== undefined) {
subRow[field.columnName] = item[field.columnName];
}
});
if (typeColumn) {
subRow[typeColumn] = subTypeValue || "concurrent";
}
rowsToSave.push(subRow);
}
// 저장 시점 채번규칙 처리 (메인 행만)
for (const section of config.sections) {
if (section.repeatable || section.type === "table") continue;
for (const field of section.fields || []) {
if (field.numberingRule?.enabled && field.numberingRule?.ruleId) {
// generateOnSave 또는 generateOnOpen 모두 저장 시 실제 순번 할당
const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen;
if (shouldAllocate) {
const response = await allocateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
// 모든 행에 동일한 채번 값 적용 (공통 필드인 경우)
if (commonFields.includes(field.columnName)) {
rowsToSave.forEach((row) => {
row[field.columnName] = response.data?.generatedCode;
});
} else {
rowsToSave[0][field.columnName] = response.data?.generatedCode;
}
}
}
}
}
}
// 모든 행 저장
for (let i = 0; i < rowsToSave.length; i++) {
const row = rowsToSave[i];
// 빈 객체 체크
if (Object.keys(row).length === 0) {
continue;
}
const response = await apiClient.post(`/table-management/tables/${config.saveConfig.tableName}/add`, row);
if (!response.data?.success) {
throw new Error(response.data?.message || `${i + 1}번째 행 저장 실패`);
}
}
}, [config.sections, config.saveConfig, formData, repeatSections]);
// 다중 테이블 저장 (범용) // 다중 테이블 저장 (범용)
const saveWithMultiTable = useCallback(async () => { const saveWithMultiTable = useCallback(async () => {
const { customApiSave } = config.saveConfig; const { customApiSave } = config.saveConfig;
@ -1863,16 +1747,13 @@ export function UniversalFormModalComponent({
setSaving(true); setSaving(true);
try { try {
const { multiRowSave, customApiSave } = config.saveConfig; const { customApiSave } = config.saveConfig;
// 커스텀 API 저장 모드 // 커스텀 API 저장 모드 (다중 테이블)
if (customApiSave?.enabled) { if (customApiSave?.enabled) {
await saveWithCustomApi(); await saveWithCustomApi();
} else if (multiRowSave?.enabled) {
// 다중 행 저장
await saveMultipleRows();
} else { } else {
// 단일 저장 // 단일 테이블 저장
await saveSingleRow(); await saveSingleRow();
} }
@ -1886,7 +1767,7 @@ export function UniversalFormModalComponent({
} }
// onSave 콜백은 저장 완료 알림용으로만 사용 // onSave 콜백은 저장 완료 알림용으로만 사용
// 실제 저장은 이미 위에서 완료됨 (saveSingleRow 또는 saveMultipleRows) // 실제 저장은 이미 위에서 완료됨
// EditModal 등 부모 컴포넌트의 저장 로직이 다시 실행되지 않도록 // EditModal 등 부모 컴포넌트의 저장 로직이 다시 실행되지 않도록
// _saveCompleted 플래그를 포함하여 전달 // _saveCompleted 플래그를 포함하여 전달
if (onSave) { if (onSave) {
@ -1916,7 +1797,6 @@ export function UniversalFormModalComponent({
onSave, onSave,
validateRequiredFields, validateRequiredFields,
saveSingleRow, saveSingleRow,
saveMultipleRows,
saveWithCustomApi, saveWithCustomApi,
]); ]);

View File

@ -885,7 +885,6 @@ export function UniversalFormModalConfigPanel({
tableColumns={tableColumns} tableColumns={tableColumns}
numberingRules={numberingRules} numberingRules={numberingRules}
onLoadTableColumns={loadTableColumns} onLoadTableColumns={loadTableColumns}
availableParentFields={availableParentFields}
targetTableName={config.saveConfig?.tableName} targetTableName={config.saveConfig?.tableName}
targetTableColumns={config.saveConfig?.tableName ? tableColumns[config.saveConfig.tableName] || [] : []} targetTableColumns={config.saveConfig?.tableName ? tableColumns[config.saveConfig.tableName] || [] : []}
/> />

View File

@ -45,15 +45,6 @@ export const defaultConfig: UniversalFormModalConfig = {
saveConfig: { saveConfig: {
tableName: "", tableName: "",
primaryKeyColumn: "id", primaryKeyColumn: "id",
multiRowSave: {
enabled: false,
commonFields: [],
repeatSectionId: "",
typeColumn: "",
mainTypeValue: "main",
subTypeValue: "concurrent",
mainSectionFields: [],
},
afterSave: { afterSave: {
closeModal: true, closeModal: true,
refreshParent: true, refreshParent: true,

View File

@ -9,14 +9,14 @@ import { defaultConfig } from "./config";
/** /**
* *
* *
* , , * ,
* . * .
*/ */
export const UniversalFormModalDefinition = createComponentDefinition({ export const UniversalFormModalDefinition = createComponentDefinition({
id: "universal-form-modal", id: "universal-form-modal",
name: "범용 폼 모달", name: "범용 폼 모달",
nameEng: "Universal Form Modal", nameEng: "Universal Form Modal",
description: "섹션 기반 폼 레이아웃, 채번규칙, 다중 행 저장을 지원하는 범용 모달 컴포넌트", description: "섹션 기반 폼 레이아웃, 채번규칙을 지원하는 범용 모달 컴포넌트",
category: ComponentCategory.INPUT, category: ComponentCategory.INPUT,
webType: "form", webType: "form",
component: UniversalFormModalComponent, component: UniversalFormModalComponent,
@ -28,7 +28,7 @@ export const UniversalFormModalDefinition = createComponentDefinition({
}, },
configPanel: UniversalFormModalConfigPanel, configPanel: UniversalFormModalConfigPanel,
icon: "FormInput", icon: "FormInput",
tags: ["폼", "모달", "입력", "저장", "채번", "겸직", "다중행"], tags: ["폼", "모달", "입력", "저장", "채번"],
version: "1.0.0", version: "1.0.0",
author: "개발팀", author: "개발팀",
documentation: ` documentation: `
@ -36,22 +36,22 @@ export const UniversalFormModalDefinition = createComponentDefinition({
### ###
- ** **: , - ** **: ,
- ** **: - ** **:
- ** **: ( ) - ** **: ( )
- ** **: + - **/ **: +
- ** **: - ** **:
### ###
1. + 1. , , ( )
2. + 2. + ( )
3. + 3. +
### ###
1. 1.
2. ( , ) 2. ( )
3. 3.
4. ( ) 4. ( )
5. ( ) 5. ( )
6. ( ) 6. ( )
`, `,
}); });
@ -69,7 +69,6 @@ export type {
FormSectionConfig, FormSectionConfig,
FormFieldConfig, FormFieldConfig,
SaveConfig, SaveConfig,
MultiRowSaveConfig,
NumberingRuleConfig, NumberingRuleConfig,
SelectOptionConfig, SelectOptionConfig,
FormDataState, FormDataState,

View File

@ -65,8 +65,6 @@ interface FieldDetailSettingsModalProps {
tableColumns: { [tableName: string]: { name: string; type: string; label: string }[] }; tableColumns: { [tableName: string]: { name: string; type: string; label: string }[] };
numberingRules: { id: string; name: string }[]; numberingRules: { id: string; name: string }[];
onLoadTableColumns: (tableName: string) => void; onLoadTableColumns: (tableName: string) => void;
// 부모 화면에서 전달 가능한 필드 목록 (선택사항)
availableParentFields?: AvailableParentField[];
// 저장 테이블 정보 (타겟 컬럼 선택용) // 저장 테이블 정보 (타겟 컬럼 선택용)
targetTableName?: string; targetTableName?: string;
targetTableColumns?: { name: string; type: string; label: string }[]; targetTableColumns?: { name: string; type: string; label: string }[];
@ -81,7 +79,6 @@ export function FieldDetailSettingsModal({
tableColumns, tableColumns,
numberingRules, numberingRules,
onLoadTableColumns, onLoadTableColumns,
availableParentFields = [],
// targetTableName은 타겟 컬럼 선택 시 참고용으로 전달됨 (현재 targetTableColumns만 사용) // targetTableName은 타겟 컬럼 선택 시 참고용으로 전달됨 (현재 targetTableColumns만 사용)
targetTableName: _targetTableName, targetTableName: _targetTableName,
targetTableColumns = [], targetTableColumns = [],
@ -330,60 +327,6 @@ export function FieldDetailSettingsModal({
/> />
</div> </div>
<HelpText> </HelpText> <HelpText> </HelpText>
<Separator className="my-2" />
<div className="flex items-center justify-between">
<span className="text-[10px]"> </span>
<Switch
checked={localField.receiveFromParent || false}
onCheckedChange={(checked) => updateField({ receiveFromParent: checked })}
/>
</div>
<HelpText> </HelpText>
{/* 부모에서 값 받기 활성화 시 필드 선택 */}
{localField.receiveFromParent && (
<div className="mt-3 space-y-2 p-3 rounded-md bg-blue-50 border border-blue-200">
<Label className="text-xs font-medium text-blue-700"> </Label>
{availableParentFields.length > 0 ? (
<Select
value={localField.parentFieldName || localField.columnName}
onValueChange={(value) => updateField({ parentFieldName: value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{availableParentFields.map((pf) => (
<SelectItem key={pf.name} value={pf.name}>
<div className="flex flex-col">
<span>{pf.label || pf.name}</span>
{pf.sourceComponent && (
<span className="text-[9px] text-muted-foreground">
{pf.sourceComponent}{pf.sourceTable && ` (${pf.sourceTable})`}
</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className="space-y-1">
<Input
value={localField.parentFieldName || ""}
onChange={(e) => updateField({ parentFieldName: e.target.value })}
placeholder={`예: ${localField.columnName || "parent_field_name"}`}
className="h-8 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
. "{localField.columnName}" .
</p>
</div>
)}
</div>
)}
</div> </div>
{/* Accordion으로 고급 설정 */} {/* Accordion으로 고급 설정 */}

View File

@ -378,7 +378,11 @@ export function SaveSettingsModal({
</Label> </Label>
</div> </div>
<HelpText> ( )</HelpText> <HelpText>
1 .
<br />
: 사원 , ,
</HelpText>
<div className="flex items-center space-x-2 pt-2"> <div className="flex items-center space-x-2 pt-2">
<RadioGroupItem value="multi" id="mode-multi" /> <RadioGroupItem value="multi" id="mode-multi" />
@ -387,9 +391,13 @@ export function SaveSettingsModal({
</Label> </Label>
</div> </div>
<HelpText> <HelpText>
+ . ( )
<br /> <br />
: 주문(orders) + (order_items), (user_info) + (user_dept) 테이블: 폼의
<br />
테이블: 필드 ( )
<br />
: 사원+(user_info+user_dept), +(orders+order_items)
</HelpText> </HelpText>
</RadioGroup> </RadioGroup>
</div> </div>
@ -691,9 +699,11 @@ export function SaveSettingsModal({
</div> </div>
<HelpText> <HelpText>
. .
<br /> <br />
: 주문상세(order_items), (user_dept) (: user_id) .
<br />
.
</HelpText> </HelpText>
{(localSaveConfig.customApiSave?.multiTable?.subTables || []).length === 0 ? ( {(localSaveConfig.customApiSave?.multiTable?.subTables || []).length === 0 ? (
@ -802,13 +812,13 @@ export function SaveSettingsModal({
</div> </div>
<div> <div>
<Label className="text-[10px]"> </Label> <Label className="text-[10px]"> ()</Label>
<Select <Select
value={subTable.repeatSectionId || ""} value={subTable.repeatSectionId || ""}
onValueChange={(value) => updateSubTable(subIndex, { repeatSectionId: value })} onValueChange={(value) => updateSubTable(subIndex, { repeatSectionId: value })}
> >
<SelectTrigger className="h-7 text-xs mt-1"> <SelectTrigger className="h-7 text-xs mt-1">
<SelectValue placeholder="섹션 선택" /> <SelectValue placeholder="섹션 선택 (없으면 필드 매핑만 사용)" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{repeatSections.length === 0 ? ( {repeatSections.length === 0 ? (
@ -824,7 +834,13 @@ export function SaveSettingsModal({
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
<HelpText> </HelpText> <HelpText>
섹션: / (: 주문 )
<br />
.
<br />
1 .
</HelpText>
</div> </div>
<Separator /> <Separator />

View File

@ -639,19 +639,6 @@ export interface TableCalculationRule {
conditionalCalculation?: ConditionalCalculationConfig; conditionalCalculation?: ConditionalCalculationConfig;
} }
// 다중 행 저장 설정
export interface MultiRowSaveConfig {
enabled?: boolean; // 사용 여부 (기본: false)
commonFields?: string[]; // 모든 행에 공통 저장할 필드 (columnName 기준)
repeatSectionId?: string; // 반복 섹션 ID
typeColumn?: string; // 구분 컬럼명 (예: "employment_type")
mainTypeValue?: string; // 메인 행 값 (예: "main")
subTypeValue?: string; // 서브 행 값 (예: "concurrent")
// 메인 섹션 필드 (반복 섹션이 아닌 곳의 부서/직급 등)
mainSectionFields?: string[]; // 메인 행에만 저장할 필드
}
/** /**
* *
* 저장: 해당 (: 수주번호, ) * 저장: 해당 (: 수주번호, )
@ -672,9 +659,6 @@ export interface SaveConfig {
tableName: string; tableName: string;
primaryKeyColumn?: string; // PK 컬럼 (수정 시 사용) primaryKeyColumn?: string; // PK 컬럼 (수정 시 사용)
// 다중 행 저장 설정
multiRowSave?: MultiRowSaveConfig;
// 커스텀 API 저장 설정 (테이블 직접 저장 대신 전용 API 사용) // 커스텀 API 저장 설정 (테이블 직접 저장 대신 전용 API 사용)
customApiSave?: CustomApiSaveConfig; customApiSave?: CustomApiSaveConfig;