테이블 간 조인 관계 조회 기능 추가

- 두 테이블 간의 조인 관계를 조회하는 API를 추가하였습니다. 이 API는 메인 테이블과 디테일 테이블을 파라미터로 받아, 해당 테이블 간의 조인 관계를 반환합니다.
- DataflowService에 조인 관계 조회 메서드를 구현하여, 회사 코드에 따라 적절한 조인 정보를 필터링합니다.
- 프론트엔드에서 조인 관계를 캐시하여, 반복적인 API 호출을 줄이고 성능을 개선하였습니다.

이로 인해 마스터-디테일 저장 기능의 효율성이 향상되었습니다.
This commit is contained in:
kjs
2026-01-22 11:16:23 +09:00
parent 8c0572e0ac
commit d429e237ee
4 changed files with 324 additions and 1 deletions

View File

@@ -1237,6 +1237,9 @@ export class ButtonActionExecutor {
console.log("🔎 [handleSave] formData 키 목록:", Object.keys(context.formData));
console.log("🔎 [handleSave] formData 전체:", context.formData);
// 🆕 마스터-디테일 저장: 테이블 간 조인 관계 캐시
const joinRelationshipCache: Record<string, { mainColumn: string; detailColumn: string } | null> = {};
for (const [fieldKey, fieldValue] of Object.entries(context.formData)) {
console.log(`🔎 [handleSave] 필드 검사: ${fieldKey}`, {
type: typeof fieldValue,
@@ -1266,6 +1269,34 @@ export class ButtonActionExecutor {
itemCount: parsedData.length,
});
// 🆕 마스터-디테일 조인 관계 조회 (메인 테이블 → 리피터 테이블)
let joinRelationship: { mainColumn: string; detailColumn: string } | null = null;
const cacheKey = `${tableName}:${repeaterTargetTable}`;
if (tableName && repeaterTargetTable && tableName !== repeaterTargetTable) {
// 캐시에서 먼저 확인
if (cacheKey in joinRelationshipCache) {
joinRelationship = joinRelationshipCache[cacheKey];
} else {
try {
const joinResponse = await apiClient.get(
`/button-dataflow/join-relationship/${tableName}/${repeaterTargetTable}`
);
if (joinResponse.data?.success && joinResponse.data?.data?.found) {
joinRelationship = {
mainColumn: joinResponse.data.data.mainColumn,
detailColumn: joinResponse.data.data.detailColumn,
};
console.log(`🔗 [handleSave] 조인 관계 발견: ${tableName}.${joinRelationship.mainColumn}${repeaterTargetTable}.${joinRelationship.detailColumn}`);
}
} catch (joinError) {
console.warn(`⚠️ [handleSave] 조인 관계 조회 실패:`, joinError);
}
// 결과를 캐시에 저장 (없어도 null로 저장하여 재조회 방지)
joinRelationshipCache[cacheKey] = joinRelationship;
}
}
// 🆕 범용 폼 모달의 공통 필드 추출 (order_no, manager_id 등)
// "범용_폼_모달" 키에서 공통 필드를 가져옴
const universalFormData = context.formData["범용_폼_모달"] as Record<string, unknown> | undefined;
@@ -1303,6 +1334,18 @@ export class ButtonActionExecutor {
}
console.log("📋 [handleSave] 최종 공통 필드 (규칙 기반 자동 추출):", commonFields);
// 🆕 마스터-디테일 조인: 메인 테이블의 조인 컬럼 값을 commonFields에 추가
if (joinRelationship) {
const mainColumnValue = context.formData[joinRelationship.mainColumn];
if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") {
// 리피터 테이블의 조인 컬럼에 메인 테이블의 값 주입
commonFields[joinRelationship.detailColumn] = mainColumnValue;
console.log(`🔗 [handleSave] 조인 컬럼 값 주입: ${joinRelationship.detailColumn} = ${mainColumnValue}`);
} else {
console.warn(`⚠️ [handleSave] 조인 컬럼 값이 없음: ${joinRelationship.mainColumn}`);
}
}
for (const item of parsedData) {
// 메타 필드 제거 (eslint 경고 무시 - 의도적으로 분리)
@@ -1455,15 +1498,51 @@ export class ButtonActionExecutor {
if (!Array.isArray(rows) || rows.length === 0) continue;
// 🆕 마스터-디테일 조인 관계 조회 (메인 테이블 → RepeatScreenModal 테이블)
let joinRelationship: { mainColumn: string; detailColumn: string } | null = null;
if (tableName && targetTable && tableName !== targetTable) {
const cacheKey = `${tableName}:${targetTable}`;
if (cacheKey in joinRelationshipCache) {
joinRelationship = joinRelationshipCache[cacheKey];
} else {
try {
const joinResponse = await apiClient.get(
`/button-dataflow/join-relationship/${tableName}/${targetTable}`
);
if (joinResponse.data?.success && joinResponse.data?.data?.found) {
joinRelationship = {
mainColumn: joinResponse.data.data.mainColumn,
detailColumn: joinResponse.data.data.detailColumn,
};
console.log(`🔗 [handleSave] RepeatScreenModal 조인 관계 발견: ${tableName}.${joinRelationship.mainColumn}${targetTable}.${joinRelationship.detailColumn}`);
}
} catch (joinError) {
console.warn(`⚠️ [handleSave] RepeatScreenModal 조인 관계 조회 실패:`, joinError);
}
joinRelationshipCache[cacheKey] = joinRelationship;
}
}
// 🆕 조인 컬럼 값 준비 (메인 테이블에서 가져옴)
let joinColumnData: Record<string, any> = {};
if (joinRelationship) {
const mainColumnValue = context.formData[joinRelationship.mainColumn];
if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") {
joinColumnData[joinRelationship.detailColumn] = mainColumnValue;
console.log(`🔗 [handleSave] RepeatScreenModal 조인 컬럼 값: ${joinRelationship.detailColumn} = ${mainColumnValue}`);
}
}
console.log(`📦 [handleSave] ${targetTable} 테이블 저장:`, rows);
for (const row of rows) {
const { _isNew, _targetTable, id, ...dataToSave } = row;
// 사용자 정보 추가 + 채번 규칙 값 병합
// 사용자 정보 추가 + 채번 규칙 값 병합 + 조인 컬럼 값 추가
const dataWithMeta = {
...dataToSave,
...numberingFields, // 채번 규칙 값 (shipment_plan_no 등)
...joinColumnData, // 🆕 조인 컬럼 값 (마스터-디테일 관계)
created_by: context.userId,
updated_by: context.userId,
company_code: context.companyCode,
@@ -1497,6 +1576,96 @@ export class ButtonActionExecutor {
}
}
// 🆕 v2-repeat-container 데이터 저장 처리 (_repeatContainerTables에 그룹화된 데이터)
const repeatContainerTables = context.formData._repeatContainerTables as Record<string, any[]> | undefined;
if (repeatContainerTables && Object.keys(repeatContainerTables).length > 0) {
console.log("📦 [handleSave] v2-RepeatContainer 데이터 저장 시작:", Object.keys(repeatContainerTables));
for (const [targetTable, rows] of Object.entries(repeatContainerTables)) {
if (!Array.isArray(rows) || rows.length === 0) continue;
// 🆕 마스터-디테일 조인 관계 조회
let joinRelationship: { mainColumn: string; detailColumn: string } | null = null;
if (tableName && targetTable && tableName !== targetTable) {
const cacheKey = `${tableName}:${targetTable}`;
if (cacheKey in joinRelationshipCache) {
joinRelationship = joinRelationshipCache[cacheKey];
} else {
try {
const joinResponse = await apiClient.get(
`/button-dataflow/join-relationship/${tableName}/${targetTable}`
);
if (joinResponse.data?.success && joinResponse.data?.data?.found) {
joinRelationship = {
mainColumn: joinResponse.data.data.mainColumn,
detailColumn: joinResponse.data.data.detailColumn,
};
console.log(`🔗 [handleSave] RepeatContainer 조인 관계 발견: ${tableName}.${joinRelationship.mainColumn}${targetTable}.${joinRelationship.detailColumn}`);
}
} catch (joinError) {
console.warn(`⚠️ [handleSave] RepeatContainer 조인 관계 조회 실패:`, joinError);
}
joinRelationshipCache[cacheKey] = joinRelationship;
}
}
// 조인 컬럼 값 준비
let joinColumnData: Record<string, any> = {};
if (joinRelationship) {
const mainColumnValue = context.formData[joinRelationship.mainColumn];
if (mainColumnValue !== undefined && mainColumnValue !== null && mainColumnValue !== "") {
joinColumnData[joinRelationship.detailColumn] = mainColumnValue;
console.log(`🔗 [handleSave] RepeatContainer 조인 컬럼 값: ${joinRelationship.detailColumn} = ${mainColumnValue}`);
}
}
console.log(`📦 [handleSave] ${targetTable} 테이블 저장 (RepeatContainer): ${rows.length}`);
for (const row of rows) {
const { _isDirty, _sectionIndex, _targetTable, id, ...dataToSave } = row;
// 변경되지 않은 행은 건너뛰기
if (_isDirty === false) {
console.log(`⏭️ [handleSave] ${targetTable} 변경 없음 건너뜀 (index: ${_sectionIndex})`);
continue;
}
// 사용자 정보 추가 + 조인 컬럼 값 추가
const dataWithMeta = {
...dataToSave,
...joinColumnData, // 조인 컬럼 값
created_by: context.userId,
updated_by: context.userId,
company_code: context.companyCode,
};
try {
if (!id) {
// INSERT (id가 없으면 새 레코드)
console.log(`📝 [handleSave] ${targetTable} INSERT (RepeatContainer):`, dataWithMeta);
const insertResult = await apiClient.post(
`/table-management/tables/${targetTable}/add`,
dataWithMeta,
);
console.log(`✅ [handleSave] ${targetTable} INSERT 완료:`, insertResult.data);
} else {
// UPDATE (id가 있으면 기존 레코드)
const originalData = { id };
const updatedData = { ...dataWithMeta, id };
console.log(`📝 [handleSave] ${targetTable} UPDATE (RepeatContainer):`, { originalData, updatedData });
const updateResult = await apiClient.put(`/table-management/tables/${targetTable}/edit`, {
originalData,
updatedData,
});
console.log(`✅ [handleSave] ${targetTable} UPDATE 완료:`, updateResult.data);
}
} catch (error: any) {
console.error(`❌ [handleSave] ${targetTable} 저장 실패 (RepeatContainer):`, error.response?.data || error.message);
}
}
}
}
// 🆕 v3.9: RepeatScreenModal 집계 저장 처리
const aggregationConfigs = context.formData._repeatScreenModal_aggregations as Array<{
resultField: string;