- 최신 메시지 버튼: 스크롤 컨테이너 밖으로 이동, 입력창 위 중앙 고정 - 스크롤 하단: rAF + 600ms 지연 2회 (이미지 비동기 로드 대응) - 캡처: 버튼 클릭 즉시 오버레이 + domToPng 병렬 실행, mouseup에서 await (font:false 최적화)
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
interface Rect {
|
|
x: number;
|
|
y: number;
|
|
w: number;
|
|
h: number;
|
|
}
|
|
|
|
interface ScreenCaptureProps {
|
|
capturePromise: Promise<HTMLImageElement>;
|
|
onCapture: (file: File) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export function ScreenCapture({ capturePromise, 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);
|
|
|
|
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; }
|
|
|
|
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);
|
|
|
|
canvas.toBlob((blob) => {
|
|
if (!blob) { onCancel(); return; }
|
|
onCapture(new File([blob], `capture-${Date.now()}.png`, { type: "image/png" }));
|
|
}, "image/png");
|
|
} catch {
|
|
onCancel();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<canvas ref={canvasRef} className="hidden" />
|
|
<div
|
|
className="fixed inset-0 bg-black/40 select-none cursor-crosshair"
|
|
style={{ zIndex: 99999 }}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={handleMouseUp}
|
|
>
|
|
<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>
|
|
{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>
|
|
</>
|
|
);
|
|
}
|