Compare commits

..

No commits in common. "efc9175fec1b1a5c496b62063b4ec65bfd70ddd1" and "eb61506acd4cc3304d54717113b35f5e90cb76d2" have entirely different histories.

1 changed files with 186 additions and 290 deletions

View File

@ -197,10 +197,6 @@ export function UniversalFormModalComponent({
// 로딩 상태 // 로딩 상태
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// 채번규칙 원본 값 추적 (수동 모드 감지용)
// key: columnName, value: 자동 생성된 원본 값
const [numberingOriginalValues, setNumberingOriginalValues] = useState<Record<string, string>>({});
// 🆕 수정 모드: 원본 그룹 데이터 (INSERT/UPDATE/DELETE 추적용) // 🆕 수정 모드: 원본 그룹 데이터 (INSERT/UPDATE/DELETE 추적용)
const [originalGroupedData, setOriginalGroupedData] = useState<any[]>([]); const [originalGroupedData, setOriginalGroupedData] = useState<any[]>([]);
const groupedDataInitializedRef = useRef(false); const groupedDataInitializedRef = useRef(false);
@ -231,8 +227,9 @@ export function UniversalFormModalComponent({
const currentIdString = currentId !== undefined ? String(currentId) : undefined; const currentIdString = currentId !== undefined ? String(currentId) : undefined;
// 생성 모드에서 부모로부터 전달받은 데이터 해시 (ID가 없을 때만) // 생성 모드에서 부모로부터 전달받은 데이터 해시 (ID가 없을 때만)
const createModeDataHash = const createModeDataHash = !currentIdString && initialData && Object.keys(initialData).length > 0
!currentIdString && initialData && Object.keys(initialData).length > 0 ? JSON.stringify(initialData) : undefined; ? JSON.stringify(initialData)
: undefined;
// 이미 초기화되었고, ID가 동일하고, 생성 모드 데이터도 동일하면 스킵 // 이미 초기화되었고, ID가 동일하고, 생성 모드 데이터도 동일하면 스킵
if (hasInitialized.current && lastInitializedId.current === currentIdString) { if (hasInitialized.current && lastInitializedId.current === currentIdString) {
@ -438,7 +435,7 @@ export function UniversalFormModalComponent({
// console.log("[채번] 섹션 검사:", section.title, { type: section.type, repeatable: section.repeatable, fieldsCount: section.fields?.length }); // console.log("[채번] 섹션 검사:", section.title, { type: section.type, repeatable: section.repeatable, fieldsCount: section.fields?.length });
if (section.repeatable || section.type === "table") continue; if (section.repeatable || section.type === "table") continue;
for (const field of section.fields || []) { for (const field of (section.fields || [])) {
// generateOnOpen은 기본값 true (undefined일 경우 true로 처리) // generateOnOpen은 기본값 true (undefined일 경우 true로 처리)
const shouldGenerateOnOpen = field.numberingRule?.generateOnOpen !== false; const shouldGenerateOnOpen = field.numberingRule?.generateOnOpen !== false;
// console.log("[채번] 필드 검사:", field.columnName, { // console.log("[채번] 필드 검사:", field.columnName, {
@ -460,19 +457,12 @@ export function UniversalFormModalComponent({
// generateOnOpen: 미리보기만 표시 (DB 시퀀스 증가 안 함) // generateOnOpen: 미리보기만 표시 (DB 시퀀스 증가 안 함)
const response = await previewNumberingCode(field.numberingRule.ruleId); const response = await previewNumberingCode(field.numberingRule.ruleId);
if (response.success && response.data?.generatedCode) { if (response.success && response.data?.generatedCode) {
const generatedCode = response.data.generatedCode; updatedData[field.columnName] = response.data.generatedCode;
updatedData[field.columnName] = generatedCode;
// 저장 시 실제 할당을 위해 ruleId 저장 (TextInput과 동일한 키 형식) // 저장 시 실제 할당을 위해 ruleId 저장 (TextInput과 동일한 키 형식)
const ruleIdKey = `${field.columnName}_numberingRuleId`; const ruleIdKey = `${field.columnName}_numberingRuleId`;
updatedData[ruleIdKey] = field.numberingRule.ruleId; updatedData[ruleIdKey] = field.numberingRule.ruleId;
// 원본 채번 값 저장 (수동 모드 감지용)
setNumberingOriginalValues((prev) => ({
...prev,
[field.columnName]: generatedCode,
}));
hasChanges = true; hasChanges = true;
numberingGeneratedRef.current = true; // 생성 완료 표시 numberingGeneratedRef.current = true; // 생성 완료 표시
// console.log( // console.log(
@ -544,7 +534,7 @@ export function UniversalFormModalComponent({
continue; continue;
} else { } else {
// 일반 섹션 필드 초기화 // 일반 섹션 필드 초기화
for (const field of section.fields || []) { for (const field of (section.fields || [])) {
// 기본값 설정 // 기본값 설정
let value = field.defaultValue ?? ""; let value = field.defaultValue ?? "";
@ -572,9 +562,7 @@ export function UniversalFormModalComponent({
const triggerValue = effectiveInitialData[group.triggerField]; const triggerValue = effectiveInitialData[group.triggerField];
if (triggerValue === group.triggerValueOnAdd) { if (triggerValue === group.triggerValueOnAdd) {
newActivatedGroups.add(key); newActivatedGroups.add(key);
console.log( console.log(`[initializeForm] 옵셔널 그룹 자동 활성화: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`);
`[initializeForm] 옵셔널 그룹 자동 활성화: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`,
);
// 활성화된 그룹의 필드값도 초기화 // 활성화된 그룹의 필드값도 초기화
for (const field of group.fields || []) { for (const field of group.fields || []) {
@ -646,8 +634,8 @@ export function UniversalFormModalComponent({
// 마스터 테이블명 확인 (saveConfig에서) // 마스터 테이블명 확인 (saveConfig에서)
// 1. customApiSave.multiTable.mainTable.tableName (다중 테이블 저장) // 1. customApiSave.multiTable.mainTable.tableName (다중 테이블 저장)
// 2. saveConfig.tableName (단일 테이블 저장) // 2. saveConfig.tableName (단일 테이블 저장)
const masterTable = const masterTable = config.saveConfig?.customApiSave?.multiTable?.mainTable?.tableName
config.saveConfig?.customApiSave?.multiTable?.mainTable?.tableName || config.saveConfig?.tableName; || config.saveConfig?.tableName;
// 디테일 테이블의 컬럼 목록 조회 // 디테일 테이블의 컬럼 목록 조회
const columnsResponse = await apiClient.get(`/table-management/tables/${detailTable}/columns`); const columnsResponse = await apiClient.get(`/table-management/tables/${detailTable}/columns`);
@ -680,10 +668,9 @@ export function UniversalFormModalComponent({
// detail_settings에서 referenceTable 확인 // detail_settings에서 referenceTable 확인
if (!refTable && col.detail_settings) { if (!refTable && col.detail_settings) {
try { try {
const settings = const settings = typeof col.detail_settings === "string"
typeof col.detail_settings === "string" ? JSON.parse(col.detail_settings)
? JSON.parse(col.detail_settings) : col.detail_settings;
: col.detail_settings;
refTable = settings.referenceTable; refTable = settings.referenceTable;
} catch { } catch {
// JSON 파싱 실패 무시 // JSON 파싱 실패 무시
@ -696,10 +683,9 @@ export function UniversalFormModalComponent({
let refColumn = col.reference_column || col.referenceColumn; let refColumn = col.reference_column || col.referenceColumn;
if (!refColumn && col.detail_settings) { if (!refColumn && col.detail_settings) {
try { try {
const settings = const settings = typeof col.detail_settings === "string"
typeof col.detail_settings === "string" ? JSON.parse(col.detail_settings)
? JSON.parse(col.detail_settings) : col.detail_settings;
: col.detail_settings;
refColumn = settings.referenceColumn; refColumn = settings.referenceColumn;
} catch { } catch {
// JSON 파싱 실패 무시 // JSON 파싱 실패 무시
@ -708,9 +694,7 @@ export function UniversalFormModalComponent({
// 마스터 데이터에 해당 컬럼 값이 있는지 확인 // 마스터 데이터에 해당 컬럼 값이 있는지 확인
if (refColumn && effectiveInitialData[refColumn]) { if (refColumn && effectiveInitialData[refColumn]) {
console.log( console.log(`[initializeForm] 테이블 섹션 ${section.id}: 엔티티 관계 감지 - ${colName}${masterTable}.${refColumn}`);
`[initializeForm] 테이블 섹션 ${section.id}: 엔티티 관계 감지 - ${colName}${masterTable}.${refColumn}`,
);
linkColumn = { masterField: refColumn, detailField: colName }; linkColumn = { masterField: refColumn, detailField: colName };
break; break;
} }
@ -726,13 +710,10 @@ export function UniversalFormModalComponent({
for (const pattern of priorityPatterns) { for (const pattern of priorityPatterns) {
for (const masterKey of masterKeys) { for (const masterKey of masterKeys) {
if ( if (masterKey.endsWith(pattern) &&
masterKey.endsWith(pattern) && detailColumns.includes(masterKey) &&
detailColumns.includes(masterKey) && effectiveInitialData[masterKey] &&
effectiveInitialData[masterKey] && masterKey !== "id" && masterKey !== "company_code") {
masterKey !== "id" &&
masterKey !== "company_code"
) {
console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 패턴 감지 - ${masterKey}`); console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 패턴 감지 - ${masterKey}`);
linkColumn = { masterField: masterKey, detailField: masterKey }; linkColumn = { masterField: masterKey, detailField: masterKey };
break; break;
@ -745,13 +726,10 @@ export function UniversalFormModalComponent({
// 방법 3: 일반 공통 컬럼 (마지막 폴백) // 방법 3: 일반 공통 컬럼 (마지막 폴백)
if (!linkColumn) { if (!linkColumn) {
for (const masterKey of masterKeys) { for (const masterKey of masterKeys) {
if ( if (detailColumns.includes(masterKey) &&
detailColumns.includes(masterKey) && effectiveInitialData[masterKey] &&
effectiveInitialData[masterKey] && masterKey !== "id" && masterKey !== "company_code" &&
masterKey !== "id" && !masterKey.startsWith("__")) {
masterKey !== "company_code" &&
!masterKey.startsWith("__")
) {
console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 감지 - ${masterKey}`); console.log(`[initializeForm] 테이블 섹션 ${section.id}: 공통 컬럼 감지 - ${masterKey}`);
linkColumn = { masterField: masterKey, detailField: masterKey }; linkColumn = { masterField: masterKey, detailField: masterKey };
break; break;
@ -772,9 +750,7 @@ export function UniversalFormModalComponent({
// 마스터 테이블의 연결 필드 값 가져오기 // 마스터 테이블의 연결 필드 값 가져오기
const masterValue = effectiveInitialData[linkColumn.masterField]; const masterValue = effectiveInitialData[linkColumn.masterField];
if (!masterValue) { if (!masterValue) {
console.log( console.log(`[initializeForm] 테이블 섹션 ${section.id}: masterField(${linkColumn.masterField}) 값 없음, 스킵`);
`[initializeForm] 테이블 섹션 ${section.id}: masterField(${linkColumn.masterField}) 값 없음, 스킵`,
);
continue; continue;
} }
@ -791,24 +767,17 @@ export function UniversalFormModalComponent({
[linkColumn.detailField]: { value: masterValue, operator: "equals" }, [linkColumn.detailField]: { value: masterValue, operator: "equals" },
}; };
console.log( console.log(`[initializeForm] 테이블 섹션 ${section.id}: API 요청 - URL: /table-management/tables/${detailTable}/data`);
`[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 요청 - search:`,
JSON.stringify(searchCondition),
);
const response = await apiClient.post(`/table-management/tables/${detailTable}/data`, { const response = await apiClient.post(`/table-management/tables/${detailTable}/data`, {
search: searchCondition, // filters가 아닌 search로 전달 search: searchCondition, // filters가 아닌 search로 전달
page: 1, page: 1,
size: 1000, // pageSize가 아닌 size로 전달 size: 1000, // pageSize가 아닌 size로 전달
autoFilter: { enabled: true }, // 멀티테넌시 필터 적용 autoFilter: { enabled: true }, // 멀티테넌시 필터 적용
}); });
console.log( console.log(`[initializeForm] 테이블 섹션 ${section.id}: API 응답 - success: ${response.data?.success}, total: ${response.data?.data?.total}, dataLength: ${response.data?.data?.data?.length}`);
`[initializeForm] 테이블 섹션 ${section.id}: API 응답 - success: ${response.data?.success}, total: ${response.data?.data?.total}, dataLength: ${response.data?.data?.data?.length}`,
);
if (response.data?.success) { if (response.data?.success) {
// 다양한 응답 구조 처리 // 다양한 응답 구조 처리
@ -934,7 +903,7 @@ export function UniversalFormModalComponent({
_index: index, _index: index,
}; };
for (const field of section.fields || []) { for (const field of (section.fields || [])) {
item[field.columnName] = field.defaultValue ?? ""; item[field.columnName] = field.defaultValue ?? "";
} }
@ -944,42 +913,8 @@ export function UniversalFormModalComponent({
// 필드 값 변경 핸들러 // 필드 값 변경 핸들러
const handleFieldChange = useCallback( const handleFieldChange = useCallback(
(columnName: string, value: any) => { (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) => { setFormData((prev) => {
const newData = { ...prev, [columnName]: value }; 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 사용) // onChange는 렌더링 외부에서 호출해야 함 (setTimeout 사용)
if (onChange) { if (onChange) {
setTimeout(() => onChange(newData), 0); setTimeout(() => onChange(newData), 0);
@ -987,7 +922,7 @@ export function UniversalFormModalComponent({
return newData; return newData;
}); });
}, },
[onChange, numberingOriginalValues, config.sections], [onChange],
); );
// 반복 섹션 필드 값 변경 핸들러 // 반복 섹션 필드 값 변경 핸들러
@ -1060,53 +995,47 @@ export function UniversalFormModalComponent({
}, []); }, []);
// 옵셔널 필드 그룹 활성화 // 옵셔널 필드 그룹 활성화
const activateOptionalFieldGroup = useCallback( const activateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => {
(sectionId: string, groupId: string) => { const section = config.sections.find((s) => s.id === sectionId);
const section = config.sections.find((s) => s.id === sectionId); const group = section?.optionalFieldGroups?.find((g) => g.id === groupId);
const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); if (!group) return;
if (!group) return;
const key = `${sectionId}-${groupId}`; const key = `${sectionId}-${groupId}`;
setActivatedOptionalFieldGroups((prev) => { setActivatedOptionalFieldGroups((prev) => {
const newSet = new Set(prev); const newSet = new Set(prev);
newSet.add(key); newSet.add(key);
return newSet; return newSet;
}); });
// 연동 필드 값 변경 (추가 시) // 연동 필드 값 변경 (추가 시)
if (group.triggerField && group.triggerValueOnAdd !== undefined) { if (group.triggerField && group.triggerValueOnAdd !== undefined) {
handleFieldChange(group.triggerField, group.triggerValueOnAdd); handleFieldChange(group.triggerField, group.triggerValueOnAdd);
} }
}, }, [config, handleFieldChange]);
[config, handleFieldChange],
);
// 옵셔널 필드 그룹 비활성화 // 옵셔널 필드 그룹 비활성화
const deactivateOptionalFieldGroup = useCallback( const deactivateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => {
(sectionId: string, groupId: string) => { const section = config.sections.find((s) => s.id === sectionId);
const section = config.sections.find((s) => s.id === sectionId); const group = section?.optionalFieldGroups?.find((g) => g.id === groupId);
const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); if (!group) return;
if (!group) return;
const key = `${sectionId}-${groupId}`; const key = `${sectionId}-${groupId}`;
setActivatedOptionalFieldGroups((prev) => { setActivatedOptionalFieldGroups((prev) => {
const newSet = new Set(prev); const newSet = new Set(prev);
newSet.delete(key); newSet.delete(key);
return newSet; return newSet;
}); });
// 연동 필드 값 변경 (제거 시) // 연동 필드 값 변경 (제거 시)
if (group.triggerField && group.triggerValueOnRemove !== undefined) { if (group.triggerField && group.triggerValueOnRemove !== undefined) {
handleFieldChange(group.triggerField, group.triggerValueOnRemove); handleFieldChange(group.triggerField, group.triggerValueOnRemove);
} }
// 옵셔널 필드 그룹 필드 값 초기화 // 옵셔널 필드 그룹 필드 값 초기화
(group.fields || []).forEach((field) => { (group.fields || []).forEach((field) => {
handleFieldChange(field.columnName, field.defaultValue || ""); handleFieldChange(field.columnName, field.defaultValue || "");
}); });
}, }, [config, handleFieldChange]);
[config, handleFieldChange],
);
// Select 옵션 로드 // Select 옵션 로드
const loadSelectOptions = useCallback( const loadSelectOptions = useCallback(
@ -1152,11 +1081,13 @@ export function UniversalFormModalComponent({
// categoryKey 형식: "tableName.columnName" // categoryKey 형식: "tableName.columnName"
const [categoryTable, categoryColumn] = optionConfig.categoryKey.split("."); const [categoryTable, categoryColumn] = optionConfig.categoryKey.split(".");
if (categoryTable && categoryColumn) { 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) { if (response.data?.success && response.data?.data) {
// 코드값을 DB에 저장하고 라벨값을 화면에 표시 // 라벨값을 DB에 저장 (화면에 표시되는 값 그대로 저장)
options = response.data.data.map((item: any) => ({ options = response.data.data.map((item: any) => ({
value: item.valueCode || item.value_code, value: item.valueLabel || item.value_label,
label: item.valueLabel || item.value_label, label: item.valueLabel || item.value_label,
})); }));
} }
@ -1231,7 +1162,7 @@ export function UniversalFormModalComponent({
for (const section of config.sections) { for (const section of config.sections) {
if (section.repeatable || section.type === "table") continue; // 반복 섹션 및 테이블 섹션은 별도 검증 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) { if (field.required && !field.hidden && !field.numberingRule?.hidden) {
const value = formData[field.columnName]; const value = formData[field.columnName];
if (value === undefined || value === null || value === "") { if (value === undefined || value === null || value === "") {
@ -1263,45 +1194,19 @@ export function UniversalFormModalComponent({
} }
}); });
// 저장 시점 채번규칙 처리 // 저장 시점 채번규칙 처리 (generateOnSave만 처리)
for (const section of config.sections) { for (const section of config.sections) {
// 테이블 타입 섹션은 건너뛰기 // 테이블 타입 섹션은 건너뛰기
if (section.type === "table") continue; if (section.type === "table") continue;
for (const field of section.fields || []) { for (const field of (section.fields || [])) {
if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { if (field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId) {
const ruleIdKey = `${field.columnName}_numberingRuleId`; const response = await allocateNumberingCode(field.numberingRule.ruleId);
const hasRuleId = dataToSave[ruleIdKey]; // 사용자가 수정하지 않았으면 ruleId 유지됨 if (response.success && response.data?.generatedCode) {
dataToSave[field.columnName] = response.data.generatedCode;
// 채번 규칙 할당 조건 console.log(`[채번 할당] ${field.columnName} = ${response.data.generatedCode}`);
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 { } else {
console.log( console.error(`[채번 실패] ${field.columnName}:`, response.error);
`[채번 스킵] ${field.columnName}: 사용자가 직접 입력한 값 유지 = ${dataToSave[field.columnName]}`,
);
} }
} }
} }
@ -1309,30 +1214,22 @@ export function UniversalFormModalComponent({
// 별도 테이블에 저장해야 하는 테이블 섹션 목록 // 별도 테이블에 저장해야 하는 테이블 섹션 목록
const tableSectionsForSeparateTable = config.sections.filter( const tableSectionsForSeparateTable = config.sections.filter(
(s) => (s) => s.type === "table" &&
s.type === "table" &&
s.tableConfig?.saveConfig?.targetTable && s.tableConfig?.saveConfig?.targetTable &&
s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName, s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName
); );
// 테이블 섹션이 있고 메인 테이블에 품목별로 저장하는 경우 (공통 + 개별 병합 저장) // 테이블 섹션이 있고 메인 테이블에 품목별로 저장하는 경우 (공통 + 개별 병합 저장)
// targetTable이 없거나 메인 테이블과 같은 경우 // targetTable이 없거나 메인 테이블과 같은 경우
const tableSectionsForMainTable = config.sections.filter( const tableSectionsForMainTable = config.sections.filter(
(s) => (s) => s.type === "table" &&
s.type === "table" &&
(!s.tableConfig?.saveConfig?.targetTable || (!s.tableConfig?.saveConfig?.targetTable ||
s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName), s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName)
); );
console.log("[saveSingleRow] 메인 테이블:", config.saveConfig.tableName); console.log("[saveSingleRow] 메인 테이블:", config.saveConfig.tableName);
console.log( console.log("[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:", tableSectionsForMainTable.map(s => s.id));
"[saveSingleRow] 메인 테이블에 저장할 테이블 섹션:", console.log("[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:", tableSectionsForSeparateTable.map(s => s.id));
tableSectionsForMainTable.map((s) => s.id),
);
console.log(
"[saveSingleRow] 별도 테이블에 저장할 테이블 섹션:",
tableSectionsForSeparateTable.map((s) => s.id),
);
console.log("[saveSingleRow] 테이블 섹션 데이터 키:", Object.keys(tableSectionData)); console.log("[saveSingleRow] 테이블 섹션 데이터 키:", Object.keys(tableSectionData));
console.log("[saveSingleRow] dataToSave 키:", Object.keys(dataToSave)); console.log("[saveSingleRow] dataToSave 키:", Object.keys(dataToSave));
@ -1379,7 +1276,7 @@ export function UniversalFormModalComponent({
const response = await apiClient.post( const response = await apiClient.post(
`/table-management/tables/${config.saveConfig.tableName}/add`, `/table-management/tables/${config.saveConfig.tableName}/add`,
rowToSave, rowToSave
); );
if (!response.data?.success) { if (!response.data?.success) {
@ -1486,7 +1383,7 @@ export function UniversalFormModalComponent({
const saveResponse = await apiClient.post( const saveResponse = await apiClient.post(
`/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`, `/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`,
itemToSave, itemToSave
); );
if (!saveResponse.data?.success) { if (!saveResponse.data?.success) {
@ -1496,13 +1393,7 @@ 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 () => { const saveMultipleRows = useCallback(async () => {
@ -1578,7 +1469,7 @@ export function UniversalFormModalComponent({
for (const section of config.sections) { for (const section of config.sections) {
if (section.repeatable || section.type === "table") continue; 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) { if (field.numberingRule?.enabled && field.numberingRule?.ruleId) {
// generateOnSave 또는 generateOnOpen 모두 저장 시 실제 순번 할당 // generateOnSave 또는 generateOnOpen 모두 저장 시 실제 순번 할당
const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen; const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen;
@ -1653,7 +1544,7 @@ export function UniversalFormModalComponent({
for (const section of config.sections) { for (const section of config.sections) {
if (section.repeatable || section.type === "table") continue; 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) { if (field.numberingRule?.enabled && field.numberingRule?.ruleId) {
// 신규 생성이거나 값이 없는 경우에만 채번 // 신규 생성이거나 값이 없는 경우에만 채번
@ -2011,7 +1902,9 @@ export function UniversalFormModalComponent({
// 메인 표시 컬럼 (displayColumn) // 메인 표시 컬럼 (displayColumn)
const mainDisplayVal = row[lfg.displayColumn || ""] || ""; const mainDisplayVal = row[lfg.displayColumn || ""] || "";
// 서브 표시 컬럼 (subDisplayColumn이 있으면 사용, 없으면 valueColumn 사용) // 서브 표시 컬럼 (subDisplayColumn이 있으면 사용, 없으면 valueColumn 사용)
const subDisplayVal = lfg.subDisplayColumn ? row[lfg.subDisplayColumn] || "" : row[valueColumn] || ""; const subDisplayVal = lfg.subDisplayColumn
? (row[lfg.subDisplayColumn] || "")
: (row[valueColumn] || "");
switch (lfg.displayFormat) { switch (lfg.displayFormat) {
case "code_name": case "code_name":
@ -2030,10 +1923,7 @@ export function UniversalFormModalComponent({
matches.forEach((match) => { matches.forEach((match) => {
const columnName = match.slice(1, -1); // { } 제거 const columnName = match.slice(1, -1); // { } 제거
const columnValue = row[columnName]; const columnValue = row[columnName];
result = result.replace( result = result.replace(match, columnValue !== undefined && columnValue !== null ? String(columnValue) : "");
match,
columnValue !== undefined && columnValue !== null ? String(columnValue) : "",
);
}); });
} }
return result; return result;
@ -2090,12 +1980,7 @@ export function UniversalFormModalComponent({
<SelectContent> <SelectContent>
{sourceData.length > 0 ? ( {sourceData.length > 0 ? (
sourceData sourceData
.filter( .filter((row) => row[valueColumn] !== null && row[valueColumn] !== undefined && String(row[valueColumn]) !== "")
(row) =>
row[valueColumn] !== null &&
row[valueColumn] !== undefined &&
String(row[valueColumn]) !== "",
)
.map((row, index) => ( .map((row, index) => (
<SelectItem key={`${row[valueColumn]}_${index}`} value={String(row[valueColumn])}> <SelectItem key={`${row[valueColumn]}_${index}`} value={String(row[valueColumn])}>
{getDisplayText(row)} {getDisplayText(row)}
@ -2359,7 +2244,9 @@ export function UniversalFormModalComponent({
{/* 옵셔널 필드 그룹 렌더링 */} {/* 옵셔널 필드 그룹 렌더링 */}
{section.optionalFieldGroups && section.optionalFieldGroups.length > 0 && ( {section.optionalFieldGroups && section.optionalFieldGroups.length > 0 && (
<div className="mt-4 space-y-3"> <div className="mt-4 space-y-3">
{section.optionalFieldGroups.map((group) => renderOptionalFieldGroup(section, group, sectionColumns))} {section.optionalFieldGroups.map((group) =>
renderOptionalFieldGroup(section, group, sectionColumns)
)}
</div> </div>
)} )}
</CardContent> </CardContent>
@ -2387,7 +2274,7 @@ export function UniversalFormModalComponent({
const renderOptionalFieldGroup = ( const renderOptionalFieldGroup = (
section: FormSectionConfig, section: FormSectionConfig,
group: OptionalFieldGroupConfig, group: OptionalFieldGroupConfig,
sectionColumns: number, sectionColumns: number
) => { ) => {
const key = `${section.id}-${group.id}`; const key = `${section.id}-${group.id}`;
const isActivated = activatedOptionalFieldGroups.has(key); const isActivated = activatedOptionalFieldGroups.has(key);
@ -2406,7 +2293,9 @@ export function UniversalFormModalComponent({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-muted-foreground text-sm font-medium">{group.title}</p> <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> </div>
<Button <Button
variant="outline" variant="outline"
@ -2445,10 +2334,16 @@ export function UniversalFormModalComponent({
<div className="flex items-center justify-between p-3"> <div className="flex items-center justify-between p-3">
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<button className="flex items-center gap-2 text-left hover:opacity-80"> <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> <div>
<p className="text-sm font-medium">{group.title}</p> <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> </div>
</button> </button>
</CollapsibleTrigger> </CollapsibleTrigger>
@ -2478,8 +2373,8 @@ export function UniversalFormModalComponent({
formData[field.columnName], formData[field.columnName],
(value) => handleFieldChange(field.columnName, value), (value) => handleFieldChange(field.columnName, value),
`${section.id}-${group.id}-${field.id}`, `${section.id}-${group.id}-${field.id}`,
groupColumns, groupColumns
), )
)} )}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
@ -2493,7 +2388,9 @@ export function UniversalFormModalComponent({
<div className="mb-3 flex items-center justify-between"> <div className="mb-3 flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium">{group.title}</p> <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> </div>
<Button <Button
variant="ghost" variant="ghost"
@ -2520,8 +2417,8 @@ export function UniversalFormModalComponent({
formData[field.columnName], formData[field.columnName],
(value) => handleFieldChange(field.columnName, value), (value) => handleFieldChange(field.columnName, value),
`${section.id}-${group.id}-${field.id}`, `${section.id}-${group.id}-${field.id}`,
groupColumns, groupColumns
), )
)} )}
</div> </div>
</div> </div>
@ -2649,8 +2546,7 @@ export function UniversalFormModalComponent({
<div className="text-muted-foreground text-center"> <div className="text-muted-foreground text-center">
<p className="font-medium">{config.modal.title || "범용 폼 모달"}</p> <p className="font-medium">{config.modal.title || "범용 폼 모달"}</p>
<p className="mt-1 text-xs"> <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>
<p className="mt-1 text-xs"> : {config.saveConfig.tableName || "(미설정)"}</p> <p className="mt-1 text-xs"> : {config.saveConfig.tableName || "(미설정)"}</p>
</div> </div>