모의투자 관리 기능 추가

This commit is contained in:
Macbook
2026-05-31 03:47:07 +09:00
parent 9d7dddfa57
commit 16d0e2c226
78 changed files with 4688 additions and 972 deletions
+93
View File
@@ -0,0 +1,93 @@
/** 화면 캡처 영역 → 업로드용 PNG File */
export interface ScreenCaptureRect {
left: number;
top: number;
width: number;
height: number;
}
export function clientRectToVideoPixels(
rect: ScreenCaptureRect,
videoEl: HTMLVideoElement,
): ScreenCaptureRect | null {
const bounds = videoEl.getBoundingClientRect();
if (bounds.width <= 0 || bounds.height <= 0) return null;
const vw = videoEl.videoWidth;
const vh = videoEl.videoHeight;
if (vw <= 0 || vh <= 0) return null;
const toVidX = (cx: number) => ((cx - bounds.left) / bounds.width) * vw;
const toVidY = (cy: number) => ((cy - bounds.top) / bounds.height) * vh;
const x1 = Math.max(0, Math.min(vw, Math.round(toVidX(rect.left))));
const y1 = Math.max(0, Math.min(vh, Math.round(toVidY(rect.top))));
const x2 = Math.max(0, Math.min(vw, Math.round(toVidX(rect.left + rect.width))));
const y2 = Math.max(0, Math.min(vh, Math.round(toVidY(rect.top + rect.height))));
const width = x2 - x1;
const height = y2 - y1;
if (width < 2 || height < 2) return null;
return { left: x1, top: y1, width, height };
}
export async function cropVideoFrameToFile(
videoEl: HTMLVideoElement,
rect: ScreenCaptureRect,
): Promise<File> {
const canvas = document.createElement('canvas');
canvas.width = rect.width;
canvas.height = rect.height;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('CANVAS_FAILED');
ctx.drawImage(
videoEl,
rect.left,
rect.top,
rect.width,
rect.height,
0,
0,
rect.width,
rect.height,
);
const blob = await new Promise<Blob | null>(resolve => {
canvas.toBlob(resolve, 'image/png');
});
if (!blob) throw new Error('BLOB_FAILED');
return new File([blob], `screen-capture-${Date.now()}.png`, {
type: 'image/png',
lastModified: Date.now(),
});
}
export async function requestDisplayCaptureStream(): Promise<MediaStream> {
if (!navigator.mediaDevices?.getDisplayMedia) {
throw new Error('DISPLAY_CAPTURE_UNSUPPORTED');
}
return navigator.mediaDevices.getDisplayMedia({
video: { displaySurface: 'browser' } as MediaTrackConstraints,
audio: false,
});
}
export function stopMediaStream(stream: MediaStream | null): void {
stream?.getTracks().forEach(t => t.stop());
}
export function captureErrorMessage(e: unknown): string {
if (e instanceof Error) {
if (e.name === 'NotAllowedError' || e.message === 'DISPLAY_CAPTURE_UNSUPPORTED') {
return '화면 캡처 권한이 거부되었거나 이 브라우저에서 지원하지 않습니다.';
}
if (e.message === 'REGION_TOO_SMALL') {
return '선택 영역이 너무 작습니다. 다시 드래그해 주세요.';
}
if (e.message) return e.message;
}
return '화면 캡처에 실패했습니다.';
}