설비 수정모달 데이터 안넘어오는 현상 수정

This commit is contained in:
kjs 2025-12-12 14:37:24 +09:00
parent 309d4be31d
commit 722718b7ed
3 changed files with 165 additions and 131 deletions

View File

@ -62,7 +62,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// 폼 데이터 상태 (편집 데이터로 초기화됨)
const [formData, setFormData] = useState<Record<string, any>>({});
const [originalData, setOriginalData] = useState<Record<string, any>>({});
// 🆕 그룹 데이터 상태 (같은 order_no의 모든 품목)
const [groupData, setGroupData] = useState<Record<string, any>[]>([]);
const [originalGroupData, setOriginalGroupData] = useState<Record<string, any>[]>([]);
@ -118,7 +118,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// 전역 모달 이벤트 리스너
useEffect(() => {
const handleOpenEditModal = (event: CustomEvent) => {
const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName, isCreateMode } = event.detail;
const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName, isCreateMode } =
event.detail;
setModalState({
isOpen: true,
@ -136,8 +137,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
setFormData(editData || {});
// 🆕 isCreateMode가 true이면 originalData를 빈 객체로 설정 (INSERT 모드)
// originalData가 비어있으면 INSERT, 있으면 UPDATE로 처리됨
setOriginalData(isCreateMode ? {} : (editData || {}));
setOriginalData(isCreateMode ? {} : editData || {});
if (isCreateMode) {
console.log("[EditModal] 생성 모드로 열림, 초기값:", editData);
}
@ -170,7 +171,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
useEffect(() => {
if (modalState.isOpen && modalState.screenId) {
loadScreenData(modalState.screenId);
// 🆕 그룹 데이터 조회 (groupByColumns가 있는 경우)
if (modalState.groupByColumns && modalState.groupByColumns.length > 0 && modalState.tableName) {
loadGroupData();
@ -308,7 +309,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// universal-form-modal 등에서 자체 저장 완료 후 호출된 경우 스킵
if (saveData?._saveCompleted) {
console.log("[EditModal] 자체 저장 완료된 컴포넌트에서 호출됨 - 저장 스킵");
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
if (modalState.onSave) {
try {
@ -317,7 +318,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
console.error("onSave 콜백 에러:", callbackError);
}
}
handleClose();
return;
}
@ -342,13 +343,13 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// 🆕 날짜 필드 정규화 함수 (YYYY-MM-DD 형식으로 변환)
const normalizeDateField = (value: any): string | null => {
if (!value) return null;
// ISO 8601 형식 (2025-11-26T00:00:00.000Z) 또는 Date 객체
if (value instanceof Date || typeof value === "string") {
try {
const date = new Date(value);
if (isNaN(date.getTime())) return null;
// YYYY-MM-DD 형식으로 변환
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
@ -359,7 +360,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
return null;
}
}
return null;
};
@ -380,7 +381,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
const insertData: Record<string, any> = { ...currentData };
console.log("📦 [신규 품목] 복사 직후 insertData:", insertData);
console.log("📋 [신규 품목] insertData 키 목록:", Object.keys(insertData));
delete insertData.id; // id는 자동 생성되므로 제거
// 🆕 날짜 필드 정규화 (YYYY-MM-DD 형식으로 변환)
@ -464,9 +465,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
for (const currentData of groupData) {
if (currentData.id) {
// id 기반 매칭 (인덱스 기반 X)
const originalItemData = originalGroupData.find(
(orig) => orig.id === currentData.id
);
const originalItemData = originalGroupData.find((orig) => orig.id === currentData.id);
if (!originalItemData) {
console.warn(`원본 데이터를 찾을 수 없습니다 (id: ${currentData.id})`);
@ -476,13 +475,13 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// 🆕 값 정규화 함수 (타입 통일)
const normalizeValue = (val: any, fieldName?: string): any => {
if (val === null || val === undefined || val === "") return null;
// 날짜 필드인 경우 YYYY-MM-DD 형식으로 정규화
if (fieldName && dateFields.includes(fieldName)) {
const normalizedDate = normalizeDateField(val);
return normalizedDate;
}
if (typeof val === "string" && !isNaN(Number(val))) {
// 숫자로 변환 가능한 문자열은 숫자로
return Number(val);
@ -539,9 +538,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// 3⃣ 삭제된 품목 제거 (원본에는 있지만 현재 데이터에는 없는 항목)
const currentIds = new Set(groupData.map((item) => item.id).filter(Boolean));
const deletedItems = originalGroupData.filter(
(orig) => orig.id && !currentIds.has(orig.id)
);
const deletedItems = originalGroupData.filter((orig) => orig.id && !currentIds.has(orig.id));
for (const deletedItem of deletedItems) {
console.log("🗑️ 품목 삭제:", deletedItem);
@ -549,7 +546,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
try {
const response = await dynamicFormApi.deleteFormDataFromTable(
deletedItem.id,
screenData.screenInfo.tableName
screenData.screenInfo.tableName,
);
if (response.success) {
@ -592,11 +589,11 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
// originalData가 비어있으면 INSERT, 있으면 UPDATE
const isCreateMode = Object.keys(originalData).length === 0;
if (isCreateMode) {
// INSERT 모드
console.log("[EditModal] INSERT 모드 - 새 데이터 생성:", formData);
const response = await dynamicFormApi.saveFormData({
screenId: modalState.screenId!,
tableName: screenData.screenInfo.tableName,
@ -701,10 +698,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
return (
<Dialog open={modalState.isOpen} onOpenChange={handleClose}>
<DialogContent
className={`${modalStyle.className} ${className || ""} max-w-none`}
style={modalStyle.style}
>
<DialogContent className={`${modalStyle.className} ${className || ""} max-w-none`} style={modalStyle.style}>
<DialogHeader className="shrink-0 border-b px-4 py-3">
<div className="flex items-center gap-2">
<DialogTitle className="text-base">{modalState.title || "데이터 수정"}</DialogTitle>
@ -717,7 +711,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
</div>
</DialogHeader>
<div className="flex flex-1 items-center justify-center overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-transparent">
<div className="flex flex-1 items-center justify-center overflow-y-auto [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent">
{loading ? (
<div className="flex h-full items-center justify-center">
<div className="text-center">
@ -751,7 +745,6 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
},
};
const groupedDataProp = groupData.length > 0 ? groupData : undefined;
// 🔑 첨부파일 컴포넌트가 행(레코드) 단위로 파일을 저장할 수 있도록 tableName 추가
@ -760,7 +753,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
tableName: screenData.screenInfo?.tableName, // 테이블명 추가
screenId: modalState.screenId, // 화면 ID 추가
};
// 🔍 디버깅: enrichedFormData 확인
console.log("🔑 [EditModal] enrichedFormData 생성:", {
"screenData.screenInfo": screenData.screenInfo,
@ -775,6 +768,7 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
component={adjustedComponent}
allComponents={screenData.components}
formData={enrichedFormData}
originalData={originalData} // 🆕 원본 데이터 전달 (수정 모드에서 UniversalFormModal 초기화용)
onFormDataChange={(fieldName, value) => {
// 🆕 그룹 데이터가 있으면 처리
if (groupData.length > 0) {
@ -787,14 +781,14 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
prev.map((item) => ({
...item,
[fieldName]: value,
}))
})),
);
}
} else {
setFormData((prev) => ({
...prev,
[fieldName]: value,
}));
setFormData((prev) => ({
...prev,
[fieldName]: value,
}));
}
}}
screenInfo={{

View File

@ -47,7 +47,7 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
selectedRowsData?: any[];
// 테이블 정렬 정보 (엑셀 다운로드용)
sortBy?: string;
sortOrder?: "asc" | "desc";
@ -57,10 +57,10 @@ export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
// 플로우 선택된 데이터 정보 (플로우 위젯 선택 액션용)
flowSelectedData?: any[];
flowSelectedStepId?: number | null;
// 🆕 같은 화면의 모든 컴포넌트 (TableList 자동 감지용)
allComponents?: any[];
// 🆕 부모창에서 전달된 그룹 데이터 (모달에서 부모 데이터 접근용)
groupedData?: Record<string, any>[];
}
@ -109,11 +109,11 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트
// 🆕 ScreenContext에서 splitPanelPosition 가져오기 (중첩 화면에서도 작동)
const splitPanelPosition = screenContext?.splitPanelPosition;
// 🆕 tableName이 props로 전달되지 않으면 ScreenContext에서 가져오기
const effectiveTableName = tableName || screenContext?.tableName;
const effectiveScreenId = screenId || screenContext?.screenId;
// 🆕 props에서 onSave 추출 (명시적으로 선언되지 않은 경우 ...props에서 추출)
const propsOnSave = (props as any).onSave as (() => Promise<void>) | undefined;
const finalOnSave = onSave || propsOnSave;
@ -169,10 +169,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
if (!shouldFetchStatus) return;
let isMounted = true;
const fetchStatus = async () => {
if (!isMounted) return;
try {
const response = await apiClient.post(`/table-management/tables/${statusTableName}/data`, {
page: 1,
@ -180,12 +180,12 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
search: { [statusKeyField]: userId },
autoFilter: true,
});
if (!isMounted) return;
const rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || [];
const firstRow = Array.isArray(rows) ? rows[0] : null;
if (response.data?.success && firstRow) {
const newStatus = firstRow[statusFieldName];
if (newStatus !== vehicleStatus) {
@ -206,10 +206,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 즉시 실행
setStatusLoading(true);
fetchStatus();
// 2초마다 갱신
const interval = setInterval(fetchStatus, 2000);
return () => {
isMounted = false;
clearInterval(interval);
@ -219,22 +219,22 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 버튼 비활성화 조건 계산
const isOperationButtonDisabled = useMemo(() => {
const actionConfig = component.componentConfig?.action;
if (actionConfig?.type !== "operation_control") return false;
// 1. 출발지/도착지 필수 체크
if (actionConfig?.requireLocationFields) {
const departureField = actionConfig.trackingDepartureField || "departure";
const destinationField = actionConfig.trackingArrivalField || "destination";
const departure = formData?.[departureField];
const destination = formData?.[destinationField];
// console.log("🔍 [ButtonPrimary] 출발지/도착지 체크:", {
// departureField, destinationField, departure, destination,
// buttonLabel: component.label
// console.log("🔍 [ButtonPrimary] 출발지/도착지 체크:", {
// departureField, destinationField, departure, destination,
// buttonLabel: component.label
// });
if (!departure || departure === "" || !destination || destination === "") {
// console.log("🚫 [ButtonPrimary] 출발지/도착지 미선택 → 비활성화:", component.label);
return true;
@ -246,20 +246,20 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
const statusField = actionConfig.statusCheckField || "status";
// API 조회 결과를 우선 사용 (실시간 DB 상태 반영)
const currentStatus = vehicleStatus || formData?.[statusField];
const conditionType = actionConfig.statusConditionType || "enableOn";
const conditionValues = (actionConfig.statusConditionValues || "")
.split(",")
.map((v: string) => v.trim())
.filter((v: string) => v);
// console.log("🔍 [ButtonPrimary] 상태 조건 체크:", {
// console.log("🔍 [ButtonPrimary] 상태 조건 체크:", {
// statusField,
// formDataStatus: formData?.[statusField],
// apiStatus: vehicleStatus,
// currentStatus,
// conditionType,
// conditionValues,
// currentStatus,
// conditionType,
// conditionValues,
// buttonLabel: component.label,
// });
@ -274,7 +274,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// console.log("🚫 [ButtonPrimary] 상태값 없음 → 비활성화:", component.label);
return true;
}
if (conditionValues.length > 0) {
if (conditionType === "enableOn") {
// 이 상태일 때만 활성화
@ -539,7 +539,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
*/
const handleTransferDataAction = async (actionConfig: any) => {
const dataTransferConfig = actionConfig.dataTransfer;
if (!dataTransferConfig) {
toast.error("데이터 전달 설정이 없습니다.");
return;
@ -553,15 +553,15 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
try {
// 1. 소스 컴포넌트에서 데이터 가져오기
let sourceProvider = screenContext.getDataProvider(dataTransferConfig.sourceComponentId);
// 🆕 소스 컴포넌트를 찾을 수 없으면, 현재 화면에서 테이블 리스트 자동 탐색
// (조건부 컨테이너의 다른 섹션으로 전환했을 때 이전 컴포넌트 ID가 남아있는 경우 대응)
if (!sourceProvider) {
console.log(`⚠️ [ButtonPrimary] 지정된 소스 컴포넌트를 찾을 수 없음: ${dataTransferConfig.sourceComponentId}`);
console.log(`🔍 [ButtonPrimary] 현재 화면에서 DataProvider 자동 탐색...`);
const allProviders = screenContext.getAllDataProviders();
// 테이블 리스트 우선 탐색
for (const [id, provider] of allProviders) {
if (provider.componentType === "table-list") {
@ -570,16 +570,18 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
break;
}
}
// 테이블 리스트가 없으면 첫 번째 DataProvider 사용
if (!sourceProvider && allProviders.size > 0) {
const firstEntry = allProviders.entries().next().value;
if (firstEntry) {
sourceProvider = firstEntry[1];
console.log(`✅ [ButtonPrimary] 첫 번째 DataProvider 사용: ${firstEntry[0]} (${sourceProvider.componentType})`);
console.log(
`✅ [ButtonPrimary] 첫 번째 DataProvider 사용: ${firstEntry[0]} (${sourceProvider.componentType})`,
);
}
}
if (!sourceProvider) {
toast.error("데이터를 제공할 수 있는 컴포넌트를 찾을 수 없습니다.");
return;
@ -587,12 +589,12 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
}
const rawSourceData = sourceProvider.getSelectedData();
// 🆕 배열이 아닌 경우 배열로 변환
const sourceData = Array.isArray(rawSourceData) ? rawSourceData : (rawSourceData ? [rawSourceData] : []);
const sourceData = Array.isArray(rawSourceData) ? rawSourceData : rawSourceData ? [rawSourceData] : [];
console.log("📦 소스 데이터:", { rawSourceData, sourceData, isArray: Array.isArray(rawSourceData) });
if (!sourceData || sourceData.length === 0) {
toast.warning("선택된 데이터가 없습니다.");
return;
@ -600,31 +602,32 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 1.5. 추가 데이터 소스 처리 (예: 조건부 컨테이너의 카테고리 값)
let additionalData: Record<string, any> = {};
// 방법 1: additionalSources 설정에서 가져오기
if (dataTransferConfig.additionalSources && Array.isArray(dataTransferConfig.additionalSources)) {
for (const additionalSource of dataTransferConfig.additionalSources) {
const additionalProvider = screenContext.getDataProvider(additionalSource.componentId);
if (additionalProvider) {
const additionalValues = additionalProvider.getSelectedData();
if (additionalValues && additionalValues.length > 0) {
// 첫 번째 값 사용 (조건부 컨테이너는 항상 1개)
const firstValue = additionalValues[0];
// fieldName이 지정되어 있으면 그 필드만 추출
if (additionalSource.fieldName) {
additionalData[additionalSource.fieldName] = firstValue[additionalSource.fieldName] || firstValue.condition || firstValue;
additionalData[additionalSource.fieldName] =
firstValue[additionalSource.fieldName] || firstValue.condition || firstValue;
} else {
// fieldName이 없으면 전체 객체 병합
additionalData = { ...additionalData, ...firstValue };
}
console.log("📦 추가 데이터 수집 (additionalSources):", {
sourceId: additionalSource.componentId,
fieldName: additionalSource.fieldName,
value: additionalData[additionalSource.fieldName || 'all'],
value: additionalData[additionalSource.fieldName || "all"],
});
}
}
@ -639,7 +642,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
const conditionalValue = formData.__conditionalContainerValue;
const conditionalLabel = formData.__conditionalContainerLabel;
const controlField = formData.__conditionalContainerControlField; // 🆕 제어 필드명 직접 사용
// 🆕 controlField가 있으면 그것을 필드명으로 사용 (자동 매핑!)
if (controlField) {
additionalData[controlField] = conditionalValue;
@ -651,7 +654,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
} else {
// controlField가 없으면 기존 방식: formData에서 같은 값을 가진 키 찾기
for (const [key, value] of Object.entries(formData)) {
if (value === conditionalValue && !key.startsWith('__')) {
if (value === conditionalValue && !key.startsWith("__")) {
additionalData[key] = conditionalValue;
console.log("📦 조건부 컨테이너 값 자동 포함:", {
fieldName: key,
@ -661,12 +664,12 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
break;
}
}
// 못 찾았으면 기본 필드명 사용
if (!Object.keys(additionalData).some(k => !k.startsWith('__'))) {
additionalData['condition_type'] = conditionalValue;
if (!Object.keys(additionalData).some((k) => !k.startsWith("__"))) {
additionalData["condition_type"] = conditionalValue;
console.log("📦 조건부 컨테이너 값 (기본 필드명):", {
fieldName: 'condition_type',
fieldName: "condition_type",
value: conditionalValue,
});
}
@ -698,7 +701,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 4. 매핑 규칙 적용 + 추가 데이터 병합
const mappedData = sourceData.map((row) => {
const mappedRow = applyMappingRules(row, dataTransferConfig.mappingRules || []);
// 추가 데이터를 모든 행에 포함
return {
...mappedRow,
@ -718,7 +721,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
if (dataTransferConfig.targetType === "component") {
// 같은 화면의 컴포넌트로 전달
const targetReceiver = screenContext.getDataReceiver(dataTransferConfig.targetComponentId);
if (!targetReceiver) {
toast.error(`타겟 컴포넌트를 찾을 수 없습니다: ${dataTransferConfig.targetComponentId}`);
return;
@ -730,7 +733,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
mode: dataTransferConfig.mode || "append",
mappingRules: dataTransferConfig.mappingRules || [],
});
toast.success(`${sourceData.length}개 항목이 전달되었습니다.`);
} else if (dataTransferConfig.targetType === "splitPanel") {
// 🆕 분할 패널의 반대편 화면으로 전달
@ -738,17 +741,18 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
toast.error("분할 패널 컨텍스트를 찾을 수 없습니다. 이 버튼이 분할 패널 내부에 있는지 확인하세요.");
return;
}
// 🆕 useSplitPanelPosition 훅으로 위치 가져오기 (중첩된 화면에서도 작동)
// screenId로 찾는 것은 직접 임베드된 화면에서만 작동하므로,
// screenId로 찾는 것은 직접 임베드된 화면에서만 작동하므로,
// SplitPanelPositionProvider로 전달된 위치를 우선 사용
const currentPosition = splitPanelPosition || (screenId ? splitPanelContext.getPositionByScreenId(screenId) : null);
const currentPosition =
splitPanelPosition || (screenId ? splitPanelContext.getPositionByScreenId(screenId) : null);
if (!currentPosition) {
toast.error("분할 패널 내 위치를 확인할 수 없습니다. screenId: " + screenId);
return;
}
console.log("📦 분할 패널 데이터 전달:", {
currentPosition,
splitPanelPositionFromHook: splitPanelPosition,
@ -756,14 +760,14 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
leftScreenId: splitPanelContext.leftScreenId,
rightScreenId: splitPanelContext.rightScreenId,
});
const result = await splitPanelContext.transferToOtherSide(
currentPosition,
mappedData,
dataTransferConfig.targetComponentId, // 특정 컴포넌트 지정 (선택사항)
dataTransferConfig.mode || "append"
dataTransferConfig.mode || "append",
);
if (result.success) {
toast.success(result.message);
} else {
@ -782,7 +786,6 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
if (dataTransferConfig.clearAfterTransfer) {
sourceProvider.clearSelection();
}
} catch (error: any) {
console.error("❌ 데이터 전달 실패:", error);
toast.error(error.message || "데이터 전달 중 오류가 발생했습니다.");
@ -816,16 +819,20 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 2. groupedData (부모창에서 모달로 전달된 데이터)
// 3. modalDataStore (분할 패널 등에서 선택한 데이터)
let effectiveSelectedRowsData = selectedRowsData;
// groupedData가 있으면 우선 사용 (모달에서 부모 데이터 접근)
if ((!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) && groupedData && groupedData.length > 0) {
if (
(!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) &&
groupedData &&
groupedData.length > 0
) {
effectiveSelectedRowsData = groupedData;
console.log("🔗 [ButtonPrimaryComponent] groupedData에서 부모창 데이터 가져옴:", {
count: groupedData.length,
data: groupedData,
});
}
// modalDataStore에서 선택된 데이터 가져오기 (분할 패널 등에서 선택한 데이터)
if ((!effectiveSelectedRowsData || effectiveSelectedRowsData.length === 0) && effectiveTableName) {
try {
@ -833,11 +840,17 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
const dataRegistry = useModalDataStore.getState().dataRegistry;
const modalData = dataRegistry[effectiveTableName];
if (modalData && modalData.length > 0) {
effectiveSelectedRowsData = modalData;
// modalDataStore는 {id, originalData, additionalData} 형태로 저장됨
// originalData를 추출하여 실제 행 데이터를 가져옴
effectiveSelectedRowsData = modalData.map((item: any) => {
// originalData가 있으면 그것을 사용, 없으면 item 자체 사용 (하위 호환성)
return item.originalData || item;
});
console.log("🔗 [ButtonPrimaryComponent] modalDataStore에서 선택된 데이터 가져옴:", {
tableName: effectiveTableName,
count: modalData.length,
data: modalData,
rawData: modalData,
extractedData: effectiveSelectedRowsData,
});
}
} catch (error) {
@ -847,7 +860,8 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 삭제 액션인데 선택된 데이터가 없으면 경고 메시지 표시하고 중단
const hasDataToDelete =
(effectiveSelectedRowsData && effectiveSelectedRowsData.length > 0) || (flowSelectedData && flowSelectedData.length > 0);
(effectiveSelectedRowsData && effectiveSelectedRowsData.length > 0) ||
(flowSelectedData && flowSelectedData.length > 0);
if (processedConfig.action.type === "delete" && !hasDataToDelete) {
toast.warning("삭제할 항목을 먼저 선택해주세요.");
@ -1064,15 +1078,14 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
alignItems: "center",
justifyContent: "center",
// 🔧 크기에 따른 패딩 조정
padding:
componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem",
padding: componentConfig.size === "sm" ? "0 0.75rem" : componentConfig.size === "lg" ? "0 1.25rem" : "0 1rem",
margin: "0",
lineHeight: "1.25",
boxShadow: finalDisabled ? "none" : "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
// 디자인 모드와 인터랙티브 모드 모두에서 사용자 스타일 적용 (width/height 제외)
...(component.style ? Object.fromEntries(
Object.entries(component.style).filter(([key]) => key !== 'width' && key !== 'height')
) : {}),
...(component.style
? Object.fromEntries(Object.entries(component.style).filter(([key]) => key !== "width" && key !== "height"))
: {}),
};
const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
@ -1094,7 +1107,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
<button
type={componentConfig.actionType || "button"}
disabled={finalDisabled}
className="transition-colors duration-150 hover:opacity-90 active:scale-95 transition-transform"
className="transition-colors transition-transform duration-150 hover:opacity-90 active:scale-95"
style={buttonElementStyle}
onClick={handleClick}
onDragStart={onDragStart}

View File

@ -200,29 +200,49 @@ export function UniversalFormModalComponent({
// 초기 데이터를 한 번만 캡처 (컴포넌트 마운트 시)
const capturedInitialData = useRef<Record<string, any> | undefined>(undefined);
const hasInitialized = useRef(false);
// 마지막으로 초기화된 데이터의 ID를 추적 (수정 모달에서 다른 항목 선택 시 재초기화 필요)
const lastInitializedId = useRef<string | undefined>(undefined);
// 초기화 - 최초 마운트 시에만 실행
// 초기화 - 최초 마운트 시 또는 initialData의 ID가 변경되었을 때 실행
useEffect(() => {
// 이미 초기화되었으면 스킵
if (hasInitialized.current) {
// initialData에서 ID 값 추출 (id, ID, objid 등)
const currentId = initialData?.id || initialData?.ID || initialData?.objid;
const currentIdString = currentId !== undefined ? String(currentId) : undefined;
// 이미 초기화되었고, ID가 동일하면 스킵
if (hasInitialized.current && lastInitializedId.current === currentIdString) {
return;
}
// 🆕 수정 모드: initialData에 데이터가 있으면서 ID가 변경된 경우 재초기화
if (hasInitialized.current && currentIdString && lastInitializedId.current !== currentIdString) {
console.log("[UniversalFormModal] ID 변경 감지 - 재초기화:", {
prevId: lastInitializedId.current,
newId: currentIdString,
initialData: initialData,
});
// 채번 플래그 초기화 (새 항목이므로)
numberingGeneratedRef.current = false;
isGeneratingRef.current = false;
}
// 최초 initialData 캡처 (이후 변경되어도 이 값 사용)
if (initialData && Object.keys(initialData).length > 0) {
capturedInitialData.current = JSON.parse(JSON.stringify(initialData)); // 깊은 복사
lastInitializedId.current = currentIdString;
console.log("[UniversalFormModal] 초기 데이터 캡처:", capturedInitialData.current);
}
hasInitialized.current = true;
initializeForm();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 빈 의존성 배열 - 마운트 시 한 번만 실행
}, [initialData?.id, initialData?.ID, initialData?.objid]); // ID 값 변경 시 재초기화
// config 변경 시에만 재초기화 (initialData 변경은 무시) - 채번규칙 제외
useEffect(() => {
if (!hasInitialized.current) return; // 최초 초기화 전이면 스킵
console.log('[useEffect config 변경] 재초기화 스킵 (채번 중복 방지)');
console.log("[useEffect config 변경] 재초기화 스킵 (채번 중복 방지)");
// initializeForm(); // 주석 처리 - config 변경 시 재초기화 안 함 (채번 중복 방지)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]);
@ -230,7 +250,7 @@ export function UniversalFormModalComponent({
// 컴포넌트 unmount 시 채번 플래그 초기화
useEffect(() => {
return () => {
console.log('[채번] 컴포넌트 unmount - 플래그 초기화');
console.log("[채번] 컴포넌트 unmount - 플래그 초기화");
numberingGeneratedRef.current = false;
isGeneratingRef.current = false;
};
@ -241,7 +261,7 @@ export function UniversalFormModalComponent({
useEffect(() => {
const handleBeforeFormSave = (event: Event) => {
if (!(event instanceof CustomEvent) || !event.detail?.formData) return;
// 설정에 정의된 필드 columnName 목록 수집
const configuredFields = new Set<string>();
config.sections.forEach((section) => {
@ -251,10 +271,10 @@ export function UniversalFormModalComponent({
}
});
});
console.log("[UniversalFormModal] beforeFormSave 이벤트 수신");
console.log("[UniversalFormModal] 설정된 필드 목록:", Array.from(configuredFields));
// UniversalFormModal에 설정된 필드만 병합 (채번 규칙 포함)
// 외부 formData에 이미 값이 있어도 UniversalFormModal 값으로 덮어씀
// (UniversalFormModal이 해당 필드의 주인이므로)
@ -267,7 +287,7 @@ export function UniversalFormModalComponent({
}
}
}
// 반복 섹션 데이터도 병합 (필요한 경우)
if (Object.keys(repeatSections).length > 0) {
for (const [sectionId, items] of Object.entries(repeatSections)) {
@ -277,9 +297,9 @@ export function UniversalFormModalComponent({
}
}
};
window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
return () => {
window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
};
@ -313,11 +333,18 @@ export function UniversalFormModalComponent({
// 폼 초기화
const initializeForm = useCallback(async () => {
console.log('[initializeForm] 시작');
console.log("[initializeForm] 시작");
// 캡처된 initialData 사용 (props로 전달된 initialData가 아닌)
const effectiveInitialData = capturedInitialData.current || initialData;
console.log("[initializeForm] 초기 데이터:", {
capturedInitialData: capturedInitialData.current,
initialData: initialData,
effectiveInitialData: effectiveInitialData,
hasData: effectiveInitialData && Object.keys(effectiveInitialData).length > 0,
});
const newFormData: FormDataState = {};
const newRepeatSections: { [sectionId: string]: RepeatSectionItem[] } = {};
const newCollapsed = new Set<string>();
@ -365,9 +392,9 @@ export function UniversalFormModalComponent({
setOriginalData(effectiveInitialData || {});
// 채번규칙 자동 생성
console.log('[initializeForm] generateNumberingValues 호출');
console.log("[initializeForm] generateNumberingValues 호출");
await generateNumberingValues(newFormData);
console.log('[initializeForm] 완료');
console.log("[initializeForm] 완료");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config]); // initialData는 의존성에서 제거 (capturedInitialData.current 사용)
@ -388,23 +415,23 @@ export function UniversalFormModalComponent({
// 채번규칙 자동 생성 (중복 호출 방지)
const numberingGeneratedRef = useRef(false);
const isGeneratingRef = useRef(false); // 진행 중 플래그 추가
const generateNumberingValues = useCallback(
async (currentFormData: FormDataState) => {
// 이미 생성되었거나 진행 중이면 스킵
if (numberingGeneratedRef.current) {
console.log('[채번] 이미 생성됨 - 스킵');
console.log("[채번] 이미 생성됨 - 스킵");
return;
}
if (isGeneratingRef.current) {
console.log('[채번] 생성 진행 중 - 스킵');
console.log("[채번] 생성 진행 중 - 스킵");
return;
}
isGeneratingRef.current = true; // 진행 중 표시
console.log('[채번] 생성 시작');
console.log("[채번] 생성 시작");
const updatedData = { ...currentFormData };
let hasChanges = false;
@ -436,7 +463,7 @@ export function UniversalFormModalComponent({
}
isGeneratingRef.current = false; // 진행 완료
if (hasChanges) {
setFormData(updatedData);
}