리피터 데이터 저장 로직 개선 및 이벤트 처리 추가

- EditModal, InteractiveScreenViewer, SaveModal 컴포넌트에서 리피터 데이터(배열)를 마스터 저장에서 제외하고, 별도로 저장하는 로직을 추가하였습니다.
- 리피터 데이터 저장 이벤트를 발생시켜 UnifiedRepeater 컴포넌트가 이를 리스닝하도록 개선하였습니다.
- 각 컴포넌트에서 최종 저장 데이터 로그를 업데이트하여, 저장 과정에서의 데이터 흐름을 명확히 하였습니다.

이로 인해 데이터 저장의 효율성과 리피터 관리의 일관성이 향상되었습니다.
This commit is contained in:
kjs 2026-01-22 14:23:38 +09:00
parent d429e237ee
commit 1d068e0a20
15 changed files with 441 additions and 957 deletions

View File

@ -811,15 +811,40 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
}
console.log("[EditModal] 최종 저장 데이터:", dataToSave);
// 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (UnifiedRepeater가 별도로 저장)
const masterDataToSave: Record<string, any> = {};
Object.entries(dataToSave).forEach(([key, value]) => {
if (!Array.isArray(value)) {
masterDataToSave[key] = value;
} else {
console.log(`🔄 [EditModal] 리피터 데이터 제외 (별도 저장): ${key}, ${value.length}개 항목`);
}
});
console.log("[EditModal] 최종 저장 데이터:", masterDataToSave);
const response = await dynamicFormApi.saveFormData({
screenId: modalState.screenId!,
tableName: screenData.screenInfo.tableName,
data: dataToSave,
data: masterDataToSave,
});
if (response.success) {
const masterRecordId = response.data?.id || formData.id;
// 🆕 리피터 데이터 저장 이벤트 발생 (UnifiedRepeater 컴포넌트가 리스닝)
window.dispatchEvent(
new CustomEvent("repeaterSave", {
detail: {
parentId: masterRecordId,
masterRecordId,
mainFormData: formData,
tableName: screenData.screenInfo.tableName,
},
}),
);
console.log("📋 [EditModal] repeaterSave 이벤트 발생:", { masterRecordId, tableName: screenData.screenInfo.tableName });
toast.success("데이터가 생성되었습니다.");
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)

View File

