import { CloseOutlined, CodeOutlined, CopyOutlined, DesktopOutlined, MinusOutlined, MobileOutlined, ReloadOutlined, } from '@ant-design/icons'; import { Button } from 'antd'; import { useEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, } from 'react'; import { createPortal } from 'react-dom'; import { PreviewAppWindow } from './PreviewAppWindow'; import { copyTextToClipboard } from '../../utils/clipboard'; import { resolvePreviewAppOrigin, type PreviewRuntimeConsoleBridgeMessage, type PreviewRuntimeConsoleLevel, type PreviewTargetDescriptor, } from './previewRuntime'; type PreviewAppOverlayProps = { pathname: string; search?: string; targetDescriptor?: PreviewTargetDescriptor; onClose: () => void; }; type DragPosition = { x: number; y: number; }; type DetachedConsoleSize = { width: number; height: number; }; const HEADER_HEIGHT = 44; const MINIMIZED_WIDTH = 168; const MOBILE_SHELL_WIDTH = 430; const MOBILE_SHELL_HEIGHT = 860; const VIEWPORT_PADDING = 12; const MAX_CONSOLE_ENTRIES = 300; const DETACHED_CONSOLE_WIDTH = 460; const DETACHED_CONSOLE_HEIGHT = 340; const DETACHED_CONSOLE_MIN_WIDTH = 360; const DETACHED_CONSOLE_MAX_WIDTH = 840; const DETACHED_CONSOLE_MIN_HEIGHT = 240; const DETACHED_CONSOLE_MAX_HEIGHT = 620; const DETACHED_CONSOLE_DRAGGING_CLASS = 'preview-app-overlay-console-dragging'; const CONSOLE_LEVELS: PreviewRuntimeConsoleLevel[] = ['log', 'info', 'warn', 'error', 'debug']; type PreviewConsoleEntry = PreviewRuntimeConsoleBridgeMessage & { id: string; }; function clamp(value: number, min: number, max: number) { return Math.min(Math.max(value, min), max); } function formatConsoleEntryCopyText(entry: PreviewConsoleEntry) { return [ `${entry.level.toUpperCase()} ${new Date(entry.timestamp).toLocaleTimeString('ko-KR', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', })}`, entry.args.join('\n'), ] .filter((value) => value.trim().length > 0) .join('\n'); } function getDefaultMinimizedPosition(): DragPosition { if (typeof window === 'undefined') { return { x: VIEWPORT_PADDING, y: VIEWPORT_PADDING, }; } return { x: Math.max(VIEWPORT_PADDING, window.innerWidth - MINIMIZED_WIDTH - VIEWPORT_PADDING), y: Math.max(VIEWPORT_PADDING, window.innerHeight - HEADER_HEIGHT - VIEWPORT_PADDING), }; } function getMobileShellMetrics() { if (typeof window === 'undefined') { return { width: MOBILE_SHELL_WIDTH, height: MOBILE_SHELL_HEIGHT, }; } return { width: Math.min(MOBILE_SHELL_WIDTH, Math.max(320, window.innerWidth - VIEWPORT_PADDING * 2)), height: Math.min(MOBILE_SHELL_HEIGHT, Math.max(520, window.innerHeight - VIEWPORT_PADDING * 2)), }; } function getDefaultMobileShellPosition(): DragPosition { if (typeof window === 'undefined') { return { x: VIEWPORT_PADDING, y: VIEWPORT_PADDING, }; } const { width, height } = getMobileShellMetrics(); return { x: Math.max(VIEWPORT_PADDING, (window.innerWidth - width) / 2), y: Math.max(VIEWPORT_PADDING, (window.innerHeight - height) / 2), }; } function getDetachedConsoleMetrics() { if (typeof window === 'undefined') { return { width: DETACHED_CONSOLE_WIDTH, height: DETACHED_CONSOLE_HEIGHT, }; } return { width: Math.min(DETACHED_CONSOLE_WIDTH, Math.max(320, window.innerWidth - VIEWPORT_PADDING * 2)), height: Math.min(DETACHED_CONSOLE_HEIGHT, Math.max(220, window.innerHeight - VIEWPORT_PADDING * 2)), }; } function getDetachedConsoleSizeBounds() { if (typeof window === 'undefined') { return { minWidth: DETACHED_CONSOLE_MIN_WIDTH, maxWidth: DETACHED_CONSOLE_MAX_WIDTH, minHeight: DETACHED_CONSOLE_MIN_HEIGHT, maxHeight: DETACHED_CONSOLE_MAX_HEIGHT, }; } return { minWidth: Math.min(DETACHED_CONSOLE_MIN_WIDTH, Math.max(320, window.innerWidth - VIEWPORT_PADDING * 2)), maxWidth: Math.max( Math.min(DETACHED_CONSOLE_MAX_WIDTH, window.innerWidth - VIEWPORT_PADDING * 2), Math.min(DETACHED_CONSOLE_MIN_WIDTH, Math.max(320, window.innerWidth - VIEWPORT_PADDING * 2)), ), minHeight: Math.min( DETACHED_CONSOLE_MIN_HEIGHT, Math.max(220, window.innerHeight - VIEWPORT_PADDING * 2), ), maxHeight: Math.max( Math.min(DETACHED_CONSOLE_MAX_HEIGHT, window.innerHeight - VIEWPORT_PADDING * 2), Math.min(DETACHED_CONSOLE_MIN_HEIGHT, Math.max(220, window.innerHeight - VIEWPORT_PADDING * 2)), ), }; } function normalizeDetachedConsoleSize(size: DetachedConsoleSize): DetachedConsoleSize { const { minWidth, maxWidth, minHeight, maxHeight } = getDetachedConsoleSizeBounds(); return { width: clamp(size.width, minWidth, maxWidth), height: clamp(size.height, minHeight, maxHeight), }; } function getDefaultDetachedConsoleSize(): DetachedConsoleSize { return normalizeDetachedConsoleSize({ width: DETACHED_CONSOLE_WIDTH, height: DETACHED_CONSOLE_HEIGHT, }); } function getDefaultDetachedConsolePosition(): DragPosition { if (typeof window === 'undefined') { return { x: VIEWPORT_PADDING, y: VIEWPORT_PADDING, }; } const { width, height } = getDetachedConsoleMetrics(); return { x: Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING), y: Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING), }; } export function PreviewAppOverlay({ pathname, search = '', targetDescriptor = null, onClose, }: PreviewAppOverlayProps) { const iframeRef = useRef(null); const minimizedPositionRef = useRef(getDefaultMinimizedPosition()); const mobileShellPositionRef = useRef(getDefaultMobileShellPosition()); const detachedConsolePositionRef = useRef(getDefaultDetachedConsolePosition()); const detachedConsoleSizeRef = useRef(getDefaultDetachedConsoleSize()); const rootRef = useRef(null); const consoleBodyRef = useRef(null); const dragStateRef = useRef<{ pointerId: number; lastX: number; lastY: number; captureTarget: HTMLDivElement; } | null>(null); const detachedConsoleDragStateRef = useRef<{ pointerId: number; lastX: number; lastY: number; captureTarget: HTMLDivElement; } | null>(null); const detachedConsoleResizeStateRef = useRef<{ pointerId: number; startX: number; startY: number; startWidth: number; startHeight: number; captureTarget: HTMLDivElement; } | null>(null); const dragMovedRef = useRef(false); const [minimized, setMinimized] = useState(false); const [deviceMode, setDeviceMode] = useState<'desktop' | 'mobile'>('mobile'); const [consoleOpen, setConsoleOpen] = useState(false); const [consoleDetached, setConsoleDetached] = useState(false); const [consoleEntries, setConsoleEntries] = useState([]); const [consoleLevelFilter, setConsoleLevelFilter] = useState(CONSOLE_LEVELS); const [reloadKey, setReloadKey] = useState(0); const [isMobileViewport, setIsMobileViewport] = useState(() => typeof window !== 'undefined' ? window.matchMedia('(max-width: 768px)').matches : false, ); const [position, setPosition] = useState(() => minimizedPositionRef.current); const [detachedConsolePosition, setDetachedConsolePosition] = useState( () => detachedConsolePositionRef.current, ); const [detachedConsoleSize, setDetachedConsoleSize] = useState( () => detachedConsoleSizeRef.current, ); const isDesktopMobileShell = !minimized && deviceMode === 'mobile' && !isMobileViewport; const canDetachConsole = !isMobileViewport; const showDetachedConsole = consoleOpen && consoleDetached && canDetachConsole; const showAttachedConsole = consoleOpen && !showDetachedConsole; const previewOrigin = useMemo(() => resolvePreviewAppOrigin(), []); const clearDetachedConsoleDraggingState = () => { if (typeof document === 'undefined') { return; } document.body.classList.remove(DETACHED_CONSOLE_DRAGGING_CLASS); }; useEffect(() => { setConsoleEntries([]); }, [pathname, search, targetDescriptor, deviceMode]); useEffect(() => { document.body.classList.add('preview-app-overlay-open'); return () => { document.body.classList.remove('preview-app-overlay-open'); document.body.classList.remove(DETACHED_CONSOLE_DRAGGING_CLASS); }; }, []); useEffect(() => { if (typeof window === 'undefined') { return; } const mediaQuery = window.matchMedia('(max-width: 768px)'); const handleChange = (event: MediaQueryListEvent) => { setIsMobileViewport(event.matches); }; setIsMobileViewport(mediaQuery.matches); mediaQuery.addEventListener('change', handleChange); return () => { mediaQuery.removeEventListener('change', handleChange); }; }, []); useEffect(() => { if (isMobileViewport) { setConsoleDetached(false); } }, [isMobileViewport]); useEffect(() => { if (!minimized && !isDesktopMobileShell) { return; } const resizeListener = () => { if (minimized) { setPosition((current) => { const nextPosition = { x: clamp( current.x, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerWidth - MINIMIZED_WIDTH - VIEWPORT_PADDING), ), y: clamp( current.y, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerHeight - HEADER_HEIGHT - VIEWPORT_PADDING), ), }; minimizedPositionRef.current = nextPosition; return nextPosition; }); return; } const { width, height } = getMobileShellMetrics(); setPosition((current) => ({ x: clamp( current.x, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING), ), y: clamp( current.y, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING), ), })); }; window.addEventListener('resize', resizeListener); resizeListener(); return () => { window.removeEventListener('resize', resizeListener); }; }, [isDesktopMobileShell, minimized]); useEffect(() => { if (minimized) { minimizedPositionRef.current = position; } }, [minimized, position]); useEffect(() => { if (isDesktopMobileShell) { mobileShellPositionRef.current = position; } }, [isDesktopMobileShell, position]); useEffect(() => { if (!minimized && !isDesktopMobileShell) { dragStateRef.current = null; dragMovedRef.current = false; return; } const handlePointerMove = (event: PointerEvent) => { const dragState = dragStateRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; } const deltaX = event.clientX - dragState.lastX; const deltaY = event.clientY - dragState.lastY; if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) { dragMovedRef.current = true; } dragState.lastX = event.clientX; dragState.lastY = event.clientY; const maxX = minimized ? Math.max(VIEWPORT_PADDING, window.innerWidth - MINIMIZED_WIDTH - VIEWPORT_PADDING) : Math.max( VIEWPORT_PADDING, window.innerWidth - getMobileShellMetrics().width - VIEWPORT_PADDING, ); const maxY = minimized ? Math.max(VIEWPORT_PADDING, window.innerHeight - HEADER_HEIGHT - VIEWPORT_PADDING) : Math.max( VIEWPORT_PADDING, window.innerHeight - getMobileShellMetrics().height - VIEWPORT_PADDING, ); setPosition((current) => { const nextPosition = { x: clamp(current.x + deltaX, VIEWPORT_PADDING, maxX), y: clamp(current.y + deltaY, VIEWPORT_PADDING, maxY), }; if (minimized) { minimizedPositionRef.current = nextPosition; } return nextPosition; }); }; const finishPointerDrag = (event: PointerEvent) => { const dragState = dragStateRef.current; if (!dragState || dragState.pointerId !== event.pointerId) { return; } if (dragState.captureTarget.hasPointerCapture(event.pointerId)) { dragState.captureTarget.releasePointerCapture(event.pointerId); } dragStateRef.current = null; }; window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', finishPointerDrag); window.addEventListener('pointercancel', finishPointerDrag); return () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', finishPointerDrag); window.removeEventListener('pointercancel', finishPointerDrag); }; }, [isDesktopMobileShell, minimized]); useEffect(() => { if (isDesktopMobileShell) { const { width, height } = getMobileShellMetrics(); const nextPosition = { x: clamp( mobileShellPositionRef.current.x, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING), ), y: clamp( mobileShellPositionRef.current.y, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING), ), }; mobileShellPositionRef.current = nextPosition; setPosition(nextPosition); } }, [isDesktopMobileShell]); useEffect(() => { if (!showDetachedConsole) { detachedConsoleDragStateRef.current = null; detachedConsoleResizeStateRef.current = null; clearDetachedConsoleDraggingState(); return; } const resizeListener = () => { const { width, height } = normalizeDetachedConsoleSize(detachedConsoleSizeRef.current); setDetachedConsolePosition((current) => { const nextPosition = { x: clamp( current.x, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING), ), y: clamp( current.y, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING), ), }; detachedConsolePositionRef.current = nextPosition; return nextPosition; }); }; window.addEventListener('resize', resizeListener); resizeListener(); return () => { window.removeEventListener('resize', resizeListener); }; }, [showDetachedConsole]); useEffect(() => { if (!showDetachedConsole) { return; } const handlePointerMove = (event: PointerEvent) => { const dragState = detachedConsoleDragStateRef.current; const resizeState = detachedConsoleResizeStateRef.current; if (dragState && dragState.pointerId === event.pointerId) { event.preventDefault(); const deltaX = event.clientX - dragState.lastX; const deltaY = event.clientY - dragState.lastY; dragState.lastX = event.clientX; dragState.lastY = event.clientY; const { width, height } = normalizeDetachedConsoleSize(detachedConsoleSizeRef.current); setDetachedConsolePosition((current) => { const nextPosition = { x: clamp( current.x + deltaX, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerWidth - width - VIEWPORT_PADDING), ), y: clamp( current.y + deltaY, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerHeight - height - VIEWPORT_PADDING), ), }; detachedConsolePositionRef.current = nextPosition; return nextPosition; }); return; } if (resizeState && resizeState.pointerId === event.pointerId) { event.preventDefault(); updateDetachedConsoleSize({ width: resizeState.startWidth + (event.clientX - resizeState.startX), height: resizeState.startHeight + (event.clientY - resizeState.startY), }); } }; const finishPointerDrag = (event: PointerEvent) => { const dragState = detachedConsoleDragStateRef.current; const resizeState = detachedConsoleResizeStateRef.current; if (dragState?.pointerId === event.pointerId) { if (dragState.captureTarget.hasPointerCapture(event.pointerId)) { dragState.captureTarget.releasePointerCapture(event.pointerId); } detachedConsoleDragStateRef.current = null; } if (resizeState?.pointerId === event.pointerId) { if (resizeState.captureTarget.hasPointerCapture(event.pointerId)) { resizeState.captureTarget.releasePointerCapture(event.pointerId); } detachedConsoleResizeStateRef.current = null; } clearDetachedConsoleDraggingState(); }; window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', finishPointerDrag); window.addEventListener('pointercancel', finishPointerDrag); return () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', finishPointerDrag); window.removeEventListener('pointercancel', finishPointerDrag); }; }, [showDetachedConsole]); useEffect(() => { const handleMessage = (event: MessageEvent) => { if (event.origin !== previewOrigin) { return; } if (event.source !== iframeRef.current?.contentWindow) { return; } const payload = event.data as PreviewRuntimeConsoleBridgeMessage | null; if (!payload || payload.source !== 'sm-home.preview-runtime.console' || !Array.isArray(payload.args)) { return; } const nextEntry: PreviewConsoleEntry = { ...payload, id: `${payload.timestamp}-${Math.random().toString(36).slice(2, 10)}`, }; setConsoleEntries((current) => { const nextEntries = [...current, nextEntry]; if (nextEntries.length <= MAX_CONSOLE_ENTRIES) { return nextEntries; } return nextEntries.slice(nextEntries.length - MAX_CONSOLE_ENTRIES); }); }; window.addEventListener('message', handleMessage); return () => { window.removeEventListener('message', handleMessage); }; }, [previewOrigin]); useEffect(() => { if (!consoleOpen) { return; } const nextFrame = window.requestAnimationFrame(() => { const container = consoleBodyRef.current; if (!container) { return; } container.scrollTop = container.scrollHeight; }); return () => { window.cancelAnimationFrame(nextFrame); }; }, [consoleEntries, consoleOpen]); const latestConsoleEntry = consoleEntries.length > 0 ? consoleEntries[consoleEntries.length - 1] : null; const latestConsoleHref = latestConsoleEntry?.href?.trim() || iframeRef.current?.src || ''; const consoleLevelCounts = useMemo( () => CONSOLE_LEVELS.reduce>( (counts, level) => ({ ...counts, [level]: consoleEntries.filter((entry) => entry.level === level).length, }), { log: 0, info: 0, warn: 0, error: 0, debug: 0, }, ), [consoleEntries], ); const filteredConsoleEntries = useMemo( () => consoleEntries.filter((entry) => consoleLevelFilter.includes(entry.level)), [consoleEntries, consoleLevelFilter], ); const errorEntryCount = consoleLevelCounts.error; const warnEntryCount = consoleLevelCounts.warn; const hasAllConsoleLevelsSelected = consoleLevelFilter.length === CONSOLE_LEVELS.length; const hasAnyConsoleLevelsSelected = consoleLevelFilter.length > 0; const handleHeaderPointerDown = (event: ReactPointerEvent) => { if (!minimized && !isDesktopMobileShell) { return; } const rootRect = rootRef.current?.getBoundingClientRect(); if (!rootRect) { return; } dragStateRef.current = { pointerId: event.pointerId, lastX: event.clientX, lastY: event.clientY, captureTarget: event.currentTarget, }; dragMovedRef.current = false; event.currentTarget.setPointerCapture(event.pointerId); event.preventDefault(); }; const handleHeaderPointerUp = (event: ReactPointerEvent) => { const dragState = dragStateRef.current; if (dragState?.pointerId === event.pointerId) { if (dragState.captureTarget.hasPointerCapture(event.pointerId)) { dragState.captureTarget.releasePointerCapture(event.pointerId); } dragStateRef.current = null; } }; const handleMinimizeToggle = () => { setMinimized((current) => { if (current) { if (deviceMode === 'mobile' && !isMobileViewport) { setPosition(mobileShellPositionRef.current); } return false; } if (isDesktopMobileShell) { mobileShellPositionRef.current = position; } setPosition(minimizedPositionRef.current); return true; }); }; const handleActionButtonClick = (event: ReactMouseEvent) => { event.stopPropagation(); }; const handleDetachedConsolePointerDown = (event: ReactPointerEvent) => { if (!showDetachedConsole || event.button !== 0) { return; } detachedConsoleDragStateRef.current = { pointerId: event.pointerId, lastX: event.clientX, lastY: event.clientY, captureTarget: event.currentTarget, }; document.body.classList.add(DETACHED_CONSOLE_DRAGGING_CLASS); event.currentTarget.setPointerCapture(event.pointerId); event.stopPropagation(); event.preventDefault(); }; const handleDetachedConsolePointerUp = (event: ReactPointerEvent) => { const dragState = detachedConsoleDragStateRef.current; if (dragState?.pointerId === event.pointerId) { if (dragState.captureTarget.hasPointerCapture(event.pointerId)) { dragState.captureTarget.releasePointerCapture(event.pointerId); } detachedConsoleDragStateRef.current = null; } clearDetachedConsoleDraggingState(); event.stopPropagation(); }; const handleDetachedConsoleResizePointerDown = (event: ReactPointerEvent) => { if (!showDetachedConsole || event.button !== 0) { return; } detachedConsoleResizeStateRef.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, startWidth: detachedConsoleSizeRef.current.width, startHeight: detachedConsoleSizeRef.current.height, captureTarget: event.currentTarget, }; document.body.classList.add(DETACHED_CONSOLE_DRAGGING_CLASS); event.currentTarget.setPointerCapture(event.pointerId); event.stopPropagation(); event.preventDefault(); }; const handleDetachedConsoleResizePointerUp = (event: ReactPointerEvent) => { const resizeState = detachedConsoleResizeStateRef.current; if (resizeState?.pointerId === event.pointerId) { if (resizeState.captureTarget.hasPointerCapture(event.pointerId)) { resizeState.captureTarget.releasePointerCapture(event.pointerId); } detachedConsoleResizeStateRef.current = null; } clearDetachedConsoleDraggingState(); event.stopPropagation(); }; const updateDetachedConsoleSize = (nextSize: DetachedConsoleSize) => { const normalized = normalizeDetachedConsoleSize(nextSize); detachedConsoleSizeRef.current = normalized; setDetachedConsoleSize(normalized); setDetachedConsolePosition((current) => { const nextPosition = { x: clamp( current.x, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerWidth - normalized.width - VIEWPORT_PADDING), ), y: clamp( current.y, VIEWPORT_PADDING, Math.max(VIEWPORT_PADDING, window.innerHeight - normalized.height - VIEWPORT_PADDING), ), }; detachedConsolePositionRef.current = nextPosition; return nextPosition; }); }; const toggleConsoleLevel = (level: PreviewRuntimeConsoleLevel) => { setConsoleLevelFilter((current) => { if (current.includes(level)) { if (current.length === 1) { return current; } return current.filter((item) => item !== level); } return CONSOLE_LEVELS.filter((item) => item === level || current.includes(item)); }); }; const renderConsolePanel = (detached: boolean) => (
{detached ? 'Console Detached' : 'Console'}
로그 {consoleEntries.length} 경고 {warnEntryCount} 오류 {errorEntryCount}
{latestConsoleHref || 'Preview URL 확인 대기 중'}
{ event.stopPropagation(); }} > {canDetachConsole ? ( ) : null} {detached ? ( ) : null}
{CONSOLE_LEVELS.map((level) => ( ))}
{filteredConsoleEntries.length ? ( filteredConsoleEntries.map((entry) => { const entryTimeLabel = new Date(entry.timestamp).toLocaleTimeString('ko-KR', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', }); return (
{entry.level.toUpperCase()}
{entry.args.join('\n')}
); }) ) : (
{consoleEntries.length && !hasAnyConsoleLevelsSelected ? '표시할 콘솔 레벨이 선택되지 않았습니다.' : consoleEntries.length ? '선택한 레벨에 해당하는 콘솔 로그가 아직 없습니다.' : 'iframe 콘솔 로그가 아직 없습니다.'}
{consoleEntries.length ? '필터를 조정하거나 Preview 앱에서 동작을 다시 재현해 주세요.' : 'Preview 앱에서 동작을 재현하면 이 패널에서 바로 확인할 수 있습니다.'}
)}
{detached ? (
) : null}
); return createPortal( <>
{ if (minimized && !dragMovedRef.current) { setMinimized(false); } }} onPointerDown={handleHeaderPointerDown} onPointerUp={handleHeaderPointerUp} onPointerCancel={handleHeaderPointerUp} >
{minimized ? (
) : ( <>
{!minimized && !isMobileViewport ? ( ) : null} {!minimized ? (
{showAttachedConsole ? renderConsolePanel(false) : null}
{showDetachedConsole ? renderConsolePanel(true) : null} , document.body, ); }