2025-11-05 16:36:32 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import * as React from "react";
|
|
|
|
|
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
|
|
|
|
import { X } from "lucide-react";
|
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
2025-11-06 12:11:49 +09:00
|
|
|
|
// 🆕 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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
2025-11-05 16:36:32 +09:00
|
|
|
|
|
|
|
|
|
|
const ResizableDialogTrigger = DialogPrimitive.Trigger;
|
|
|
|
|
|
|
|
|
|
|
|
const ResizableDialogPortal = DialogPrimitive.Portal;
|
|
|
|
|
|
|
|
|
|
|
|
const ResizableDialogClose = DialogPrimitive.Close;
|
|
|
|
|
|
|
|
|
|
|
|
const ResizableDialogOverlay = React.forwardRef<
|
|
|
|
|
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
|
|
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
|
|
|
|
>(({ className, ...props }, ref) => (
|
|
|
|
|
|
<DialogPrimitive.Overlay
|
|
|
|
|
|
ref={ref}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
|
|
|
|
className
|
|
|
|
|
|
)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
));
|
|
|
|
|
|
ResizableDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
|
|
|
|
|
|
|
|
|
|
interface ResizableDialogContentProps
|
|
|
|
|
|
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
|
|
|
|
|
minWidth?: number;
|
|
|
|
|
|
minHeight?: number;
|
|
|
|
|
|
maxWidth?: number;
|
|
|
|
|
|
maxHeight?: number;
|
|
|
|
|
|
defaultWidth?: number;
|
|
|
|
|
|
defaultHeight?: number;
|
|
|
|
|
|
modalId?: string; // localStorage 저장용 고유 ID
|
|
|
|
|
|
userId?: string; // 사용자별 저장용
|
2025-11-06 12:11:49 +09:00
|
|
|
|
open?: boolean; // 🆕 모달 열림/닫힘 상태 (외부에서 전달)
|
2025-11-10 09:33:29 +09:00
|
|
|
|
disableFlexLayout?: boolean; // 🆕 flex 레이아웃 비활성화 (absolute 레이아웃용)
|
2025-11-05 16:36:32 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ResizableDialogContent = React.forwardRef<
|
|
|
|
|
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
|
|
|
|
ResizableDialogContentProps
|
|
|
|
|
|
>(
|
|
|
|
|
|
(
|
|
|
|
|
|
{
|
|
|
|
|
|
className,
|
|
|
|
|
|
children,
|
|
|
|
|
|
minWidth = 400,
|
|
|
|
|
|
minHeight = 300,
|
2025-11-06 12:11:49 +09:00
|
|
|
|
maxWidth = 1600,
|
|
|
|
|
|
maxHeight = 1200,
|
2025-11-05 16:36:32 +09:00
|
|
|
|
defaultWidth = 600,
|
|
|
|
|
|
defaultHeight = 500,
|
|
|
|
|
|
modalId,
|
|
|
|
|
|
userId = "guest",
|
2025-11-06 12:11:49 +09:00
|
|
|
|
open: externalOpen, // 🆕 외부에서 전달받은 open 상태
|
2025-11-10 09:33:29 +09:00
|
|
|
|
disableFlexLayout = false, // 🆕 flex 레이아웃 비활성화
|
2025-11-05 16:36:32 +09:00
|
|
|
|
style: userStyle,
|
|
|
|
|
|
...props
|
|
|
|
|
|
},
|
|
|
|
|
|
ref
|
|
|
|
|
|
) => {
|
|
|
|
|
|
const contentRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 고정된 ID 생성 (한번 생성되면 컴포넌트 생명주기 동안 유지)
|
|
|
|
|
|
const stableIdRef = React.useRef<string | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
if (!stableIdRef.current) {
|
|
|
|
|
|
if (modalId) {
|
|
|
|
|
|
stableIdRef.current = modalId;
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// // console.log("✅ ResizableDialog - 명시적 modalId 사용:", modalId);
|
2025-11-05 16:36:32 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
// className 기반 ID 생성
|
|
|
|
|
|
if (className) {
|
|
|
|
|
|
const hash = className.split('').reduce((acc, char) => {
|
|
|
|
|
|
return ((acc << 5) - acc) + char.charCodeAt(0);
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("🔄 ResizableDialog - className 기반 ID 생성:", { className, generatedId: stableIdRef.current });
|
2025-11-05 16:36:32 +09:00
|
|
|
|
} else if (userStyle) {
|
|
|
|
|
|
// userStyle 기반 ID 생성
|
|
|
|
|
|
const styleStr = JSON.stringify(userStyle);
|
|
|
|
|
|
const hash = styleStr.split('').reduce((acc, char) => {
|
|
|
|
|
|
return ((acc << 5) - acc) + char.charCodeAt(0);
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
stableIdRef.current = `modal-${Math.abs(hash).toString(36)}`;
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("🔄 ResizableDialog - userStyle 기반 ID 생성:", { userStyle, generatedId: stableIdRef.current });
|
2025-11-05 16:36:32 +09:00
|
|
|
|
} else {
|
|
|
|
|
|
// 기본 ID
|
|
|
|
|
|
stableIdRef.current = 'modal-default';
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// console.log("⚠️ ResizableDialog - 기본 ID 사용 (모든 모달이 같은 크기 공유)");
|
2025-11-05 16:36:32 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const effectiveModalId = stableIdRef.current;
|
|
|
|
|
|
|
|
|
|
|
|
// 실제 렌더링된 크기를 감지하여 초기 크기로 사용
|
|
|
|
|
|
const getInitialSize = React.useCallback(() => {
|
|
|
|
|
|
if (typeof window === 'undefined') return { width: defaultWidth, height: defaultHeight };
|
|
|
|
|
|
|
|
|
|
|
|
// 1순위: userStyle에서 크기 추출 (화면관리에서 지정한 크기 - 항상 초기값으로 사용)
|
|
|
|
|
|
if (userStyle) {
|
|
|
|
|
|
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) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
width: Math.max(minWidth, Math.min(maxWidth, styleWidth)),
|
|
|
|
|
|
height: Math.max(minHeight, Math.min(maxHeight, 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)),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3순위: defaultWidth/defaultHeight 사용
|
|
|
|
|
|
return { width: defaultWidth, height: defaultHeight };
|
|
|
|
|
|
}, [defaultWidth, defaultHeight, minWidth, minHeight, maxWidth, maxHeight, userStyle]);
|
|
|
|
|
|
|
|
|
|
|
|
const [size, setSize] = React.useState(getInitialSize);
|
|
|
|
|
|
const [isResizing, setIsResizing] = React.useState(false);
|
|
|
|
|
|
const [resizeDirection, setResizeDirection] = React.useState<string>("");
|
|
|
|
|
|
const [isInitialized, setIsInitialized] = React.useState(false);
|
2025-11-06 12:11:49 +09:00
|
|
|
|
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(() => {
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("🔍 모달 상태 변화 감지:", { actualOpen, wasOpen, externalOpen, contextOpen: context.open, effectiveModalId });
|
2025-11-06 12:11:49 +09:00
|
|
|
|
|
|
|
|
|
|
if (actualOpen && !wasOpen) {
|
|
|
|
|
|
// 모달이 방금 열림
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// console.log("🔓 모달 열림 감지, 초기화 리셋:", { effectiveModalId });
|
2025-11-06 12:11:49 +09:00
|
|
|
|
setIsInitialized(false);
|
|
|
|
|
|
setWasOpen(true);
|
|
|
|
|
|
} else if (!actualOpen && wasOpen) {
|
|
|
|
|
|
// 모달이 방금 닫힘
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// console.log("🔒 모달 닫힘 감지:", { effectiveModalId });
|
2025-11-06 12:11:49 +09:00
|
|
|
|
setWasOpen(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [actualOpen, wasOpen, effectiveModalId, externalOpen, context.open]);
|
|
|
|
|
|
|
|
|
|
|
|
// modalId가 변경되면 초기화 리셋 (다른 모달이 열린 경우)
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
|
if (effectiveModalId !== lastModalId) {
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("🔄 모달 ID 변경 감지, 초기화 리셋:", { 이전: lastModalId, 현재: effectiveModalId, isInitialized });
|
2025-11-06 12:11:49 +09:00
|
|
|
|
setIsInitialized(false);
|
|
|
|
|
|
setUserResized(false); // 사용자 리사이징 플래그도 리셋
|
|
|
|
|
|
setLastModalId(effectiveModalId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [effectiveModalId, lastModalId, isInitialized]);
|
2025-11-05 16:36:32 +09:00
|
|
|
|
|
2025-11-06 12:11:49 +09:00
|
|
|
|
// 모달이 열릴 때 초기 크기 설정 (localStorage와 내용 크기 중 큰 값 사용)
|
2025-11-05 16:36:32 +09:00
|
|
|
|
React.useEffect(() => {
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("🔍 초기 크기 설정 useEffect 실행:", { isInitialized, hasContentRef: !!contentRef.current, effectiveModalId });
|
2025-11-06 12:11:49 +09:00
|
|
|
|
|
2025-11-05 16:36:32 +09:00
|
|
|
|
if (!isInitialized) {
|
2025-11-06 12:11:49 +09:00
|
|
|
|
// 내용의 실제 크기 측정 (약간의 지연 후, contentRef가 준비될 때까지 대기)
|
|
|
|
|
|
// 여러 번 시도하여 contentRef가 준비될 때까지 대기
|
|
|
|
|
|
let attempts = 0;
|
|
|
|
|
|
const maxAttempts = 10;
|
2025-11-05 16:36:32 +09:00
|
|
|
|
|
2025-11-06 12:11:49 +09:00
|
|
|
|
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;
|
|
|
|
|
|
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("📏 모달 내용 크기 측정:", { attempt: attempts, scrollWidth: contentRef.current.scrollWidth, scrollHeight: contentRef.current.scrollHeight, clientWidth: contentRef.current.clientWidth, clientHeight: contentRef.current.clientHeight, contentWidth, contentHeight });
|
2025-11-06 12:11:49 +09:00
|
|
|
|
} else {
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("⚠️ contentRef 없음, 재시도:", { attempt: attempts, maxAttempts, defaultWidth, defaultHeight });
|
2025-11-05 16:36:32 +09:00
|
|
|
|
|
2025-11-06 12:11:49 +09:00
|
|
|
|
// contentRef가 아직 없으면 재시도
|
|
|
|
|
|
if (attempts < maxAttempts) {
|
|
|
|
|
|
setTimeout(measureContent, 100);
|
2025-11-05 16:36:32 +09:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-06 12:11:49 +09:00
|
|
|
|
|
|
|
|
|
|
// 패딩 추가 (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))),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// console.log("📐 내용 기반 크기:", contentBasedSize);
|
2025-11-06 12:11:49 +09:00
|
|
|
|
|
|
|
|
|
|
// localStorage에서 저장된 크기 확인
|
|
|
|
|
|
let finalSize = contentBasedSize;
|
|
|
|
|
|
|
|
|
|
|
|
if (effectiveModalId && typeof window !== 'undefined') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
|
|
|
|
|
|
const saved = localStorage.getItem(storageKey);
|
|
|
|
|
|
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("📦 localStorage 확인:", { effectiveModalId, userId, storageKey, saved: saved ? "있음" : "없음" });
|
2025-11-06 12:11:49 +09:00
|
|
|
|
|
|
|
|
|
|
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)),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// console.log("💾 사용자가 리사이징한 크기 복원:", savedSize);
|
2025-11-06 12:11:49 +09:00
|
|
|
|
|
|
|
|
|
|
// ✅ 중요: 사용자가 명시적으로 리사이징한 경우, 사용자 크기를 우선 사용
|
|
|
|
|
|
// (사용자가 의도적으로 작게 만든 것을 존중)
|
|
|
|
|
|
finalSize = savedSize;
|
|
|
|
|
|
setUserResized(true);
|
|
|
|
|
|
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("✅ 최종 크기 (사용자가 설정한 크기 우선 적용):", { savedSize, contentBasedSize, finalSize, note: "사용자가 리사이징한 크기를 그대로 사용합니다" });
|
2025-11-06 12:11:49 +09:00
|
|
|
|
} else {
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// console.log("ℹ️ 자동 계산된 크기는 무시, 내용 크기 사용");
|
2025-11-06 12:11:49 +09:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// console.log("ℹ️ localStorage에 저장된 크기 없음, 내용 크기 사용");
|
2025-11-06 12:11:49 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// console.error("❌ 모달 크기 복원 실패:", error);
|
2025-11-06 12:11:49 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSize(finalSize);
|
|
|
|
|
|
setIsInitialized(true);
|
|
|
|
|
|
};
|
2025-11-05 16:36:32 +09:00
|
|
|
|
|
2025-11-06 12:11:49 +09:00
|
|
|
|
// 첫 시도는 300ms 후에 시작
|
|
|
|
|
|
setTimeout(measureContent, 300);
|
2025-11-05 16:36:32 +09:00
|
|
|
|
}
|
2025-11-06 12:11:49 +09:00
|
|
|
|
}, [isInitialized, getInitialSize, effectiveModalId, userId, minWidth, maxWidth, minHeight, maxHeight, defaultWidth, defaultHeight]);
|
2025-11-05 16:36:32 +09:00
|
|
|
|
|
|
|
|
|
|
const startResize = (direction: string) => (e: React.MouseEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
setIsResizing(true);
|
|
|
|
|
|
setResizeDirection(direction);
|
|
|
|
|
|
|
|
|
|
|
|
const startX = e.clientX;
|
|
|
|
|
|
const startY = e.clientY;
|
|
|
|
|
|
const startWidth = size.width;
|
|
|
|
|
|
const startHeight = size.height;
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
|
|
|
|
const deltaX = moveEvent.clientX - startX;
|
|
|
|
|
|
const deltaY = moveEvent.clientY - startY;
|
|
|
|
|
|
|
|
|
|
|
|
let newWidth = startWidth;
|
|
|
|
|
|
let newHeight = startHeight;
|
|
|
|
|
|
|
|
|
|
|
|
if (direction.includes("e")) {
|
|
|
|
|
|
newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + deltaX));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (direction.includes("w")) {
|
|
|
|
|
|
newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth - deltaX));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (direction.includes("s")) {
|
|
|
|
|
|
newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight + deltaY));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (direction.includes("n")) {
|
|
|
|
|
|
newHeight = Math.max(minHeight, Math.min(maxHeight, startHeight - deltaY));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setSize({ width: newWidth, height: newHeight });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMouseUp = () => {
|
|
|
|
|
|
setIsResizing(false);
|
|
|
|
|
|
setResizeDirection("");
|
|
|
|
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
|
|
|
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
|
|
|
|
|
2025-11-06 12:11:49 +09:00
|
|
|
|
// 사용자가 리사이징했음을 표시
|
|
|
|
|
|
setUserResized(true);
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 중요: 현재 실제 DOM 크기를 저장 (state가 아닌 실제 크기)
|
|
|
|
|
|
if (effectiveModalId && typeof window !== 'undefined' && contentRef.current) {
|
2025-11-05 16:36:32 +09:00
|
|
|
|
try {
|
|
|
|
|
|
const storageKey = `modal_size_${effectiveModalId}_${userId}`;
|
2025-11-06 12:11:49 +09:00
|
|
|
|
|
|
|
|
|
|
// 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, // 사용자가 직접 리사이징했음을 표시
|
|
|
|
|
|
};
|
2025-11-05 16:36:32 +09:00
|
|
|
|
localStorage.setItem(storageKey, JSON.stringify(currentSize));
|
2025-11-06 13:26:54 +09:00
|
|
|
|
// console.log("💾 localStorage에 크기 저장 (사용자 리사이징):", { effectiveModalId, userId, storageKey, size: currentSize, stateSize: { width: size.width, height: size.height } });
|
2025-11-05 16:36:32 +09:00
|
|
|
|
} catch (error) {
|
2025-11-06 12:46:08 +09:00
|
|
|
|
// console.error("❌ 모달 크기 저장 실패:", error);
|
2025-11-05 16:36:32 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
|
|
|
|
document.addEventListener("mouseup", handleMouseUp);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ResizableDialogPortal>
|
|
|
|
|
|
<ResizableDialogOverlay />
|
|
|
|
|
|
<DialogPrimitive.Content
|
|
|
|
|
|
ref={ref}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"fixed left-[50%] top-[50%] z-50 translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
|
|
|
|
|
isResizing && "select-none",
|
|
|
|
|
|
className
|
|
|
|
|
|
)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
...userStyle,
|
|
|
|
|
|
width: `${size.width}px`,
|
|
|
|
|
|
height: `${size.height}px`,
|
|
|
|
|
|
maxWidth: "95vw",
|
|
|
|
|
|
maxHeight: "95vh",
|
|
|
|
|
|
minWidth: `${minWidth}px`,
|
|
|
|
|
|
minHeight: `${minHeight}px`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-11-10 09:33:29 +09:00
|
|
|
|
<div
|
|
|
|
|
|
ref={contentRef}
|
|
|
|
|
|
className="h-full w-full"
|
|
|
|
|
|
style={{ display: 'block', overflow: 'hidden' }}
|
|
|
|
|
|
>
|
2025-11-05 16:36:32 +09:00
|
|
|
|
{children}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 리사이즈 핸들 */}
|
|
|
|
|
|
{/* 오른쪽 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute right-0 top-0 h-full w-2 cursor-ew-resize hover:bg-primary/20 transition-colors"
|
|
|
|
|
|
onMouseDown={startResize("e")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* 아래 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute bottom-0 left-0 w-full h-2 cursor-ns-resize hover:bg-primary/20 transition-colors"
|
|
|
|
|
|
onMouseDown={startResize("s")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* 오른쪽 아래 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute right-0 bottom-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/30 transition-colors"
|
|
|
|
|
|
onMouseDown={startResize("se")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* 왼쪽 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute left-0 top-0 h-full w-2 cursor-ew-resize hover:bg-primary/20 transition-colors"
|
|
|
|
|
|
onMouseDown={startResize("w")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* 위 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute top-0 left-0 w-full h-2 cursor-ns-resize hover:bg-primary/20 transition-colors"
|
|
|
|
|
|
onMouseDown={startResize("n")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* 왼쪽 아래 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute left-0 bottom-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/30 transition-colors"
|
|
|
|
|
|
onMouseDown={startResize("sw")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* 오른쪽 위 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute right-0 top-0 w-4 h-4 cursor-nesw-resize hover:bg-primary/30 transition-colors"
|
|
|
|
|
|
onMouseDown={startResize("ne")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* 왼쪽 위 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="absolute left-0 top-0 w-4 h-4 cursor-nwse-resize hover:bg-primary/30 transition-colors"
|
|
|
|
|
|
onMouseDown={startResize("nw")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<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">
|
|
|
|
|
|
<X className="h-4 w-4" />
|
|
|
|
|
|
<span className="sr-only">Close</span>
|
|
|
|
|
|
</DialogPrimitive.Close>
|
|
|
|
|
|
</DialogPrimitive.Content>
|
|
|
|
|
|
</ResizableDialogPortal>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
ResizableDialogContent.displayName = DialogPrimitive.Content.displayName;
|
|
|
|
|
|
|
|
|
|
|
|
const ResizableDialogHeader = ({
|
|
|
|
|
|
className,
|
|
|
|
|
|
...props
|
|
|
|
|
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex flex-col space-y-1.5 text-center sm:text-left flex-shrink-0",
|
|
|
|
|
|
className
|
|
|
|
|
|
)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
ResizableDialogHeader.displayName = "ResizableDialogHeader";
|
|
|
|
|
|
|
|
|
|
|
|
const ResizableDialogFooter = ({
|
|
|
|
|
|
className,
|
|
|
|
|
|
...props
|
|
|
|
|
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 flex-shrink-0",
|
|
|
|
|
|
className
|
|
|
|
|
|
)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
ResizableDialogFooter.displayName = "ResizableDialogFooter";
|
|
|
|
|
|
|
|
|
|
|
|
const ResizableDialogTitle = React.forwardRef<
|
|
|
|
|
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
|
|
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
|
|
|
|
>(({ className, ...props }, ref) => (
|
|
|
|
|
|
<DialogPrimitive.Title
|
|
|
|
|
|
ref={ref}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"text-lg font-semibold leading-none tracking-tight",
|
|
|
|
|
|
className
|
|
|
|
|
|
)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
));
|
|
|
|
|
|
ResizableDialogTitle.displayName = DialogPrimitive.Title.displayName;
|
|
|
|
|
|
|
|
|
|
|
|
const ResizableDialogDescription = React.forwardRef<
|
|
|
|
|
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
|
|
|
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
|
|
|
|
>(({ className, ...props }, ref) => (
|
|
|
|
|
|
<DialogPrimitive.Description
|
|
|
|
|
|
ref={ref}
|
|
|
|
|
|
className={cn("text-sm text-muted-foreground", className)}
|
|
|
|
|
|
{...props}
|
|
|
|
|
|
/>
|
|
|
|
|
|
));
|
|
|
|
|
|
ResizableDialogDescription.displayName =
|
|
|
|
|
|
DialogPrimitive.Description.displayName;
|
|
|
|
|
|
|
|
|
|
|
|
export {
|
|
|
|
|
|
ResizableDialog,
|
|
|
|
|
|
ResizableDialogPortal,
|
|
|
|
|
|
ResizableDialogOverlay,
|
|
|
|
|
|
ResizableDialogClose,
|
|
|
|
|
|
ResizableDialogTrigger,
|
|
|
|
|
|
ResizableDialogContent,
|
|
|
|
|
|
ResizableDialogHeader,
|
|
|
|
|
|
ResizableDialogFooter,
|
|
|
|
|
|
ResizableDialogTitle,
|
|
|
|
|
|
ResizableDialogDescription,
|
|
|
|
|
|
};
|
|
|
|
|
|
|