@ -1655,10 +1655,20 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
company_code: mappedData.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode
};
// 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (UnifiedRepeater가 별도로 저장)
const masterDataWithUserInfo: Record<string, any> = {};
Object.entries(dataWithUserInfo).forEach(([key, value]) => {
if (!Array.isArray(value)) {
masterDataWithUserInfo[key] = value;
} else {
console.log(`🔄 리피터 데이터 제외 (별도 저장): ${key}, ${value.length}개 항목`);
}
});
const saveData: DynamicFormData = {
screenId: screenInfo.id,
tableName: tableName,
data: dataWithUserInfo,
data: masterDataWithUserInfo,
};
console.log("🚀 API 저장 요청:", saveData);
@ -1666,6 +1676,21 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
const result = await dynamicFormApi.saveFormData(saveData);
if (result.success) {
const masterRecordId = result.data?.id || formData.id;
// 🆕 리피터 데이터 저장 이벤트 발생 (UnifiedRepeater 컴포넌트가 리스닝)
window.dispatchEvent(
new CustomEvent("repeaterSave", {
detail: {
parentId: masterRecordId,
masterRecordId,
mainFormData: formData,
tableName: tableName,
},
}),
);
console.log("📋 repeaterSave 이벤트 발생:", { masterRecordId, tableName });
alert("저장되었습니다.");
// console.log("✅ 저장 성공:", result.data);

View File

@ -532,9 +532,20 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
}
try {
// 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (UnifiedRepeater가 별도로 저장)
const masterFormData: Record<string, any> = {};
Object.entries(formData).forEach(([key, value]) => {
// 배열 데이터는 리피터 데이터이므로 제외
if (!Array.isArray(value)) {
masterFormData[key] = value;
} else {
console.log(`🔄 리피터 데이터 제외 (별도 저장): ${key}, ${value.length}개 항목`);
}
});
const saveData: DynamicFormData = {
tableName: screenInfo.tableName,
data: formData,
data: masterFormData,
};
// console.log("💾 저장 액션 실행:", saveData);

View File

@ -206,13 +206,23 @@ export const SaveModal: React.FC<SaveModalProps> = ({
company_code: dataToSave.company_code || companyCodeValue, // ✅ 입력값 우선, 없으면 user.companyCode
};
// 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (UnifiedRepeater가 별도로 저장)
const masterDataWithUserInfo: Record<string, any> = {};
Object.entries(dataWithUserInfo).forEach(([key, value]) => {
if (!Array.isArray(value)) {
masterDataWithUserInfo[key] = value;
} else {
console.log(`🔄 [SaveModal] 리피터 데이터 제외 (별도 저장): ${key}, ${value.length}개 항목`);
}
});
// 테이블명 결정
const tableName = screenData.tableName || components.find((c) => c.columnName)?.tableName || "dynamic_form_data";
const saveData: DynamicFormData = {
screenId: screenId,
tableName: tableName,
data: dataWithUserInfo,
data: masterDataWithUserInfo,
};
console.log("💾 저장 요청 데이터:", saveData);
@ -221,6 +231,21 @@ export const SaveModal: React.FC<SaveModalProps> = ({
const result = await dynamicFormApi.saveFormData(saveData);
if (result.success) {
const masterRecordId = result.data?.id || dataToSave.id;
// 🆕 리피터 데이터 저장 이벤트 발생 (UnifiedRepeater 컴포넌트가 리스닝)
window.dispatchEvent(
new CustomEvent("repeaterSave", {
detail: {
parentId: masterRecordId,
masterRecordId,
mainFormData: dataToSave,
tableName: tableName,
},
}),
);
console.log("📋 [SaveModal] repeaterSave 이벤트 발생:", { masterRecordId, tableName });
// ✅ 저장 성공
toast.success(initialData ? "수정되었습니다!" : "저장되었습니다!");

View File

@ -85,21 +85,25 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
const isModalMode = config.renderMode === "modal";
// 전역 리피터 등록
// 🆕 useCustomTable이 설정된 경우 mainTableName 사용 (실제 저장될 테이블)
useEffect(() => {
const tableName = config.dataSource?.tableName;
if (tableName) {
const targetTableName = config.useCustomTable && config.mainTableName
? config.mainTableName
: config.dataSource?.tableName;
if (targetTableName) {
if (!window.__unifiedRepeaterInstances) {
window.__unifiedRepeaterInstances = new Set();
}
window.__unifiedRepeaterInstances.add(tableName);
window.__unifiedRepeaterInstances.add(targetTableName);
}
return () => {
if (tableName && window.__unifiedRepeaterInstances) {
window.__unifiedRepeaterInstances.delete(tableName);
if (targetTableName && window.__unifiedRepeaterInstances) {
window.__unifiedRepeaterInstances.delete(targetTableName);
}
};
}, [config.dataSource?.tableName]);
}, [config.useCustomTable, config.mainTableName, config.dataSource?.tableName]);
// 저장 이벤트 리스너
useEffect(() => {
@ -115,11 +119,11 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
const masterRecordId = event.detail?.masterRecordId || mainFormData?.id;
if (!tableName || data.length === 0) {
console.log("📋 UnifiedRepeater 저장 스킵:", { tableName, dataLength: data.length });
return;
}
console.log("📋 UnifiedRepeater 저장 시작:", {
// UnifiedRepeater 저장 시작
const saveInfo = {
tableName,
useCustomTable: config.useCustomTable,
mainTableName: config.mainTableName,
@ -152,10 +156,24 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
// 커스텀 테이블: 리피터 데이터만 저장
mergedData = { ...cleanRow };
// 🆕 FK 자동 연결
if (config.foreignKeyColumn && masterRecordId) {
mergedData[config.foreignKeyColumn] = masterRecordId;
console.log(`📎 FK 자동 연결: ${config.foreignKeyColumn} = ${masterRecordId}`);
// 🆕 FK 자동 연결 - foreignKeySourceColumn이 설정된 경우 해당 컬럼 값 사용
if (config.foreignKeyColumn) {
// foreignKeySourceColumn이 있으면 mainFormData에서 해당 컬럼 값 사용
// 없으면 마스터 레코드 ID 사용 (기존 동작)
const sourceColumn = config.foreignKeySourceColumn;
let fkValue: any;
if (sourceColumn && mainFormData && mainFormData[sourceColumn] !== undefined) {
// mainFormData에서 참조 컬럼 값 가져오기
fkValue = mainFormData[sourceColumn];
} else {
// 기본: 마스터 레코드 ID 사용
fkValue = masterRecordId;
}
if (fkValue !== undefined && fkValue !== null) {
mergedData[config.foreignKeyColumn] = fkValue;
}
}
} else {
// 기존 방식: 메인 폼 데이터 병합
@ -177,7 +195,6 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData);
}
console.log("✅ UnifiedRepeater 저장 완료:", data.length, "건 →", tableName);
} catch (error) {
console.error("❌ UnifiedRepeater 저장 실패:", error);
throw error;
@ -252,13 +269,6 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
config.dataSource?.referenceKey ||
"id";
console.log("🔄 [UnifiedRepeater] 엔티티 참조 정보 조회:", {
foreignKey,
resolvedSourceTable: refTable,
resolvedReferenceKey: refKey,
configSourceTable: config.dataSource?.sourceTable,
});
setResolvedSourceTable(refTable);
setResolvedReferenceKey(refKey);
} else {
@ -424,11 +434,29 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
const handleDataChange = useCallback(
(newData: any[]) => {
setData(newData);
onDataChange?.(newData);
// 🆕 _targetTable 메타데이터 포함하여 전달 (백엔드에서 테이블 분리용)
if (onDataChange) {
const targetTable = config.useCustomTable && config.mainTableName
? config.mainTableName
: config.dataSource?.tableName;
if (targetTable) {
// 각 행에 _targetTable 추가
const dataWithTarget = newData.map(row => ({
...row,
_targetTable: targetTable,
}));
onDataChange(dataWithTarget);
} else {
onDataChange(newData);
}
}
// 🆕 데이터 변경 시 자동으로 컬럼 너비 조정
setAutoWidthTrigger((prev) => prev + 1);
},
[onDataChange],
[onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName],
);
// 행 변경 핸들러
@ -436,11 +464,26 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
(index: number, newRow: any) => {
const newData = [...data];
newData[index] = newRow;
// 🆕 handleDataChange 대신 직접 호출 (행 변경마다 너비 조정은 불필요)
setData(newData);
onDataChange?.(newData);
// 🆕 _targetTable 메타데이터 포함
if (onDataChange) {
const targetTable = config.useCustomTable && config.mainTableName
? config.mainTableName
: config.dataSource?.tableName;
if (targetTable) {
const dataWithTarget = newData.map(row => ({
...row,
_targetTable: targetTable,
}));
onDataChange(dataWithTarget);
} else {
onDataChange(newData);
}
}
},
[data, onDataChange],
[data, onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName],
);
// 행 삭제 핸들러
@ -672,13 +715,6 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
return;
}
console.log("📥 [UnifiedRepeater] componentDataTransfer 수신:", {
targetComponentId,
dataCount: transferData?.length,
mode,
myId: parentId,
});
if (!transferData || transferData.length === 0) {
return;
}
@ -719,12 +755,6 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
const customEvent = event as CustomEvent;
const { data: transferData, mappingRules, mode, sourcePosition } = customEvent.detail || {};
console.log("📥 [UnifiedRepeater] splitPanelDataTransfer 수신:", {
dataCount: transferData?.length,
mode,
sourcePosition,
});
if (!transferData || transferData.length === 0) {
return;
}

View File

@ -192,7 +192,6 @@ export function registerUnifiedComponents(): void {
continue;
}
ComponentRegistry.registerComponent(definition);
console.log(`✅ Unified 컴포넌트 등록: ${definition.id}`);
} catch (error) {
console.error(`❌ Unified 컴포넌트 등록 실패: ${definition.id}`, error);
}

View File

@ -66,7 +66,6 @@ export const useMultiLang = (options: { companyCode?: string } = {}) => {
const storedLocaleLoaded = localStorage.getItem("userLocaleLoaded");
if (storedLocaleLoaded === "true" && storedLocale) {
console.log("🌐 localStorage에서 사용자 로케일 사용:", storedLocale);
setUserLang(storedLocale);
globalUserLang = storedLocale;

View File

@ -441,10 +441,20 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
);
case "unified-repeater":
// 🆕 저장 설정 추출 (useCustomTable, mainTableName, foreignKeyColumn)
const repeaterTargetTable = config.useCustomTable && config.mainTableName
? config.mainTableName
: config.dataSource?.tableName;
return (
<UnifiedRepeater
config={{
renderMode: config.renderMode || "inline",
// 🆕 저장 설정 추가
useCustomTable: config.useCustomTable,
mainTableName: config.mainTableName,
foreignKeyColumn: config.foreignKeyColumn,
foreignKeySourceColumn: config.foreignKeySourceColumn, // 🆕 FK 소스 컬럼 추가
dataSource: {
tableName: config.dataSource?.tableName || props.tableName || "",
foreignKey: config.dataSource?.foreignKey || "",
@ -467,13 +477,19 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
}}
parentId={props.formData?.[config.dataSource?.referenceKey] || props.formData?.id}
onDataChange={(data) => {
console.log("UnifiedRepeater data changed:", data);
// 🆕 formData 업데이트 (부모로 데이터 전달)
if (props.onFormDataChange) {
// _targetTable 메타데이터 추가
const dataWithTargetTable = data.map((item: any) => ({
...item,
_targetTable: repeaterTargetTable,
}));
props.onFormDataChange(component.id || "repeaterData", dataWithTargetTable);
}
}}
onRowClick={(row) => {
console.log("UnifiedRepeater row clicked:", row);
}}
onButtonClick={(action, row, buttonConfig) => {
console.log("UnifiedRepeater button clicked:", action, row, buttonConfig);
}}
/>
);
@ -793,17 +809,6 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
NewComponentRenderer.prototype &&
NewComponentRenderer.prototype.render;
if (componentType === "table-search-widget") {
console.log("🔍 [DynamicComponentRenderer] table-search-widget 렌더링 분기:", {
isClass,
hasPrototype: !!NewComponentRenderer.prototype,
hasRender: !!NewComponentRenderer.prototype?.render,
componentName: NewComponentRenderer.name,
componentProp: rendererProps.component,
screenId: rendererProps.screenId,
});
}
if (isClass) {
// 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속)
const rendererInstance = new NewComponentRenderer(rendererProps);
@ -812,29 +817,6 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// 함수형 컴포넌트
// refreshKey를 React key로 전달하여 컴포넌트 리마운트 강제
// 🔧 디버깅: table-search-widget인 경우 직접 호출 후 반환
if (componentType === "table-search-widget") {
console.log("🔧🔧🔧 [DynamicComponentRenderer] TableSearchWidget 직접 호출 반환");
console.log("🔧 [DynamicComponentRenderer] NewComponentRenderer 함수 확인:", {
name: NewComponentRenderer.name,
toString: NewComponentRenderer.toString().substring(0, 200),
});
try {
const result = NewComponentRenderer(rendererProps);
console.log("🔧 [DynamicComponentRenderer] TableSearchWidget 결과 상세:", {
resultType: typeof result,
type: result?.type?.name || result?.type || "unknown",
propsKeys: result?.props ? Object.keys(result.props) : [],
propsStyle: result?.props?.style,
propsChildren: typeof result?.props?.children,
});
// 직접 호출 결과를 반환
return result;
} catch (directCallError) {
console.error("❌ [DynamicComponentRenderer] TableSearchWidget 직접 호출 실패:", directCallError);
}
}
return <NewComponentRenderer key={refreshKey} {...rendererProps} />;
}
}

View File

@ -102,26 +102,18 @@ export function RepeaterTable({
const loadCategoryOptions = async () => {
// category 타입이면서 categoryRef가 있는 컬럼들 찾기
const categoryColumns = visibleColumns.filter((col) => col.type === "category");
console.log(
"🔍 [RepeaterTable] 카테고리 컬럼 확인:",
categoryColumns.map((col) => ({ field: col.field, type: col.type, categoryRef: col.categoryRef })),
);
const categoryRefs = categoryColumns
.filter((col) => col.categoryRef)
.map((col) => col.categoryRef!)
.filter((ref, index, self) => self.indexOf(ref) === index); // 중복 제거
console.log("🔍 [RepeaterTable] categoryRefs:", categoryRefs);
if (categoryRefs.length === 0) {
console.log("⚠️ [RepeaterTable] categoryRef가 있는 컬럼이 없음");
return;
}
for (const categoryRef of categoryRefs) {
if (categoryOptionsMap[categoryRef]) {
console.log(`⏭️ [RepeaterTable] ${categoryRef} 이미 로드됨`);
continue;
}
@ -141,16 +133,13 @@ export function RepeaterTable({
continue;
}
console.log(`🌐 [RepeaterTable] API 호출: /table-categories/${tableName}/${columnName}/values`);
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`);
console.log("📥 [RepeaterTable] API 응답:", response.data);
if (response.data?.success && response.data.data) {
const options = response.data.data.map((item: any) => ({
value: item.valueCode || item.value_code,
label: item.valueLabel || item.value_label || item.displayValue || item.display_value || item.label,
}));
console.log(`✅ [RepeaterTable] ${categoryRef} 옵션 로드 성공:`, options);
setCategoryOptionsMap((prev) => ({
...prev,
[categoryRef]: options,

View File

@ -90,6 +90,10 @@ export function SimpleRepeaterTableComponent({
const newRowDefaults = componentConfig?.newRowDefaults || {};
const summaryConfig = componentConfig?.summaryConfig;
const maxHeight = componentConfig?.maxHeight || propMaxHeight || "240px";
// 🆕 컴포넌트 레벨의 저장 테이블 설정
const componentTargetTable = componentConfig?.targetTable || componentConfig?.saveTable;
const componentFkColumn = componentConfig?.fkColumn;
// value는 formData[columnName] 우선, 없으면 prop 사용
const columnName = component?.columnName;
@ -289,7 +293,44 @@ export function SimpleRepeaterTableComponent({
return;
}
// 🆕 테이블별로 데이터 그룹화
// 🆕 컴포넌트 레벨의 targetTable이 설정되어 있으면 우선 사용
if (componentTargetTable) {
console.log("✅ [SimpleRepeaterTable] 컴포넌트 레벨 저장 테이블 사용:", componentTargetTable);
// 모든 행을 해당 테이블에 저장
const dataToSave = value.map((row: any) => {
// 메타데이터 필드 제외 (_, _rowIndex 등)
const cleanRow: Record<string, any> = {};
Object.keys(row).forEach((key) => {
if (!key.startsWith("_")) {
cleanRow[key] = row[key];
}
});
return {
...cleanRow,
_targetTable: componentTargetTable,
};
});
// CustomEvent의 detail에 데이터 추가
if (event instanceof CustomEvent && event.detail) {
const key = columnName || component?.id || "repeater_data";
event.detail.formData[key] = dataToSave;
console.log("✅ [SimpleRepeaterTable] 저장 데이터 준비:", {
key,
targetTable: componentTargetTable,
itemCount: dataToSave.length,
});
}
// 기존 onFormDataChange도 호출 (호환성)
if (onFormDataChange && columnName) {
onFormDataChange(columnName, dataToSave);
}
return;
}
// 🆕 컬럼별 targetConfig가 있는 경우 기존 로직 사용
const dataByTable: Record<string, any[]> = {};
for (const row of value) {
@ -318,6 +359,16 @@ export function SimpleRepeaterTableComponent({
}
}
// 컬럼별 설정도 없으면 기본 동작 (formData에 직접 추가)
if (Object.keys(dataByTable).length === 0) {
console.log("⚠️ [SimpleRepeaterTable] targetTable 설정 없음 - 기본 저장");
if (event instanceof CustomEvent && event.detail) {
const key = columnName || component?.id || "repeater_data";
event.detail.formData[key] = value;
}
return;
}
// _rowIndex 제거
Object.keys(dataByTable).forEach((tableName) => {
dataByTable[tableName] = dataByTable[tableName].map((row: any) => {
@ -360,7 +411,7 @@ export function SimpleRepeaterTableComponent({
return () => {
window.removeEventListener("beforeFormSave", handleSaveRequest as EventListener);
};
}, [value, columns, columnName, component?.id, onFormDataChange]);
}, [value, columns, columnName, component?.id, onFormDataChange, componentTargetTable]);
const handleCellEdit = (rowIndex: number, field: string, cellValue: any) => {
const newRow = { ...value[rowIndex], [field]: cellValue };

View File

@ -260,9 +260,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 객체인 경우 tableName 속성 추출 시도
if (typeof finalSelectedTable === "object" && finalSelectedTable !== null) {
console.warn("⚠️ selectedTable이 객체입니다:", finalSelectedTable);
finalSelectedTable = (finalSelectedTable as any).tableName || (finalSelectedTable as any).name || tableName;
console.log("✅ 객체에서 추출한 테이블명:", finalSelectedTable);
}
tableConfig.selectedTable = finalSelectedTable;
@ -353,12 +351,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
});
console.log("🔍 [TableListComponent] filters → searchValues:", {
filtersCount: filters.length,
filters: filters.map((f) => ({ col: f.columnName, op: f.operator, val: f.value })),
searchValues: newSearchValues,
});
setSearchValues(newSearchValues);
setCurrentPage(1); // 필터 변경 시 첫 페이지로
}, [filters]);
@ -761,7 +753,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
});
if (hasChanges) {
console.log("🔗 [TableList] 연결된 필터 값 변경:", newFilterValues);
setLinkedFilterValues(newFilterValues);
// searchValues에 연결된 필터 값 병합
@ -817,13 +808,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
componentType: "table",
receiveData: async (receivedData: any[], config: DataReceiverConfig) => {
console.log("📥 TableList 데이터 수신:", {
componentId: component.id,
receivedDataCount: receivedData.length,
mode: config.mode,
currentDataCount: data.length,
});
try {
let newData: any[] = [];
@ -831,13 +815,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
case "append":
// 기존 데이터에 추가
newData = [...data, ...receivedData];
console.log("✅ Append 모드: 기존 데이터에 추가", { newDataCount: newData.length });
break;
case "replace":
// 기존 데이터를 완전히 교체
newData = receivedData;
console.log("✅ Replace 모드: 데이터 교체", { newDataCount: newData.length });
break;
case "merge":
@ -853,7 +835,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
});
newData = Array.from(existingMap.values());
console.log("✅ Merge 모드: 데이터 병합", { newDataCount: newData.length });
break;
}
@ -862,10 +843,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 총 아이템 수 업데이트
setTotalItems(newData.length);
console.log("✅ 데이터 수신 완료:", { finalDataCount: newData.length });
} catch (error) {
console.error("❌ 데이터 수신 실패:", error);
throw error;
}
},
@ -899,7 +877,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
componentId: component.id,
componentType: "table-list",
receiveData: async (incomingData: any[], mode: "append" | "replace" | "merge") => {
console.log("📥 [TableListComponent] 분할 패널에서 데이터 수신:", {
// 분할 패널에서 데이터 수신 처리
const receiveInfo = {
count: incomingData.length,
mode,
position: currentSplitPosition,
@ -937,24 +916,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 컬럼의 고유 값 조회 함수
const getColumnUniqueValues = async (columnName: string) => {
console.log("🔍 [getColumnUniqueValues] 호출됨:", {
columnName,
dataLength: data.length,
columnMeta: columnMeta[columnName],
sampleData: data[0],
});
const meta = columnMeta[columnName];
const inputType = meta?.inputType || "text";
// 카테고리 타입인 경우 전체 정의된 값 조회 (백엔드 API)
if (inputType === "category") {
try {
console.log("🔍 [getColumnUniqueValues] 카테고리 전체 값 조회:", {
tableName: tableConfig.selectedTable,
columnName,
});
// API 클라이언트 사용 (쿠키 인증 자동 처리)
const { apiClient } = await import("@/lib/api/client");
const response = await apiClient.get(`/table-categories/${tableConfig.selectedTable}/${columnName}/values`);
@ -965,24 +932,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
label: item.valueLabel, // 카멜케이스
}));
console.log("✅ [getColumnUniqueValues] 카테고리 전체 값:", {
columnName,
count: categoryOptions.length,
options: categoryOptions,
});
return categoryOptions;
} else {
console.warn("⚠️ [getColumnUniqueValues] 응답 형식 오류:", response.data);
}
} catch (error: any) {
console.error("❌ [getColumnUniqueValues] 카테고리 조회 실패:", {
error: error.message,
response: error.response?.data,
status: error.response?.status,
columnName,
tableName: tableConfig.selectedTable,
});
} catch {
// 에러 시 현재 데이터 기반으로 fallback
}
}
@ -991,15 +943,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const isLabelType = ["category", "entity", "code"].includes(inputType);
const labelField = isLabelType ? `${columnName}_name` : columnName;
console.log("🔍 [getColumnUniqueValues] 데이터 기반 조회:", {
columnName,
inputType,
isLabelType,
labelField,
hasLabelField: data[0] && labelField in data[0],
sampleLabelValue: data[0] ? data[0][labelField] : undefined,
});
// 현재 로드된 데이터에서 고유 값 추출
const uniqueValuesMap = new Map<string, string>(); // value -> label
@ -1020,15 +963,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}))
.sort((a, b) => a.label.localeCompare(b.label));
console.log("✅ [getColumnUniqueValues] 데이터 기반 결과:", {
columnName,
inputType,
isLabelType,
labelField,
uniqueCount: result.length,
values: result,
});
return result;
};
@ -1105,10 +1039,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
setSortColumn(column);
setSortDirection(direction);
hasInitializedSort.current = true;
console.log("📂 localStorage에서 정렬 상태 복원:", { column, direction });
}
} catch (error) {
console.error("❌ 정렬 상태 복원 실패:", error);
} catch {
// 정렬 상태 복원 실패 - 무시
}
}
}, [tableConfig.selectedTable, userId]);
@ -1124,12 +1057,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (savedOrder) {
try {
const parsedOrder = JSON.parse(savedOrder);
console.log("📂 localStorage에서 컬럼 순서 불러오기:", { storageKey, columnOrder: parsedOrder });
setColumnOrder(parsedOrder);
// 부모 컴포넌트에 초기 컬럼 순서 전달
if (onSelectedRowsChange && parsedOrder.length > 0) {
console.log("✅ 초기 컬럼 순서 전달:", parsedOrder);
// 초기 데이터도 함께 전달 (컬럼 순서대로 재정렬)
const initialData = data.map((row: any) => {
@ -1177,8 +1108,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onSelectedRowsChange([], [], sortColumn, sortDirection, parsedOrder, initialData);
}
} catch (error) {
console.error("❌ 컬럼 순서 파싱 실패:", error);
} catch {
// 컬럼 순서 파싱 실패 - 무시
}
}
}, [tableConfig.selectedTable, userId, data.length]); // data.length 추가 (데이터 로드 후 실행)
@ -1336,11 +1267,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const parts = columnName.split(".");
targetTable = parts[0]; // 조인된 테이블명 (예: item_info)
targetColumn = parts[1]; // 실제 컬럼명 (예: material)
console.log("🔗 [TableList] 엔티티 조인 컬럼 감지:", {
originalColumn: columnName,
targetTable,
targetColumn,
});
}
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`);
@ -1438,8 +1364,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 조인 테이블의 컬럼 inputType 정보 가져오기 (이미 import된 tableTypeApi 사용)
const inputTypes = await tableTypeApi.getColumnInputTypes(joinedTable);
console.log(`📡 [TableList] 조인 테이블 inputType 로드 [${joinedTable}]:`, inputTypes);
for (const col of columns) {
const inputTypeInfo = inputTypes.find((it: any) => it.columnName === col.actualColumn);
@ -1448,17 +1372,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
inputType: inputTypeInfo?.inputType,
};
console.log(
` 🔗 [${col.columnName}] (실제: ${col.actualColumn}) inputType: ${inputTypeInfo?.inputType || "unknown"}`,
);
// inputType이 category인 경우 카테고리 매핑 로드
if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
try {
console.log(`📡 [TableList] 조인 테이블 카테고리 로드 시도 [${col.columnName}]:`, {
url: `/table-categories/${joinedTable}/${col.actualColumn}/values`,
});
const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
@ -1476,20 +1392,19 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
mappings[col.columnName] = mapping;
}
}
} catch (error) {
console.log(` [TableList] 조인 테이블 카테고리 없음 (${col.columnName})`);
} catch {
// 조인 테이블 카테고리 없음 - 무시
}
}
}
} catch (error) {
console.error(`❌ [TableList] 조인 테이블 inputType 로드 실패 [${joinedTable}]:`, error);
console.error(`조인 테이블 inputType 로드 실패 [${joinedTable}]:`, error);
}
}
// 조인 컬럼 메타데이터 상태 업데이트
if (Object.keys(newJoinedColumnMeta).length > 0) {
setJoinedColumnMeta(newJoinedColumnMeta);
console.log("✅ [TableList] 조인 컬럼 메타데이터 설정:", newJoinedColumnMeta);
}
// 🆕 카테고리 연쇄관계 매핑 로드 (category_value_cascading_mapping)
@ -1515,26 +1430,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
};
}
}
console.log("✅ [TableList] 카테고리 연쇄관계 매핑 로드 완료:", {
tableName: tableConfig.selectedTable,
cascadingColumns: Object.keys(cascadingMappings),
});
}
} catch (cascadingError: any) {
// 연쇄관계 매핑이 없는 경우 무시 (404 등)
if (cascadingError?.response?.status !== 404) {
console.warn("⚠️ [TableList] 카테고리 연쇄관계 매핑 로드 실패:", cascadingError?.message);
}
}
if (Object.keys(mappings).length > 0) {
setCategoryMappings(mappings);
setCategoryMappingsKey((prev) => prev + 1);
} else {
console.warn("⚠️ [TableList] 매핑이 비어있어 상태 업데이트 스킵");
}
} catch (error) {
console.error("❌ [TableList] 카테고리 매핑 로드 실패:", error);
console.error("카테고리 매핑 로드 실패:", error);
}
};

