부트캠프 과정 중 파이널 프로젝트로 Next.js를 사용했었습니다. 개발했던 기능 중 게시물등록에 대해 학습차 기록하려 합니다.
주요기능은 텍스트와 이미지 등록으로, 이미지는 S3에 업로드 한 후 Url을 응답값으로 받아 그 값만 db에 저장하는 형식으로 구현했습니다.
const validateFile = (file: File): boolean => {
// 허용되는 이미지 확장자 리스트
const allowedExtensions = ["png", "jpg", "jpeg", "gif"];
// 파일 크기 제한: 10MB
const maxSize = 10 * 1024 * 1024; // 10MB
// 파일 확장자를 추출하고 소문자로 변환
const extension = file.name.split(".").pop()?.toLowerCase();
// 확장자가 없거나 허용된 확장자가 아닌 경우
if (!extension || !allowedExtensions.includes(extension)) {
alert("이미지는 PNG, JPG, GIF 형식만 업로드 가능합니다.");
return false;
}
// 파일 크기가 최대 허용 용량을 초과하는 경우
if (file.size > maxSize) {
alert("파일 크기는 최대 10MB를 넘을 수 없습니다.");
return false;
}
// 모든 유효성 검사를 통과한 경우 true 반환
return true;
};
}
이미지 파일의 확장자와 크기를 검사하여 허용되지 않은 파일 형식 또는 크기가 용량제한을 초과한 파일은 업로드하지 않도록 처리합니다.
const handleImageChange = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
// input 요소에서 선택한 파일을 가져옵니다.
const files = event.target.files;
if (files && files.length > 0) {
// 유효한 파일만 필터링합니다.
const validFiles = Array.from(files).filter(validateFile);
if (validFiles.length === 0) return;
// 폼 데이터를 생성하고, 선택한 파일을 추가합니다.
const formData = new FormData();
validFiles.forEach((file) => {
formData.append("files", file);
});
try {
// 서버로 폼 데이터를 전송합니다.
const response = await fetch("https://711.ha-ving.store/attach", {
method: "POST",
body: formData,
});
if (response.ok) {
// 서버로부터 응답 데이터를 JSON 형식으로 받아옵니다.
const data = await response.json();
// 이미지 URL 리스트를 업데이트합니다.
const newUrls = data.map((item: { url: string }) => item.url);
setImageUrls((prev) => [...prev, ...newUrls]);
} else {
console.error("이미지 업로드 실패");
}
} catch (error) {
console.error("에러 발생:", error);
}
}
};
파일 선택 이벤트 발생 시, 유효성 검사를 거친 파일들을 서버에 업로드합니다. 서버로부터 이미지 URL을 받아오고, 이를 상태에 저장한 후 글 등록시 서버로 전송합니다.
const handleImageDelete = (url: string) => {
setImageUrls(prev => prev.filter(image => image !== url))
}
이미지 삭제 버튼 클릭 시 해당 이미지를 URL 목록에서 제거합니다.
const handleUpdate = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 폼 제출 기본 동작 방지
const formData = new FormData(e.currentTarget); // 폼 데이터를 가져옵니다.
const category_id = selectedType; // 선택된 카테고리 ID
const title = formData.get("title"); // 제목 필드 값
const content = formData.get("content"); // 내용 필드 값
const visible = true; // 게시물 공개 여부 (true: 공개)
// 서버로 보낼 데이터를 준비합니다.
const updatedItem: Partial<{
category_id: number;
title: FormDataEntryValue | null;
content: FormDataEntryValue | null;
visible: boolean;
image_urls: string[];
id?: number;
}> = {
category_id: category_id,
title: title,
content: content,
visible: visible,
image_urls: imageUrls, // 업로드된 이미지의 URL 배열
};
// 새 게시물 작성 또는 기존 게시물 수정에 따라 URL과 메서드를 설정합니다.
let url = `https://711.ha-ving.store/boards/${category.value}/`;
let method = "POST";
if (currentPost) {
// 수정 시에는 PUT 메서드를 사용하고, URL에 게시물 ID를 포함합니다.
url = `https://711.ha-ving.store/boards/${category.value}/${currentPost.id}`;
method = "PUT";
updatedItem.id = currentPost.id;
}
try {
// 서버에 데이터 전송
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
Authorization: "Bearer YOUR_ACCESS_TOKEN", // 인증 토큰 추가
},
body: JSON.stringify(updatedItem), // JSON 형식으로 데이터 전송
});
if (response.ok) {
const responseData = await response.json();
// 게시물 작성 또는 수정 후 상세 페이지로 리디렉션
if (currentPost) {
router.push(`/boards/${category.value}/${updatedItem.id}`);
} else {
router.push(`/boards/${category.value}/${responseData.data.id}`);
}
} else {
console.error("게시물 등록을 실패했습니다.");
}
} catch (error) {
console.error("에러 발생:", error);
}
};
사용자가 폼을 제출하면, 새로운 게시물을 생성하거나 기존 게시물을 수정합니다. 이때 서버에 데이터를 전송하고, 성공 시 해당 게시물의 상세 페이지로 리디렉션됩니다.