refactor: 코드 정리 및 가독성 향상

- numberingRuleController.ts에서 API 엔드포인트의 코드 스타일을 일관되게 정리하여 가독성을 높였습니다.
- 불필요한 줄바꿈을 제거하고, 코드 블록을 명확하게 정리하여 유지보수성을 개선했습니다.
- tableManagementService.ts와 ButtonConfigPanel.tsx에서 코드 정리를 통해 일관성을 유지하고, 가독성을 향상시켰습니다.
- 전반적으로 코드의 깔끔함을 유지하고, 향후 개발 시 이해하기 쉽게 개선했습니다.
This commit is contained in:
kjs
2026-02-05 17:38:06 +09:00
parent 73d05b991c
commit e31bb970a2
11 changed files with 1570 additions and 1348 deletions

View File

@@ -6,7 +6,7 @@
* 렌더링 모드:
* - inline: 현재 테이블 컬럼 직접 입력
* - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼
*
*
* RepeaterTable 및 ItemSelectionModal 재사용
*/
@@ -63,7 +63,7 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 🆕 데이터 변경 시 자동으로 컬럼 너비 조정 트리거
const [autoWidthTrigger, setAutoWidthTrigger] = useState(0);
// 소스 테이블 컬럼 라벨 매핑
const [sourceColumnLabels, setSourceColumnLabels] = useState<Record<string, string>>({});
@@ -72,10 +72,10 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 🆕 카테고리 코드 → 라벨 매핑 (RepeaterTable 표시용)
const [categoryLabelMap, setCategoryLabelMap] = useState<Record<string, string>>({});
// 현재 테이블 컬럼 정보 (inputType 매핑용)
const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState<Record<string, any>>({});
// 동적 데이터 소스 상태
const [activeDataSources, setActiveDataSources] = useState<Record<string, string>>({});
@@ -88,10 +88,9 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 전역 리피터 등록
// 🆕 useCustomTable이 설정된 경우 mainTableName 사용 (실제 저장될 테이블)
useEffect(() => {
const targetTableName = config.useCustomTable && config.mainTableName
? config.mainTableName
: config.dataSource?.tableName;
const targetTableName =
config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
if (targetTableName) {
if (!window.__v2RepeaterInstances) {
window.__v2RepeaterInstances = new Set();
@@ -110,22 +109,21 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
useEffect(() => {
const handleSaveEvent = async (event: CustomEvent) => {
// 🆕 mainTableName이 설정된 경우 우선 사용, 없으면 dataSource.tableName 사용
const tableName = config.useCustomTable && config.mainTableName
? config.mainTableName
: config.dataSource?.tableName;
const tableName =
config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
const eventParentId = event.detail?.parentId;
const mainFormData = event.detail?.mainFormData;
// 🆕 마스터 테이블에서 생성된 ID (FK 연결용)
const masterRecordId = event.detail?.masterRecordId || mainFormData?.id;
if (!tableName || data.length === 0) {
return;
}
// V2Repeater 저장 시작
const saveInfo = {
tableName,
const saveInfo = {
tableName,
useCustomTable: config.useCustomTable,
mainTableName: config.mainTableName,
foreignKeyColumn: config.foreignKeyColumn,
@@ -145,10 +143,10 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
} catch {
console.warn("테이블 컬럼 정보 조회 실패");
}
for (let i = 0; i < data.length; i++) {
const row = data[i];
// 내부 필드 제거
const cleanRow = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_")));
@@ -157,14 +155,14 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
if (config.useCustomTable && config.mainTableName) {
// 커스텀 테이블: 리피터 데이터만 저장
mergedData = { ...cleanRow };
// 🆕 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];
@@ -172,18 +170,18 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 기본: 마스터 레코드 ID 사용
fkValue = masterRecordId;
}
if (fkValue !== undefined && fkValue !== null) {
mergedData[config.foreignKeyColumn] = fkValue;
}
}
} else {
// 기존 방식: 메인 폼 데이터 병합
const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {};
const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {};
mergedData = {
...mainFormDataWithoutId,
...cleanRow,
};
...mainFormDataWithoutId,
...cleanRow,
};
}
// 유효하지 않은 컬럼 제거
@@ -193,10 +191,9 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
filteredData[key] = value;
}
}
await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData);
}
} catch (error) {
console.error("❌ V2Repeater 저장 실패:", error);
throw error;
@@ -207,14 +204,13 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
const unsubscribe = v2EventBus.subscribe(
V2_EVENTS.REPEATER_SAVE,
async (payload) => {
const tableName = config.useCustomTable && config.mainTableName
? config.mainTableName
: config.dataSource?.tableName;
const tableName =
config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
if (payload.tableName === tableName) {
await handleSaveEvent({ detail: payload } as CustomEvent);
}
},
{ componentId: `v2-repeater-${config.dataSource?.tableName}` }
{ componentId: `v2-repeater-${config.dataSource?.tableName}` },
);
// 레거시 이벤트도 계속 지원 (점진적 마이그레이션)
@@ -223,7 +219,14 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
unsubscribe();
window.removeEventListener("repeaterSave" as any, handleSaveEvent);
};
}, [data, config.dataSource?.tableName, config.useCustomTable, config.mainTableName, config.foreignKeyColumn, parentId]);
}, [
data,
config.dataSource?.tableName,
config.useCustomTable,
config.mainTableName,
config.foreignKeyColumn,
parentId,
]);
// 현재 테이블 컬럼 정보 로드
useEffect(() => {
@@ -234,7 +237,7 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
try {
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
const columns = response.data?.data?.columns || response.data?.columns || response.data || [];
const columnMap: Record<string, any> = {};
columns.forEach((col: any) => {
const name = col.columnName || col.column_name || col.name;
@@ -320,7 +323,7 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
try {
const response = await apiClient.get(`/table-management/tables/${resolvedSourceTable}/columns`);
const columns = response.data?.data?.columns || response.data?.columns || response.data || [];
const labels: Record<string, string> = {};
const categoryCols: string[] = [];
@@ -364,13 +367,13 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
calculated: true,
width: col.width === "auto" ? undefined : col.width,
};
}
}
// 일반 입력 컬럼
let type: "text" | "number" | "date" | "select" | "category" = "text";
if (inputType === "number" || inputType === "decimal") type = "number";
else if (inputType === "date" || inputType === "datetime") type = "date";
else if (inputType === "code") type = "select";
if (inputType === "number" || inputType === "decimal") type = "number";
else if (inputType === "date" || inputType === "datetime") type = "date";
else if (inputType === "code") type = "select";
else if (inputType === "category") type = "category"; // 🆕 카테고리 타입
// 🆕 카테고리 참조 ID 가져오기 (tableName.columnName 형식)
@@ -383,19 +386,19 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
categoryRef = `${tableName}.${col.key}`;
}
}
return {
field: col.key,
label: col.title || colInfo?.displayName || col.key,
type,
editable: col.editable !== false,
width: col.width === "auto" ? undefined : col.width,
required: false,
return {
field: col.key,
label: col.title || colInfo?.displayName || col.key,
type,
editable: col.editable !== false,
width: col.width === "auto" ? undefined : col.width,
required: false,
categoryRef, // 🆕 카테고리 참조 ID 전달
hidden: col.hidden, // 🆕 히든 처리
autoFill: col.autoFill, // 🆕 자동 입력 설정
};
});
};
});
}, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]);
// 🆕 데이터 변경 시 카테고리 라벨 로드 (RepeaterTable 표시용)
@@ -451,26 +454,25 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 데이터 변경 핸들러
const handleDataChange = useCallback(
(newData: any[]) => {
setData(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);
setData(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);
},
@@ -480,26 +482,25 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 행 변경 핸들러
const handleRowChange = useCallback(
(index: number, newRow: any) => {
const newData = [...data];
newData[index] = newRow;
setData(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);
const newData = [...data];
newData[index] = newRow;
setData(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, config.useCustomTable, config.mainTableName, config.dataSource?.tableName],
);
@@ -507,16 +508,16 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 행 삭제 핸들러
const handleRowDelete = useCallback(
(index: number) => {
const newData = data.filter((_, i) => i !== index);
const newData = data.filter((_, i) => i !== index);
handleDataChange(newData); // 🆕 handleDataChange 사용
// 선택 상태 업데이트
const newSelected = new Set<number>();
selectedRows.forEach((i) => {
if (i < index) newSelected.add(i);
else if (i > index) newSelected.add(i - 1);
});
setSelectedRows(newSelected);
// 선택 상태 업데이트
const newSelected = new Set<number>();
selectedRows.forEach((i) => {
if (i < index) newSelected.add(i);
else if (i > index) newSelected.add(i - 1);
});
setSelectedRows(newSelected);
},
[data, selectedRows, handleDataChange],
);
@@ -535,30 +536,30 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
if (!col.autoFill || col.autoFill.type === "none") return undefined;
const now = new Date();
switch (col.autoFill.type) {
case "currentDate":
return now.toISOString().split("T")[0]; // YYYY-MM-DD
case "currentDateTime":
return now.toISOString().slice(0, 19).replace("T", " "); // YYYY-MM-DD HH:mm:ss
case "sequence":
return rowIndex + 1; // 1부터 시작하는 순번
case "numbering":
// 채번은 별도 비동기 처리 필요
return null; // null 반환하여 비동기 처리 필요함을 표시
case "fromMainForm":
if (col.autoFill.sourceField && mainFormData) {
return mainFormData[col.autoFill.sourceField];
}
return "";
case "fixed":
return col.autoFill.fixedValue ?? "";
default:
return undefined;
}
@@ -568,19 +569,22 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 🆕 채번 API 호출 (비동기)
// 🆕 수동 입력 부분 지원을 위해 userInputCode 파라미터 추가
const generateNumberingCode = useCallback(async (ruleId: string, userInputCode?: string, formData?: Record<string, any>): Promise<string> => {
try {
const result = await allocateNumberingCode(ruleId, userInputCode, formData);
if (result.success && result.data?.generatedCode) {
return result.data.generatedCode;
const generateNumberingCode = useCallback(
async (ruleId: string, userInputCode?: string, formData?: Record<string, any>): Promise<string> => {
try {
const result = await allocateNumberingCode(ruleId, userInputCode, formData);
if (result.success && result.data?.generatedCode) {
return result.data.generatedCode;
}
console.error("채번 실패:", result.error);
return "";
} catch (error) {
console.error("채번 API 호출 실패:", error);
return "";
}
console.error("채번 실패:", result.error);
return "";
} catch (error) {
console.error("채번 API 호출 실패:", error);
return "";
}
}, []);
},
[],
);
// 🆕 행 추가 (inline 모드 또는 모달 열기) - 비동기로 변경
const handleAddRow = useCallback(async () => {
@@ -589,7 +593,7 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
} else {
const newRow: any = { _id: `new_${Date.now()}` };
const currentRowCount = data.length;
// 먼저 동기적 자동 입력 값 적용
for (const col of config.columns) {
const autoValue = generateAutoFillValueSync(col, currentRowCount);
@@ -599,10 +603,10 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
} else if (autoValue !== undefined) {
newRow[col.key] = autoValue;
} else {
newRow[col.key] = "";
newRow[col.key] = "";
}
}
const newData = [...data, newRow];
handleDataChange(newData);
}
@@ -611,23 +615,23 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 모달에서 항목 선택 - 비동기로 변경
const handleSelectItems = useCallback(
async (items: Record<string, unknown>[]) => {
const fkColumn = config.dataSource?.foreignKey;
const fkColumn = config.dataSource?.foreignKey;
const currentRowCount = data.length;
// 채번이 필요한 컬럼 찾기
const numberingColumns = config.columns.filter(
(col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId
(col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId,
);
const newRows = await Promise.all(
items.map(async (item, index) => {
const row: any = { _id: `new_${Date.now()}_${Math.random()}` };
const row: any = { _id: `new_${Date.now()}_${Math.random()}` };
// FK 값 저장 (resolvedReferenceKey 사용)
if (fkColumn && item[resolvedReferenceKey]) {
row[fkColumn] = item[resolvedReferenceKey];
}
}
// 모든 컬럼 처리 (순서대로)
for (const col of config.columns) {
if (col.isSourceDisplay) {
@@ -643,20 +647,28 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
row[col.key] = autoValue;
} else if (row[col.key] === undefined) {
// 입력 컬럼: 빈 값으로 초기화
row[col.key] = "";
}
row[col.key] = "";
}
}
}
return row;
})
return row;
}),
);
const newData = [...data, ...newRows];
const newData = [...data, ...newRows];
handleDataChange(newData);
setModalOpen(false);
setModalOpen(false);
},
[config.dataSource?.foreignKey, resolvedReferenceKey, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode],
[
config.dataSource?.foreignKey,
resolvedReferenceKey,
config.columns,
data,
handleDataChange,
generateAutoFillValueSync,
generateNumberingCode,
],
);
// 소스 컬럼 목록 (모달용) - 🆕 columns 배열에서 isSourceDisplay인 것만 필터링
@@ -670,19 +682,19 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 🆕 beforeFormSave 이벤트에서 채번 placeholder를 실제 값으로 변환
const dataRef = useRef(data);
dataRef.current = data;
useEffect(() => {
const handleBeforeFormSave = async (event: Event) => {
const customEvent = event as CustomEvent;
const formData = customEvent.detail?.formData;
if (!formData || !dataRef.current.length) return;
// 채번 placeholder가 있는 행들을 찾아서 실제 값으로 변환
const processedData = await Promise.all(
dataRef.current.map(async (row) => {
const newRow = { ...row };
for (const key of Object.keys(newRow)) {
const value = newRow[key];
if (typeof value === "string" && value.startsWith("__NUMBERING_RULE__")) {
@@ -706,16 +718,16 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
}
}
}
return newRow;
}),
);
// 처리된 데이터를 formData에 추가
const fieldName = config.fieldName || "repeaterData";
formData[fieldName] = processedData;
};
// V2 EventBus 구독
const unsubscribe = v2EventBus.subscribe(
V2_EVENTS.FORM_SAVE_COLLECT,
@@ -726,12 +738,12 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
} as CustomEvent;
await handleBeforeFormSave(fakeEvent);
},
{ componentId: `v2-repeater-${config.dataSource?.tableName}` }
{ componentId: `v2-repeater-${config.dataSource?.tableName}` },
);
// 레거시 이벤트도 계속 지원 (점진적 마이그레이션)
window.addEventListener("beforeFormSave", handleBeforeFormSave);
return () => {
unsubscribe();
window.removeEventListener("beforeFormSave", handleBeforeFormSave);
@@ -744,20 +756,20 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
const handleComponentDataTransfer = async (event: Event) => {
const customEvent = event as CustomEvent;
const { targetComponentId, data: transferData, mappingRules, mode } = customEvent.detail || {};
// 이 컴포넌트가 대상인지 확인
if (targetComponentId !== parentId && targetComponentId !== config.fieldName) {
return;
}
if (!transferData || transferData.length === 0) {
return;
}
// 데이터 매핑 처리
const mappedData = transferData.map((item: any, index: number) => {
const newRow: any = { _id: `transfer_${Date.now()}_${index}` };
if (mappingRules && mappingRules.length > 0) {
// 매핑 규칙이 있으면 적용
mappingRules.forEach((rule: any) => {
@@ -767,10 +779,10 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
// 매핑 규칙 없으면 그대로 복사
Object.assign(newRow, item);
}
return newRow;
});
// mode에 따라 데이터 처리
if (mode === "replace") {
handleDataChange(mappedData);
@@ -784,20 +796,20 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
handleDataChange([...data, ...mappedData]);
}
};
// splitPanelDataTransfer: 분할 패널에서 전역 이벤트로 전달
const handleSplitPanelDataTransfer = async (event: Event) => {
const customEvent = event as CustomEvent;
const { data: transferData, mappingRules, mode, sourcePosition } = customEvent.detail || {};
if (!transferData || transferData.length === 0) {
return;
}
// 데이터 매핑 처리
const mappedData = transferData.map((item: any, index: number) => {
const newRow: any = { _id: `transfer_${Date.now()}_${index}` };
if (mappingRules && mappingRules.length > 0) {
mappingRules.forEach((rule: any) => {
newRow[rule.targetField] = item[rule.sourceField];
@@ -805,10 +817,10 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
} else {
Object.assign(newRow, item);
}
return newRow;
});
// mode에 따라 데이터 처리
if (mode === "replace") {
handleDataChange(mappedData);
@@ -816,7 +828,7 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
handleDataChange([...data, ...mappedData]);
}
};
// V2 EventBus 구독
const unsubscribeComponent = v2EventBus.subscribe(
V2_EVENTS.COMPONENT_DATA_TRANSFER,
@@ -831,7 +843,7 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
} as CustomEvent;
handleComponentDataTransfer(fakeEvent);
},
{ componentId: `v2-repeater-${config.dataSource?.tableName}` }
{ componentId: `v2-repeater-${config.dataSource?.tableName}` },
);
const unsubscribeSplitPanel = v2EventBus.subscribe(
@@ -846,13 +858,13 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
} as CustomEvent;
handleSplitPanelDataTransfer(fakeEvent);
},
{ componentId: `v2-repeater-${config.dataSource?.tableName}` }
{ componentId: `v2-repeater-${config.dataSource?.tableName}` },
);
// 레거시 이벤트도 계속 지원 (점진적 마이그레이션)
window.addEventListener("componentDataTransfer", handleComponentDataTransfer as EventListener);
window.addEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener);
return () => {
unsubscribeComponent();
unsubscribeSplitPanel();
@@ -928,11 +940,7 @@ V2Repeater.displayName = "V2Repeater";
// V2ErrorBoundary로 래핑된 안전한 버전 export
export const SafeV2Repeater: React.FC<V2RepeaterProps> = (props) => {
return (
<V2ErrorBoundary
componentId={props.parentId || "v2-repeater"}
componentType="V2Repeater"
fallbackStyle="compact"
>
<V2ErrorBoundary componentId={props.parentId || "v2-repeater"} componentType="V2Repeater" fallbackStyle="compact">
<V2Repeater {...props} />
</V2ErrorBoundary>
);