Files
jaejadle/nextjs/app/(subpages)/(news)/announcements/create/page.tsx
Mayne0213 f78454c2a1
Some checks failed
Build Docker Image / build-and-push (push) Has been cancelled
CI / lint-and-build (push) Has been cancelled
CHORE(merge): merge from develop
- Initial setup and all features from develop branch
- Includes: auth, deploy, docker, style fixes
- K3S deployment configuration
2026-01-06 17:29:16 +09:00

181 lines
5.7 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { createAnnouncement, uploadFile } from "@/lib/services";
import { useAuth } from "@/hooks";
import ImageUpload, { PendingImage } from "@/components/ImageUpload";
interface AnnouncementFormData {
title: string;
isImportant: boolean;
}
export default function CreateAnnouncementPage() {
const [pendingImages, setPendingImages] = useState<PendingImage[]>([]);
const router = useRouter();
const { user, isLoading } = useAuth();
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<AnnouncementFormData>({
defaultValues: {
title: "",
isImportant: false,
},
});
// 로그인하지 않은 경우 리다이렉트
useEffect(() => {
if (!isLoading && !user) {
alert("로그인이 필요합니다.");
router.push("/login");
}
}, [isLoading, user, router]);
const onSubmit = async (data: AnnouncementFormData) => {
if (!user) return;
try {
// 이미지 업로드
let uploadedFiles: {
fileKey: string;
fileName: string;
fileSize: number;
mimeType: string;
}[] = [];
if (pendingImages.length > 0) {
const sortedImages = [...pendingImages].sort((a, b) => a.order - b.order);
const uploadPromises = sortedImages.map(async (img) => {
const result = await uploadFile(img.file, "/announcement");
return {
fileKey: result.fileKey,
fileName: img.file.name,
fileSize: img.file.size,
mimeType: img.file.type,
};
});
uploadedFiles = await Promise.all(uploadPromises);
}
await createAnnouncement({
...data,
content: "", // 내용 필드는 빈 문자열로 전송
authorId: user.id,
files: uploadedFiles.length > 0 ? uploadedFiles : undefined,
});
// 미리보기 URL 정리
pendingImages.forEach((img) => {
if (img.preview) URL.revokeObjectURL(img.preview);
});
alert("주보가 등록되었습니다.");
router.push("/announcements");
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : "주보 등록에 실패했습니다.";
alert(errorMessage);
}
};
if (isLoading) {
return (
<div className="min-h-screen bg-white w-full flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
if (!user) {
return null;
}
return (
<div className="min-h-screen bg-white w-full">
<div className="py-12 px-4">
<div className="max-w-4xl mx-auto">
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 w-full">
{/* 제목 */}
<div>
<label className="block text-sm font-semibold text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="text"
{...register("title", {
required: "제목을 입력해주세요",
minLength: {
value: 2,
message: "제목은 2자 이상이어야 합니다",
},
})}
disabled={isSubmitting}
placeholder="제목을 입력해주세요"
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:border-transparent disabled:opacity-50 transition-all ${
errors.title
? "border-red-300 focus:ring-red-400"
: "border-gray-300 focus:ring-blue-500"
}`}
/>
{errors.title && (
<p className="text-red-500 text-sm mt-1">
{errors.title.message}
</p>
)}
</div>
{/* 중요 공지 체크박스 */}
<div className="flex items-center">
<input
type="checkbox"
id="isImportant"
{...register("isImportant")}
disabled={isSubmitting}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
/>
<label
htmlFor="isImportant"
className="ml-2 text-sm font-medium text-gray-700"
>
</label>
</div>
{/* 이미지 업로드 */}
<ImageUpload
images={pendingImages}
onImagesChange={setPendingImages}
disabled={isSubmitting}
/>
{/* 버튼 */}
<div className="flex gap-4 pt-4">
<button
type="submit"
disabled={isSubmitting}
className="flex-1 px-6 py-3 bg-linear-to-br from-[#7ba5d6] to-[#6b95c6] hover:from-[#6b95c6] hover:to-[#5b85b6] text-white rounded-lg shadow-md hover:shadow-lg transition-all font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? "등록 중..." : "등록하기"}
</button>
<button
type="button"
onClick={() => router.back()}
disabled={isSubmitting}
className="px-6 py-3 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors font-semibold disabled:opacity-50"
>
</button>
</div>
</form>
</div>
</div>
</div>
);
}