[RAPID-fix] 스크롤/캡처 3가지 수정

- 스크롤 하단 이동: useLayoutEffect → useEffect+rAF (이미지 레이아웃 완료 후 스크롤)
- 스크롤 버튼: 리스너 deps를 room.id로 변경 (room 없을 때 미연결 문제 해결)
- 캡처 속도: domToPng(느림) → getDisplayMedia 네이티브 API(즉시 캡처)
This commit is contained in:
2026-03-31 15:42:14 +09:00
parent 0d020e260d
commit ce91f7f77e
2 changed files with 28 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { MessageSquare, Pencil, Check, X, ChevronsDown } from "lucide-react";
import { useMessages, useMarkAsRead, useUpdateRoom } from "@/hooks/useMessenger";
import { useAuth } from "@/hooks/useAuth";
@@ -38,11 +38,16 @@ export function ChatPanel({ room, messageInputRef, onCaptureClick }: ChatPanelPr
const lastMessageId = messages?.[messages.length - 1]?.id;
useLayoutEffect(() => {
const el = scrollRef.current;
if (el) el.scrollTop = el.scrollHeight;
// Scroll to bottom when messages load or room changes
// useEffect + rAF to let images/content finish laying out first
useEffect(() => {
requestAnimationFrame(() => {
const el = scrollRef.current;
if (el) el.scrollTop = el.scrollHeight;
});
}, [lastMessageId, selectedRoomId]);
// Re-attach scroll listener whenever room changes (scrollRef mounts after room is set)
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
@@ -51,7 +56,7 @@ export function ChatPanel({ room, messageInputRef, onCaptureClick }: ChatPanelPr
};
el.addEventListener("scroll", onScroll);
return () => el.removeEventListener("scroll", onScroll);
}, []);
}, [room?.id]);
if (!room) {
return (

View File

@@ -41,20 +41,29 @@ export function MessengerModal() {
const handleCaptureClick = useCallback(async () => {
try {
const { domToPng } = await import("modern-screenshot");
const scale = Math.max(window.devicePixelRatio || 1, 2);
const dataUrl = await domToPng(document.body, {
width: window.innerWidth,
height: window.innerHeight,
scale,
const stream = await (navigator.mediaDevices as any).getDisplayMedia({
video: true,
preferCurrentTab: true,
});
const video = document.createElement("video");
video.srcObject = stream;
await new Promise<void>((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 = dataUrl;
await new Promise((res) => { img.onload = res; });
img.src = canvas.toDataURL("image/png");
await new Promise<void>((res) => { img.onload = () => res(); });
capturedImgRef.current = img;
setCapturing(true);
} catch {
// fail silently
// user cancelled
}
}, []);