fix: Improve TableListComponent and UniversalFormModalComponent for better data handling
- Updated TableListComponent to use flex-nowrap and overflow-hidden for better badge rendering. - Enhanced UniversalFormModalComponent to maintain the latest formData using a ref, preventing stale closures during form save events. - Improved data merging logic in UniversalFormModalComponent to ensure accurate updates and maintain original data integrity. - Refactored buttonActions to streamline table section data collection and merging, ensuring proper handling of modified and original data during save operations.
This commit is contained in:
parent
60b1ac1442
commit
38dda2f807
|
|
@ -4319,7 +4319,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 다중 값인 경우: 여러 배지 렌더링
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-nowrap gap-1 overflow-hidden">
|
||||
{values.map((val, idx) => {
|
||||
const categoryData = mapping?.[val];
|
||||
const displayLabel = categoryData?.label || val;
|
||||
|
|
@ -4328,7 +4328,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// 배지 없음 옵션: color가 없거나, "none"이거나, 매핑 데이터가 없으면 텍스트만 표시
|
||||
if (!displayColor || displayColor === "none" || !categoryData) {
|
||||
return (
|
||||
<span key={idx} className="text-sm">
|
||||
<span key={idx} className="shrink-0 whitespace-nowrap text-sm">
|
||||
{displayLabel}
|
||||
{idx < values.length - 1 && ", "}
|
||||
</span>
|
||||
|
|
@ -4342,7 +4342,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
backgroundColor: displayColor,
|
||||
borderColor: displayColor,
|
||||
}}
|
||||
className="text-white"
|
||||
className="shrink-0 whitespace-nowrap text-white"
|
||||
>
|
||||
{displayLabel}
|
||||
</Badge>
|
||||
|
|
|
|||
|
|
@ -247,6 +247,10 @@ export function UniversalFormModalComponent({
|
|||
|
||||
// 폼 데이터 상태
|
||||
const [formData, setFormData] = useState<FormDataState>({});
|
||||
// formDataRef: 항상 최신 formData를 유지하는 ref
|
||||
// React 상태 업데이트는 비동기적이므로, handleBeforeFormSave 등에서
|
||||
// 클로저의 formData가 오래된 값을 참조하는 문제를 방지
|
||||
const formDataRef = useRef<FormDataState>({});
|
||||
const [, setOriginalData] = useState<Record<string, any>>({});
|
||||
|
||||
// 반복 섹션 데이터
|
||||
|
|
@ -398,18 +402,19 @@ export function UniversalFormModalComponent({
|
|||
console.log("[UniversalFormModal] beforeFormSave 이벤트 수신");
|
||||
console.log("[UniversalFormModal] 설정된 필드 목록:", Array.from(configuredFields));
|
||||
|
||||
// formDataRef.current 사용: blur+click 동시 처리 시 클로저의 formData가 오래된 문제 방지
|
||||
const latestFormData = formDataRef.current;
|
||||
|
||||
// 🆕 시스템 필드 병합: id는 설정 여부와 관계없이 항상 전달 (UPDATE/INSERT 판단용)
|
||||
// - 신규 등록: formData.id가 없으므로 영향 없음
|
||||
// - 편집 모드: formData.id가 있으면 메인 테이블 UPDATE에 사용
|
||||
if (formData.id !== undefined && formData.id !== null && formData.id !== "") {
|
||||
event.detail.formData.id = formData.id;
|
||||
console.log(`[UniversalFormModal] 시스템 필드 병합: id =`, formData.id);
|
||||
if (latestFormData.id !== undefined && latestFormData.id !== null && latestFormData.id !== "") {
|
||||
event.detail.formData.id = latestFormData.id;
|
||||
console.log(`[UniversalFormModal] 시스템 필드 병합: id =`, latestFormData.id);
|
||||
}
|
||||
|
||||
// UniversalFormModal에 설정된 필드만 병합 (채번 규칙 포함)
|
||||
// 외부 formData에 이미 값이 있어도 UniversalFormModal 값으로 덮어씀
|
||||
// (UniversalFormModal이 해당 필드의 주인이므로)
|
||||
for (const [key, value] of Object.entries(formData)) {
|
||||
for (const [key, value] of Object.entries(latestFormData)) {
|
||||
// 설정에 정의된 필드 또는 채번 규칙 ID 필드만 병합
|
||||
const isConfiguredField = configuredFields.has(key);
|
||||
const isNumberingRuleId = key.endsWith("_numberingRuleId");
|
||||
|
|
@ -432,17 +437,13 @@ export function UniversalFormModalComponent({
|
|||
}
|
||||
|
||||
// 🆕 테이블 섹션 데이터 병합 (품목 리스트 등)
|
||||
// 참고: initializeForm에서 DB 로드 시 __tableSection_ (더블),
|
||||
// handleTableDataChange에서 수정 시 _tableSection_ (싱글) 사용
|
||||
for (const [key, value] of Object.entries(formData)) {
|
||||
// 싱글/더블 언더스코어 모두 처리
|
||||
// formDataRef.current(= latestFormData) 사용: React 상태 커밋 전에도 최신 데이터 보장
|
||||
for (const [key, value] of Object.entries(latestFormData)) {
|
||||
// _tableSection_ 과 __tableSection_ 모두 원본 키 그대로 전달
|
||||
// buttonActions.ts에서 DB데이터(__tableSection_)와 수정데이터(_tableSection_)를 구분하여 병합
|
||||
if ((key.startsWith("_tableSection_") || key.startsWith("__tableSection_")) && Array.isArray(value)) {
|
||||
// 저장 시에는 _tableSection_ 키로 통일 (buttonActions.ts에서 이 키를 기대)
|
||||
const normalizedKey = key.startsWith("__tableSection_")
|
||||
? key.replace("__tableSection_", "_tableSection_")
|
||||
: key;
|
||||
event.detail.formData[normalizedKey] = value;
|
||||
console.log(`[UniversalFormModal] 테이블 섹션 병합: ${key} → ${normalizedKey}, ${value.length}개 항목`);
|
||||
event.detail.formData[key] = value;
|
||||
console.log(`[UniversalFormModal] 테이블 섹션 병합: ${key}, ${value.length}개 항목`);
|
||||
}
|
||||
|
||||
// 🆕 원본 테이블 섹션 데이터도 병합 (삭제 추적용)
|
||||
|
|
@ -457,6 +458,22 @@ export function UniversalFormModalComponent({
|
|||
event.detail.formData._originalGroupedData = originalGroupedData;
|
||||
console.log(`[UniversalFormModal] 원본 그룹 데이터 병합: ${originalGroupedData.length}개`);
|
||||
}
|
||||
|
||||
// 🆕 부모 formData의 중첩 객체(modalKey)도 최신 데이터로 업데이트
|
||||
// onChange(setTimeout)가 아직 부모에 전파되지 않았을 수 있으므로 직접 업데이트
|
||||
for (const parentKey of Object.keys(event.detail.formData)) {
|
||||
const parentValue = event.detail.formData[parentKey];
|
||||
if (parentValue && typeof parentValue === "object" && !Array.isArray(parentValue)) {
|
||||
const hasTableSection = Object.keys(parentValue).some(
|
||||
(k) => k.startsWith("_tableSection_") || k.startsWith("__tableSection_"),
|
||||
);
|
||||
if (hasTableSection) {
|
||||
event.detail.formData[parentKey] = { ...latestFormData };
|
||||
console.log(`[UniversalFormModal] 부모 중첩 객체 업데이트: ${parentKey}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
|
||||
|
|
@ -482,10 +499,11 @@ export function UniversalFormModalComponent({
|
|||
|
||||
// 테이블 섹션 데이터 설정
|
||||
const tableSectionKey = `_tableSection_${tableSection.id}`;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[tableSectionKey]: _groupedData,
|
||||
}));
|
||||
setFormData((prev) => {
|
||||
const newData = { ...prev, [tableSectionKey]: _groupedData };
|
||||
formDataRef.current = newData;
|
||||
return newData;
|
||||
});
|
||||
|
||||
groupedDataInitializedRef.current = true;
|
||||
}, [_groupedData, config.sections]);
|
||||
|
|
@ -965,6 +983,7 @@ export function UniversalFormModalComponent({
|
|||
}
|
||||
|
||||
setFormData(newFormData);
|
||||
formDataRef.current = newFormData;
|
||||
setRepeatSections(newRepeatSections);
|
||||
setCollapsedSections(newCollapsed);
|
||||
setActivatedOptionalFieldGroups(newActivatedGroups);
|
||||
|
|
@ -1132,6 +1151,9 @@ export function UniversalFormModalComponent({
|
|||
console.log(`[연쇄 드롭다운] 부모 ${columnName} 변경 → 자식 ${childField} 초기화`);
|
||||
}
|
||||
|
||||
// ref 즉시 업데이트 (React 상태 커밋 전에도 최신 데이터 접근 가능)
|
||||
formDataRef.current = newData;
|
||||
|
||||
// onChange는 렌더링 외부에서 호출해야 함 (setTimeout 사용)
|
||||
if (onChange) {
|
||||
setTimeout(() => onChange(newData), 0);
|
||||
|
|
|
|||
|
|
@ -4267,7 +4267,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 다중 값인 경우: 여러 배지 렌더링
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-nowrap gap-1 overflow-hidden">
|
||||
{values.map((val, idx) => {
|
||||
const categoryData = mapping?.[val];
|
||||
const displayLabel = categoryData?.label || val;
|
||||
|
|
@ -4276,7 +4276,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// 배지 없음 옵션: color가 없거나, "none"이거나, 매핑 데이터가 없으면 텍스트만 표시
|
||||
if (!displayColor || displayColor === "none" || !categoryData) {
|
||||
return (
|
||||
<span key={idx} className="text-sm">
|
||||
<span key={idx} className="shrink-0 whitespace-nowrap text-sm">
|
||||
{displayLabel}
|
||||
{idx < values.length - 1 && ", "}
|
||||
</span>
|
||||
|
|
@ -4290,7 +4290,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
backgroundColor: displayColor,
|
||||
borderColor: displayColor,
|
||||
}}
|
||||
className="text-white"
|
||||
className="shrink-0 whitespace-nowrap text-white"
|
||||
>
|
||||
{displayLabel}
|
||||
</Badge>
|
||||
|
|
|
|||
|
|
@ -2108,31 +2108,72 @@ export class ButtonActionExecutor {
|
|||
const sections: any[] = modalComponentConfig?.sections || [];
|
||||
const saveConfig = modalComponentConfig?.saveConfig || {};
|
||||
|
||||
// _tableSection_ 데이터 추출
|
||||
// 테이블 섹션 데이터 수집: DB 전체 데이터를 베이스로, 수정 데이터를 오버라이드
|
||||
const tableSectionData: Record<string, any[]> = {};
|
||||
const commonFieldsData: Record<string, any> = {};
|
||||
|
||||
// 🆕 원본 그룹 데이터 추출 (수정 모드에서 UPDATE/DELETE 추적용)
|
||||
// modalData 내부 또는 최상위 formData에서 찾음
|
||||
// 원본 그룹 데이터 추출 (수정 모드에서 UPDATE/DELETE 추적용)
|
||||
const originalGroupedData: any[] = modalData._originalGroupedData || formData._originalGroupedData || [];
|
||||
|
||||
// 1단계: DB 데이터(__tableSection_)와 수정 데이터(_tableSection_)를 별도로 수집
|
||||
const dbSectionData: Record<string, any[]> = {};
|
||||
const modifiedSectionData: Record<string, any[]> = {};
|
||||
|
||||
// 1-1: modalData(부모의 중첩 객체)에서 수집
|
||||
for (const [key, value] of Object.entries(modalData)) {
|
||||
// initializeForm: __tableSection_ (더블), 수정 시: _tableSection_ (싱글) → 통일 처리
|
||||
if (key.startsWith("_tableSection_") || key.startsWith("__tableSection_")) {
|
||||
if (Array.isArray(value)) {
|
||||
const normalizedKey = key.startsWith("__tableSection_")
|
||||
? key.replace("__tableSection_", "")
|
||||
: key.replace("_tableSection_", "");
|
||||
// 싱글 언더스코어 키(수정된 데이터)가 더블 언더스코어 키(초기 데이터)보다 우선
|
||||
if (!tableSectionData[normalizedKey] || key.startsWith("_tableSection_")) {
|
||||
tableSectionData[normalizedKey] = value as any[];
|
||||
}
|
||||
}
|
||||
if (key.startsWith("__tableSection_") && Array.isArray(value)) {
|
||||
const sectionId = key.replace("__tableSection_", "");
|
||||
dbSectionData[sectionId] = value;
|
||||
} else if (key.startsWith("_tableSection_") && Array.isArray(value)) {
|
||||
const sectionId = key.replace("_tableSection_", "");
|
||||
modifiedSectionData[sectionId] = value;
|
||||
} else if (!key.startsWith("_")) {
|
||||
commonFieldsData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 1-2: top-level formData에서도 수집 (handleBeforeFormSave가 직접 설정한 최신 데이터)
|
||||
// modalData(중첩 객체)가 아직 업데이트되지 않았을 수 있으므로 보완
|
||||
for (const [key, value] of Object.entries(formData)) {
|
||||
if (key === universalFormModalKey) continue;
|
||||
if (key.startsWith("__tableSection_") && Array.isArray(value)) {
|
||||
const sectionId = key.replace("__tableSection_", "");
|
||||
if (!dbSectionData[sectionId]) {
|
||||
dbSectionData[sectionId] = value;
|
||||
}
|
||||
} else if (key.startsWith("_tableSection_") && Array.isArray(value)) {
|
||||
const sectionId = key.replace("_tableSection_", "");
|
||||
if (!modifiedSectionData[sectionId]) {
|
||||
modifiedSectionData[sectionId] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2단계: DB 데이터를 베이스로, 수정 데이터를 아이템별로 병합하여 전체 데이터 구성
|
||||
// - DB 데이터(__tableSection_): initializeForm에서 로드한 전체 컬럼 데이터
|
||||
// - 수정 데이터(_tableSection_): _groupedData 또는 사용자 UI 수정을 통해 설정된 데이터 (일부 컬럼만 포함 가능)
|
||||
// - 병합: { ...dbItem, ...modItem } → DB 전체 컬럼 유지 + 수정된 필드만 오버라이드
|
||||
const allSectionIds = new Set([...Object.keys(dbSectionData), ...Object.keys(modifiedSectionData)]);
|
||||
|
||||
for (const sectionId of allSectionIds) {
|
||||
const dbItems = dbSectionData[sectionId] || [];
|
||||
const modItems = modifiedSectionData[sectionId];
|
||||
|
||||
if (modItems) {
|
||||
tableSectionData[sectionId] = modItems.map((modItem) => {
|
||||
if (modItem.id) {
|
||||
const dbItem = dbItems.find((db) => String(db.id) === String(modItem.id));
|
||||
if (dbItem) {
|
||||
return { ...dbItem, ...modItem };
|
||||
}
|
||||
}
|
||||
return modItem;
|
||||
});
|
||||
} else {
|
||||
tableSectionData[sectionId] = dbItems;
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블 섹션 데이터가 없고 원본 데이터도 없으면 처리하지 않음
|
||||
const hasTableSectionData = Object.values(tableSectionData).some((arr) => arr.length > 0);
|
||||
if (!hasTableSectionData && originalGroupedData.length === 0) {
|
||||
|
|
@ -2262,28 +2303,26 @@ export class ButtonActionExecutor {
|
|||
|
||||
// 각 테이블 섹션 처리
|
||||
for (const [sectionId, currentItems] of Object.entries(tableSectionData)) {
|
||||
// 🆕 해당 섹션의 설정 찾기
|
||||
const sectionConfig = sections.find((s) => s.id === sectionId);
|
||||
const targetTableName = sectionConfig?.tableConfig?.saveConfig?.targetTable;
|
||||
|
||||
// 🆕 실제 저장할 테이블 결정
|
||||
// - targetTable이 있으면 해당 테이블에 저장
|
||||
// - targetTable이 없으면 메인 테이블에 저장
|
||||
const saveTableName = targetTableName || tableName!;
|
||||
|
||||
// 섹션별 DB 원본 데이터 조회 (전체 컬럼 보장)
|
||||
// _originalTableSectionData_: initializeForm에서 DB 로드 시 저장한 원본 데이터
|
||||
const sectionOriginalKey = `_originalTableSectionData_${sectionId}`;
|
||||
const sectionOriginalData: any[] = modalData[sectionOriginalKey] || formData[sectionOriginalKey] || [];
|
||||
|
||||
// 1️⃣ 신규 품목 INSERT (id가 없는 항목)
|
||||
const newItems = currentItems.filter((item) => !item.id);
|
||||
for (const item of newItems) {
|
||||
const rowToSave = { ...commonFieldsData, ...item, ...userInfo };
|
||||
|
||||
// 내부 메타데이터 제거
|
||||
Object.keys(rowToSave).forEach((key) => {
|
||||
if (key.startsWith("_")) {
|
||||
delete rowToSave[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 🆕 메인 레코드 ID 연결 (별도 테이블에 저장하는 경우)
|
||||
if (targetTableName && mainRecordId && saveConfig.primaryKeyColumn) {
|
||||
rowToSave[saveConfig.primaryKeyColumn] = mainRecordId;
|
||||
}
|
||||
|
|
@ -2303,28 +2342,30 @@ export class ButtonActionExecutor {
|
|||
insertedCount++;
|
||||
}
|
||||
|
||||
// 2️⃣ 기존 품목 UPDATE (id가 있는 항목, 변경된 경우만)
|
||||
// 2️⃣ 기존 품목 UPDATE (id가 있는 항목)
|
||||
// 전체 데이터 기반 저장: DB 데이터(__tableSection_)를 베이스로 수정 데이터가 병합된 완전한 item 사용
|
||||
const existingItems = currentItems.filter((item) => item.id);
|
||||
for (const item of existingItems) {
|
||||
const originalItem = originalGroupedData.find((orig) => orig.id === item.id);
|
||||
// DB 원본 데이터 우선 사용 (전체 컬럼 보장), 없으면 originalGroupedData에서 탐색
|
||||
const originalItem =
|
||||
sectionOriginalData.find((orig) => String(orig.id) === String(item.id)) ||
|
||||
originalGroupedData.find((orig) => String(orig.id) === String(item.id));
|
||||
|
||||
// 마스터/디테일 분리 시: 디테일 데이터만 사용 (마스터 필드 병합 안 함)
|
||||
// 같은 테이블 시: 공통 필드도 병합 (공유 필드 업데이트 필요)
|
||||
const dataToSave = hasSeparateTargetTable ? item : { ...commonFieldsData, ...item };
|
||||
|
||||
if (!originalItem) {
|
||||
// 🆕 폴백 로직: 원본 데이터가 없어도 id가 있으면 UPDATE 시도
|
||||
// originalGroupedData 전달이 누락된 경우를 처리
|
||||
console.warn(`⚠️ [UPDATE] 원본 데이터 없음 - id가 있으므로 UPDATE 시도 (폴백): id=${item.id}`);
|
||||
// 원본 없음: 전체 데이터로 UPDATE 실행
|
||||
console.warn(`⚠️ [UPDATE] 원본 데이터 없음 - 전체 데이터로 UPDATE: id=${item.id}`);
|
||||
|
||||
// 마스터/디테일 테이블 분리 시: commonFieldsData 병합하지 않음
|
||||
// → 동명 컬럼(예: memo)이 마스터 값으로 덮어씌워지는 문제 방지
|
||||
const rowToUpdate = hasSeparateTargetTable
|
||||
? { ...item, ...userInfo }
|
||||
: { ...commonFieldsData, ...item, ...userInfo };
|
||||
const rowToUpdate = { ...dataToSave, ...userInfo };
|
||||
Object.keys(rowToUpdate).forEach((key) => {
|
||||
if (key.startsWith("_")) {
|
||||
delete rowToUpdate[key];
|
||||
}
|
||||
});
|
||||
|
||||
// id를 유지하고 UPDATE 실행
|
||||
const updateResult = await DynamicFormApi.updateFormData(item.id, {
|
||||
tableName: saveTableName,
|
||||
data: rowToUpdate,
|
||||
|
|
@ -2338,20 +2379,14 @@ export class ButtonActionExecutor {
|
|||
continue;
|
||||
}
|
||||
|
||||
// 변경 사항 확인
|
||||
// 마스터/디테일 테이블이 분리된 경우(hasSeparateTargetTable):
|
||||
// 마스터 필드(commonFieldsData)를 디테일에 병합하지 않음
|
||||
// → 동명 컬럼(예: memo)이 마스터 값으로 덮어씌워지는 문제 방지
|
||||
// 같은 테이블인 경우: 공통 필드 병합 유지 (공유 필드 업데이트 필요)
|
||||
const dataForComparison = hasSeparateTargetTable ? item : { ...commonFieldsData, ...item };
|
||||
const hasChanges = this.checkForChanges(originalItem, dataForComparison);
|
||||
// 변경 사항 확인: 원본(DB) vs 현재(병합된 전체 데이터)
|
||||
const hasChanges = this.checkForChanges(originalItem, dataToSave);
|
||||
|
||||
if (hasChanges) {
|
||||
// 변경된 필드만 추출하여 부분 업데이트
|
||||
const updateResult = await DynamicFormApi.updateFormDataPartial(
|
||||
item.id,
|
||||
originalItem,
|
||||
dataForComparison,
|
||||
dataToSave,
|
||||
saveTableName,
|
||||
);
|
||||
|
||||
|
|
@ -2360,16 +2395,11 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
|
||||
updatedCount++;
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ 삭제된 품목 DELETE (원본에는 있지만 현재에는 없는 항목)
|
||||
// 🆕 테이블 섹션별 원본 데이터 사용 (우선), 없으면 전역 originalGroupedData 사용
|
||||
const sectionOriginalKey = `_originalTableSectionData_${sectionId}`;
|
||||
const sectionOriginalData: any[] = modalData[sectionOriginalKey] || formData[sectionOriginalKey] || [];
|
||||
|
||||
// 섹션별 원본 데이터가 있으면 사용, 없으면 전역 originalGroupedData 사용
|
||||
// 섹션별 DB 원본 데이터 사용 (위에서 이미 조회), 없으면 전역 originalGroupedData 사용
|
||||
const originalDataForDelete = sectionOriginalData.length > 0 ? sectionOriginalData : originalGroupedData;
|
||||
|
||||
// ⚠️ id 타입 통일: 문자열로 변환하여 비교 (숫자 vs 문자열 불일치 방지)
|
||||
|
|
|
|||
Loading…
Reference in New Issue