Skip to content

Commit

Permalink
Merge pull request #57 from Kakaotech-10/feature/image
Browse files Browse the repository at this point in the history
✨ 프론트 이미지 저장(게시글)
  • Loading branch information
hardlife0 authored Nov 6, 2024
2 parents 508296e + 2bdc40e commit d3942a7
Show file tree
Hide file tree
Showing 8 changed files with 370 additions and 43 deletions.
72 changes: 60 additions & 12 deletions src/api/useGetPost.jsx
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -37,30 +31,30 @@ 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)) {
throw new Error(`Missing required member field: ${field}`);
}
}

// 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,
role: postData.member.role,
course: postData.member.course,
name: postData.member.name,
nameEnglish: postData.member.nameEnglish,
profileImage: "", // 빈 문자열로 설정
profileImage: postData.member.profileImage || "",
},
};
} catch (error) {
Expand All @@ -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,
};
}
};
63 changes: 63 additions & 0 deletions src/api/useImageUpload.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// useImageUpload.jsx
import api from "./config";
import { getPresignedUrl, uploadToS3 } from "./useImageUrl";

export const useImageUpload = () => {
const handleImageUpload = async (postId, files, onProgress = () => {}) => {
try {
// 1. 파일 검증
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)
);

if (!presignedData?.urls?.length) {
throw new Error("업로드 URL을 받지 못했습니다.");
}

// 3. S3 업로드
const uploadResults = [];
const totalFiles = presignedData.urls.length;

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}`);

const s3Url = await uploadToS3(urlInfo.url, file, (progress) => {
const totalProgress = (i * 100 + progress) / totalFiles;
onProgress(totalProgress);
});

uploadResults.push({
fileName: urlInfo.fileName,
url: s3Url,
});
}

// 4. 이미지 URL 저장
const response = await api.post(`/api/v1/posts/${postId}/images`, {
urls: uploadResults,
});

return response.data.images;
} catch (error) {
console.error("Image upload process failed:", error);
throw error;
}
};

return { handleImageUpload };
};
49 changes: 49 additions & 0 deletions src/api/useImageUrl.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import axios from "axios";
import api from "./config";

export const uploadToS3 = async (presignedUrl, file, onProgress = () => {}) => {
try {
console.log("Starting upload:", {
fileName: file.name,
fileType: file.type,
fileSize: file.size,
});

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);
}
},
});

console.log("Upload response:", response);

// 성공 시 S3 URL 반환
const s3Url = presignedUrl.split("?")[0];
return s3Url;
} catch (error) {
console.error("Upload error:", error);
throw new Error(`파일 업로드 실패: ${error.message}`);
}
};

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을 가져오는데 실패했습니다.");
}
};
21 changes: 21 additions & 0 deletions src/api/useSaveImage.jsx
Original file line number Diff line number Diff line change
@@ -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 저장 중 오류가 발생했습니다.");
}
};
20 changes: 18 additions & 2 deletions src/component/Postlist.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@ const Postlist = ({ id, title, imageUrl, likes }) => {
return (
<div className="postlist-container" data-post-id={id}>
<div className="postlist-img">
<img src={imageUrl} alt="게시물 이미지" />
{imageUrl ? (
<img
src={imageUrl}
alt="게시물 이미지"
onError={(e) => {
console.error("Image load error:", imageUrl);
e.target.onerror = null;
e.target.src = "/default-image.png"; // 기본 이미지 경로
}}
/>
) : (
<div className="no-image"></div>
)}
</div>
<div className="postlist-title">{title}</div>

Expand All @@ -21,8 +33,12 @@ const Postlist = ({ id, title, imageUrl, likes }) => {
Postlist.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
imageUrl: PropTypes.string.isRequired,
imageUrl: PropTypes.string,
likes: PropTypes.string.isRequired,
};

Postlist.defaultProps = {
imageUrl: "",
};

export default Postlist;
22 changes: 21 additions & 1 deletion src/containers/PostForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ const Post = ({ post, currentUserId }) => {
</div>
<div className="post-content">
<div className="post-text">{post.post_content}</div>
{post.imageUrls && post.imageUrls.length > 0 && (
<div className="post-image-container">
{post.imageUrls.map((url, index) => (
<img
key={index}
src={url}
alt={`게시물 이미지 ${index + 1}`}
className="post-image"
/>
))}
</div>
)}
</div>
<div className="post-footer">
<div className="reactions">
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Loading

0 comments on commit d3942a7

Please sign in to comment.