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 && (