feat(UniversalFormModal): 수정 모드 INSERT/UPDATE/DELETE 지원
_groupedData를 테이블 섹션에 초기화하여 기존 품목 표시 originalGroupedData로 원본 데이터 보관하여 변경 추적 handleUniversalFormModalTableSectionSave()에 INSERT/UPDATE/DELETE 분기 로직 구현 EditModal, ConditionalSectionViewer에서 UniversalFormModal 감지 시 onSave 미전달 저장 완료 후 closeEditModal 이벤트 발생
This commit is contained in:
parent
9fb94da493
commit
a1b05b8982
|
|
@ -976,6 +976,19 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
|||
|
||||
const groupedDataProp = groupData.length > 0 ? groupData : undefined;
|
||||
|
||||
// 🆕 UniversalFormModal이 있는지 확인 (자체 저장 로직 사용)
|
||||
// 최상위 컴포넌트 또는 조건부 컨테이너 내부 화면에 universal-form-modal이 있는지 확인
|
||||
const hasUniversalFormModal = screenData.components.some(
|
||||
(c) => {
|
||||
// 최상위에 universal-form-modal이 있는 경우
|
||||
if (c.componentType === "universal-form-modal") return true;
|
||||
// 조건부 컨테이너 내부에 universal-form-modal이 있는 경우
|
||||
// (조건부 컨테이너가 있으면 내부 화면에서 universal-form-modal을 사용하는 것으로 가정)
|
||||
if (c.componentType === "conditional-container") return true;
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
// 🔑 첨부파일 컴포넌트가 행(레코드) 단위로 파일을 저장할 수 있도록 tableName 추가
|
||||
const enrichedFormData = {
|
||||
...(groupData.length > 0 ? groupData[0] : formData),
|
||||
|
|
@ -1024,7 +1037,9 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
|||
id: modalState.screenId!,
|
||||
tableName: screenData.screenInfo?.tableName,
|
||||
}}
|
||||
onSave={handleSave}
|
||||
// 🆕 UniversalFormModal이 있으면 onSave 전달 안 함 (자체 저장 로직 사용)
|
||||
// ModalRepeaterTable만 있으면 기존대로 onSave 전달 (호환성 유지)
|
||||
onSave={hasUniversalFormModal ? undefined : handleSave}
|
||||
isInModal={true}
|
||||
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
|
||||
groupedData={groupedDataProp}
|
||||
|
|
|
|||
|
|
@ -150,46 +150,54 @@ export function ConditionalSectionViewer({
|
|||
/* 실행 모드: 실제 화면 렌더링 */
|
||||
<div className="w-full">
|
||||
{/* 화면 크기만큼의 절대 위치 캔버스 */}
|
||||
<div
|
||||
className="relative mx-auto"
|
||||
style={{
|
||||
width: screenResolution?.width ? `${screenResolution.width}px` : "100%",
|
||||
height: screenResolution?.height ? `${screenResolution.height}px` : "auto",
|
||||
minHeight: "200px",
|
||||
}}
|
||||
>
|
||||
{components.map((component) => {
|
||||
const { position = { x: 0, y: 0, z: 1 }, size = { width: 200, height: 40 } } = component;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: position.x || 0,
|
||||
top: position.y || 0,
|
||||
width: size.width || 200,
|
||||
height: size.height || 40,
|
||||
zIndex: position.z || 1,
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
component={component}
|
||||
isInteractive={true}
|
||||
screenId={screenInfo?.id}
|
||||
tableName={screenInfo?.tableName}
|
||||
userId={userId}
|
||||
userName={userName}
|
||||
companyCode={user?.companyCode}
|
||||
formData={enhancedFormData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
groupedData={groupedData}
|
||||
onSave={onSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* UniversalFormModal이 있으면 onSave 전달하지 않음 (자체 저장 로직 사용) */}
|
||||
{(() => {
|
||||
const hasUniversalFormModal = components.some(
|
||||
(c) => c.componentType === "universal-form-modal"
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="relative mx-auto"
|
||||
style={{
|
||||
width: screenResolution?.width ? `${screenResolution.width}px` : "100%",
|
||||
height: screenResolution?.height ? `${screenResolution.height}px` : "auto",
|
||||
minHeight: "200px",
|
||||
}}
|
||||
>
|
||||
{components.map((component) => {
|
||||
const { position = { x: 0, y: 0, z: 1 }, size = { width: 200, height: 40 } } = component;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: position.x || 0,
|
||||
top: position.y || 0,
|
||||
width: size.width || 200,
|
||||
height: size.height || 40,
|
||||
zIndex: position.z || 1,
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
component={component}
|
||||
isInteractive={true}
|
||||
screenId={screenInfo?.id}
|
||||
tableName={screenInfo?.tableName}
|
||||
userId={userId}
|
||||
userName={userName}
|
||||
companyCode={user?.companyCode}
|
||||
formData={enhancedFormData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
groupedData={groupedData}
|
||||
onSave={hasUniversalFormModal ? undefined : onSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -339,6 +339,27 @@ export function TableSectionRenderer({
|
|||
// 날짜 일괄 적용 완료 플래그 (컬럼별로 한 번만 적용)
|
||||
const [batchAppliedFields, setBatchAppliedFields] = useState<Set<string>>(new Set());
|
||||
|
||||
// 초기 데이터 로드 완료 플래그 (무한 루프 방지)
|
||||
const initialDataLoadedRef = React.useRef(false);
|
||||
|
||||
// formData에서 초기 테이블 데이터 로드 (수정 모드에서 _groupedData 표시)
|
||||
useEffect(() => {
|
||||
// 이미 초기화되었으면 스킵
|
||||
if (initialDataLoadedRef.current) return;
|
||||
|
||||
const tableSectionKey = `_tableSection_${sectionId}`;
|
||||
const initialData = formData[tableSectionKey];
|
||||
|
||||
if (Array.isArray(initialData) && initialData.length > 0) {
|
||||
console.log("[TableSectionRenderer] 초기 데이터 로드:", {
|
||||
sectionId,
|
||||
itemCount: initialData.length,
|
||||
});
|
||||
setTableData(initialData);
|
||||
initialDataLoadedRef.current = true;
|
||||
}
|
||||
}, [sectionId, formData]);
|
||||
|
||||
// RepeaterColumnConfig로 변환
|
||||
const columns: RepeaterColumnConfig[] = (tableConfig.columns || []).map(convertToRepeaterColumn);
|
||||
|
||||
|
|
|
|||
|
|
@ -195,6 +195,10 @@ export function UniversalFormModalComponent({
|
|||
// 로딩 상태
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// 🆕 수정 모드: 원본 그룹 데이터 (INSERT/UPDATE/DELETE 추적용)
|
||||
const [originalGroupedData, setOriginalGroupedData] = useState<any[]>([]);
|
||||
const groupedDataInitializedRef = useRef(false);
|
||||
|
||||
// 삭제 확인 다이얼로그
|
||||
const [deleteDialog, setDeleteDialog] = useState<{
|
||||
open: boolean;
|
||||
|
|
@ -304,6 +308,12 @@ export function UniversalFormModalComponent({
|
|||
console.log(`[UniversalFormModal] 반복 섹션 병합: ${sectionKey}`, items);
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 수정 모드: 원본 그룹 데이터 전달 (UPDATE/DELETE 추적용)
|
||||
if (originalGroupedData.length > 0) {
|
||||
event.detail.formData._originalGroupedData = originalGroupedData;
|
||||
console.log(`[UniversalFormModal] 원본 그룹 데이터 병합: ${originalGroupedData.length}개`);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
|
||||
|
|
@ -311,7 +321,37 @@ export function UniversalFormModalComponent({
|
|||
return () => {
|
||||
window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
|
||||
};
|
||||
}, [formData, repeatSections, config.sections]);
|
||||
}, [formData, repeatSections, config.sections, originalGroupedData]);
|
||||
|
||||
// 🆕 수정 모드: _groupedData가 있으면 테이블 섹션 초기화
|
||||
useEffect(() => {
|
||||
if (!_groupedData || _groupedData.length === 0) return;
|
||||
if (groupedDataInitializedRef.current) return; // 이미 초기화됨
|
||||
|
||||
// 테이블 타입 섹션 찾기
|
||||
const tableSection = config.sections.find((s) => s.type === "table");
|
||||
if (!tableSection) {
|
||||
console.log("[UniversalFormModal] 테이블 섹션 없음 - _groupedData 무시");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[UniversalFormModal] 수정 모드 - 테이블 섹션 초기화:", {
|
||||
sectionId: tableSection.id,
|
||||
itemCount: _groupedData.length,
|
||||
});
|
||||
|
||||
// 원본 데이터 저장 (수정/삭제 추적용)
|
||||
setOriginalGroupedData(JSON.parse(JSON.stringify(_groupedData)));
|
||||
|
||||
// 테이블 섹션 데이터 설정
|
||||
const tableSectionKey = `_tableSection_${tableSection.id}`;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[tableSectionKey]: _groupedData,
|
||||
}));
|
||||
|
||||
groupedDataInitializedRef.current = true;
|
||||
}, [_groupedData, config.sections]);
|
||||
|
||||
// 필드 레벨 linkedFieldGroup 데이터 로드
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1490,6 +1490,7 @@ export class ButtonActionExecutor {
|
|||
/**
|
||||
* 🆕 Universal Form Modal 테이블 섹션 병합 저장 처리
|
||||
* 범용_폼_모달 내부의 공통 필드 + _tableSection_ 데이터를 병합하여 품목별로 저장
|
||||
* 수정 모드: INSERT/UPDATE/DELETE 지원
|
||||
*/
|
||||
private static async handleUniversalFormModalTableSectionSave(
|
||||
config: ButtonActionConfig,
|
||||
|
|
@ -1518,6 +1519,10 @@ export class ButtonActionExecutor {
|
|||
const tableSectionData: Record<string, any[]> = {};
|
||||
const commonFieldsData: Record<string, any> = {};
|
||||
|
||||
// 🆕 원본 그룹 데이터 추출 (수정 모드에서 UPDATE/DELETE 추적용)
|
||||
// modalData 내부 또는 최상위 formData에서 찾음
|
||||
const originalGroupedData: any[] = modalData._originalGroupedData || formData._originalGroupedData || [];
|
||||
|
||||
for (const [key, value] of Object.entries(modalData)) {
|
||||
if (key.startsWith("_tableSection_")) {
|
||||
const sectionId = key.replace("_tableSection_", "");
|
||||
|
|
@ -1532,11 +1537,13 @@ export class ButtonActionExecutor {
|
|||
commonFields: Object.keys(commonFieldsData),
|
||||
tableSections: Object.keys(tableSectionData),
|
||||
tableSectionCounts: Object.entries(tableSectionData).map(([k, v]) => ({ [k]: v.length })),
|
||||
originalGroupedDataCount: originalGroupedData.length,
|
||||
isEditMode: originalGroupedData.length > 0,
|
||||
});
|
||||
|
||||
// 테이블 섹션 데이터가 없으면 처리하지 않음
|
||||
// 테이블 섹션 데이터가 없고 원본 데이터도 없으면 처리하지 않음
|
||||
const hasTableSectionData = Object.values(tableSectionData).some((arr) => arr.length > 0);
|
||||
if (!hasTableSectionData) {
|
||||
if (!hasTableSectionData && originalGroupedData.length === 0) {
|
||||
console.log("⚠️ [handleUniversalFormModalTableSectionSave] 테이블 섹션 데이터 없음 - 일반 저장으로 전환");
|
||||
return { handled: false, success: false };
|
||||
}
|
||||
|
|
@ -1554,14 +1561,17 @@ export class ButtonActionExecutor {
|
|||
company_code: context.companyCode || "",
|
||||
};
|
||||
|
||||
let totalSaved = 0;
|
||||
let insertedCount = 0;
|
||||
let updatedCount = 0;
|
||||
let deletedCount = 0;
|
||||
|
||||
// 각 테이블 섹션의 품목별로 저장
|
||||
for (const [sectionId, items] of Object.entries(tableSectionData)) {
|
||||
console.log(`🔄 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 저장 시작: ${items.length}개 품목`);
|
||||
// 각 테이블 섹션 처리
|
||||
for (const [sectionId, currentItems] of Object.entries(tableSectionData)) {
|
||||
console.log(`🔄 [handleUniversalFormModalTableSectionSave] 섹션 ${sectionId} 처리 시작: ${currentItems.length}개 품목`);
|
||||
|
||||
for (const item of items) {
|
||||
// 공통 필드 + 품목 데이터 병합
|
||||
// 1️⃣ 신규 품목 INSERT (id가 없는 항목)
|
||||
const newItems = currentItems.filter((item) => !item.id);
|
||||
for (const item of newItems) {
|
||||
const rowToSave = { ...commonFieldsData, ...item, ...userInfo };
|
||||
|
||||
// 내부 메타데이터 제거
|
||||
|
|
@ -1571,9 +1581,8 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
});
|
||||
|
||||
console.log("📝 [handleUniversalFormModalTableSectionSave] 저장할 행:", rowToSave);
|
||||
console.log("➕ [INSERT] 신규 품목:", rowToSave);
|
||||
|
||||
// INSERT 실행
|
||||
const saveResult = await DynamicFormApi.saveFormData({
|
||||
screenId: screenId!,
|
||||
tableName: tableName!,
|
||||
|
|
@ -1581,19 +1590,100 @@ export class ButtonActionExecutor {
|
|||
});
|
||||
|
||||
if (!saveResult.success) {
|
||||
throw new Error(saveResult.message || "품목 저장 실패");
|
||||
throw new Error(saveResult.message || "신규 품목 저장 실패");
|
||||
}
|
||||
|
||||
totalSaved++;
|
||||
insertedCount++;
|
||||
}
|
||||
|
||||
// 2️⃣ 기존 품목 UPDATE (id가 있는 항목, 변경된 경우만)
|
||||
const existingItems = currentItems.filter((item) => item.id);
|
||||
for (const item of existingItems) {
|
||||
const originalItem = originalGroupedData.find((orig) => orig.id === item.id);
|
||||
|
||||
if (!originalItem) {
|
||||
console.warn(`⚠️ [UPDATE] 원본 데이터 없음 - INSERT로 처리: id=${item.id}`);
|
||||
// 원본이 없으면 신규로 처리
|
||||
const rowToSave = { ...commonFieldsData, ...item, ...userInfo };
|
||||
Object.keys(rowToSave).forEach((key) => {
|
||||
if (key.startsWith("_")) {
|
||||
delete rowToSave[key];
|
||||
}
|
||||
});
|
||||
delete rowToSave.id; // id 제거하여 INSERT
|
||||
|
||||
const saveResult = await DynamicFormApi.saveFormData({
|
||||
screenId: screenId!,
|
||||
tableName: tableName!,
|
||||
data: rowToSave,
|
||||
});
|
||||
|
||||
if (!saveResult.success) {
|
||||
throw new Error(saveResult.message || "품목 저장 실패");
|
||||
}
|
||||
|
||||
insertedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 변경 사항 확인 (공통 필드 포함)
|
||||
const currentDataWithCommon = { ...commonFieldsData, ...item };
|
||||
const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon);
|
||||
|
||||
if (hasChanges) {
|
||||
console.log(`🔄 [UPDATE] 품목 수정: id=${item.id}`);
|
||||
|
||||
// 변경된 필드만 추출하여 부분 업데이트
|
||||
const updateResult = await DynamicFormApi.updateFormDataPartial(
|
||||
item.id,
|
||||
originalItem,
|
||||
currentDataWithCommon,
|
||||
tableName!,
|
||||
);
|
||||
|
||||
if (!updateResult.success) {
|
||||
throw new Error(updateResult.message || "품목 수정 실패");
|
||||
}
|
||||
|
||||
updatedCount++;
|
||||
} else {
|
||||
console.log(`⏭️ [SKIP] 변경 없음: id=${item.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ 삭제된 품목 DELETE (원본에는 있지만 현재에는 없는 항목)
|
||||
const currentIds = new Set(currentItems.map((item) => item.id).filter(Boolean));
|
||||
const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(orig.id));
|
||||
|
||||
for (const deletedItem of deletedItems) {
|
||||
console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}`);
|
||||
|
||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(tableName!, deletedItem.id);
|
||||
|
||||
if (!deleteResult.success) {
|
||||
throw new Error(deleteResult.message || "품목 삭제 실패");
|
||||
}
|
||||
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ [handleUniversalFormModalTableSectionSave] 총 ${totalSaved}개 행 저장 완료`);
|
||||
toast.success(`${totalSaved}개 항목이 저장되었습니다.`);
|
||||
// 결과 메시지 생성
|
||||
const resultParts: string[] = [];
|
||||
if (insertedCount > 0) resultParts.push(`${insertedCount}개 추가`);
|
||||
if (updatedCount > 0) resultParts.push(`${updatedCount}개 수정`);
|
||||
if (deletedCount > 0) resultParts.push(`${deletedCount}개 삭제`);
|
||||
|
||||
const resultMessage = resultParts.length > 0 ? resultParts.join(", ") : "변경 사항 없음";
|
||||
|
||||
console.log(`✅ [handleUniversalFormModalTableSectionSave] 완료: ${resultMessage}`);
|
||||
toast.success(`저장 완료: ${resultMessage}`);
|
||||
|
||||
// 저장 성공 이벤트 발생
|
||||
window.dispatchEvent(new CustomEvent("saveSuccess"));
|
||||
window.dispatchEvent(new CustomEvent("refreshTable"));
|
||||
// EditModal 닫기 이벤트 발생
|
||||
window.dispatchEvent(new CustomEvent("closeEditModal"));
|
||||
|
||||
return { handled: true, success: true };
|
||||
} catch (error: any) {
|
||||
|
|
@ -1603,6 +1693,38 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 객체 간 변경 사항 확인
|
||||
*/
|
||||
private static checkForChanges(original: Record<string, any>, current: Record<string, any>): boolean {
|
||||
// 비교할 필드 목록 (메타데이터 제외)
|
||||
const fieldsToCompare = new Set([
|
||||
...Object.keys(original).filter((k) => !k.startsWith("_")),
|
||||
...Object.keys(current).filter((k) => !k.startsWith("_")),
|
||||
]);
|
||||
|
||||
for (const field of fieldsToCompare) {
|
||||
// 시스템 필드는 비교에서 제외
|
||||
if (["created_date", "updated_date", "created_by", "updated_by", "writer"].includes(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const originalValue = original[field];
|
||||
const currentValue = current[field];
|
||||
|
||||
// null/undefined 통일 처리
|
||||
const normalizedOriginal = originalValue === null || originalValue === undefined ? "" : String(originalValue);
|
||||
const normalizedCurrent = currentValue === null || currentValue === undefined ? "" : String(currentValue);
|
||||
|
||||
if (normalizedOriginal !== normalizedCurrent) {
|
||||
console.log(` 📝 변경 감지: ${field} = "${normalizedOriginal}" → "${normalizedCurrent}"`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 배치 저장 액션 처리 (SelectedItemsDetailInput용 - 새로운 데이터 구조)
|
||||
* ItemData[] → 각 품목의 details 배열을 개별 레코드로 저장
|
||||
|
|
|
|||
Loading…
Reference in New Issue