Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs 2026-01-06 13:45:48 +09:00
commit efc9175fec
1 changed files with 290 additions and 186 deletions

View File

@ -197,6 +197,10 @@ export function UniversalFormModalComponent({
// 로딩 상태
const [saving, setSaving] = useState(false);
// 채번규칙 원본 값 추적 (수동 모드 감지용)
// key: columnName, value: 자동 생성된 원본 값
const [numberingOriginalValues, setNumberingOriginalValues] = useState<Record<string, string>>({});
// 🆕 수정 모드: 원본 그룹 데이터 (INSERT/UPDATE/DELETE 추적용)
const [originalGroupedData, setOriginalGroupedData] = useState<any[]>([]);
const groupedDataInitializedRef = useRef(false);
@ -221,15 +225,14 @@ export function UniversalFormModalComponent({
// hasInitialized: hasInitialized.current,
// lastInitializedId: lastInitializedId.current,
// });
// initialData에서 ID 값 추출 (id, ID, objid 등)
const currentId = initialData?.id || initialData?.ID || initialData?.objid;
const currentIdString = currentId !== undefined ? String(currentId) : undefined;
// 생성 모드에서 부모로부터 전달받은 데이터 해시 (ID가 없을 때만)
const createModeDataHash = !currentIdString && initialData && Object.keys(initialData).length > 0
? JSON.stringify(initialData)
: undefined;
const createModeDataHash =
!currentIdString && initialData && Object.keys(initialData).length > 0 ? JSON.stringify(initialData) : undefined;
// 이미 초기화되었고, ID가 동일하고, 생성 모드 데이터도 동일하면 스킵
if (hasInitialized.current && lastInitializedId.current === currentIdString) {
@ -241,7 +244,7 @@ export function UniversalFormModalComponent({
return;
}
}
// 🆕 컴포넌트 remount 감지: hasInitialized가 true인데 formData가 비어있으면 재초기화
// (React의 Strict Mode나 EmbeddedScreen 리렌더링으로 인한 remount)
if (hasInitialized.current && !currentIdString) {
@ -435,7 +438,7 @@ export function UniversalFormModalComponent({
// console.log("[채번] 섹션 검사:", section.title, { type: section.type, repeatable: section.repeatable, fieldsCount: section.fields?.length });
if (section.repeatable || section.type === "table") continue;
for (const field of (section.fields || [])) {
for (const field of section.fields || []) {
// generateOnOpen은 기본값 true (undefined일 경우 true로 처리)
const shouldGenerateOnOpen = field.numberingRule?.generateOnOpen !== false;
// console.log("[채번] 필드 검사:", field.columnName, {
@ -457,12 +460,19 @@ export function UniversalFormModalComponent({
// generateOnOpen: 미리보기만 표시 (DB 시퀀스 증가 안 함)
const response = await previewNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
updatedData[field.columnName] = response.data.generatedCode;
const generatedCode = response.data.generatedCode;
updatedData[field.columnName] = generatedCode;
// 저장 시 실제 할당을 위해 ruleId 저장 (TextInput과 동일한 키 형식)
const ruleIdKey = `${field.columnName}_numberingRuleId`;
updatedData[ruleIdKey] = field.numberingRule.ruleId;
// 원본 채번 값 저장 (수동 모드 감지용)
setNumberingOriginalValues((prev) => ({
...prev,
[field.columnName]: generatedCode,
}));
hasChanges = true;
numberingGeneratedRef.current = true; // 생성 완료 표시
// console.log(
@ -534,7 +544,7 @@ export function UniversalFormModalComponent({
continue;
} else {
// 일반 섹션 필드 초기화
for (const field of (section.fields || [])) {
for (const field of section.fields || []) {
// 기본값 설정
let value = field.defaultValue ?? "";
@ -556,14 +566,16 @@ export function UniversalFormModalComponent({
if (section.optionalFieldGroups) {
for (const group of section.optionalFieldGroups) {
const key = `${section.id}-${group.id}`;
// 수정 모드: triggerField 값이 triggerValueOnAdd와 일치하면 그룹 자동 활성화
if (effectiveInitialData && group.triggerField && group.triggerValueOnAdd !== undefined) {
const triggerValue = effectiveInitialData[group.triggerField];
if (triggerValue === group.triggerValueOnAdd) {
newActivatedGroups.add(key);
console.log(`[initializeForm] 옵셔널 그룹 자동 활성화: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`);
console.log(
`[initializeForm] 옵셔널 그룹 자동 활성화: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`,
);
// 활성화된 그룹의 필드값도 초기화
for (const field of group.fields || []) {
let value = field.defaultValue ?? "";
@ -575,7 +587,7 @@ export function UniversalFormModalComponent({
}
}
}
// 신규 등록 모드: triggerValueOnRemove를 기본값으로 설정
if (group.triggerField && group.triggerValueOnRemove !== undefined) {
// effectiveInitialData에 해당 값이 없는 경우에만 기본값 설정
@ -595,7 +607,7 @@ export function UniversalFormModalComponent({
sectionsCount: config.sections.length,
effectiveInitialDataKeys: Object.keys(effectiveInitialData),
});
for (const section of config.sections) {
if (section.type !== "table" || !section.tableConfig) {
continue;
@ -634,67 +646,71 @@ export function UniversalFormModalComponent({
// 마스터 테이블명 확인 (saveConfig에서)
// 1. customApiSave.multiTable.mainTable.tableName (다중 테이블 저장)
// 2. saveConfig.tableName (단일 테이블 저장)
const masterTable = config.saveConfig?.customApiSave?.multiTable?.mainTable?.tableName
|| config.saveConfig?.tableName;
const masterTable =
config.saveConfig?.customApiSave?.multiTable?.mainTable?.tableName || config.saveConfig?.tableName;
// 디테일 테이블의 컬럼 목록 조회
const columnsResponse = await apiClient.get(`/table-management/tables/${detailTable}/columns`);
if (columnsResponse.data?.success && columnsResponse.data?.data) {
// API 응답 구조: { success, data: { columns: [...], total, page, ... } }
const columnsArray = columnsResponse.data.data.columns || columnsResponse.data.data || [];
const detailColumnsData = Array.isArray(columnsArray) ? columnsArray : [];
const detailColumns = detailColumnsData.map((col: any) => col.column_name || col.columnName);
const masterKeys = Object.keys(effectiveInitialData);
console.log(`[initializeForm] 테이블 섹션 ${section.id}: 연결 필드 자동 감지`, {
masterTable,
detailTable,
detailColumnsCount: detailColumnsData.length,
});
// 방법 1: 엔티티 관계 기반 감지 (정확)
// 디테일 테이블에서 마스터 테이블을 참조하는 엔티티 컬럼 찾기
if (masterTable) {
for (const col of detailColumnsData) {
const colName = col.column_name || col.columnName;
const inputType = col.input_type || col.inputType;
// 엔티티 타입 컬럼 확인
if (inputType === "entity") {
// reference_table 또는 detail_settings에서 참조 테이블 확인
let refTable = col.reference_table || col.referenceTable;
// detail_settings에서 referenceTable 확인
if (!refTable && col.detail_settings) {
try {
const settings = typeof col.detail_settings === "string"
? JSON.parse(col.detail_settings)
: col.detail_settings;
const settings =
typeof col.detail_settings === "string"
? JSON.parse(col.detail_settings)
: col.detail_settings;
refTable = settings.referenceTable;
} catch {
// JSON 파싱 실패 무시
}
}
// 마스터 테이블을 참조하는 컬럼 발견
if (refTable === masterTable) {
// 참조 컬럼 확인 (마스터 테이블의 어떤 컬럼을 참조하는지)
let refColumn = col.reference_column || col.referenceColumn;
if (!refColumn && col.detail_settings) {
try {
const settings = typeof col.detail_settings === "string"
? JSON.parse(col.detail_settings)
: col.detail_settings;
const settings =
typeof col.detail_settings === "string"
? JSON.parse(col.detail_settings)
: col.detail_settings;
refColumn = settings.referenceColumn;
} catch {
// JSON 파싱 실패 무시
}
}
// 마스터 데이터에 해당 컬럼 값이 있는지 확인
if (refColumn && effectiveInitialData[refColumn]) {
console.log(`[initializeForm] 테이블 섹션 ${section.id}: 엔티티 관계 감지 - ${colName}${masterTable}.${refColumn}`);
console.log(
`[initializeForm] 테이블 섹션 ${section.id}: 엔티티 관계 감지 - ${colName}${masterTable}.${refColumn}`,
);
linkColumn = { masterField: refColumn, detailField: colName };
break;
}
@ -702,18 +718,21 @@ export function UniversalFormModalComponent({
}
}
}
// 방법 2: 공통 컬럼 패턴 기반 감지 (폴백)
// 엔티티 관계가 없으면 공통 컬럼명 패턴으로 찾기
if (!linkColumn) {
const priorityPatterns = ["_no", "_number", "_code", "_id"];
for (const pattern of priorityPatterns) {
for (const masterKey of masterKeys) {
if (masterKey.endsWith(pattern) &&
detailColumns.includes(masterKey) &&
effectiveInitialData[masterKey] &&
masterKey !== "id" && masterKey !== "company_code") {
if (
masterKey.endsWith(pattern) &&
detailColumns.includes(masterKey) &&
effectiveInitialData[masterKey] &&
masterKey !== "id" &&
masterKey !== "company_code"
) {
console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 패턴 감지 - ${masterKey}`);
linkColumn = { masterField: masterKey, detailField: masterKey };
break;
@ -722,14 +741,17 @@ export function UniversalFormModalComponent({
if (linkColumn) break;
}
}
// 방법 3: 일반 공통 컬럼 (마지막 폴백)
if (!linkColumn) {
for (const masterKey of masterKeys) {
if (detailColumns.includes(masterKey) &&
effectiveInitialData[masterKey] &&
masterKey !== "id" && masterKey !== "company_code" &&
!masterKey.startsWith("__")) {
if (
detailColumns.includes(masterKey) &&
effectiveInitialData[masterKey] &&
masterKey !== "id" &&
masterKey !== "company_code" &&
!masterKey.startsWith("__")
) {
console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 감지 - ${masterKey}`);
linkColumn = { masterField: masterKey, detailField: masterKey };
break;
@ -750,7 +772,9 @@ export function UniversalFormModalComponent({
// 마스터 테이블의 연결 필드 값 가져오기
const masterValue = effectiveInitialData[linkColumn.masterField];
if (!masterValue) {
console.log(`[initializeForm] 테이블 섹션 ${section.id}: masterField(${linkColumn.masterField}) 값 없음, 스킵`);
console.log(
`[initializeForm] 테이블 섹션 ${section.id}: masterField(${linkColumn.masterField}) 값 없음, 스킵`,
);
continue;
}
@ -767,23 +791,30 @@ export function UniversalFormModalComponent({
[linkColumn.detailField]: { value: masterValue, operator: "equals" },
};
console.log(`[initializeForm] 테이블 섹션 ${section.id}: API 요청 - URL: /table-management/tables/${detailTable}/data`);
console.log(`[initializeForm] 테이블 섹션 ${section.id}: API 요청 - search:`, JSON.stringify(searchCondition));
console.log(
`[initializeForm] 테이블 섹션 ${section.id}: API 요청 - URL: /table-management/tables/${detailTable}/data`,
);
console.log(
`[initializeForm] 테이블 섹션 ${section.id}: API 요청 - search:`,
JSON.stringify(searchCondition),
);
const response = await apiClient.post(`/table-management/tables/${detailTable}/data`, {
search: searchCondition, // filters가 아닌 search로 전달
search: searchCondition, // filters가 아닌 search로 전달
page: 1,
size: 1000, // pageSize가 아닌 size로 전달
autoFilter: { enabled: true }, // 멀티테넌시 필터 적용
size: 1000, // pageSize가 아닌 size로 전달
autoFilter: { enabled: true }, // 멀티테넌시 필터 적용
});
console.log(`[initializeForm] 테이블 섹션 ${section.id}: API 응답 - success: ${response.data?.success}, total: ${response.data?.data?.total}, dataLength: ${response.data?.data?.data?.length}`);
console.log(
`[initializeForm] 테이블 섹션 ${section.id}: API 응답 - success: ${response.data?.success}, total: ${response.data?.data?.total}, dataLength: ${response.data?.data?.data?.length}`,
);
if (response.data?.success) {
// 다양한 응답 구조 처리
let items: any[] = [];
const data = response.data.data;
if (Array.isArray(data)) {
items = data;
} else if (data?.items && Array.isArray(data.items)) {
@ -793,7 +824,7 @@ export function UniversalFormModalComponent({
} else if (data?.data && Array.isArray(data.data)) {
items = data.data;
}
console.log(`[initializeForm] 테이블 섹션 ${section.id}: ${items.length}건 로드됨`, items);
// 테이블 섹션 데이터를 formData에 저장 (TableSectionRenderer에서 사용)
@ -818,35 +849,35 @@ export function UniversalFormModalComponent({
if (multiTable && effectiveInitialData) {
const pkColumn = multiTable.mainTable?.primaryKeyColumn;
const pkValue = effectiveInitialData[pkColumn];
// PK 값이 있으면 수정 모드로 판단
if (pkValue) {
console.log("[initializeForm] 수정 모드 - 서브 테이블 데이터 로드 시작");
for (const subTableConfig of multiTable.subTables || []) {
// loadOnEdit 옵션이 활성화된 경우에만 로드
if (!subTableConfig.enabled || !subTableConfig.options?.loadOnEdit) {
continue;
}
const { tableName, linkColumn, repeatSectionId, fieldMappings, options } = subTableConfig;
if (!tableName || !linkColumn?.subColumn || !repeatSectionId) {
continue;
}
try {
// 서브 테이블에서 데이터 조회
const filters: Record<string, any> = {
[linkColumn.subColumn]: pkValue,
};
// 서브 항목만 로드 (메인 항목 제외)
if (options?.loadOnlySubItems && options?.mainMarkerColumn) {
filters[options.mainMarkerColumn] = options.subMarkerValue ?? false;
}
console.log(`[initializeForm] 서브 테이블 ${tableName} 조회:`, filters);
const response = await apiClient.get(`/table-management/tables/${tableName}/data`, {
params: {
filters: JSON.stringify(filters),
@ -854,11 +885,11 @@ export function UniversalFormModalComponent({
pageSize: 100,
},
});
if (response.data?.success && response.data?.data?.items) {
const subItems = response.data.data.items;
console.log(`[initializeForm] 서브 테이블 ${tableName} 데이터 ${subItems.length}건 로드됨`);
// 역매핑: 서브 테이블 데이터 → 반복 섹션 데이터
const repeatItems: RepeatSectionItem[] = subItems.map((item: any, index: number) => {
const repeatItem: RepeatSectionItem = {
@ -866,17 +897,17 @@ export function UniversalFormModalComponent({
_index: index,
_originalData: item, // 원본 데이터 보관 (수정 시 필요)
};
// 필드 매핑 역변환 (targetColumn → formField)
for (const mapping of fieldMappings || []) {
if (mapping.formField && mapping.targetColumn) {
repeatItem[mapping.formField] = item[mapping.targetColumn];
}
}
return repeatItem;
});
// 반복 섹션에 데이터 설정
newRepeatSections[repeatSectionId] = repeatItems;
setRepeatSections({ ...newRepeatSections });
@ -903,7 +934,7 @@ export function UniversalFormModalComponent({
_index: index,
};
for (const field of (section.fields || [])) {
for (const field of section.fields || []) {
item[field.columnName] = field.defaultValue ?? "";
}
@ -913,8 +944,42 @@ export function UniversalFormModalComponent({
// 필드 값 변경 핸들러
const handleFieldChange = useCallback(
(columnName: string, value: any) => {
// 채번규칙 필드의 수동 모드 감지
const originalNumberingValue = numberingOriginalValues[columnName];
const ruleIdKey = `${columnName}_numberingRuleId`;
// 해당 필드의 채번규칙 설정 찾기
let fieldConfig: FormFieldConfig | undefined;
for (const section of config.sections) {
if (section.type === "table" || section.repeatable) continue;
fieldConfig = section.fields?.find((f) => f.columnName === columnName);
if (fieldConfig) break;
// 옵셔널 필드 그룹에서도 찾기
for (const group of section.optionalFieldGroups || []) {
fieldConfig = group.fields?.find((f) => f.columnName === columnName);
if (fieldConfig) break;
}
if (fieldConfig) break;
}
setFormData((prev) => {
const newData = { ...prev, [columnName]: value };
// 채번규칙이 활성화된 필드이고, "사용자 수정 가능"이 ON인 경우
if (fieldConfig?.numberingRule?.enabled && fieldConfig?.numberingRule?.editable && originalNumberingValue) {
// 사용자가 값을 수정했으면 (원본과 다르면) ruleId 제거 → 수동 모드
if (value !== originalNumberingValue) {
delete newData[ruleIdKey];
console.log(`[채번 수동 모드] ${columnName}: 사용자가 값 수정 → ruleId 제거`);
} else {
// 원본 값으로 복구하면 ruleId 복구 → 자동 모드
if (fieldConfig.numberingRule.ruleId) {
newData[ruleIdKey] = fieldConfig.numberingRule.ruleId;
console.log(`[채번 자동 모드] ${columnName}: 원본 값 복구 → ruleId 복구`);
}
}
}
// onChange는 렌더링 외부에서 호출해야 함 (setTimeout 사용)
if (onChange) {
setTimeout(() => onChange(newData), 0);
@ -922,7 +987,7 @@ export function UniversalFormModalComponent({
return newData;
});
},
[onChange],
[onChange, numberingOriginalValues, config.sections],
);
// 반복 섹션 필드 값 변경 핸들러
@ -995,47 +1060,53 @@ export function UniversalFormModalComponent({
}, []);
// 옵셔널 필드 그룹 활성화
const activateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => {
const section = config.sections.find((s) => s.id === sectionId);
const group = section?.optionalFieldGroups?.find((g) => g.id === groupId);
if (!group) return;
const activateOptionalFieldGroup = useCallback(
(sectionId: string, groupId: string) => {
const section = config.sections.find((s) => s.id === sectionId);
const group = section?.optionalFieldGroups?.find((g) => g.id === groupId);
if (!group) return;
const key = `${sectionId}-${groupId}`;
setActivatedOptionalFieldGroups((prev) => {
const newSet = new Set(prev);
newSet.add(key);
return newSet;
});
const key = `${sectionId}-${groupId}`;
setActivatedOptionalFieldGroups((prev) => {
const newSet = new Set(prev);
newSet.add(key);
return newSet;
});
// 연동 필드 값 변경 (추가 시)
if (group.triggerField && group.triggerValueOnAdd !== undefined) {
handleFieldChange(group.triggerField, group.triggerValueOnAdd);
}
}, [config, handleFieldChange]);
// 연동 필드 값 변경 (추가 시)
if (group.triggerField && group.triggerValueOnAdd !== undefined) {
handleFieldChange(group.triggerField, group.triggerValueOnAdd);
}
},
[config, handleFieldChange],
);
// 옵셔널 필드 그룹 비활성화
const deactivateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => {
const section = config.sections.find((s) => s.id === sectionId);
const group = section?.optionalFieldGroups?.find((g) => g.id === groupId);
if (!group) return;
const deactivateOptionalFieldGroup = useCallback(
(sectionId: string, groupId: string) => {
const section = config.sections.find((s) => s.id === sectionId);
const group = section?.optionalFieldGroups?.find((g) => g.id === groupId);
if (!group) return;
const key = `${sectionId}-${groupId}`;
setActivatedOptionalFieldGroups((prev) => {
const newSet = new Set(prev);
newSet.delete(key);
return newSet;
});
const key = `${sectionId}-${groupId}`;
setActivatedOptionalFieldGroups((prev) => {
const newSet = new Set(prev);
newSet.delete(key);
return newSet;
});
// 연동 필드 값 변경 (제거 시)
if (group.triggerField && group.triggerValueOnRemove !== undefined) {
handleFieldChange(group.triggerField, group.triggerValueOnRemove);
}
// 연동 필드 값 변경 (제거 시)
if (group.triggerField && group.triggerValueOnRemove !== undefined) {
handleFieldChange(group.triggerField, group.triggerValueOnRemove);
}
// 옵셔널 필드 그룹 필드 값 초기화
(group.fields || []).forEach((field) => {
handleFieldChange(field.columnName, field.defaultValue || "");
});
}, [config, handleFieldChange]);
// 옵셔널 필드 그룹 필드 값 초기화
(group.fields || []).forEach((field) => {
handleFieldChange(field.columnName, field.defaultValue || "");
});
},
[config, handleFieldChange],
);
// Select 옵션 로드
const loadSelectOptions = useCallback(
@ -1081,13 +1152,11 @@ export function UniversalFormModalComponent({
// categoryKey 형식: "tableName.columnName"
const [categoryTable, categoryColumn] = optionConfig.categoryKey.split(".");
if (categoryTable && categoryColumn) {
const response = await apiClient.get(
`/table-categories/${categoryTable}/${categoryColumn}/values`
);
const response = await apiClient.get(`/table-categories/${categoryTable}/${categoryColumn}/values`);
if (response.data?.success && response.data?.data) {
// 라벨값을 DB에 저장 (화면에 표시되는 값 그대로 저장)
// 코드값을 DB에 저장하고 라벨값을 화면에 표시
options = response.data.data.map((item: any) => ({
value: item.valueLabel || item.value_label,
value: item.valueCode || item.value_code,
label: item.valueLabel || item.value_label,
}));
}
@ -1162,7 +1231,7 @@ export function UniversalFormModalComponent({
for (const section of config.sections) {
if (section.repeatable || section.type === "table") continue; // 반복 섹션 및 테이블 섹션은 별도 검증
for (const field of (section.fields || [])) {
for (const field of section.fields || []) {
if (field.required && !field.hidden && !field.numberingRule?.hidden) {
const value = formData[field.columnName];
if (value === undefined || value === null || value === "") {
@ -1178,7 +1247,7 @@ export function UniversalFormModalComponent({
// 단일 행 저장
const saveSingleRow = useCallback(async () => {
const dataToSave = { ...formData };
// 테이블 섹션 데이터 추출 (별도 저장용)
const tableSectionData: Record<string, any[]> = {};
@ -1194,19 +1263,45 @@ export function UniversalFormModalComponent({
}
});
// 저장 시점 채번규칙 처리 (generateOnSave만 처리)
// 저장 시점 채번규칙 처리
for (const section of config.sections) {
// 테이블 타입 섹션은 건너뛰기
if (section.type === "table") continue;
for (const field of (section.fields || [])) {
if (field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId) {
const response = await allocateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
dataToSave[field.columnName] = response.data.generatedCode;
console.log(`[채번 할당] ${field.columnName} = ${response.data.generatedCode}`);
for (const field of section.fields || []) {
if (field.numberingRule?.enabled && field.numberingRule?.ruleId) {
const ruleIdKey = `${field.columnName}_numberingRuleId`;
const hasRuleId = dataToSave[ruleIdKey]; // 사용자가 수정하지 않았으면 ruleId 유지됨
// 채번 규칙 할당 조건
const shouldAllocate =
// 1. generateOnSave가 ON인 경우: 항상 저장 시점에 할당
field.numberingRule.generateOnSave ||
// 2. editable이 OFF인 경우: 사용자 입력 무시하고 채번 규칙으로 덮어씌움
!field.numberingRule.editable ||
// 3. editable이 ON이고 사용자가 수정하지 않은 경우 (ruleId 유지됨): 실제 번호 할당
(field.numberingRule.editable && hasRuleId);
if (shouldAllocate) {
const response = await allocateNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) {
dataToSave[field.columnName] = response.data.generatedCode;
let reason = "(알 수 없음)";
if (field.numberingRule.generateOnSave) {
reason = "(generateOnSave)";
} else if (!field.numberingRule.editable) {
reason = "(editable=OFF, 강제 덮어씌움)";
} else if (hasRuleId) {
reason = "(editable=ON, 사용자 미수정)";
}
console.log(`[채번 할당] ${field.columnName} = ${response.data.generatedCode} ${reason}`);
} else {
console.error(`[채번 실패] ${field.columnName}:`, response.error);
}
} else {
console.error(`[채번 실패] ${field.columnName}:`, response.error);
console.log(
`[채번 스킵] ${field.columnName}: 사용자가 직접 입력한 값 유지 = ${dataToSave[field.columnName]}`,
);
}
}
}
@ -1214,22 +1309,30 @@ export function UniversalFormModalComponent({
// 별도 테이블에 저장해야 하는 테이블 섹션 목록
const tableSectionsForSeparateTable = config.sections.filter(
(s) => s.type === "table" &&
s.tableConfig?.saveConfig?.targetTable &&
s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName
(s) =>
s.type === "table" &&
s.tableConfig?.saveConfig?.targetTable &&
s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName,
);
// 테이블 섹션이 있고 메인 테이블에 품목별로 저장하는 경우 (공통 + 개별 병합 저장)
// targetTable이 없거나 메인 테이블과 같은 경우
const tableSectionsForMainTable = config.sections.filter(
(s) => s.type === "table" &&
(!s.tableConfig?.saveConfig?.targetTable ||
s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName)
(s) =>
s.type === "table" &&
(!s.tableConfig?.saveConfig?.targetTable ||
s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName),
);
console.log("[saveSingleRow] 메인 테이블:", config.saveConfig.tableName);
console.log("[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:", tableSectionsForMainTable.map(s => s.id));
console.log("[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:", tableSectionsForSeparateTable.map(s => s.id));
console.log(
"[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:",
tableSectionsForMainTable.map((s) => s.id),
);
console.log(
"[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:",
tableSectionsForSeparateTable.map((s) => s.id),
);
console.log("[saveSingleRow] 테이블 섹션 데이터 키:", Object.keys(tableSectionData));
console.log("[saveSingleRow] dataToSave 키:", Object.keys(dataToSave));
@ -1237,58 +1340,58 @@ export function UniversalFormModalComponent({
// 공통 저장 필드 수집 (sectionSaveModes 설정에 따라)
const commonFieldsData: Record<string, any> = {};
const { sectionSaveModes } = config.saveConfig;
// 필드 타입 섹션에서 공통 저장 필드 수집
for (const section of config.sections) {
if (section.type === "table") continue;
const sectionMode = sectionSaveModes?.find((s) => s.sectionId === section.id);
const defaultMode = "common"; // 필드 타입 섹션의 기본값은 공통 저장
const sectionSaveMode = sectionMode?.saveMode || defaultMode;
if (section.fields) {
for (const field of section.fields) {
const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName);
const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode;
if (fieldSaveMode === "common" && dataToSave[field.columnName] !== undefined) {
commonFieldsData[field.columnName] = dataToSave[field.columnName];
}
}
}
}
// 각 테이블 섹션의 품목 데이터에 공통 필드 병합하여 저장
for (const tableSection of tableSectionsForMainTable) {
const sectionData = tableSectionData[tableSection.id] || [];
if (sectionData.length > 0) {
// 품목별로 행 저장
for (const item of sectionData) {
const rowToSave = { ...commonFieldsData, ...item };
// _sourceData 등 내부 메타데이터 제거
Object.keys(rowToSave).forEach((key) => {
if (key.startsWith("_")) {
delete rowToSave[key];
}
});
const response = await apiClient.post(
`/table-management/tables/${config.saveConfig.tableName}/add`,
rowToSave
rowToSave,
);
if (!response.data?.success) {
throw new Error(response.data?.message || "품목 저장 실패");
}
}
// 이미 저장했으므로 아래 로직에서 다시 저장하지 않도록 제거
delete tableSectionData[tableSection.id];
}
}
// 품목이 없으면 공통 데이터만 저장하지 않음 (품목이 필요한 화면이므로)
// 다른 테이블 섹션이 있는 경우에만 메인 데이터 저장
const hasOtherTableSections = Object.keys(tableSectionData).length > 0;
@ -1303,7 +1406,7 @@ export function UniversalFormModalComponent({
if (!response.data?.success) {
throw new Error(response.data?.message || "저장 실패");
}
// 테이블 섹션 데이터 저장 (별도 테이블에)
for (const section of config.sections) {
if (section.type === "table" && section.tableConfig?.saveConfig?.targetTable) {
@ -1311,35 +1414,35 @@ export function UniversalFormModalComponent({
if (sectionData && sectionData.length > 0) {
// 메인 레코드 ID가 필요한 경우 (response.data에서 가져오기)
const mainRecordId = response.data?.data?.id;
// 공통 저장 필드 수집: 다른 섹션(필드 타입)에서 공통 저장으로 설정된 필드 값
// 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual'
const commonFieldsData: Record<string, any> = {};
const { sectionSaveModes } = config.saveConfig;
// 다른 섹션에서 공통 저장으로 설정된 필드 값 수집
for (const otherSection of config.sections) {
if (otherSection.id === section.id) continue; // 현재 테이블 섹션은 건너뛰기
const sectionMode = sectionSaveModes?.find((s) => s.sectionId === otherSection.id);
// 기본값: 필드 타입 섹션은 'common', 테이블 타입 섹션은 'individual'
const defaultMode = otherSection.type === "table" ? "individual" : "common";
const sectionSaveMode = sectionMode?.saveMode || defaultMode;
// 필드 타입 섹션의 필드들 처리
if (otherSection.type !== "table" && otherSection.fields) {
for (const field of otherSection.fields) {
// 필드별 오버라이드 확인
const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName);
const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode;
// 공통 저장이면 formData에서 값을 가져와 모든 품목에 적용
if (fieldSaveMode === "common" && formData[field.columnName] !== undefined) {
commonFieldsData[field.columnName] = formData[field.columnName];
}
}
}
// 🆕 선택적 필드 그룹 (optionalFieldGroups)도 처리
if (otherSection.optionalFieldGroups && otherSection.optionalFieldGroups.length > 0) {
for (const optGroup of otherSection.optionalFieldGroups) {
@ -1354,13 +1457,13 @@ export function UniversalFormModalComponent({
}
}
}
console.log("[saveSingleRow] 별도 테이블 저장 - 공통 필드:", Object.keys(commonFieldsData));
for (const item of sectionData) {
// 공통 필드 병합 + 개별 품목 데이터
const itemToSave = { ...commonFieldsData, ...item };
// saveToTarget: false인 컬럼은 저장에서 제외
const columns = section.tableConfig?.columns || [];
for (const col of columns) {
@ -1368,24 +1471,24 @@ export function UniversalFormModalComponent({
delete itemToSave[col.field];
}
}
// _sourceData 등 내부 메타데이터 제거
Object.keys(itemToSave).forEach((key) => {
if (key.startsWith("_")) {
delete itemToSave[key];
}
});
// 메인 레코드와 연결이 필요한 경우
if (mainRecordId && config.saveConfig.primaryKeyColumn) {
itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId;
}
const saveResponse = await apiClient.post(
`/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`,
itemToSave
itemToSave,
);
if (!saveResponse.data?.success) {
throw new Error(saveResponse.data?.message || `${section.title || "테이블 섹션"} 저장 실패`);
}
@ -1393,7 +1496,13 @@ export function UniversalFormModalComponent({
}
}
}
}, [config.sections, config.saveConfig.tableName, config.saveConfig.primaryKeyColumn, config.saveConfig.sectionSaveModes, formData]);
}, [
config.sections,
config.saveConfig.tableName,
config.saveConfig.primaryKeyColumn,
config.saveConfig.sectionSaveModes,
formData,
]);
// 다중 행 저장 (겸직 등)
const saveMultipleRows = useCallback(async () => {
@ -1469,7 +1578,7 @@ export function UniversalFormModalComponent({
for (const section of config.sections) {
if (section.repeatable || section.type === "table") continue;
for (const field of (section.fields || [])) {
for (const field of section.fields || []) {
if (field.numberingRule?.enabled && field.numberingRule?.ruleId) {
// generateOnSave 또는 generateOnOpen 모두 저장 시 실제 순번 할당
const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen;
@ -1525,7 +1634,7 @@ export function UniversalFormModalComponent({
}
});
});
// 1-0. receiveFromParent 필드 값도 mainData에 추가 (서브 테이블 저장용)
// 이 필드들은 메인 테이블에는 저장되지 않지만, 서브 테이블 저장 시 필요할 수 있음
config.sections.forEach((section) => {
@ -1544,7 +1653,7 @@ export function UniversalFormModalComponent({
for (const section of config.sections) {
if (section.repeatable || section.type === "table") continue;
for (const field of (section.fields || [])) {
for (const field of section.fields || []) {
// 채번규칙이 활성화된 필드 처리
if (field.numberingRule?.enabled && field.numberingRule?.ruleId) {
// 신규 생성이거나 값이 없는 경우에만 채번
@ -1589,7 +1698,7 @@ export function UniversalFormModalComponent({
}
const subItems: Record<string, any>[] = [];
// 반복 섹션이 있는 경우에만 반복 데이터 처리
if (subTableConfig.repeatSectionId) {
const repeatData = repeatSections[subTableConfig.repeatSectionId] || [];
@ -1902,10 +2011,8 @@ export function UniversalFormModalComponent({
// 메인 표시 컬럼 (displayColumn)
const mainDisplayVal = row[lfg.displayColumn || ""] || "";
// 서브 표시 컬럼 (subDisplayColumn이 있으면 사용, 없으면 valueColumn 사용)
const subDisplayVal = lfg.subDisplayColumn
? (row[lfg.subDisplayColumn] || "")
: (row[valueColumn] || "");
const subDisplayVal = lfg.subDisplayColumn ? row[lfg.subDisplayColumn] || "" : row[valueColumn] || "";
switch (lfg.displayFormat) {
case "code_name":
// 서브 - 메인 형식
@ -1923,7 +2030,10 @@ export function UniversalFormModalComponent({
matches.forEach((match) => {
const columnName = match.slice(1, -1); // { } 제거
const columnValue = row[columnName];
result = result.replace(match, columnValue !== undefined && columnValue !== null ? String(columnValue) : "");
result = result.replace(
match,
columnValue !== undefined && columnValue !== null ? String(columnValue) : "",
);
});
}
return result;
@ -1980,7 +2090,12 @@ export function UniversalFormModalComponent({
<SelectContent>
{sourceData.length > 0 ? (
sourceData
.filter((row) => row[valueColumn] !== null && row[valueColumn] !== undefined && String(row[valueColumn]) !== "")
.filter(
(row) =>
row[valueColumn] !== null &&
row[valueColumn] !== undefined &&
String(row[valueColumn]) !== "",
)
.map((row, index) => (
<SelectItem key={`${row[valueColumn]}_${index}`} value={String(row[valueColumn])}>
{getDisplayText(row)}
@ -2240,13 +2355,11 @@ export function UniversalFormModalComponent({
),
)}
</div>
{/* 옵셔널 필드 그룹 렌더링 */}
{section.optionalFieldGroups && section.optionalFieldGroups.length > 0 && (
<div className="mt-4 space-y-3">
{section.optionalFieldGroups.map((group) =>
renderOptionalFieldGroup(section, group, sectionColumns)
)}
{section.optionalFieldGroups.map((group) => renderOptionalFieldGroup(section, group, sectionColumns))}
</div>
)}
</CardContent>
@ -2274,7 +2387,7 @@ export function UniversalFormModalComponent({
const renderOptionalFieldGroup = (
section: FormSectionConfig,
group: OptionalFieldGroupConfig,
sectionColumns: number
sectionColumns: number,
) => {
const key = `${section.id}-${group.id}`;
const isActivated = activatedOptionalFieldGroups.has(key);
@ -2293,9 +2406,7 @@ export function UniversalFormModalComponent({
<div className="flex items-center justify-between">
<div>
<p className="text-muted-foreground text-sm font-medium">{group.title}</p>
{group.description && (
<p className="text-muted-foreground/70 mt-0.5 text-xs">{group.description}</p>
)}
{group.description && <p className="text-muted-foreground/70 mt-0.5 text-xs">{group.description}</p>}
</div>
<Button
variant="outline"
@ -2334,16 +2445,10 @@ export function UniversalFormModalComponent({
<div className="flex items-center justify-between p-3">
<CollapsibleTrigger asChild>
<button className="flex items-center gap-2 text-left hover:opacity-80">
{isCollapsed ? (
<ChevronRight className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
{isCollapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
<div>
<p className="text-sm font-medium">{group.title}</p>
{group.description && (
<p className="text-muted-foreground mt-0.5 text-xs">{group.description}</p>
)}
{group.description && <p className="text-muted-foreground mt-0.5 text-xs">{group.description}</p>}
</div>
</button>
</CollapsibleTrigger>
@ -2373,8 +2478,8 @@ export function UniversalFormModalComponent({
formData[field.columnName],
(value) => handleFieldChange(field.columnName, value),
`${section.id}-${group.id}-${field.id}`,
groupColumns
)
groupColumns,
),
)}
</div>
</CollapsibleContent>
@ -2388,9 +2493,7 @@ export function UniversalFormModalComponent({
<div className="mb-3 flex items-center justify-between">
<div>
<p className="text-sm font-medium">{group.title}</p>
{group.description && (
<p className="text-muted-foreground mt-0.5 text-xs">{group.description}</p>
)}
{group.description && <p className="text-muted-foreground mt-0.5 text-xs">{group.description}</p>}
</div>
<Button
variant="ghost"
@ -2417,8 +2520,8 @@ export function UniversalFormModalComponent({
formData[field.columnName],
(value) => handleFieldChange(field.columnName, value),
`${section.id}-${group.id}-${field.id}`,
groupColumns
)
groupColumns,
),
)}
</div>
</div>
@ -2546,7 +2649,8 @@ export function UniversalFormModalComponent({
<div className="text-muted-foreground text-center">
<p className="font-medium">{config.modal.title || "범용 폼 모달"}</p>
<p className="mt-1 text-xs">
{config.sections.length} |{config.sections.reduce((acc, s) => acc + (s.fields?.length || 0), 0)}
{config.sections.length} |{config.sections.reduce((acc, s) => acc + (s.fields?.length || 0), 0)}
</p>
<p className="mt-1 text-xs"> : {config.saveConfig.tableName || "(미설정)"}</p>
</div>