fix: SelectedItemsDetailInput 수정 모드에서 null 레코드 삽입 방지
- buttonActions.ts: formData가 배열인 경우 일반 저장 건너뜀 - SelectedItemsDetailInput이 UPSERT를 완료한 후 일반 저장이 실행되어 null 레코드가 삽입되던 문제 해결 - ScreenModal에서 그룹 레코드를 배열로 전달하는 경우 감지하여 처리 - skipDefaultSave 플래그가 제대로 작동하지 않던 문제 근본 해결
This commit is contained in:
parent
640351d812
commit
86313c5e89
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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 호출 완료 (날짜 정규화됨)");
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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}:`, {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue