playreel/frontend/components/poster-dropzone.tsx
2026-09-08 13:48:20 +09:00

107 lines
3.8 KiB
TypeScript

"use client";
import { useCallback, useRef, useState } from "react";
interface Props {
file: File | null;
onFile: (f: File | null) => void;
}
/* 고해상도 원본 수급이 어려운 현실을 반영해 해상도는 차단하지 않는다.
낮으면 알려만 주고 업로드는 그대로 진행시킨다. */
const SOFT_LONG_EDGE = 1000;
export default function PosterDropzone({ file, onFile }: Props) {
const inputRef = useRef<HTMLInputElement>(null);
const [preview, setPreview] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [dragging, setDragging] = useState(false);
const accept = useCallback((f: File) => {
if (!/^image\/(jpeg|png|webp|gif)$/.test(f.type)) {
setError("JPG / PNG / WEBP / GIF 파일만 올릴 수 있습니다.");
return;
}
setError(null);
const url = URL.createObjectURL(f);
const img = new Image();
img.onload = () => {
const edge = Math.max(img.width, img.height);
setNotice(edge < SOFT_LONG_EDGE
? `긴 변 ${edge}px입니다. 그대로 진행할 수 있고, 깊은 줌 구간만 다소 부드럽게 보일 수 있습니다.`
: null);
setPreview(url);
onFile(f);
};
img.src = url;
}, [onFile]);
return (
<div>
<div
className="card-inner"
onClick={() => inputRef.current?.click()}
onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
onDragLeave={() => setDragging(false)}
onDrop={(e) => {
e.preventDefault();
setDragging(false);
const f = e.dataTransfer.files?.[0];
if (f) accept(f);
}}
style={{
// 포스터는 세로물 — 입력창도 4:5 세로 비율이어야 직관적
aspectRatio: "4 / 5",
maxWidth: 400,
margin: "0 auto",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
padding: preview ? 0 : "var(--s-5)",
textAlign: "center",
cursor: "pointer",
borderStyle: preview ? "solid" : "dashed",
borderColor: dragging ? "var(--color-mint)" : "var(--color-border-gray-700)",
background: dragging ? "var(--color-mint-10)" : "rgba(18, 26, 29, 0.4)",
}}
>
{preview ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={preview} alt="포스터 미리보기" style={{ width: "100%", height: "100%", objectFit: "contain" }} />
) : (
<div>
<p style={{ fontSize: 16, fontWeight: 700, margin: 0, lineHeight: 1.5 }}>
<br />
</p>
<p style={{ fontSize: "var(--text-sm)", color: "var(--color-text-gray-400)", marginTop: "0.75rem" }}>
JPG · PNG · WEBP · GIF<br />
</p>
</div>
)}
<input
ref={inputRef}
type="file"
accept="image/jpeg,image/png,image/webp,image/gif"
hidden
onChange={(e) => { const f = e.target.files?.[0]; if (f) accept(f); }}
/>
</div>
{error && (
<p style={{ fontSize: "var(--text-sm)", color: "#ff7a7a", marginTop: "0.75rem" }}>{error}</p>
)}
{notice && (
<p style={{ fontSize: "var(--text-sm)", color: "var(--color-text-gray-400)", marginTop: "0.75rem", lineHeight: 1.5 }}>
{notice}
</p>
)}
{file && (
<p style={{ fontSize: "var(--text-sm)", color: "var(--color-text-gray-400)", marginTop: "0.5rem" }}>
{file.name} · {(file.size / 1024 / 1024).toFixed(1)}MB
</p>
)}
</div>
);
}