107 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
}
|