Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management

This commit is contained in:
kjs 2025-11-25 09:34:59 +09:00
commit a46a2a664f
15 changed files with 627 additions and 202 deletions

View File

@ -496,16 +496,27 @@ export class DynamicFormService {
for (const repeater of mergedRepeaterData) {
for (const item of repeater.data) {
// 헤더 + 품목을 병합
const mergedData = { ...dataToInsert, ...item };
const rawMergedData = { ...dataToInsert, ...item };
// 타입 변환
Object.keys(mergedData).forEach((columnName) => {
// 🆕 실제 테이블 컬럼만 필터링 (조인/계산 컬럼 제외)
const validColumnNames = columnInfo.map((col) => col.column_name);
const mergedData: Record<string, any> = {};
Object.keys(rawMergedData).forEach((columnName) => {
// 실제 테이블 컬럼인지 확인
if (validColumnNames.includes(columnName)) {
const column = columnInfo.find((col) => col.column_name === columnName);
if (column) {
// 타입 변환
mergedData[columnName] = this.convertValueForPostgreSQL(
mergedData[columnName],
rawMergedData[columnName],
column.data_type
);
} else {
mergedData[columnName] = rawMergedData[columnName];
}
} else {
console.log(`⚠️ 조인/계산 컬럼 제외: ${columnName} (값: ${rawMergedData[columnName]})`);
}
});

View File

@ -57,16 +57,18 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
// 폼 데이터 상태 추가
const [formData, setFormData] = useState<Record<string, any>>({});
// 연속 등록 모드 상태 (localStorage에 저장하여 리렌더링에 영향받지 않도록)
const continuousModeRef = useRef(false);
const [, setForceUpdate] = useState(0); // 강제 리렌더링용 (값은 사용하지 않음)
// 연속 등록 모드 상태 (state로 변경 - 체크박스 UI 업데이트를 위해)
const [continuousMode, setContinuousMode] = useState(false);
// 화면 리셋 키 (컴포넌트 강제 리마운트용)
const [resetKey, setResetKey] = useState(0);
// localStorage에서 연속 모드 상태 복원
useEffect(() => {
const savedMode = localStorage.getItem("screenModal_continuousMode");
if (savedMode === "true") {
continuousModeRef.current = true;
// console.log("🔄 연속 모드 복원: true");
setContinuousMode(true);
console.log("🔄 연속 모드 복원: true");
}
}, []);
@ -162,29 +164,39 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
});
setScreenData(null);
setFormData({});
continuousModeRef.current = false;
setContinuousMode(false);
localStorage.setItem("screenModal_continuousMode", "false"); // localStorage에 저장
// console.log("🔄 연속 모드 초기화: false");
console.log("🔄 연속 모드 초기화: false");
};
// 저장 성공 이벤트 처리 (연속 등록 모드 지원)
const handleSaveSuccess = () => {
const isContinuousMode = continuousModeRef.current;
// console.log("💾 저장 성공 이벤트 수신");
// console.log("📌 현재 연속 모드 상태 (ref):", isContinuousMode);
// console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode"));
const isContinuousMode = continuousMode;
console.log("💾 저장 성공 이벤트 수신");
console.log("📌 현재 연속 모드 상태:", isContinuousMode);
console.log("📌 localStorage:", localStorage.getItem("screenModal_continuousMode"));
if (isContinuousMode) {
// 연속 모드: 폼만 초기화하고 모달은 유지
// console.log("✅ 연속 모드 활성화 - 폼만 초기화");
console.log("✅ 연속 모드 활성화 - 폼 초기화 및 화면 리셋");
// 폼만 초기화 (연속 모드 상태는 localStorage에 저장되어 있으므로 유지됨)
// 1. 폼 데이터 초기화
setFormData({});
// 2. 리셋 키 변경 (컴포넌트 강제 리마운트)
setResetKey(prev => prev + 1);
console.log("🔄 resetKey 증가 - 컴포넌트 리마운트");
// 3. 화면 데이터 다시 로드 (채번 규칙 새로 생성)
if (modalState.screenId) {
console.log("🔄 화면 데이터 다시 로드:", modalState.screenId);
loadScreenData(modalState.screenId);
}
toast.success("저장되었습니다. 계속 입력하세요.");
} else {
// 일반 모드: 모달 닫기
// console.log("❌ 일반 모드 - 모달 닫기");
console.log("❌ 일반 모드 - 모달 닫기");
handleCloseModal();
}
};
@ -198,7 +210,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
window.removeEventListener("closeSaveModal", handleCloseModal);
window.removeEventListener("saveSuccessInModal", handleSaveSuccess);
};
}, []); // 의존성 제거 (ref 사용으로 최신 상태 참조)
}, [continuousMode]); // continuousMode 의존성 추가
// 화면 데이터 로딩
useEffect(() => {
@ -415,18 +427,21 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
setFormData({}); // 폼 데이터 초기화
};
// 모달 크기 설정 - 화면 내용에 맞게 동적 조정
// 모달 크기 설정 - 화면관리 설정 크기 + 헤더/푸터
const getModalStyle = () => {
if (!screenDimensions) {
return {
className: "w-fit min-w-[400px] max-w-4xl max-h-[90vh] overflow-hidden p-0",
style: {},
style: undefined, // undefined로 변경 - defaultWidth/defaultHeight 사용
};
}
// 헤더 높이를 최소화 (제목 영역만)
const headerHeight = 60; // DialogHeader 최소 높이 (타이틀 + 최소 패딩)
const totalHeight = screenDimensions.height + headerHeight;
// 화면관리에서 설정한 크기 = 컨텐츠 영역 크기
// 실제 모달 크기 = 컨텐츠 + 헤더 + 연속등록 체크박스
const headerHeight = 60; // DialogHeader (타이틀 + 패딩)
const footerHeight = 52; // 연속 등록 모드 체크박스 영역
const totalHeight = screenDimensions.height + headerHeight + footerHeight;
return {
className: "overflow-hidden p-0",
@ -504,7 +519,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
<ResizableDialog open={modalState.isOpen} onOpenChange={handleClose}>
<ResizableDialogContent
className={`${modalStyle.className} ${className || ""}`}
style={modalStyle.style}
{...(modalStyle.style && { style: modalStyle.style })} // undefined일 때는 prop 자체를 전달하지 않음
defaultWidth={600}
defaultHeight={800}
minWidth={500}
@ -530,7 +545,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
</div>
</ResizableDialogHeader>
<div className="flex-1 overflow-auto p-6">
<div className="flex-1 overflow-auto">
{loading ? (
<div className="flex h-full items-center justify-center">
<div className="text-center">
@ -568,7 +583,7 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
return (
<InteractiveScreenViewerDynamic
key={component.id}
key={`${component.id}-${resetKey}`}
component={adjustedComponent}
allComponents={screenData.components}
formData={formData}
@ -607,13 +622,12 @@ export const ScreenModal: React.FC<ScreenModalProps> = ({ className }) => {
<div className="flex items-center gap-2">
<Checkbox
id="continuous-mode"
checked={continuousModeRef.current}
checked={continuousMode}
onCheckedChange={(checked) => {
const isChecked = checked === true;
continuousModeRef.current = isChecked;
setContinuousMode(isChecked);
localStorage.setItem("screenModal_continuousMode", String(isChecked));
setForceUpdate((prev) => prev + 1); // 체크박스 UI 업데이트를 위한 강제 리렌더링
// console.log("🔄 연속 모드 변경:", isChecked);
console.log("🔄 연속 모드 변경:", isChecked);
}}
/>
<Label htmlFor="continuous-mode" className="cursor-pointer text-sm font-normal select-none">

View File

@ -24,6 +24,8 @@ interface EditModalState {
modalSize: "sm" | "md" | "lg" | "xl";
editData: Record<string, any>;
onSave?: () => void;
groupByColumns?: string[]; // 🆕 그룹핑 컬럼 (예: ["order_no"])
tableName?: string; // 🆕 테이블명 (그룹 조회용)
}
interface EditModalProps {
@ -40,6 +42,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
modalSize: "md",
editData: {},
onSave: undefined,
groupByColumns: undefined,
tableName: undefined,
});
const [screenData, setScreenData] = useState<{
@ -58,6 +62,10 @@ 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>[]>([]);
// 화면의 실제 크기 계산 함수 (ScreenModal과 동일)
const calculateScreenDimensions = (components: ComponentData[]) => {
@ -92,25 +100,25 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
const contentWidth = maxX - minX;
const contentHeight = maxY - minY;
// 적절한 여백 추가
const paddingX = 40;
const paddingY = 40;
// 적절한 여백 추가 (주석처리 - 사용자 설정 크기 그대로 사용)
// const paddingX = 40;
// const paddingY = 40;
const finalWidth = Math.max(contentWidth + paddingX, 400);
const finalHeight = Math.max(contentHeight + paddingY, 300);
const finalWidth = Math.max(contentWidth, 400); // padding 제거
const finalHeight = Math.max(contentHeight, 300); // padding 제거
return {
width: Math.min(finalWidth, window.innerWidth * 0.95),
height: Math.min(finalHeight, window.innerHeight * 0.9),
offsetX: Math.max(0, minX - paddingX / 2),
offsetY: Math.max(0, minY - paddingY / 2),
offsetX: Math.max(0, minX), // paddingX 제거
offsetY: Math.max(0, minY), // paddingY 제거
};
};
// 전역 모달 이벤트 리스너
useEffect(() => {
const handleOpenEditModal = (event: CustomEvent) => {
const { screenId, title, description, modalSize, editData, onSave } = event.detail;
const { screenId, title, description, modalSize, editData, onSave, groupByColumns, tableName } = event.detail;
setModalState({
isOpen: true,
@ -120,6 +128,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
modalSize: modalSize || "lg",
editData: editData || {},
onSave,
groupByColumns, // 🆕 그룹핑 컬럼
tableName, // 🆕 테이블명
});
// 편집 데이터로 폼 데이터 초기화
@ -154,9 +164,78 @@ 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();
}
}
}, [modalState.isOpen, modalState.screenId]);
// 🆕 그룹 데이터 조회 함수
const loadGroupData = async () => {
if (!modalState.tableName || !modalState.groupByColumns || modalState.groupByColumns.length === 0) {
console.warn("테이블명 또는 그룹핑 컬럼이 없습니다.");
return;
}
try {
console.log("🔍 그룹 데이터 조회 시작:", {
tableName: modalState.tableName,
groupByColumns: modalState.groupByColumns,
editData: modalState.editData,
});
// 그룹핑 컬럼 값 추출 (예: order_no = "ORD-20251124-001")
const groupValues: Record<string, any> = {};
modalState.groupByColumns.forEach((column) => {
if (modalState.editData[column]) {
groupValues[column] = modalState.editData[column];
}
});
if (Object.keys(groupValues).length === 0) {
console.warn("그룹핑 컬럼 값이 없습니다:", modalState.groupByColumns);
return;
}
console.log("🔍 그룹 조회 요청:", {
tableName: modalState.tableName,
groupValues,
});
// 같은 그룹의 모든 레코드 조회 (entityJoinApi 사용)
const { entityJoinApi } = await import("@/lib/api/entityJoin");
const response = await entityJoinApi.getTableDataWithJoins(modalState.tableName, {
page: 1,
size: 1000,
search: groupValues, // search 파라미터로 전달 (백엔드에서 WHERE 조건으로 처리)
enableEntityJoin: true,
});
console.log("🔍 그룹 조회 응답:", response);
// entityJoinApi는 배열 또는 { data: [] } 형식으로 반환
const dataArray = Array.isArray(response) ? response : response?.data || [];
if (dataArray.length > 0) {
console.log("✅ 그룹 데이터 조회 성공:", dataArray);
setGroupData(dataArray);
setOriginalGroupData(JSON.parse(JSON.stringify(dataArray))); // Deep copy
toast.info(`${dataArray.length}개의 관련 품목을 불러왔습니다.`);
} else {
console.warn("그룹 데이터가 없습니다:", response);
setGroupData([modalState.editData]); // 기본값: 선택된 행만
setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]);
}
} catch (error: any) {
console.error("❌ 그룹 데이터 조회 오류:", error);
toast.error("관련 데이터를 불러오는 중 오류가 발생했습니다.");
setGroupData([modalState.editData]); // 기본값: 선택된 행만
setOriginalGroupData([JSON.parse(JSON.stringify(modalState.editData))]);
}
};
const loadScreenData = async (screenId: number) => {
try {
setLoading(true);
@ -208,10 +287,14 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
modalSize: "md",
editData: {},
onSave: undefined,
groupByColumns: undefined,
tableName: undefined,
});
setScreenData(null);
setFormData({});
setOriginalData({});
setGroupData([]); // 🆕
setOriginalGroupData([]); // 🆕
};
// 저장 버튼 클릭 시 - UPDATE 액션 실행
@ -222,7 +305,104 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
try {
// 🆕 그룹 데이터가 있는 경우: 모든 품목 일괄 수정
if (groupData.length > 0) {
console.log("🔄 그룹 데이터 일괄 수정 시작:", {
groupDataLength: groupData.length,
originalGroupDataLength: originalGroupData.length,
});
let updatedCount = 0;
for (let i = 0; i < groupData.length; i++) {
const currentData = groupData[i];
const originalItemData = originalGroupData[i];
if (!originalItemData) {
console.warn(`원본 데이터가 없습니다 (index: ${i})`);
continue;
}
// 변경된 필드만 추출
const changedData: Record<string, any> = {};
// 🆕 sales_order_mng 테이블의 실제 컬럼만 포함 (조인된 컬럼 제외)
const salesOrderColumns = [
"id",
"order_no",
"customer_code",
"customer_name",
"order_date",
"delivery_date",
"item_code",
"quantity",
"unit_price",
"amount",
"status",
"notes",
"created_at",
"updated_at",
"company_code",
];
Object.keys(currentData).forEach((key) => {
// sales_order_mng 테이블의 컬럼만 처리 (조인 컬럼 제외)
if (!salesOrderColumns.includes(key)) {
return;
}
if (currentData[key] !== originalItemData[key]) {
changedData[key] = currentData[key];
}
});
// 변경사항이 없으면 스킵
if (Object.keys(changedData).length === 0) {
console.log(`변경사항 없음 (index: ${i})`);
continue;
}
// 기본키 확인
const recordId = originalItemData.id || Object.values(originalItemData)[0];
// UPDATE 실행
const response = await dynamicFormApi.updateFormDataPartial(
recordId,
originalItemData,
changedData,
screenData.screenInfo.tableName,
);
if (response.success) {
updatedCount++;
console.log(`✅ 품목 ${i + 1} 수정 성공 (id: ${recordId})`);
} else {
console.error(`❌ 품목 ${i + 1} 수정 실패 (id: ${recordId}):`, response.message);
}
}
if (updatedCount > 0) {
toast.success(`${updatedCount}개의 품목이 수정되었습니다.`);
// 부모 컴포넌트의 onSave 콜백 실행 (테이블 새로고침)
if (modalState.onSave) {
try {
modalState.onSave();
} catch (callbackError) {
console.error("⚠️ onSave 콜백 에러:", callbackError);
}
}
handleClose();
} else {
toast.info("변경된 내용이 없습니다.");
handleClose();
}
return;
}
// 기존 로직: 단일 레코드 수정
const changedData: Record<string, any> = {};
Object.keys(formData).forEach((key) => {
if (formData[key] !== originalData[key]) {
@ -269,16 +449,18 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
}
};
// 모달 크기 설정 - ScreenModal과 동일
// 모달 크기 설정 - 화면관리 설정 크기 + 헤더
const getModalStyle = () => {
if (!screenDimensions) {
return {
className: "w-fit min-w-[400px] max-w-4xl max-h-[90vh] overflow-hidden p-0",
style: {},
style: undefined, // undefined로 변경 - defaultWidth/defaultHeight 사용
};
}
const headerHeight = 60;
// 화면관리에서 설정한 크기 = 컨텐츠 영역 크기
// 실제 모달 크기 = 컨텐츠 + 헤더
const headerHeight = 60; // DialogHeader
const totalHeight = screenDimensions.height + headerHeight;
return {
@ -339,6 +521,13 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
maxHeight: "100%",
}}
>
{/* 🆕 그룹 데이터가 있으면 안내 메시지 표시 */}
{groupData.length > 1 && (
<div className="absolute left-4 top-4 z-10 rounded-md bg-blue-50 px-3 py-2 text-xs text-blue-700 shadow-sm">
{groupData.length}
</div>
)}
{screenData.components.map((component) => {
// 컴포넌트 위치를 offset만큼 조정
const offsetX = screenDimensions?.offsetX || 0;
@ -353,23 +542,51 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
},
};
// 🔍 디버깅: 컴포넌트 렌더링 시점의 groupData 확인
if (component.id === screenData.components[0]?.id) {
console.log("🔍 [EditModal] InteractiveScreenViewerDynamic props:", {
componentId: component.id,
groupDataLength: groupData.length,
groupData: groupData,
formData: groupData.length > 0 ? groupData[0] : formData,
});
}
return (
<InteractiveScreenViewerDynamic
key={component.id}
component={adjustedComponent}
allComponents={screenData.components}
formData={formData}
formData={groupData.length > 0 ? groupData[0] : formData}
onFormDataChange={(fieldName, value) => {
// 🆕 그룹 데이터가 있으면 처리
if (groupData.length > 0) {
// ModalRepeaterTable의 경우 배열 전체를 받음
if (Array.isArray(value)) {
setGroupData(value);
} else {
// 일반 필드는 모든 항목에 동일하게 적용
setGroupData((prev) =>
prev.map((item) => ({
...item,
[fieldName]: value,
}))
);
}
} else {
setFormData((prev) => ({
...prev,
[fieldName]: value,
}));
}
}}
screenInfo={{
id: modalState.screenId!,
tableName: screenData.screenInfo?.tableName,
}}
onSave={handleSave}
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
groupedData={groupData.length > 0 ? groupData : undefined}
/>
);
})}

View File

@ -46,6 +46,8 @@ interface InteractiveScreenViewerProps {
userId?: string;
userName?: string;
companyCode?: string;
// 🆕 그룹 데이터 (EditModal에서 전달)
groupedData?: Record<string, any>[];
}
export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerProps> = ({
@ -61,6 +63,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
userId: externalUserId,
userName: externalUserName,
companyCode: externalCompanyCode,
groupedData,
}) => {
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
const { userName: authUserName, user: authUser } = useAuth();
@ -332,6 +335,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
console.log("🔍 테이블에서 선택된 행 데이터:", selectedData);
setSelectedRowsData(selectedData);
}}
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
groupedData={groupedData}
flowSelectedData={flowSelectedData}
flowSelectedStepId={flowSelectedStepId}
onFlowSelectedDataChange={(selectedData, stepId) => {

View File

@ -216,10 +216,16 @@ export const SaveModal: React.FC<SaveModalProps> = ({
return y + height;
}));
const padding = 40;
// 컨텐츠 영역 크기 (화면관리 설정 크기)
const contentWidth = Math.max(maxX, 400);
const contentHeight = Math.max(maxY, 300);
// 실제 모달 크기 = 컨텐츠 + 헤더
const headerHeight = 60; // DialogHeader
return {
width: Math.max(maxX + padding, 400),
height: Math.max(maxY + padding, 300),
width: contentWidth,
height: contentHeight + headerHeight, // 헤더 높이 포함
};
};
@ -229,8 +235,12 @@ export const SaveModal: React.FC<SaveModalProps> = ({
<ResizableDialog open={isOpen} onOpenChange={(open) => !isSaving && !open && onClose()}>
<ResizableDialogContent
modalId={`save-modal-${screenId}`}
defaultWidth={dynamicSize.width + 48}
defaultHeight={dynamicSize.height + 120}
style={{
width: `${dynamicSize.width}px`,
height: `${dynamicSize.height}px`, // 화면관리 설정 크기 그대로 사용
}}
defaultWidth={600} // 폴백용 기본값
defaultHeight={400} // 폴백용 기본값
minWidth={400}
minHeight={300}
className="gap-0 p-0"

View File

@ -122,6 +122,10 @@ const ResizableDialogContent = React.forwardRef<
// 1순위: userStyle에서 크기 추출 (화면관리에서 지정한 크기 - 항상 초기값으로 사용)
if (userStyle) {
console.log("🔍 userStyle 감지:", userStyle);
console.log("🔍 userStyle.width 타입:", typeof userStyle.width, "값:", userStyle.width);
console.log("🔍 userStyle.height 타입:", typeof userStyle.height, "값:", userStyle.height);
const styleWidth = typeof userStyle.width === 'string'
? parseInt(userStyle.width)
: userStyle.width;
@ -129,24 +133,41 @@ const ResizableDialogContent = React.forwardRef<
? parseInt(userStyle.height)
: userStyle.height;
console.log("📏 파싱된 크기:", {
styleWidth,
styleHeight,
"styleWidth truthy?": !!styleWidth,
"styleHeight truthy?": !!styleHeight,
minWidth,
maxWidth,
minHeight,
maxHeight
});
if (styleWidth && styleHeight) {
return {
const finalSize = {
width: Math.max(minWidth, Math.min(maxWidth, styleWidth)),
height: Math.max(minHeight, Math.min(maxHeight, styleHeight)),
};
console.log("✅ userStyle 크기 사용:", finalSize);
return finalSize;
} else {
console.log("❌ styleWidth 또는 styleHeight가 falsy:", { styleWidth, styleHeight });
}
}
// 2순위: 현재 렌더링된 크기 사용
if (contentRef.current) {
const rect = contentRef.current.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
return {
width: Math.max(minWidth, Math.min(maxWidth, rect.width)),
height: Math.max(minHeight, Math.min(maxHeight, rect.height)),
};
}
}
console.log("⚠️ userStyle 없음, defaultWidth/defaultHeight 사용:", { defaultWidth, defaultHeight });
// 2순위: 현재 렌더링된 크기 사용 (주석처리 - 모달이 열린 후 늘어나는 현상 방지)
// if (contentRef.current) {
// const rect = contentRef.current.getBoundingClientRect();
// if (rect.width > 0 && rect.height > 0) {
// return {
// width: Math.max(minWidth, Math.min(maxWidth, rect.width)),
// height: Math.max(minHeight, Math.min(maxHeight, rect.height)),
// };
// }
// }
// 3순위: defaultWidth/defaultHeight 사용
return { width: defaultWidth, height: defaultHeight };
@ -156,6 +177,58 @@ const ResizableDialogContent = React.forwardRef<
const [isResizing, setIsResizing] = React.useState(false);
const [resizeDirection, setResizeDirection] = React.useState<string>("");
const [isInitialized, setIsInitialized] = React.useState(false);
// userStyle이 변경되면 크기 업데이트 (화면 데이터 로딩 완료 시)
React.useEffect(() => {
// 1. localStorage에서 사용자가 리사이징한 크기 확인
let savedSize: { width: number; height: number; userResized: boolean } | null = null;
if (effectiveModalId && typeof window !== 'undefined') {
try {
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
const saved = localStorage.getItem(storageKey);
if (saved) {
const parsed = JSON.parse(saved);
if (parsed.userResized) {
savedSize = {
width: Math.max(minWidth, Math.min(maxWidth, parsed.width)),
height: Math.max(minHeight, Math.min(maxHeight, parsed.height)),
userResized: true,
};
console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
}
}
} catch (error) {
console.error("❌ 모달 크기 복원 실패:", error);
}
}
// 2. 우선순위: 사용자 리사이징 > userStyle > 기본값
if (savedSize && savedSize.userResized) {
// 사용자가 리사이징한 크기 우선
setSize({ width: savedSize.width, height: savedSize.height });
setUserResized(true);
console.log("✅ 사용자 리사이징 크기 적용:", savedSize);
} else if (userStyle && userStyle.width && userStyle.height) {
// 화면관리에서 설정한 크기
const styleWidth = typeof userStyle.width === 'string'
? parseInt(userStyle.width)
: userStyle.width;
const styleHeight = typeof userStyle.height === 'string'
? parseInt(userStyle.height)
: userStyle.height;
if (styleWidth && styleHeight) {
const newSize = {
width: Math.max(minWidth, Math.min(maxWidth, styleWidth)),
height: Math.max(minHeight, Math.min(maxHeight, styleHeight)),
};
console.log("🔄 userStyle 크기 적용:", newSize);
setSize(newSize);
}
}
}, [userStyle, minWidth, maxWidth, minHeight, maxHeight, effectiveModalId, userId]);
const [lastModalId, setLastModalId] = React.useState<string | null>(null);
const [userResized, setUserResized] = React.useState(false); // 사용자가 실제로 리사이징했는지 추적
@ -192,97 +265,98 @@ const ResizableDialogContent = React.forwardRef<
}, [effectiveModalId, lastModalId, isInitialized]);
// 모달이 열릴 때 초기 크기 설정 (localStorage와 내용 크기 중 큰 값 사용)
React.useEffect(() => {
// console.log("🔍 초기 크기 설정 useEffect 실행:", { isInitialized, hasContentRef: !!contentRef.current, effectiveModalId });
if (!isInitialized) {
// 내용의 실제 크기 측정 (약간의 지연 후, contentRef가 준비될 때까지 대기)
// 여러 번 시도하여 contentRef가 준비될 때까지 대기
let attempts = 0;
const maxAttempts = 10;
const measureContent = () => {
attempts++;
// scrollHeight/scrollWidth를 사용하여 실제 내용 크기 측정 (스크롤 포함)
let contentWidth = defaultWidth;
let contentHeight = defaultHeight;
if (contentRef.current) {
// scrollHeight/scrollWidth 그대로 사용 (여유 공간 제거)
contentWidth = contentRef.current.scrollWidth || defaultWidth;
contentHeight = contentRef.current.scrollHeight || defaultHeight;
// console.log("📏 모달 내용 크기 측정:", { attempt: attempts, scrollWidth: contentRef.current.scrollWidth, scrollHeight: contentRef.current.scrollHeight, clientWidth: contentRef.current.clientWidth, clientHeight: contentRef.current.clientHeight, contentWidth, contentHeight });
} else {
// console.log("⚠️ contentRef 없음, 재시도:", { attempt: attempts, maxAttempts, defaultWidth, defaultHeight });
// contentRef가 아직 없으면 재시도
if (attempts < maxAttempts) {
setTimeout(measureContent, 100);
return;
}
}
// 패딩 추가 (p-6 * 2 = 48px)
const paddingAndMargin = 48;
const initialSize = getInitialSize();
// 내용 크기 기반 최소 크기 계산
const contentBasedSize = {
width: Math.max(minWidth, Math.min(maxWidth, Math.max(contentWidth + paddingAndMargin, initialSize.width))),
height: Math.max(minHeight, Math.min(maxHeight, Math.max(contentHeight + paddingAndMargin, initialSize.height))),
};
// console.log("📐 내용 기반 크기:", contentBasedSize);
// localStorage에서 저장된 크기 확인
let finalSize = contentBasedSize;
if (effectiveModalId && typeof window !== 'undefined') {
try {
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
const saved = localStorage.getItem(storageKey);
// console.log("📦 localStorage 확인:", { effectiveModalId, userId, storageKey, saved: saved ? "있음" : "없음" });
if (saved) {
const parsed = JSON.parse(saved);
// userResized 플래그 확인
if (parsed.userResized) {
const savedSize = {
width: Math.max(minWidth, Math.min(maxWidth, parsed.width)),
height: Math.max(minHeight, Math.min(maxHeight, parsed.height)),
};
// console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
// ✅ 중요: 사용자가 명시적으로 리사이징한 경우, 사용자 크기를 우선 사용
// (사용자가 의도적으로 작게 만든 것을 존중)
finalSize = savedSize;
setUserResized(true);
// console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", { savedSize, contentBasedSize, finalSize, note: "사용자가 리사이징한 크기를 그대로 사용합니다" });
} else {
// console.log(" 자동 계산된 크기는 무시, 내용 크기 사용");
}
} else {
// console.log(" localStorage에 저장된 크기 없음, 내용 크기 사용");
}
} catch (error) {
// console.error("❌ 모달 크기 복원 실패:", error);
}
}
setSize(finalSize);
setIsInitialized(true);
};
// 첫 시도는 300ms 후에 시작
setTimeout(measureContent, 300);
}
}, [isInitialized, getInitialSize, effectiveModalId, userId, minWidth, maxWidth, minHeight, maxHeight, defaultWidth, defaultHeight]);
// 주석처리 - 사용자가 설정한 크기(userStyle)만 사용하도록 변경
// React.useEffect(() => {
// // console.log("🔍 초기 크기 설정 useEffect 실행:", { isInitialized, hasContentRef: !!contentRef.current, effectiveModalId });
//
// if (!isInitialized) {
// // 내용의 실제 크기 측정 (약간의 지연 후, contentRef가 준비될 때까지 대기)
// // 여러 번 시도하여 contentRef가 준비될 때까지 대기
// let attempts = 0;
// const maxAttempts = 10;
//
// const measureContent = () => {
// attempts++;
//
// // scrollHeight/scrollWidth를 사용하여 실제 내용 크기 측정 (스크롤 포함)
// let contentWidth = defaultWidth;
// let contentHeight = defaultHeight;
//
// // if (contentRef.current) {
// // // scrollHeight/scrollWidth 그대로 사용 (여유 공간 제거)
// // contentWidth = contentRef.current.scrollWidth || defaultWidth;
// // contentHeight = contentRef.current.scrollHeight || defaultHeight;
// //
// // // console.log("📏 모달 내용 크기 측정:", { attempt: attempts, scrollWidth: contentRef.current.scrollWidth, scrollHeight: contentRef.current.scrollHeight, clientWidth: contentRef.current.clientWidth, clientHeight: contentRef.current.clientHeight, contentWidth, contentHeight });
// // } else {
// // // console.log("⚠️ contentRef 없음, 재시도:", { attempt: attempts, maxAttempts, defaultWidth, defaultHeight });
// //
// // // contentRef가 아직 없으면 재시도
// // if (attempts < maxAttempts) {
// // setTimeout(measureContent, 100);
// // return;
// // }
// // }
//
// // 패딩 추가 (p-6 * 2 = 48px)
// const paddingAndMargin = 48;
// const initialSize = getInitialSize();
//
// // 내용 크기 기반 최소 크기 계산
// const contentBasedSize = {
// width: Math.max(minWidth, Math.min(maxWidth, Math.max(contentWidth + paddingAndMargin, initialSize.width))),
// height: Math.max(minHeight, Math.min(maxHeight, Math.max(contentHeight + paddingAndMargin, initialSize.height))),
// };
//
// // console.log("📐 내용 기반 크기:", contentBasedSize);
//
// // localStorage에서 저장된 크기 확인
// let finalSize = contentBasedSize;
//
// if (effectiveModalId && typeof window !== 'undefined') {
// try {
// const storageKey = `modal_size_${effectiveModalId}_${userId}`;
// const saved = localStorage.getItem(storageKey);
//
// // console.log("📦 localStorage 확인:", { effectiveModalId, userId, storageKey, saved: saved ? "있음" : "없음" });
//
// if (saved) {
// const parsed = JSON.parse(saved);
//
// // userResized 플래그 확인
// if (parsed.userResized) {
// const savedSize = {
// width: Math.max(minWidth, Math.min(maxWidth, parsed.width)),
// height: Math.max(minHeight, Math.min(maxHeight, parsed.height)),
// };
//
// // console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
//
// // ✅ 중요: 사용자가 명시적으로 리사이징한 경우, 사용자 크기를 우선 사용
// // (사용자가 의도적으로 작게 만든 것을 존중)
// finalSize = savedSize;
// setUserResized(true);
//
// // console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", { savedSize, contentBasedSize, finalSize, note: "사용자가 리사이징한 크기를 그대로 사용합니다" });
// } else {
// // console.log(" 자동 계산된 크기는 무시, 내용 크기 사용");
// }
// } else {
// // console.log(" localStorage에 저장된 크기 없음, 내용 크기 사용");
// }
// } catch (error) {
// // console.error("❌ 모달 크기 복원 실패:", error);
// }
// }
//
// setSize(finalSize);
// setIsInitialized(true);
// };
//
// // 첫 시도는 300ms 후에 시작
// setTimeout(measureContent, 300);
// }
// }, [isInitialized, getInitialSize, effectiveModalId, userId, minWidth, maxWidth, minHeight, maxHeight, defaultWidth, defaultHeight]);
const startResize = (direction: string) => (e: React.MouseEvent) => {
e.preventDefault();
@ -433,6 +507,37 @@ const ResizableDialogContent = React.forwardRef<
onMouseDown={startResize("nw")}
/>
{/* 리셋 버튼 (사용자가 리사이징한 경우만 표시) */}
{userResized && (
<button
onClick={() => {
// localStorage에서 저장된 크기 삭제
if (effectiveModalId && typeof window !== 'undefined') {
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
localStorage.removeItem(storageKey);
console.log("🗑️ 저장된 모달 크기 삭제:", storageKey);
}
// 화면관리 설정 크기로 복원
const initialSize = getInitialSize();
setSize(initialSize);
setUserResized(false);
console.log("🔄 기본 크기로 리셋:", initialSize);
}}
className="absolute right-12 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
style={{ zIndex: 20 }}
title="기본 크기로 리셋"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
<path d="M3 21v-5h5"/>
</svg>
<span className="sr-only"> </span>
</button>
)}
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
style={{ zIndex: 20 }}

View File

@ -107,6 +107,8 @@ export interface DynamicComponentRendererProps {
onClose?: () => void;
// 테이블 선택된 행 정보 (다중 선택 액션용)
selectedRows?: any[];
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
groupedData?: Record<string, any>[];
selectedRowsData?: any[];
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
// 테이블 정렬 정보 (엑셀 다운로드용)
@ -279,7 +281,17 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// modal-repeater-table은 배열 데이터를 다루므로 빈 배열로 초기화
let currentValue;
if (componentType === "modal-repeater-table") {
currentValue = formData?.[fieldName] || [];
// 🆕 EditModal에서 전달된 groupedData가 있으면 우선 사용
currentValue = props.groupedData || formData?.[fieldName] || [];
// 디버깅 로그
console.log("🔍 [DynamicComponentRenderer] ModalRepeaterTable value 설정:", {
hasGroupedData: !!props.groupedData,
groupedDataLength: props.groupedData?.length || 0,
fieldName,
formDataValue: formData?.[fieldName],
finalValueLength: Array.isArray(currentValue) ? currentValue.length : 0,
});
} else {
currentValue = formData?.[fieldName] || "";
}
@ -380,6 +392,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
isPreview,
// 디자인 모드 플래그 전달 - isPreview와 명확히 구분
isDesignMode: props.isDesignMode !== undefined ? props.isDesignMode : false,
// 🆕 그룹 데이터 전달 (EditModal → ConditionalContainer → ModalRepeaterTable)
groupedData: props.groupedData,
};
// 렌더러가 클래스인지 함수인지 확인

View File

@ -175,7 +175,7 @@ export function AutocompleteSearchInputComponent({
onFocus={handleInputFocus}
placeholder={placeholder}
disabled={disabled}
className="h-8 pr-16 text-xs sm:h-10 sm:text-sm"
className="h-8 pr-16 text-xs sm:h-10 sm:text-sm !bg-background"
/>
<div className="absolute right-1 top-1/2 flex -translate-y-1/2 items-center gap-1">
{loading && (
@ -198,7 +198,7 @@ export function AutocompleteSearchInputComponent({
{/* 드롭다운 결과 */}
{isOpen && (results.length > 0 || loading) && (
<div className="absolute z-50 mt-1 max-h-[300px] w-full overflow-y-auto rounded-md border bg-background shadow-lg">
<div className="absolute z-[100] mt-1 max-h-[300px] w-full overflow-y-auto rounded-md border bg-background shadow-lg">
{loading && results.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground">
<Loader2 className="mx-auto mb-2 h-4 w-4 animate-spin" />

View File

@ -40,6 +40,7 @@ export function ConditionalContainerComponent({
componentId,
style,
className,
groupedData, // 🆕 그룹 데이터
}: ConditionalContainerProps) {
console.log("🎯 ConditionalContainerComponent 렌더링!", {
isDesignMode,
@ -177,6 +178,7 @@ export function ConditionalContainerComponent({
showBorder={showBorder}
formData={formData}
onFormDataChange={onFormDataChange}
groupedData={groupedData}
/>
))}
</div>
@ -196,6 +198,7 @@ export function ConditionalContainerComponent({
showBorder={showBorder}
formData={formData}
onFormDataChange={onFormDataChange}
groupedData={groupedData}
/>
) : null
)

View File

@ -3,6 +3,7 @@
import React, { useState, useEffect } from "react";
import { ConditionalSectionViewerProps } from "./types";
import { RealtimePreview } from "@/components/screen/RealtimePreviewDynamic";
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
import { cn } from "@/lib/utils";
import { Loader2 } from "lucide-react";
import { screenApi } from "@/lib/api/screen";
@ -24,6 +25,7 @@ export function ConditionalSectionViewer({
showBorder = true,
formData,
onFormDataChange,
groupedData, // 🆕 그룹 데이터
}: ConditionalSectionViewerProps) {
const { userId, userName, user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
@ -135,13 +137,24 @@ export function ConditionalSectionViewer({
minHeight: "200px",
}}
>
{components.map((component) => (
<RealtimePreview
{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}
isSelected={false}
isDesignMode={false}
onClick={() => {}}
isInteractive={true}
screenId={screenInfo?.id}
tableName={screenInfo?.tableName}
userId={userId}
@ -149,8 +162,11 @@ export function ConditionalSectionViewer({
companyCode={user?.companyCode}
formData={formData}
onFormDataChange={onFormDataChange}
groupedData={groupedData}
/>
))}
</div>
);
})}
</div>
</div>
)}

View File

@ -45,6 +45,7 @@ export interface ConditionalContainerProps {
onChange?: (value: string) => void;
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
// 화면 편집기 관련
isDesignMode?: boolean; // 디자인 모드 여부
@ -75,5 +76,6 @@ export interface ConditionalSectionViewerProps {
// 폼 데이터 전달
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
groupedData?: Record<string, any>[]; // 🆕 그룹 데이터
}

View File

@ -291,13 +291,47 @@ export function ModalRepeaterTableComponent({
return;
}
// 🔥 sourceColumns에 포함된 컬럼 제외 (조인된 컬럼 제거)
console.log("🔍 [ModalRepeaterTable] 필터링 전 데이터:", {
sourceColumns,
sourceTable,
targetTable,
sampleItem: value[0],
itemKeys: value[0] ? Object.keys(value[0]) : [],
});
const filteredData = value.map((item: any) => {
const filtered: Record<string, any> = {};
Object.keys(item).forEach((key) => {
// sourceColumns에 포함된 컬럼은 제외 (item_info 테이블의 컬럼)
if (sourceColumns.includes(key)) {
console.log(`${key} 제외 (sourceColumn)`);
return;
}
// 메타데이터 필드도 제외
if (key.startsWith("_")) {
console.log(`${key} 제외 (메타데이터)`);
return;
}
filtered[key] = item[key];
});
return filtered;
});
console.log("✅ [ModalRepeaterTable] 필터링 후 데이터:", {
filteredItemKeys: filteredData[0] ? Object.keys(filteredData[0]) : [],
sampleFilteredItem: filteredData[0],
});
// 🔥 targetTable 메타데이터를 배열 항목에 추가
const dataWithTargetTable = targetTable
? value.map(item => ({
? filteredData.map((item: any) => ({
...item,
_targetTable: targetTable, // 백엔드가 인식할 메타데이터
}))
: value;
: filteredData;
// ✅ CustomEvent의 detail에 데이터 추가
if (event instanceof CustomEvent && event.detail) {
@ -333,9 +367,10 @@ export function ModalRepeaterTableComponent({
const calculated = calculateAll(value);
// 값이 실제로 변경된 경우만 업데이트
if (JSON.stringify(calculated) !== JSON.stringify(value)) {
onChange(calculated);
handleChange(calculated);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleAddItems = async (items: any[]) => {

View File

@ -118,10 +118,10 @@ export function RepeaterTable({
};
return (
<div className="border rounded-md overflow-hidden">
<div className="overflow-x-auto">
<div className="border rounded-md overflow-hidden bg-background">
<div className="overflow-x-auto max-h-[240px] overflow-y-auto">
<table className="w-full text-xs sm:text-sm">
<thead className="bg-muted">
<thead className="bg-muted sticky top-0 z-10">
<tr>
<th className="px-4 py-2 text-left font-medium text-muted-foreground w-12">
#
@ -141,7 +141,7 @@ export function RepeaterTable({
</th>
</tr>
</thead>
<tbody>
<tbody className="bg-background">
{data.length === 0 ? (
<tr>
<td

View File

@ -40,10 +40,10 @@ export function SectionPaperComponent({
// 배경색 매핑
const backgroundColorMap = {
default: "bg-muted/20",
muted: "bg-muted/30",
accent: "bg-accent/20",
primary: "bg-primary/5",
default: "bg-muted/40",
muted: "bg-muted/50",
accent: "bg-accent/30",
primary: "bg-primary/10",
custom: "",
};
@ -74,7 +74,7 @@ export function SectionPaperComponent({
const padding = config.padding || "md";
const rounded = config.roundedCorners || "md";
const shadow = config.shadow || "none";
const showBorder = config.showBorder || false;
const showBorder = config.showBorder !== undefined ? config.showBorder : true;
const borderStyle = config.borderStyle || "subtle";
// 커스텀 배경색 처리
@ -87,7 +87,7 @@ export function SectionPaperComponent({
<div
className={cn(
// 기본 스타일
"relative transition-colors",
"relative transition-colors overflow-visible",
// 배경색
backgroundColor !== "custom" && backgroundColorMap[backgroundColor],

View File

@ -41,7 +41,8 @@ export interface ButtonActionConfig {
// 모달/팝업 관련
modalTitle?: string;
modalTitleBlocks?: Array<{ // 🆕 블록 기반 제목 (우선순위 높음)
modalTitleBlocks?: Array<{
// 🆕 블록 기반 제목 (우선순위 높음)
id: string;
type: "text" | "field";
value: string; // type=text: 텍스트 내용, type=field: 컬럼명
@ -88,6 +89,12 @@ export interface ButtonActionConfig {
// 코드 병합 관련
mergeColumnName?: string; // 병합할 컬럼명 (예: "item_code")
mergeShowPreview?: boolean; // 병합 전 미리보기 표시 여부 (기본: true)
// 편집 관련 (수주관리 등 그룹별 다중 레코드 편집)
editMode?: "modal" | "navigate" | "inline"; // 편집 모드
editModalTitle?: string; // 편집 모달 제목
editModalDescription?: string; // 편집 모달 설명
groupByColumns?: string[]; // 같은 그룹의 여러 행을 함께 편집 (예: ["order_no"])
}
/**
@ -1256,14 +1263,6 @@ export class ButtonActionExecutor {
// 플로우 선택 데이터 우선 사용
let dataToEdit = flowSelectedData && flowSelectedData.length > 0 ? flowSelectedData : selectedRowsData;
console.log("🔍 handleEdit - 데이터 소스 확인:", {
hasFlowSelectedData: !!(flowSelectedData && flowSelectedData.length > 0),
flowSelectedDataLength: flowSelectedData?.length || 0,
hasSelectedRowsData: !!(selectedRowsData && selectedRowsData.length > 0),
selectedRowsDataLength: selectedRowsData?.length || 0,
dataToEditLength: dataToEdit?.length || 0,
});
// 선택된 데이터가 없는 경우
if (!dataToEdit || dataToEdit.length === 0) {
toast.error("수정할 항목을 선택해주세요.");
@ -1276,26 +1275,15 @@ export class ButtonActionExecutor {
return false;
}
console.log(`📝 편집 액션 실행: ${dataToEdit.length}개 항목`, {
dataToEdit,
targetScreenId: config.targetScreenId,
editMode: config.editMode,
});
if (dataToEdit.length === 1) {
// 단일 항목 편집
const rowData = dataToEdit[0];
console.log("📝 단일 항목 편집:", rowData);
await this.openEditForm(config, rowData, context);
} else {
// 다중 항목 편집 - 현재는 단일 편집만 지원
toast.error("현재 단일 항목 편집만 지원됩니다. 하나의 항목만 선택해주세요.");
return false;
// TODO: 향후 다중 편집 지원
// console.log("📝 다중 항목 편집:", selectedRowsData);
// this.openBulkEditForm(config, selectedRowsData, context);
}
return true;
@ -1329,7 +1317,7 @@ export class ButtonActionExecutor {
default:
// 기본값: 모달
this.openEditModal(config, rowData, context);
await this.openEditModal(config, rowData, context);
}
}
@ -1341,11 +1329,17 @@ export class ButtonActionExecutor {
rowData: any,
context: ButtonActionContext,
): Promise<void> {
console.log("🎭 편집 모달 열기:", {
targetScreenId: config.targetScreenId,
modalSize: config.modalSize,
rowData,
});
const { groupByColumns = [] } = config;
// PK 값 추출 (우선순위: id > ID > 첫 번째 필드)
let primaryKeyValue: any;
if (rowData.id !== undefined && rowData.id !== null) {
primaryKeyValue = rowData.id;
} else if (rowData.ID !== undefined && rowData.ID !== null) {
primaryKeyValue = rowData.ID;
} else {
primaryKeyValue = Object.values(rowData)[0];
}
// 1. config에 editModalDescription이 있으면 우선 사용
let description = config.editModalDescription || "";
@ -1360,7 +1354,7 @@ export class ButtonActionExecutor {
}
}
// 모달 열기 이벤트 발생
// 🔧 항상 EditModal 사용 (groupByColumns는 EditModal에서 처리)
const modalEvent = new CustomEvent("openEditModal", {
detail: {
screenId: config.targetScreenId,
@ -1368,16 +1362,15 @@ export class ButtonActionExecutor {
description: description,
modalSize: config.modalSize || "lg",
editData: rowData,
groupByColumns: groupByColumns.length > 0 ? groupByColumns : undefined, // 🆕 그룹핑 컬럼 전달
tableName: context.tableName, // 🆕 테이블명 전달
onSave: () => {
// 저장 후 테이블 새로고침
console.log("💾 편집 저장 완료 - 테이블 새로고침");
context.onRefresh?.();
},
},
});
window.dispatchEvent(modalEvent);
// 편집 모달 열기는 조용히 처리 (토스트 없음)
}
/**