From 44da4d59b9279f63fdb33271c71fd82d6fdecd19 Mon Sep 17 00:00:00 2001 From: parkseyoung Date: Wed, 6 Nov 2024 15:37:48 +0900 Subject: [PATCH 1/4] =?UTF-8?q?bug:=20403=20=EC=97=90=EB=9F=AC=20(cloudFro?= =?UTF-8?q?nt=20url=20=EC=82=AC=EC=9A=A9=20=EC=A0=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/useImageUpload.jsx | 113 ++++++++++++++++++++++ src/api/useImageUrl.jsx | 101 ++++++++++++++++++++ src/api/useSaveImage.jsx | 21 +++++ src/containers/WriteForm.jsx | 134 +++++++++++++++++++++------ src/containers/styles/WriteForm.scss | 41 ++++++++ 5 files changed, 382 insertions(+), 28 deletions(-) create mode 100644 src/api/useImageUpload.jsx create mode 100644 src/api/useImageUrl.jsx create mode 100644 src/api/useSaveImage.jsx diff --git a/src/api/useImageUpload.jsx b/src/api/useImageUpload.jsx new file mode 100644 index 0000000..770bf2e --- /dev/null +++ b/src/api/useImageUpload.jsx @@ -0,0 +1,113 @@ +// useImageUpload.jsx +import api from "./config"; +import { getPresignedUrl, uploadToS3 } from "./useImageUrl"; + +export const useImageUpload = () => { + // validateFiles를 useImageUpload 함수 내부로 이동 + const validateFiles = (files) => { + const maxSize = 10 * 1024 * 1024; // 10MB + const validTypes = ["image/jpeg", "image/png", "image/gif"]; + + return Array.from(files).map((file) => { + if (file.size > maxSize) { + throw new Error(`${file.name}의 크기가 10MB를 초과합니다.`); + } + if (!validTypes.includes(file.type)) { + throw new Error(`${file.name}은(는) 지원하지 않는 파일 형식입니다.`); + } + return file; + }); + }; + + const handleImageUpload = async (postId, files, onProgress = () => {}) => { + try { + // 1. 파일 검증 + const validFiles = validateFiles(files); + console.log( + "Processing files:", + validFiles.map((f) => f.name) + ); + onProgress(5); + + // 2. Presigned URL 요청 + const presignedData = await getPresignedUrl( + postId, + validFiles.map((f) => f.name) + ); + console.log("Received presigned URLs:", presignedData); + + if (!presignedData?.urls?.length) { + throw new Error("업로드 URL을 받지 못했습니다."); + } + + onProgress(10); + + // 3. S3 업로드 + const uploadResults = []; + const totalFiles = presignedData.urls.length; + const progressPerFile = 80 / totalFiles; + + for (let i = 0; i < totalFiles; i++) { + const urlInfo = presignedData.urls[i]; + const file = validFiles[i]; + + console.log(`Uploading file ${i + 1}/${totalFiles}: ${file.name}`); + + try { + const s3Url = await uploadToS3(urlInfo.url, file, (progress) => { + const overallProgress = + 10 + i * progressPerFile + (progress * progressPerFile) / 100; + onProgress(Math.min(90, overallProgress)); + }); + + // URL 정보 저장 + uploadResults.push({ + fileName: urlInfo.fileName, // 원본 파일명 + url: s3Url, // S3 URL + }); + + console.log(`Successfully uploaded ${file.name}`); + } catch (error) { + console.error(`Failed to upload ${file.name}:`, error); + throw error; + } + } + + // 4. 이미지 URL 저장 + if (uploadResults.length > 0) { + try { + console.log("Saving image URLs:", { urls: uploadResults }); + + // API 스펙에 맞게 요청 데이터 구성 + const response = await api.post(`/api/v1/posts/${postId}/images`, { + urls: uploadResults, + }); + + console.log("Image save response:", response.data); + onProgress(100); + + // 응답 데이터에서 이미지 정보 추출 + return response.data.images.map((image) => ({ + id: image.imageId, + postId: image.postId, + originalName: image.originalName, + storedName: image.storedName, + url: image.s3ImagePath, + createdAt: image.createdAt, + updatedAt: image.updatedAt, + })); + } catch (error) { + console.error("Failed to save image URLs:", error); + throw new Error("이미지 URL 저장 중 오류가 발생했습니다."); + } + } + + return []; + } catch (error) { + console.error("Image upload process failed:", error); + throw error; + } + }; + + return { handleImageUpload }; +}; diff --git a/src/api/useImageUrl.jsx b/src/api/useImageUrl.jsx new file mode 100644 index 0000000..a5dd446 --- /dev/null +++ b/src/api/useImageUrl.jsx @@ -0,0 +1,101 @@ +import api from "./config"; + +export const uploadToS3 = async (presignedUrl, file, onProgress = () => {}) => { + try { + console.log("Starting S3 upload:", { + fileName: file.name, + fileType: file.type, + fileSize: file.size, + url: presignedUrl, + }); + + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + // withCredentials를 false로 설정 + xhr.withCredentials = false; + + xhr.open("PUT", presignedUrl, true); + + // 필수 헤더만 설정 + xhr.setRequestHeader("Content-Type", file.type); + + // 타임아웃 설정 + xhr.timeout = 30000; // 30초 + + // 업로드 진행률 + xhr.upload.onprogress = (event) => { + if (event.lengthComputable) { + const percentComplete = (event.loaded / event.total) * 100; + console.log(`Upload progress: ${percentComplete}%`); + onProgress(percentComplete); + } + }; + + // 성공 처리 + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + const s3Url = presignedUrl.split("?")[0]; + console.log("Upload successful. S3 URL:", s3Url); + resolve(s3Url); + } else { + console.error("Upload failed with status:", xhr.status); + console.error("Response:", xhr.responseText); + reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`)); + } + }; + + // 에러 처리 + xhr.onerror = (event) => { + console.error("XHR Error details:", { + type: event.type, + target: event.target, + status: xhr.status, + statusText: xhr.statusText, + responseText: xhr.responseText, + }); + reject(new Error(`Network error during upload: ${xhr.statusText}`)); + }; + + // 타임아웃 처리 + xhr.ontimeout = () => { + reject(new Error("Upload timed out")); + }; + + // 업로드 취소 처리 + xhr.onabort = () => { + reject(new Error("Upload was aborted")); + }; + + try { + // 실제 전송 + xhr.send(file); + } catch (sendError) { + console.error("Error sending file:", sendError); + reject(new Error(`Failed to send file: ${sendError.message}`)); + } + }); + } catch (error) { + console.error("S3 upload error:", { + error, + fileName: file.name, + fileSize: file.size, + url: presignedUrl, + }); + throw error; + } +}; + +export const getPresignedUrl = async (postId, fileNames) => { + try { + const response = await api.post( + `/api/v1/posts/${postId}/images/presigned-url`, + { fileNames } + ); + console.log("Presigned URL Response:", response.data); + return response.data; + } catch (error) { + console.error("Error getting presigned URL:", error); + throw new Error("사전 서명된 URL을 가져오는데 실패했습니다."); + } +}; diff --git a/src/api/useSaveImage.jsx b/src/api/useSaveImage.jsx new file mode 100644 index 0000000..3d7305a --- /dev/null +++ b/src/api/useSaveImage.jsx @@ -0,0 +1,21 @@ +// useSaveImage.jsx +import api from "./config"; + +export const saveImages = async (postId, imageUrls) => { + try { + console.log("Saving images to backend:", { postId, imageUrls }); + + const response = await api.post(`/api/v1/posts/${postId}/images`, { + urls: imageUrls.map((url) => ({ + fileName: url.fileName, + url: url.url, + })), + }); + + console.log("Backend save response:", response.data); + return response.data; + } catch (error) { + console.error("Failed to save image URLs:", error); + throw new Error("이미지 URL 저장 중 오류가 발생했습니다."); + } +}; diff --git a/src/containers/WriteForm.jsx b/src/containers/WriteForm.jsx index 7f12af6..35b7ce8 100644 --- a/src/containers/WriteForm.jsx +++ b/src/containers/WriteForm.jsx @@ -4,8 +4,8 @@ import { useState, useEffect } from "react"; import "./styles/WriteForm.scss"; import Uploadicon from "../image/Uploadicon.svg"; import { createPost } from "../api/useCreatePost"; -import { uploadAttachments } from "../api/useAttachment"; import { editPost } from "../api/useEditPost"; +import { useImageUpload } from "../api/useImageUpload"; // 추가된 import const WriteForm = ({ onClose, @@ -30,6 +30,8 @@ const WriteForm = ({ memberName: "", memberNameEnglish: "", }); + const [isUploading, setIsUploading] = useState(false); // 업로드 상태 추가 + const { handleImageUpload } = useImageUpload(); // useImageUpload hook 사용 useEffect(() => { // localStorage에서 사용자 정보 가져오기 @@ -48,6 +50,8 @@ const WriteForm = ({ } return `${userInfo.memberNameEnglish}(${userInfo.memberName})`; }; + const [uploadProgress, setUploadProgress] = useState(0); + const [uploadStatus, setUploadStatus] = useState(""); const handleSubmit = async () => { try { @@ -56,42 +60,93 @@ const WriteForm = ({ return; } - let updatedPost; - if (editMode) { - // 수정 모드일 때 - updatedPost = await editPost(initialPost.id, initialPost.author.id, { - title, - content, - }); - } else { - // 새 게시글 작성 모드일 때 - const memberId = localStorage.getItem("userId"); - const postData = { - title, - content, - memberId: parseInt(memberId), - }; - updatedPost = await createPost(postData); - } + setIsUploading(true); + setUploadStatus("게시글 저장 중..."); + + // 게시글 저장 + const memberId = localStorage.getItem("userId"); + const postData = { + title, + content, + memberId: parseInt(memberId), + }; + let updatedPost = editMode + ? await editPost(initialPost.id, initialPost.author.id, postData) + : await createPost(postData); + + // 이미지 업로드 if (files.length > 0) { - await uploadAttachments(updatedPost.id, files); + try { + setUploadStatus("이미지 업로드 중..."); + + const images = await handleImageUpload( + updatedPost.id, + files, + (progress) => { + setUploadProgress(progress); + setUploadStatus(`이미지 업로드 중... ${Math.round(progress)}%`); + } + ); + + // 이미지 정보 추가 + updatedPost = { + ...updatedPost, + images: images.map((image) => ({ + id: image.id, + originalName: image.originalName, + url: image.url, + })), + }; + + setUploadStatus("업로드 완료!"); + } catch (uploadError) { + console.error("Image upload failed:", uploadError); + const shouldProceed = window.confirm( + `이미지 업로드 실패: ${uploadError.message}\n\n이미지 없이 게시글을 저장하시겠습니까?` + ); + if (!shouldProceed) { + throw new Error("사용자가 게시글 저장을 취소했습니다."); + } + } } onPostCreated(updatedPost); + onClose(); } catch (error) { - console.error("Error submitting post:", error); - alert( - editMode - ? "게시글 수정 중 오류가 발생했습니다." - : "게시글 작성 중 오류가 발생했습니다." - ); + console.error("Post submission failed:", error); + alert(error.message || "게시글 저장 중 오류가 발생했습니다."); + } finally { + setIsUploading(false); + setUploadStatus(""); + setUploadProgress(0); } }; const handleFileChange = (e) => { - setFiles(Array.from(e.target.files)); + const selectedFiles = Array.from(e.target.files); + + // 파일 크기 및 형식 검증 + const validFiles = selectedFiles.filter((file) => { + const maxSize = 5 * 1024 * 1024; // 5MB + const validTypes = ["image/jpeg", "image/png", "image/gif"]; + + if (file.size > maxSize) { + alert(`${file.name}의 크기가 5MB를 초과합니다.`); + return false; + } + if (!validTypes.includes(file.type)) { + alert( + `${file.name}은(는) 지원하지 않는 파일 형식입니다. (지원 형식: JPG, PNG, GIF)` + ); + return false; + } + return true; + }); + + setFiles(validFiles); }; + const handleOverlayClick = (e) => { if (e.target.className === "write-form-overlay") { onClose(); @@ -127,7 +182,27 @@ const WriteForm = ({ - {files.length > 0 &&

{files.length}개의 파일이 선택됨

} + {files.length > 0 && ( +
+

{files.length}개의 파일이 선택됨

+ +
+ )} + {isUploading && uploadProgress > 0 && ( +
+
+
+
+ {uploadStatus} +
+ )}
@@ -143,7 +218,8 @@ const WriteForm = ({ alt={editMode ? "수정" : "업로드"} className="upload-icon" onClick={handleSubmit} - style={{ cursor: "pointer" }} + style={{ cursor: isUploading ? "not-allowed" : "pointer" }} + disabled={isUploading} />
@@ -154,6 +230,7 @@ const WriteForm = ({ onChange={(e) => setTitle(e.target.value)} placeholder="제목" required + disabled={isUploading} />
@@ -163,6 +240,7 @@ const WriteForm = ({ onChange={(e) => setContent(e.target.value)} placeholder="내용을 입력하세요" required + disabled={isUploading} />
diff --git a/src/containers/styles/WriteForm.scss b/src/containers/styles/WriteForm.scss index d809852..7dc2016 100644 --- a/src/containers/styles/WriteForm.scss +++ b/src/containers/styles/WriteForm.scss @@ -152,7 +152,48 @@ } } } +.upload-progress { + margin-top: 1rem; + width: 100%; +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: #f0f0f0; + border-radius: 4px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background-color: #4caf50; + transition: width 0.3s ease-in-out; +} + +.progress-text { + display: block; + margin-top: 0.5rem; + font-size: 0.875rem; + color: #666; + text-align: center; +} + +.file-list { + margin-top: 1rem; +} + +.file-list ul { + list-style: none; + padding: 0; + margin: 0.5rem 0; +} +.file-list li { + font-size: 0.875rem; + color: #666; + margin-bottom: 0.25rem; +} @media (max-width: 1130px) { .write-form-container { height: 80vh; From 951c7b833054d6d1ed77c2ddfed4be5a3f850152 Mon Sep 17 00:00:00 2001 From: parkseyoung Date: Wed, 6 Nov 2024 17:11:25 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=A0=ED=8A=B8->s3=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/useGetPost.jsx | 72 +++++++++++++++++++++++----- src/api/useImageUpload.jsx | 92 ++++++++---------------------------- src/api/useImageUrl.jsx | 92 ++++++++---------------------------- src/component/Postlist.jsx | 26 ++++++++-- src/containers/WriteForm.jsx | 11 +---- 5 files changed, 123 insertions(+), 170 deletions(-) diff --git a/src/api/useGetPost.jsx b/src/api/useGetPost.jsx index 24ea8ac..700377c 100644 --- a/src/api/useGetPost.jsx +++ b/src/api/useGetPost.jsx @@ -1,27 +1,21 @@ +// useGetPost.jsx import api from "./config"; export const getPost = async (postId) => { try { const response = await api.get(`/api/v1/posts/${postId}`); - - // Log the entire response for debugging console.log("API Response:", response); - // Validate the response structure const data = response.data; if (!data || typeof data !== "object") { throw new Error("Invalid response format"); } console.log("Received data:", data); - - // Check if the data is nested inside a 'data' property const postData = data.data || data; - - // Log the structure of the data console.log("Post data structure:", Object.keys(postData)); - // Validate required fields + // 필수 필드 검증 const requiredFields = [ "id", "title", @@ -37,7 +31,7 @@ export const getPost = async (postId) => { } } - // Validate member object + // member 객체 검증 const memberFields = ["id", "role", "course", "name", "nameEnglish"]; for (const field of memberFields) { if (!(field in postData.member)) { @@ -45,14 +39,14 @@ export const getPost = async (postId) => { } } - // Transform the data to match our expected structure return { id: postData.id, title: postData.title, content: postData.content, createdAt: postData.createdAt, updatedAt: postData.updatedAt, - imageUrl: "", // 빈 문자열로 설정 + imageUrls: postData.cloudFrontPaths || [], // cloudFrontPath 사용 + s3ImageUrls: postData.s3ImagePaths || [], // s3ImagePaths 보관 likes: (postData.likes !== undefined ? postData.likes : 0).toString(), author: { id: postData.member.id, @@ -60,7 +54,7 @@ export const getPost = async (postId) => { course: postData.member.course, name: postData.member.name, nameEnglish: postData.member.nameEnglish, - profileImage: "", // 빈 문자열로 설정 + profileImage: postData.member.profileImage || "", }, }; } catch (error) { @@ -72,3 +66,57 @@ export const getPost = async (postId) => { throw error; } }; + +// useGetPosts.jsx +export const getPosts = async (pageNo = 0, pageSize = 10) => { + try { + const response = await api.get("/api/v1/posts", { + params: { + pageNo, + pageSize, + }, + }); + + const transformedData = { + content: response.data.content.map((post) => ({ + post_id: post.id, + post_title: post.title, + post_content: post.content, + created_at: post.createdAt, + updated_at: post.updatedAt, + member: { + member_id: post.member.id, + member_name: post.member.name, + member_name_english: post.member.nameEnglish, + course: post.member.course, + role: post.member.role, + }, + // cloudFrontPaths 사용 + imageUrls: post.cloudFrontPaths || [], + // 첫 번째 이미지를 대표 이미지로 사용 + mainImageUrl: post.cloudFrontPaths?.[0] || "", + // S3 URL도 보관 + s3ImageUrls: post.s3ImagePaths || [], + })), + totalPages: response.data.totalPages, + totalElements: response.data.totalElements, + size: response.data.size, + number: response.data.number, + first: response.data.first, + last: response.data.last, + }; + + return transformedData; + } catch (error) { + console.error("Error fetching posts:", error); + return { + content: [], + totalPages: 0, + totalElements: 0, + size: pageSize, + number: pageNo, + first: true, + last: true, + }; + } +}; diff --git a/src/api/useImageUpload.jsx b/src/api/useImageUpload.jsx index 770bf2e..3526479 100644 --- a/src/api/useImageUpload.jsx +++ b/src/api/useImageUpload.jsx @@ -3,49 +3,32 @@ import api from "./config"; import { getPresignedUrl, uploadToS3 } from "./useImageUrl"; export const useImageUpload = () => { - // validateFiles를 useImageUpload 함수 내부로 이동 - const validateFiles = (files) => { - const maxSize = 10 * 1024 * 1024; // 10MB - const validTypes = ["image/jpeg", "image/png", "image/gif"]; - - return Array.from(files).map((file) => { - if (file.size > maxSize) { - throw new Error(`${file.name}의 크기가 10MB를 초과합니다.`); - } - if (!validTypes.includes(file.type)) { - throw new Error(`${file.name}은(는) 지원하지 않는 파일 형식입니다.`); - } - return file; - }); - }; - const handleImageUpload = async (postId, files, onProgress = () => {}) => { try { // 1. 파일 검증 - const validFiles = validateFiles(files); - console.log( - "Processing files:", - validFiles.map((f) => f.name) - ); - onProgress(5); + const validFiles = files.filter((file) => { + const maxSize = 5 * 1024 * 1024; // 5MB + const validTypes = ["image/jpeg", "image/png", "image/gif"]; + return file.size <= maxSize && validTypes.includes(file.type); + }); + + if (validFiles.length === 0) { + throw new Error("업로드할 수 있는 파일이 없습니다."); + } // 2. Presigned URL 요청 const presignedData = await getPresignedUrl( postId, validFiles.map((f) => f.name) ); - console.log("Received presigned URLs:", presignedData); if (!presignedData?.urls?.length) { throw new Error("업로드 URL을 받지 못했습니다."); } - onProgress(10); - // 3. S3 업로드 const uploadResults = []; const totalFiles = presignedData.urls.length; - const progressPerFile = 80 / totalFiles; for (let i = 0; i < totalFiles; i++) { const urlInfo = presignedData.urls[i]; @@ -53,56 +36,23 @@ export const useImageUpload = () => { console.log(`Uploading file ${i + 1}/${totalFiles}: ${file.name}`); - try { - const s3Url = await uploadToS3(urlInfo.url, file, (progress) => { - const overallProgress = - 10 + i * progressPerFile + (progress * progressPerFile) / 100; - onProgress(Math.min(90, overallProgress)); - }); + const s3Url = await uploadToS3(urlInfo.url, file, (progress) => { + const totalProgress = (i * 100 + progress) / totalFiles; + onProgress(totalProgress); + }); - // URL 정보 저장 - uploadResults.push({ - fileName: urlInfo.fileName, // 원본 파일명 - url: s3Url, // S3 URL - }); - - console.log(`Successfully uploaded ${file.name}`); - } catch (error) { - console.error(`Failed to upload ${file.name}:`, error); - throw error; - } + uploadResults.push({ + fileName: urlInfo.fileName, + url: s3Url, + }); } // 4. 이미지 URL 저장 - if (uploadResults.length > 0) { - try { - console.log("Saving image URLs:", { urls: uploadResults }); - - // API 스펙에 맞게 요청 데이터 구성 - const response = await api.post(`/api/v1/posts/${postId}/images`, { - urls: uploadResults, - }); - - console.log("Image save response:", response.data); - onProgress(100); - - // 응답 데이터에서 이미지 정보 추출 - return response.data.images.map((image) => ({ - id: image.imageId, - postId: image.postId, - originalName: image.originalName, - storedName: image.storedName, - url: image.s3ImagePath, - createdAt: image.createdAt, - updatedAt: image.updatedAt, - })); - } catch (error) { - console.error("Failed to save image URLs:", error); - throw new Error("이미지 URL 저장 중 오류가 발생했습니다."); - } - } + const response = await api.post(`/api/v1/posts/${postId}/images`, { + urls: uploadResults, + }); - return []; + return response.data.images; } catch (error) { console.error("Image upload process failed:", error); throw error; diff --git a/src/api/useImageUrl.jsx b/src/api/useImageUrl.jsx index a5dd446..6663e6d 100644 --- a/src/api/useImageUrl.jsx +++ b/src/api/useImageUrl.jsx @@ -1,88 +1,36 @@ +import axios from "axios"; import api from "./config"; export const uploadToS3 = async (presignedUrl, file, onProgress = () => {}) => { try { - console.log("Starting S3 upload:", { + console.log("Starting upload:", { fileName: file.name, fileType: file.type, fileSize: file.size, - url: presignedUrl, }); - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - - // withCredentials를 false로 설정 - xhr.withCredentials = false; - - xhr.open("PUT", presignedUrl, true); - - // 필수 헤더만 설정 - xhr.setRequestHeader("Content-Type", file.type); - - // 타임아웃 설정 - xhr.timeout = 30000; // 30초 - - // 업로드 진행률 - xhr.upload.onprogress = (event) => { - if (event.lengthComputable) { - const percentComplete = (event.loaded / event.total) * 100; - console.log(`Upload progress: ${percentComplete}%`); - onProgress(percentComplete); - } - }; - - // 성공 처리 - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - const s3Url = presignedUrl.split("?")[0]; - console.log("Upload successful. S3 URL:", s3Url); - resolve(s3Url); - } else { - console.error("Upload failed with status:", xhr.status); - console.error("Response:", xhr.responseText); - reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`)); + const response = await axios.put(presignedUrl, file, { + headers: { + "Content-Type": file.type, + }, + onUploadProgress: (progressEvent) => { + if (progressEvent.total) { + const percentCompleted = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ); + onProgress(percentCompleted); } - }; - - // 에러 처리 - xhr.onerror = (event) => { - console.error("XHR Error details:", { - type: event.type, - target: event.target, - status: xhr.status, - statusText: xhr.statusText, - responseText: xhr.responseText, - }); - reject(new Error(`Network error during upload: ${xhr.statusText}`)); - }; - - // 타임아웃 처리 - xhr.ontimeout = () => { - reject(new Error("Upload timed out")); - }; + }, + }); - // 업로드 취소 처리 - xhr.onabort = () => { - reject(new Error("Upload was aborted")); - }; + console.log("Upload response:", response); - try { - // 실제 전송 - xhr.send(file); - } catch (sendError) { - console.error("Error sending file:", sendError); - reject(new Error(`Failed to send file: ${sendError.message}`)); - } - }); + // 성공 시 S3 URL 반환 + const s3Url = presignedUrl.split("?")[0]; + return s3Url; } catch (error) { - console.error("S3 upload error:", { - error, - fileName: file.name, - fileSize: file.size, - url: presignedUrl, - }); - throw error; + console.error("Upload error:", error); + throw new Error(`파일 업로드 실패: ${error.message}`); } }; diff --git a/src/component/Postlist.jsx b/src/component/Postlist.jsx index a3f1e32..435c9ac 100644 --- a/src/component/Postlist.jsx +++ b/src/component/Postlist.jsx @@ -6,7 +6,19 @@ const Postlist = ({ id, title, imageUrl, likes }) => { return (
- 게시물 이미지 + {imageUrl && imageUrl.trim() !== "" ? ( + 게시물 이미지 { + console.error(`이미지 로드 오류: ${imageUrl}`); + e.target.onerror = null; // 무한 루프 방지 + e.target.src = "/default-image.png"; // 기본 이미지 경로 설정 + }} + /> + ) : ( +
이미지가 없습니다.
+ )}
{title}
@@ -19,10 +31,14 @@ const Postlist = ({ id, title, imageUrl, likes }) => { }; Postlist.propTypes = { - id: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - imageUrl: PropTypes.string.isRequired, - likes: PropTypes.string.isRequired, + id: PropTypes.number.isRequired, // 게시물 고유 ID + title: PropTypes.string.isRequired, // 게시물 제목 + imageUrl: PropTypes.string, // 게시물 이미지 URL (선택 사항) + likes: PropTypes.string.isRequired, // 좋아요 수 +}; + +Postlist.defaultProps = { + imageUrl: "", // 이미지 URL 기본값 설정 }; export default Postlist; diff --git a/src/containers/WriteForm.jsx b/src/containers/WriteForm.jsx index 35b7ce8..4e4369e 100644 --- a/src/containers/WriteForm.jsx +++ b/src/containers/WriteForm.jsx @@ -89,16 +89,7 @@ const WriteForm = ({ } ); - // 이미지 정보 추가 - updatedPost = { - ...updatedPost, - images: images.map((image) => ({ - id: image.id, - originalName: image.originalName, - url: image.url, - })), - }; - + updatedPost = { ...updatedPost, images }; setUploadStatus("업로드 완료!"); } catch (uploadError) { console.error("Image upload failed:", uploadError); From 7e15af90392763c6808f0be2742c71b9e389323b Mon Sep 17 00:00:00 2001 From: parkseyoung Date: Wed, 6 Nov 2024 17:14:45 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20mainpage=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/component/Postlist.jsx | 20 ++++++++++---------- src/containers/PostForm.jsx | 22 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/component/Postlist.jsx b/src/component/Postlist.jsx index 435c9ac..35ffd28 100644 --- a/src/component/Postlist.jsx +++ b/src/component/Postlist.jsx @@ -6,18 +6,18 @@ const Postlist = ({ id, title, imageUrl, likes }) => { return (
- {imageUrl && imageUrl.trim() !== "" ? ( + {imageUrl ? ( 게시물 이미지 { - console.error(`이미지 로드 오류: ${imageUrl}`); - e.target.onerror = null; // 무한 루프 방지 - e.target.src = "/default-image.png"; // 기본 이미지 경로 설정 + console.error("Image load error:", imageUrl); + e.target.onerror = null; + e.target.src = "/default-image.png"; // 기본 이미지 경로 }} /> ) : ( -
이미지가 없습니다.
+
)}
{title}
@@ -31,14 +31,14 @@ const Postlist = ({ id, title, imageUrl, likes }) => { }; Postlist.propTypes = { - id: PropTypes.number.isRequired, // 게시물 고유 ID - title: PropTypes.string.isRequired, // 게시물 제목 - imageUrl: PropTypes.string, // 게시물 이미지 URL (선택 사항) - likes: PropTypes.string.isRequired, // 좋아요 수 + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + imageUrl: PropTypes.string, + likes: PropTypes.string.isRequired, }; Postlist.defaultProps = { - imageUrl: "", // 이미지 URL 기본값 설정 + imageUrl: "", }; export default Postlist; diff --git a/src/containers/PostForm.jsx b/src/containers/PostForm.jsx index 5d88a8b..80fc7cd 100644 --- a/src/containers/PostForm.jsx +++ b/src/containers/PostForm.jsx @@ -152,6 +152,18 @@ const Post = ({ post, currentUserId }) => {
{post.post_content}
+ {post.imageUrls && post.imageUrls.length > 0 && ( +
+ {post.imageUrls.map((url, index) => ( + {`게시물 + ))} +
+ )}
@@ -205,6 +217,8 @@ Post.propTypes = { post_content: PropTypes.string.isRequired, created_at: PropTypes.string.isRequired, updated_at: PropTypes.string.isRequired, + imageUrls: PropTypes.arrayOf(PropTypes.string), // 이미지 URL 배열 추가 + s3ImageUrls: PropTypes.arrayOf(PropTypes.string), // S3 URL 배열 추가 member: PropTypes.shape({ member_id: PropTypes.number.isRequired, member_name: PropTypes.string.isRequired, @@ -213,7 +227,13 @@ Post.propTypes = { role: PropTypes.string.isRequired, }).isRequired, }).isRequired, - currentUserId: PropTypes.number.isRequired, // 추가: currentUserId prop 정의 + currentUserId: PropTypes.number.isRequired, }; +Post.defaultProps = { + post: { + imageUrls: [], // 기본값 설정 + s3ImageUrls: [], // 기본값 설정 + }, +}; export default Post; From 2bdc40e9d20acc042f0673931b67ac990d8fbea2 Mon Sep 17 00:00:00 2001 From: parkseyoung Date: Wed, 6 Nov 2024 17:16:27 +0900 Subject: [PATCH 4/4] =?UTF-8?q?style:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/containers/PostForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/PostForm.jsx b/src/containers/PostForm.jsx index 80fc7cd..b8fb7b1 100644 --- a/src/containers/PostForm.jsx +++ b/src/containers/PostForm.jsx @@ -153,7 +153,7 @@ const Post = ({ post, currentUserId }) => {
{post.post_content}
{post.imageUrls && post.imageUrls.length > 0 && ( -
+
{post.imageUrls.map((url, index) => (