Compare commits

...

5 Commits

Author SHA1 Message Date
kjs efc9175fec Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management 2026-01-06 13:45:48 +09:00
SeongHyun Kim 75b5530d04 Merge remote-tracking branch 'origin/main' into ksh 2026-01-06 13:23:00 +09:00
SeongHyun Kim 40fd5f9055 feat: 채번규칙 editable 옵션 수동 모드 감지 기능 구현
모달 오픈 시 채번 미리보기 원본값 저장 (numberingOriginalValues)
handleFieldChange에서 원본값 비교하여 수동/자동 모드 전환
사용자 수정 시 ruleId 제거하여 저장 시 채번 스킵
원본값 복구 시 ruleId 복구하여 자동 모드 복원
handleSave에서 채번 할당 조건 분기 처리
2026-01-06 13:06:28 +09:00
kjs 0709b8df25 Merge pull request '범용 폼 모달 사전필터 기능 수정' (#325) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/325
2026-01-06 11:43:38 +09:00
SeongHyun Kim b3ee2b50e8 fix: 카테고리 Select 필드 저장 시 라벨값 대신 코드값 저장되도록 수정
- UniversalFormModalComponent.tsx: 카테고리 옵션 value를 valueLabel에서 valueCode로 변경
- 제어 로직 조건 비교 정상화 및 500 에러 해결
2026-01-05 18:41:49 +09:00
1 changed files with 290 additions and 186 deletions

View File

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