fix: SelectedItemsDetailInput 수정 모드에서 null 레코드 삽입 방지

- buttonActions.ts: formData가 배열인 경우 일반 저장 건너뜀
- SelectedItemsDetailInput이 UPSERT를 완료한 후 일반 저장이 실행되어 null 레코드가 삽입되던 문제 해결
- ScreenModal에서 그룹 레코드를 배열로 전달하는 경우 감지하여 처리
- skipDefaultSave 플래그가 제대로 작동하지 않던 문제 근본 해결
This commit is contained in:
kjs 2025-11-20 15:07:26 +09:00
parent 640351d812
commit 86313c5e89
5 changed files with 1011 additions and 809 deletions

View File

@ -1227,18 +1227,24 @@ class DataService {
// 새 레코드 처리 (INSERT or UPDATE) // 새 레코드 처리 (INSERT or UPDATE)
for (const newRecord of records) { for (const newRecord of records) {
console.log(`🔍 처리할 새 레코드:`, newRecord);
// 날짜 필드 정규화 // 날짜 필드 정규화
const normalizedRecord: Record<string, any> = {}; const normalizedRecord: Record<string, any> = {};
for (const [key, value] of Object.entries(newRecord)) { for (const [key, value] of Object.entries(newRecord)) {
normalizedRecord[key] = normalizeDateValue(value); normalizedRecord[key] = normalizeDateValue(value);
} }
console.log(`🔄 정규화된 레코드:`, normalizedRecord);
// 전체 레코드 데이터 (parentKeys + normalizedRecord) // 전체 레코드 데이터 (parentKeys + normalizedRecord)
const fullRecord = { ...parentKeys, ...normalizedRecord }; const fullRecord = { ...parentKeys, ...normalizedRecord };
// 고유 키: parentKeys 제외한 나머지 필드들 // 고유 키: parentKeys 제외한 나머지 필드들
const uniqueFields = Object.keys(normalizedRecord); const uniqueFields = Object.keys(normalizedRecord);
console.log(`🔑 고유 필드들:`, uniqueFields);
// 기존 레코드에서 일치하는 것 찾기 // 기존 레코드에서 일치하는 것 찾기
const existingRecord = existingRecords.rows.find((existing) => { const existingRecord = existingRecords.rows.find((existing) => {
return uniqueFields.every((field) => { return uniqueFields.every((field) => {

View File

@ -134,23 +134,32 @@ export class EntityJoinService {
`🔧 기존 display_column 사용: ${column.column_name}${displayColumn}` `🔧 기존 display_column 사용: ${column.column_name}${displayColumn}`
); );
} else { } else {
// display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정 // display_column이 "none"이거나 없는 경우 참조 테이블의 모든 컬럼 가져오기
let defaultDisplayColumn = referenceColumn; logger.info(`🔍 ${referenceTable}의 모든 컬럼 조회 중...`);
if (referenceTable === "dept_info") {
defaultDisplayColumn = "dept_name";
} else if (referenceTable === "company_info") {
defaultDisplayColumn = "company_name";
} else if (referenceTable === "user_info") {
defaultDisplayColumn = "user_name";
} else if (referenceTable === "category_values") {
defaultDisplayColumn = "category_name";
}
displayColumns = [defaultDisplayColumn]; // 참조 테이블의 모든 컬럼 이름 가져오기
logger.info( const tableColumnsResult = await query<{ column_name: string }>(
`🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name}${defaultDisplayColumn} (${referenceTable})` `SELECT column_name
FROM information_schema.columns
WHERE table_name = $1
AND table_schema = 'public'
ORDER BY ordinal_position`,
[referenceTable]
); );
logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns);
if (tableColumnsResult.length > 0) {
displayColumns = tableColumnsResult.map((col) => col.column_name);
logger.info(
`${referenceTable}의 모든 컬럼 자동 포함 (${displayColumns.length}개):`,
displayColumns.join(", ")
);
} else {
// 테이블 컬럼을 못 찾으면 기본값 사용
displayColumns = [referenceColumn];
logger.warn(
`⚠️ ${referenceTable}의 컬럼 조회 실패, 기본값 사용: ${referenceColumn}`
);
}
} }
// 별칭 컬럼명 생성 (writer -> writer_name) // 별칭 컬럼명 생성 (writer -> writer_name)
@ -346,25 +355,26 @@ export class EntityJoinService {
); );
} }
} else { } else {
// 여러 컬럼인 경우 CONCAT으로 연결 // 🆕 여러 컬럼인 경우 각 컬럼을 개별 alias로 반환 (합치지 않음)
// 기본 테이블과 조인 테이블의 컬럼을 구분해서 처리 // 예: item_info.standard_price → sourceColumn_standard_price (item_id_standard_price)
const concatParts = displayColumns displayColumns.forEach((col) => {
.map((col) => { const isJoinTableColumn =
// ✅ 개선: referenceTable이 설정되어 있으면 조인 테이블에서 가져옴 config.referenceTable && config.referenceTable !== tableName;
const isJoinTableColumn =
config.referenceTable && config.referenceTable !== tableName;
if (isJoinTableColumn) { const individualAlias = `${config.sourceColumn}_${col}`;
// 조인 테이블 컬럼은 조인 별칭 사용
return `COALESCE(${alias}.${col}::TEXT, '')`;
} else {
// 기본 테이블 컬럼은 main 별칭 사용
return `COALESCE(main.${col}::TEXT, '')`;
}
})
.join(` || '${separator}' || `);
resultColumns.push(`(${concatParts}) AS ${config.aliasColumn}`); if (isJoinTableColumn) {
// 조인 테이블 컬럼은 조인 별칭 사용
resultColumns.push(
`COALESCE(${alias}.${col}::TEXT, '') AS ${individualAlias}`
);
} else {
// 기본 테이블 컬럼은 main 별칭 사용
resultColumns.push(
`COALESCE(main.${col}::TEXT, '') AS ${individualAlias}`
);
}
});
// 🆕 referenceColumn (PK)도 함께 SELECT (parentDataMapping용) // 🆕 referenceColumn (PK)도 함께 SELECT (parentDataMapping용)
const isJoinTableColumn = const isJoinTableColumn =

View File

@ -262,7 +262,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// 🆕 apiClient를 named import로 가져오기 // 🆕 apiClient를 named import로 가져오기
const { apiClient } = await import("@/lib/api/client"); const { apiClient } = await import("@/lib/api/client");
const params: any = { const params: any = {
enableEntityJoin: true, enableEntityJoin: true, // 엔티티 조인 활성화 (모든 엔티티 컬럼 자동 포함)
}; };
if (groupByColumns.length > 0) { if (groupByColumns.length > 0) {
params.groupByColumns = JSON.stringify(groupByColumns); params.groupByColumns = JSON.stringify(groupByColumns);
@ -325,7 +325,14 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
console.log("📥 [ScreenModal] API 응답 원본:", JSON.stringify(response.data, null, 2)); console.log("📥 [ScreenModal] API 응답 원본:", JSON.stringify(response.data, null, 2));
const normalizedData = normalizeDates(response.data); const normalizedData = normalizeDates(response.data);
console.log("📥 [ScreenModal] 정규화 후:", JSON.stringify(normalizedData, null, 2)); console.log("📥 [ScreenModal] 정규화 후:", JSON.stringify(normalizedData, null, 2));
setFormData(normalizedData);
// 🔧 배열 데이터는 formData로 설정하지 않음 (SelectedItemsDetailInput만 사용)
if (Array.isArray(normalizedData)) {
console.log("⚠️ [ScreenModal] 그룹 레코드(배열)는 formData로 설정하지 않음. SelectedItemsDetailInput만 사용합니다.");
setFormData(normalizedData); // SelectedItemsDetailInput이 직접 사용
} else {
setFormData(normalizedData);
}
// setFormData 직후 확인 // setFormData 직후 확인
console.log("🔄 setFormData 호출 완료 (날짜 정규화됨)"); console.log("🔄 setFormData 호출 완료 (날짜 정규화됨)");

View File

@ -225,6 +225,7 @@ export class ButtonActionExecutor {
// 🆕 SelectedItemsDetailInput 배치 저장 처리 (fieldGroups 구조) // 🆕 SelectedItemsDetailInput 배치 저장 처리 (fieldGroups 구조)
console.log("🔍 [handleSave] formData 구조 확인:", { console.log("🔍 [handleSave] formData 구조 확인:", {
isFormDataArray: Array.isArray(context.formData),
keys: Object.keys(context.formData), keys: Object.keys(context.formData),
values: Object.entries(context.formData).map(([key, value]) => ({ values: Object.entries(context.formData).map(([key, value]) => ({
key, key,
@ -238,6 +239,14 @@ export class ButtonActionExecutor {
})) }))
}); });
// 🔧 formData 자체가 배열인 경우 (ScreenModal의 그룹 레코드 수정)
if (Array.isArray(context.formData)) {
console.log("⚠️ [handleSave] formData가 배열입니다 - SelectedItemsDetailInput이 이미 처리했으므로 일반 저장 건너뜀");
console.log("⚠️ [handleSave] formData 배열:", context.formData);
// ✅ SelectedItemsDetailInput이 이미 UPSERT를 실행했으므로 일반 저장을 건너뜀
return true; // 성공으로 반환
}
const selectedItemsKeys = Object.keys(context.formData).filter(key => { const selectedItemsKeys = Object.keys(context.formData).filter(key => {
const value = context.formData[key]; const value = context.formData[key];
console.log(`🔍 [handleSave] 필터링 체크 - ${key}:`, { console.log(`🔍 [handleSave] 필터링 체크 - ${key}:`, {