Files
vexplor_dev/frontend/components/messenger/ScreenCapture.tsx
syc0123 20b82dc57b [RAPID] 메신저 화면 캡처 기능 구현
- modern-screenshot 패키지 추가
- ScreenCapture: 전체화면 오버레이 + 드래그 영역 선택 + DOM 캡처
- MessengerModal: 캡처 버튼(Scissors) 추가, 캡처 중 모달 숨김
- MessageInput: forwardRef로 addFiles 메서드 외부 노출
- ChatPanel: messageInputRef prop 추가 및 전달

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:20:41 +09:00

121 lines
3.5 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
interface Rect {
x: number;
y: number;
w: number;
h: number;
}
interface ScreenCaptureProps {
onCapture: (file: File) => void;
onCancel: () => void;
}
export function ScreenCapture({ onCapture, onCancel }: ScreenCaptureProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [selecting, setSelecting] = useState(false);
const startRef = useRef<{ x: number; y: number } | null>(null);
const [rect, setRect] = useState<Rect | null>(null);
// ESC to cancel
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") onCancel();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [onCancel]);
const getRect = (ax: number, ay: number, bx: number, by: number): Rect => ({
x: Math.min(ax, bx),
y: Math.min(ay, by),
w: Math.abs(bx - ax),
h: Math.abs(by - ay),
});
const handleMouseDown = (e: React.MouseEvent) => {
startRef.current = { x: e.clientX, y: e.clientY };
setSelecting(true);
setRect(null);
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!selecting || !startRef.current) return;
setRect(getRect(startRef.current.x, startRef.current.y, e.clientX, e.clientY));
};
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);
startRef.current = null;
if (r.w < 4 || r.h < 4) {
onCancel();
return;
}
// Capture via modern-screenshot
try {
const { domToPng } = await import("modern-screenshot");
const dpr = window.devicePixelRatio || 1;
const dataUrl = await domToPng(document.body, {
width: window.innerWidth,
height: window.innerHeight,
style: {
transform: "none",
transformOrigin: "top left",
},
});
// Crop the captured region
const img = new Image();
img.src = dataUrl;
await new Promise((res) => { img.onload = res; });
const canvas = canvasRef.current!;
canvas.width = r.w * dpr;
canvas.height = r.h * dpr;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(img, r.x * dpr, r.y * dpr, r.w * dpr, r.h * dpr, 0, 0, r.w * dpr, r.h * dpr);
canvas.toBlob((blob) => {
if (!blob) { onCancel(); return; }
const file = new File([blob], `capture-${Date.now()}.png`, { type: "image/png" });
onCapture(file);
}, "image/png");
} catch {
onCancel();
}
};
return (
<>
<canvas ref={canvasRef} className="hidden" />
<div
className="fixed inset-0 bg-black/40 cursor-crosshair select-none"
style={{ zIndex: 99999 }}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
{/* instruction */}
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-black/70 text-white text-sm px-4 py-2 rounded-full pointer-events-none">
&nbsp;·&nbsp; ESC로
</div>
{/* selection rect */}
{rect && rect.w > 0 && rect.h > 0 && (
<div
className="absolute border-2 border-blue-400 bg-blue-400/10 pointer-events-none"
style={{ left: rect.x, top: rect.y, width: rect.w, height: rect.h }}
/>
)}
</div>
</>
);
}