From eaa47ee5dff7587cb75be46f4b89af6f8042a16e Mon Sep 17 00:00:00 2001 From: syc0123 Date: Tue, 31 Mar 2026 15:49:03 +0900 Subject: [PATCH] =?UTF-8?q?[RAPID-fix]=20=EB=A9=94=EC=8B=A0=EC=A0=80=203?= =?UTF-8?q?=EA=B0=80=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 최신 메시지 버튼: 스크롤 컨테이너 밖으로 이동, 입력창 위 중앙 고정 - 스크롤 하단: rAF + 600ms 지연 2회 (이미지 비동기 로드 대응) - 캡처: 버튼 클릭 즉시 오버레이 + domToPng 병렬 실행, mouseup에서 await (font:false 최적화) --- frontend/components/messenger/ChatPanel.tsx | 30 +++++++----- .../components/messenger/MessengerModal.tsx | 42 +++++++--------- .../components/messenger/ScreenCapture.tsx | 49 +++++++++---------- 3 files changed, 56 insertions(+), 65 deletions(-) diff --git a/frontend/components/messenger/ChatPanel.tsx b/frontend/components/messenger/ChatPanel.tsx index eacb727c..6500b8da 100644 --- a/frontend/components/messenger/ChatPanel.tsx +++ b/frontend/components/messenger/ChatPanel.tsx @@ -39,12 +39,15 @@ export function ChatPanel({ room, messageInputRef, onCaptureClick }: ChatPanelPr const lastMessageId = messages?.[messages.length - 1]?.id; // Scroll to bottom when messages load or room changes - // useEffect + rAF to let images/content finish laying out first + // Two-pass: immediate rAF + 600ms delayed (for async image loads) useEffect(() => { - requestAnimationFrame(() => { + const scrollToBottom = () => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; - }); + }; + requestAnimationFrame(scrollToBottom); + const t = setTimeout(scrollToBottom, 600); + return () => clearTimeout(t); }, [lastMessageId, selectedRoomId]); // Re-attach scroll listener whenever room changes (scrollRef mounts after room is set) @@ -113,7 +116,7 @@ export function ChatPanel({ room, messageInputRef, onCaptureClick }: ChatPanelPr })(); return ( -
+
{/* Header */}
{isEditingName ? ( @@ -189,17 +192,18 @@ export function ChatPanel({ room, messageInputRef, onCaptureClick }: ChatPanelPr {roomTyping && roomTyping.length > 0 ? `${roomTyping.join(", ")}님이 입력 중...` : ""}
- {!isAtBottom && ( - - )}
+ {!isAtBottom && ( + + )} + {/* Input */} (null); + const capturePromiseRef = useRef | null>(null); const messageInputRef = useRef(null); useEffect(() => { @@ -39,32 +40,23 @@ export function MessengerModal() { const selectedRoom = rooms.find((r) => r.id === selectedRoomId) || null; - const handleCaptureClick = useCallback(async () => { - try { - const stream = await (navigator.mediaDevices as any).getDisplayMedia({ - video: true, - preferCurrentTab: true, + const handleCaptureClick = useCallback(() => { + // Start capture immediately (messenger still visible — captures full page) + capturePromiseRef.current = (async (): Promise => { + const { domToPng } = await import("modern-screenshot"); + const dataUrl = await domToPng(document.body, { + width: window.innerWidth, + height: window.innerHeight, + scale: window.devicePixelRatio || 1, + font: false, }); - const video = document.createElement("video"); - video.srcObject = stream; - await new Promise((res) => { video.onloadedmetadata = () => res(); }); - await video.play(); - - const canvas = document.createElement("canvas"); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - canvas.getContext("2d")!.drawImage(video, 0, 0); - stream.getTracks().forEach((t: MediaStreamTrack) => t.stop()); - const img = new Image(); - img.src = canvas.toDataURL("image/png"); + img.src = dataUrl; await new Promise((res) => { img.onload = () => res(); }); - - capturedImgRef.current = img; - setCapturing(true); - } catch { - // user cancelled - } + return img; + })(); + // Show overlay immediately — don't await capture + setCapturing(true); }, []); // Position & size state @@ -187,9 +179,9 @@ export function MessengerModal() { return ( <> - {capturing && capturedImgRef.current && ( + {capturing && capturePromiseRef.current && ( { setCapturing(false); messageInputRef.current?.addFiles([file]); diff --git a/frontend/components/messenger/ScreenCapture.tsx b/frontend/components/messenger/ScreenCapture.tsx index ec962996..5c214f56 100644 --- a/frontend/components/messenger/ScreenCapture.tsx +++ b/frontend/components/messenger/ScreenCapture.tsx @@ -10,22 +10,19 @@ interface Rect { } interface ScreenCaptureProps { - capturedImg: HTMLImageElement; + capturePromise: Promise; onCapture: (file: File) => void; onCancel: () => void; } -export function ScreenCapture({ capturedImg, onCapture, onCancel }: ScreenCaptureProps) { +export function ScreenCapture({ capturePromise, onCapture, onCancel }: ScreenCaptureProps) { const canvasRef = useRef(null); const [selecting, setSelecting] = useState(false); const startRef = useRef<{ x: number; y: number } | null>(null); const [rect, setRect] = useState(null); - // ESC to cancel useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key === "Escape") onCancel(); - }; + const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [onCancel]); @@ -48,7 +45,7 @@ export function ScreenCapture({ capturedImg, onCapture, onCancel }: ScreenCaptur setRect(getRect(startRef.current.x, startRef.current.y, e.clientX, e.clientY)); }; - const handleMouseUp = (e: React.MouseEvent) => { + const handleMouseUp = async (e: React.MouseEvent) => { if (!selecting || !startRef.current) return; setSelecting(false); const r = getRect(startRef.current.x, startRef.current.y, e.clientX, e.clientY); @@ -56,33 +53,32 @@ export function ScreenCapture({ capturedImg, onCapture, onCancel }: ScreenCaptur if (r.w < 4 || r.h < 4) { onCancel(); return; } - const img = capturedImg; - const scaleX = img.naturalWidth / window.innerWidth; - const scaleY = img.naturalHeight / window.innerHeight; + try { + const img = await capturePromise; + const scaleX = img.naturalWidth / window.innerWidth; + const scaleY = img.naturalHeight / window.innerHeight; - const canvas = canvasRef.current!; - canvas.width = r.w * scaleX; - canvas.height = r.h * scaleY; - const ctx = canvas.getContext("2d")!; - ctx.drawImage( - img, - r.x * scaleX, r.y * scaleY, r.w * scaleX, r.h * scaleY, - 0, 0, r.w * scaleX, r.h * scaleY, - ); + const canvas = canvasRef.current!; + canvas.width = r.w * scaleX; + canvas.height = r.h * scaleY; + const ctx = canvas.getContext("2d")!; + ctx.drawImage(img, r.x * scaleX, r.y * scaleY, r.w * scaleX, r.h * scaleY, 0, 0, r.w * scaleX, r.h * scaleY); - canvas.toBlob((blob) => { - if (!blob) { onCancel(); return; } - const file = new File([blob], `capture-${Date.now()}.png`, { type: "image/png" }); - onCapture(file); - }, "image/png"); + canvas.toBlob((blob) => { + if (!blob) { onCancel(); return; } + onCapture(new File([blob], `capture-${Date.now()}.png`, { type: "image/png" })); + }, "image/png"); + } catch { + onCancel(); + } }; return ( <>
드래그하여 캡처 영역을 선택하세요 · ESC로 취소
- {rect && rect.w > 0 && rect.h > 0 && (