- modern-screenshot 패키지 추가 - ScreenCapture: 전체화면 오버레이 + 드래그 영역 선택 + DOM 캡처 - MessengerModal: 캡처 버튼(Scissors) 추가, 캡처 중 모달 숨김 - MessageInput: forwardRef로 addFiles 메서드 외부 노출 - ChatPanel: messageInputRef prop 추가 및 전달 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
121 lines
3.5 KiB
TypeScript
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">
|
|
드래그하여 캡처 영역을 선택하세요 · 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>
|
|
</>
|
|
);
|
|
}
|