feat: add weekly curriculum cover image upload and display
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { ImagePlus, Loader2, Trash2 } from 'lucide-react';
|
||||
import { deleteCurriculumReference, uploadCurriculumReference, type CurriculumReference } from '../lib/curriculum';
|
||||
|
||||
type Props = {
|
||||
accessToken: string;
|
||||
courseId: string;
|
||||
week: number;
|
||||
cover: CurriculumReference | null;
|
||||
onChange: (cover: CurriculumReference | null) => void;
|
||||
};
|
||||
|
||||
export function WeekCoverEditor({ accessToken, courseId, week, cover, onChange }: Props) {
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
async function save(file?: File) {
|
||||
if (busy || (!file && !cover)) return;
|
||||
if (file && (!['image/png', 'image/jpeg', 'image/webp'].includes(file.type) || file.size > 8 * 1024 * 1024)) {
|
||||
setError('8MB 이하의 PNG, JPEG, WebP 이미지를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const result = file
|
||||
? await uploadCurriculumReference(accessToken, { courseId, week, file, kind: 'cover-image' })
|
||||
: await deleteCurriculumReference(accessToken, cover!.id, courseId);
|
||||
onChange(result.weeks.find((item) => item.week === week)?.detail.coverImage ?? null);
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : '대표이미지를 저장하지 못했습니다.');
|
||||
} finally { setBusy(false); }
|
||||
}
|
||||
return (
|
||||
<section>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-[15px] font-black text-[#111827]">대표이미지</h3>
|
||||
<div className="flex gap-2">
|
||||
<button type="button" disabled={busy} onClick={() => input.current?.click()}
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-md border border-[#d1d9e6] bg-white px-3 text-[12px] font-bold disabled:opacity-50">
|
||||
{busy ? <Loader2 size={14} className="animate-spin" /> : <ImagePlus size={14} />}
|
||||
{busy ? '저장 중' : cover ? '이미지 변경' : '이미지 업로드'}
|
||||
</button>
|
||||
{cover && <button type="button" title="대표이미지 삭제" aria-label="대표이미지 삭제" disabled={busy}
|
||||
onClick={() => void save()} className="grid h-8 w-8 place-items-center rounded-md border border-[#d1d9e6] bg-white text-red-600 disabled:opacity-50"><Trash2 size={14} /></button>}
|
||||
</div>
|
||||
</div>
|
||||
<input ref={input} type="file" accept="image/png,image/jpeg,image/webp" className="hidden"
|
||||
onChange={(event) => { const file = event.target.files?.[0]; event.target.value = ''; if (file) void save(file); }} />
|
||||
{cover && <img src={cover.downloadUrl} alt={`${week}주차 대표이미지`} className="mt-3 h-[240px] w-full rounded-md object-cover object-center" />}
|
||||
{error && <p role="alert" className="mt-2 text-[13px] text-red-600">{error}</p>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user