View File

@ -32,9 +32,19 @@ const UnifiedRepeaterRenderer: React.FC<UnifiedRepeaterRendererProps> = ({
onButtonClick,
parentId,
}) => {
// component.config에서 UnifiedRepeaterConfig 추출
// component.componentConfig 또는 component.config에서 UnifiedRepeaterConfig 추출
const config: UnifiedRepeaterConfig = React.useMemo(() => {
const componentConfig = component?.config || component?.props?.config || {};
// 🆕 componentConfig 우선 (DB에서 properties.componentConfig로 저장됨)
const componentConfig = component?.componentConfig || component?.config || component?.props?.config || {};
console.log("📋 UnifiedRepeaterRenderer config 추출:", {
hasComponentConfig: !!component?.componentConfig,
hasConfig: !!component?.config,
useCustomTable: componentConfig.useCustomTable,
mainTableName: componentConfig.mainTableName,
foreignKeyColumn: componentConfig.foreignKeyColumn,
});
return {
...DEFAULT_REPEATER_CONFIG,
...componentConfig,

View File

@ -1313,11 +1313,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const parts = columnName.split(".");
targetTable = parts[0]; // 조인된 테이블명 (예: item_info)
targetColumn = parts[1]; // 실제 컬럼명 (예: material)
console.log("🔗 [TableList] 엔티티 조인 컬럼 감지:", {
originalColumn: columnName,
targetTable,
targetColumn,
});
}
const response = await apiClient.get(`/table-categories/${targetTable}/${targetColumn}/values`);
@ -1358,21 +1353,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
} else {
// 매핑 데이터가 비어있음 - 해당 컬럼에 카테고리 값이 없음
}
} else {
console.warn(`⚠️ [TableList] 카테고리 값 없음 [${columnName}]:`, {
success: response.data.success,
hasData: !!response.data.data,
isArray: Array.isArray(response.data.data),
response: response.data,
});
}
} catch (error: any) {
console.error(`❌ [TableList] 카테고리 값 로드 실패 [${columnName}]:`, {
error: error.message,
stack: error.stack,
response: error.response?.data,
status: error.response?.status,
});
} catch {
// 카테고리 값 로드 실패 - 무시
}
}
@ -1433,8 +1416,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 조인 테이블의 컬럼 inputType 정보 가져오기 (이미 import된 tableTypeApi 사용)
const inputTypes = await tableTypeApi.getColumnInputTypes(joinedTable);
console.log(`📡 [TableList] 조인 테이블 inputType 로드 [${joinedTable}]:`, inputTypes);
for (const col of columns) {
const inputTypeInfo = inputTypes.find((it: any) => it.columnName === col.actualColumn);
@ -1443,17 +1424,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
inputType: inputTypeInfo?.inputType,
};
console.log(
` 🔗 [${col.columnName}] (실제: ${col.actualColumn}) inputType: ${inputTypeInfo?.inputType || "unknown"}`,
);
// inputType이 category인 경우 카테고리 매핑 로드
if (inputTypeInfo?.inputType === "category" && !mappings[col.columnName]) {
try {
console.log(`📡 [TableList] 조인 테이블 카테고리 로드 시도 [${col.columnName}]:`, {
url: `/table-categories/${joinedTable}/${col.actualColumn}/values`,
});
const response = await apiClient.get(`/table-categories/${joinedTable}/${col.actualColumn}/values`);
if (response.data.success && response.data.data && Array.isArray(response.data.data)) {
@ -1481,20 +1454,19 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
mappings[col.columnName] = mapping;
}
}
} catch (error) {
console.log(` [TableList] 조인 테이블 카테고리 없음 (${col.columnName})`);
} catch {
// 조인 테이블 카테고리 없음 - 무시
}
}
}
} catch (error) {
console.error(`❌ [TableList] 조인 테이블 inputType 로드 실패 [${joinedTable}]:`, error);
} catch {
// 조인 테이블 inputType 로드 실패 - 무시
}
}
// 조인 컬럼 메타데이터 상태 업데이트
if (Object.keys(newJoinedColumnMeta).length > 0) {
setJoinedColumnMeta(newJoinedColumnMeta);
console.log("✅ [TableList] 조인 컬럼 메타데이터 설정:", newJoinedColumnMeta);
}
// 🆕 카테고리 연쇄관계 매핑 로드 (category_value_cascading_mapping)
@ -1522,21 +1494,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
// 카테고리 연쇄관계 매핑 로드 완료
}
} catch (cascadingError: any) {
// 연쇄관계 매핑이 없는 경우 무시 (404 등)
if (cascadingError?.response?.status !== 404) {
console.warn("⚠️ [TableList] 카테고리 연쇄관계 매핑 로드 실패:", cascadingError?.message);
}
} catch {
// 연쇄관계 매핑이 없는 경우 무시
}
if (Object.keys(mappings).length > 0) {
setCategoryMappings(mappings);
setCategoryMappingsKey((prev) => prev + 1);
} else {
console.warn("⚠️ [TableList] 매핑이 비어있어 상태 업데이트 스킵");
}
} catch (error) {
console.error("❌ [TableList] 카테고리 매핑 로드 실패:", error);
} catch {
// 카테고리 매핑 로드 실패 - 무시
}
};

View File

@ -32,9 +32,19 @@ const UnifiedRepeaterRenderer: React.FC<UnifiedRepeaterRendererProps> = ({
onButtonClick,
parentId,
}) => {
// component.config에서 UnifiedRepeaterConfig 추출
// component.componentConfig 또는 component.config에서 UnifiedRepeaterConfig 추출
const config: UnifiedRepeaterConfig = React.useMemo(() => {
const componentConfig = component?.config || component?.props?.config || {};
// 🆕 componentConfig 우선 (DB에서 properties.componentConfig로 저장됨)
const componentConfig = component?.componentConfig || component?.config || component?.props?.config || {};
console.log("📋 V2UnifiedRepeaterRenderer config 추출:", {
hasComponentConfig: !!component?.componentConfig,
hasConfig: !!component?.config,
useCustomTable: componentConfig.useCustomTable,
mainTableName: componentConfig.mainTableName,
foreignKeyColumn: componentConfig.foreignKeyColumn,
});
return {
...DEFAULT_REPEATER_CONFIG,
...componentConfig,

File diff suppressed because it is too large Load Diff