리사이징, 체크박스,엔터치면 다음 칸으로 이동, 표수정, 컬럼에서 이미지 넣는거 등등
This commit is contained in:
@@ -5,7 +5,23 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ResizableDialog = DialogPrimitive.Root;
|
||||
// 🆕 Context를 사용하여 open 상태 공유
|
||||
const ResizableDialogContext = React.createContext<{ open: boolean }>({ open: false });
|
||||
|
||||
// 🆕 ResizableDialog를 래핑하여 Context 제공
|
||||
const ResizableDialog: React.FC<React.ComponentProps<typeof DialogPrimitive.Root>> = ({
|
||||
children,
|
||||
open = false,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<ResizableDialogContext.Provider value={{ open }}>
|
||||
<DialogPrimitive.Root open={open} {...props}>
|
||||
{children}
|
||||
</DialogPrimitive.Root>
|
||||
</ResizableDialogContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const ResizableDialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
@@ -38,6 +54,7 @@ interface ResizableDialogContentProps
|
||||
defaultHeight?: number;
|
||||
modalId?: string; // localStorage 저장용 고유 ID
|
||||
userId?: string; // 사용자별 저장용
|
||||
open?: boolean; // 🆕 모달 열림/닫힘 상태 (외부에서 전달)
|
||||
}
|
||||
|
||||
const ResizableDialogContent = React.forwardRef<
|
||||
@@ -50,12 +67,13 @@ const ResizableDialogContent = React.forwardRef<
|
||||
children,
|
||||
minWidth = 400,
|
||||
minHeight = 300,
|
||||
maxWidth = 1400,
|
||||
maxHeight = 900,
|
||||
maxWidth = 1600,
|
||||
maxHeight = 1200,
|
||||
defaultWidth = 600,
|
||||
defaultHeight = 500,
|
||||
modalId,
|
||||
userId = "guest",
|
||||
open: externalOpen, // 🆕 외부에서 전달받은 open 상태
|
||||
style: userStyle,
|
||||
...props
|
||||
},
|
||||
@@ -69,6 +87,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||
if (!stableIdRef.current) {
|
||||
if (modalId) {
|
||||
stableIdRef.current = modalId;
|
||||
console.log("✅ ResizableDialog - 명시적 modalId 사용:", modalId);
|
||||
} else {
|
||||
// className 기반 ID 생성
|
||||
if (className) {
|
||||
@@ -76,6 +95,10 @@ const ResizableDialogContent = React.forwardRef<
|
||||
return ((acc << 5) - acc) + char.charCodeAt(0);
|
||||
}, 0);
|
||||
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
||||
console.log("🔄 ResizableDialog - className 기반 ID 생성:", {
|
||||
className,
|
||||
generatedId: stableIdRef.current,
|
||||
});
|
||||
} else if (userStyle) {
|
||||
// userStyle 기반 ID 생성
|
||||
const styleStr = JSON.stringify(userStyle);
|
||||
@@ -83,9 +106,14 @@ const ResizableDialogContent = React.forwardRef<
|
||||
return ((acc << 5) - acc) + char.charCodeAt(0);
|
||||
}, 0);
|
||||
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
||||
console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", {
|
||||
userStyle,
|
||||
generatedId: stableIdRef.current,
|
||||
});
|
||||
} else {
|
||||
// 기본 ID
|
||||
stableIdRef.current = 'modal-default';
|
||||
console.log("⚠️ ResizableDialog - 기본 ID 사용 (모든 모달이 같은 크기 공유)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,39 +160,170 @@ const ResizableDialogContent = React.forwardRef<
|
||||
const [isResizing, setIsResizing] = React.useState(false);
|
||||
const [resizeDirection, setResizeDirection] = React.useState<string>("");
|
||||
const [isInitialized, setIsInitialized] = React.useState(false);
|
||||
|
||||
// 모달이 열릴 때 초기 크기 설정 (localStorage 우선, 없으면 화면관리 설정)
|
||||
const [lastModalId, setLastModalId] = React.useState<string | null>(null);
|
||||
const [userResized, setUserResized] = React.useState(false); // 사용자가 실제로 리사이징했는지 추적
|
||||
|
||||
// 🆕 Context에서 open 상태 가져오기 (우선순위: externalOpen > context.open)
|
||||
const context = React.useContext(ResizableDialogContext);
|
||||
const actualOpen = externalOpen !== undefined ? externalOpen : context.open;
|
||||
|
||||
// 🆕 모달이 닫혔다가 다시 열릴 때 초기화 리셋
|
||||
const [wasOpen, setWasOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log("🔍 모달 상태 변화 감지:", {
|
||||
actualOpen,
|
||||
wasOpen,
|
||||
externalOpen,
|
||||
contextOpen: context.open,
|
||||
effectiveModalId
|
||||
});
|
||||
|
||||
if (actualOpen && !wasOpen) {
|
||||
// 모달이 방금 열림
|
||||
console.log("🔓 모달 열림 감지, 초기화 리셋:", { effectiveModalId });
|
||||
setIsInitialized(false);
|
||||
setWasOpen(true);
|
||||
} else if (!actualOpen && wasOpen) {
|
||||
// 모달이 방금 닫힘
|
||||
console.log("🔒 모달 닫힘 감지:", { effectiveModalId });
|
||||
setWasOpen(false);
|
||||
}
|
||||
}, [actualOpen, wasOpen, effectiveModalId, externalOpen, context.open]);
|
||||
|
||||
// modalId가 변경되면 초기화 리셋 (다른 모달이 열린 경우)
|
||||
React.useEffect(() => {
|
||||
if (effectiveModalId !== lastModalId) {
|
||||
console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", {
|
||||
이전: lastModalId,
|
||||
현재: effectiveModalId,
|
||||
isInitialized,
|
||||
});
|
||||
setIsInitialized(false);
|
||||
setUserResized(false); // 사용자 리사이징 플래그도 리셋
|
||||
setLastModalId(effectiveModalId);
|
||||
}
|
||||
}, [effectiveModalId, lastModalId, isInitialized]);
|
||||
|
||||
// 모달이 열릴 때 초기 크기 설정 (localStorage와 내용 크기 중 큰 값 사용)
|
||||
React.useEffect(() => {
|
||||
console.log("🔍 초기 크기 설정 useEffect 실행:", {
|
||||
isInitialized,
|
||||
hasContentRef: !!contentRef.current,
|
||||
effectiveModalId,
|
||||
});
|
||||
|
||||
if (!isInitialized) {
|
||||
const initialSize = getInitialSize();
|
||||
// 내용의 실제 크기 측정 (약간의 지연 후, contentRef가 준비될 때까지 대기)
|
||||
// 여러 번 시도하여 contentRef가 준비될 때까지 대기
|
||||
let attempts = 0;
|
||||
const maxAttempts = 10;
|
||||
|
||||
// localStorage에서 저장된 크기가 있는지 확인
|
||||
if (effectiveModalId && typeof window !== 'undefined') {
|
||||
try {
|
||||
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
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;
|
||||
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
// 저장된 크기가 있으면 그것을 사용 (사용자가 이전에 리사이즈한 크기)
|
||||
const restoredSize = {
|
||||
width: Math.max(minWidth, Math.min(maxWidth, parsed.width || initialSize.width)),
|
||||
height: Math.max(minHeight, Math.min(maxHeight, parsed.height || initialSize.height)),
|
||||
};
|
||||
setSize(restoredSize);
|
||||
setIsInitialized(true);
|
||||
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;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("모달 크기 복원 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 패딩 추가 (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);
|
||||
};
|
||||
|
||||
// 저장된 크기가 없으면 초기 크기 사용 (화면관리 설정 크기)
|
||||
setSize(initialSize);
|
||||
setIsInitialized(true);
|
||||
// 첫 시도는 300ms 후에 시작
|
||||
setTimeout(measureContent, 300);
|
||||
}
|
||||
}, [isInitialized, getInitialSize, effectiveModalId, userId, minWidth, maxWidth, minHeight, maxHeight]);
|
||||
}, [isInitialized, getInitialSize, effectiveModalId, userId, minWidth, maxWidth, minHeight, maxHeight, defaultWidth, defaultHeight]);
|
||||
|
||||
const startResize = (direction: string) => (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -206,14 +365,34 @@ const ResizableDialogContent = React.forwardRef<
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
// localStorage에 크기 저장 (리사이즈 후 새로고침해도 유지)
|
||||
if (effectiveModalId && typeof window !== 'undefined') {
|
||||
// 사용자가 리사이징했음을 표시
|
||||
setUserResized(true);
|
||||
|
||||
// ✅ 중요: 현재 실제 DOM 크기를 저장 (state가 아닌 실제 크기)
|
||||
if (effectiveModalId && typeof window !== 'undefined' && contentRef.current) {
|
||||
try {
|
||||
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
|
||||
const currentSize = { width: size.width, height: size.height };
|
||||
|
||||
// contentRef의 부모 요소(모달 컨테이너)의 실제 크기 사용
|
||||
const modalElement = contentRef.current.parentElement;
|
||||
const actualWidth = modalElement?.offsetWidth || size.width;
|
||||
const actualHeight = modalElement?.offsetHeight || size.height;
|
||||
|
||||
const currentSize = {
|
||||
width: actualWidth,
|
||||
height: actualHeight,
|
||||
userResized: true, // 사용자가 직접 리사이징했음을 표시
|
||||
};
|
||||
localStorage.setItem(storageKey, JSON.stringify(currentSize));
|
||||
console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", {
|
||||
effectiveModalId,
|
||||
userId,
|
||||
storageKey,
|
||||
size: currentSize,
|
||||
stateSize: { width: size.width, height: size.height },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("모달 크기 저장 실패:", error);
|
||||
console.error("❌ 모달 크기 저장 실패:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -243,7 +422,7 @@ const ResizableDialogContent = React.forwardRef<
|
||||
minHeight: `${minHeight}px`,
|
||||
}}
|
||||
>
|
||||
<div ref={contentRef} className="flex flex-col h-full overflow-hidden">
|
||||
<div ref={contentRef} className="flex flex-col h-full overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user