diff --git a/package-lock.json b/package-lock.json index 607bfea..4d7062f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@reduxjs/toolkit": "^2.3.0", + "@tanstack/query-core": "^5.62.3", "@tanstack/react-query": "^5.60.6", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", @@ -21,6 +22,8 @@ "@types/styled-components": "^5.1.34", "@vitejs/plugin-react": "^4.3.3", "axios": "^1.7.7", + "jotai": "^2.10.3", + "jotai-tanstack-query": "^0.9.0", "react": "^18.3.1", "react-daum-postcode": "^3.1.3", "react-dom": "^18.3.1", @@ -3947,9 +3950,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.60.6", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.6.tgz", - "integrity": "sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ==", + "version": "5.62.3", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.3.tgz", + "integrity": "sha512-Jp/nYoz8cnO7kqhOlSv8ke/0MJRJVGuZ0P/JO9KQ+f45mpN90hrerzavyTKeSoT/pOzeoOUkv1Xd0wPsxAWXfg==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -3970,6 +3973,15 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query/node_modules/@tanstack/query-core": { + "version": "5.60.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.6.tgz", + "integrity": "sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -11167,6 +11179,35 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jotai": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.10.3.tgz", + "integrity": "sha512-Nnf4IwrLhNfuz2JOQLI0V/AgwcpxvVy8Ec8PidIIDeRi4KCFpwTFIpHAAcU+yCgnw/oASYElq9UY0YdUUegsSA==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/jotai-tanstack-query": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/jotai-tanstack-query/-/jotai-tanstack-query-0.9.0.tgz", + "integrity": "sha512-8n7/xV14+ZE63dyqngtmqs6aV1bNnQJF9bJN5Bj70USc2cMFRmsVhGGzL1cSvp5E/xXasU37czFFaZ0vavmyfA==", + "peerDependencies": { + "@tanstack/query-core": "*", + "jotai": ">=2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index c3a9495..1b6f929 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@reduxjs/toolkit": "^2.3.0", + "@tanstack/query-core": "^5.62.3", "@tanstack/react-query": "^5.60.6", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", @@ -16,6 +17,8 @@ "@types/styled-components": "^5.1.34", "@vitejs/plugin-react": "^4.3.3", "axios": "^1.7.7", + "jotai": "^2.10.3", + "jotai-tanstack-query": "^0.9.0", "react": "^18.3.1", "react-daum-postcode": "^3.1.3", "react-dom": "^18.3.1", diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts index 0310cf0..26de188 100644 --- a/src/api/axiosInstance.ts +++ b/src/api/axiosInstance.ts @@ -3,9 +3,6 @@ import axios from 'axios'; const axiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_URL, // API URL timeout: 5000, // 요청 제한 시간 (ms) - headers: { - 'Content-Type': 'application/json', // JSON 형식 사용 - }, }); // 요청/응답 인터셉터 설정 (선택 사항) diff --git a/src/components/common/CategoryWrapper.tsx b/src/components/common/CategoryWrapper.tsx index 7eb632d..623bd71 100644 --- a/src/components/common/CategoryWrapper.tsx +++ b/src/components/common/CategoryWrapper.tsx @@ -39,7 +39,7 @@ const CategoryWrapper: React.FC = ({ {categories.map((category) => ( handleCategoryClick(category.id)} > {category.name} @@ -65,15 +65,15 @@ const HeaderTitle = styled.h2` white-space: nowrap; /* 텍스트 줄바꿈 방지 */ `; -const CategoryItem = styled.div<{ active: boolean }>` +const CategoryItem = styled.div<{ $active: boolean }>` padding: 1rem 1rem; - font-weight: ${({ active }) => (active ? 'bold' : 'normal')}; + font-weight: ${({ $active }) => ($active ? 'bold' : 'normal')}; cursor: pointer; position: relative; &::after { content: ''; - display: ${({ active }) => (active ? 'block' : 'none')}; + display: ${({ $active }) => ($active ? 'block' : 'none')}; height: 2px; background-color: black; width: 100%; diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index 24b0820..65bad33 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -14,6 +14,16 @@ const Header = () => { setIsMobileMenuOpen(!isMobileMenuOpen); }; + const handleCommunityClick = ( + e: React.MouseEvent + ) => { + if (!isLoggedIn || !isAdmin) { + e.preventDefault(); + alert('로그인 후 이용할 수 있는 페이지입니다.'); + setIsMobileMenuOpen(!isMobileMenuOpen); + } + }; + return ( @@ -47,14 +57,14 @@ const Header = () => { - + Community {isLoggedIn && ( {isAdmin ? 'Admin Page' : 'My Page'} @@ -63,12 +73,10 @@ const Header = () => { {isAdmin && ( - - Post Management - + Post Management - + Chat Management diff --git a/src/components/common/PostList.tsx b/src/components/common/PostList.tsx index 7ae66d7..8c33579 100644 --- a/src/components/common/PostList.tsx +++ b/src/components/common/PostList.tsx @@ -1,49 +1,35 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; -import { getPosts, Post } from '../pages/community/api/postApi'; import WriteButton from './WriteButton'; import SearchBar from './SearchBar'; import Pagination from './Pagination'; import { useNavigate } from 'react-router-dom'; +import { SSEEvent, Post } from '../../types/postTypes'; +import { getImageSrc } from '../../utils/GetImageSrc'; +import { formatDateWithOffset } from '../../utils/formatDate'; interface PostListProps { - posts: Post[]; selectedCategory: string; + posts: Post[]; + onPostSelect?: (postId: number) => void; // onPostSelect 추가 + realTimeData: Record; // 실시간 데이터 매핑 추가 } const POSTS_PER_PAGE = 6; // 한 페이지에 표시할 게시글 수 const PostList: React.FC = ({ selectedCategory, + posts, + realTimeData, + onPostSelect, hideWriteButton, }) => { const navigate = useNavigate(); - const [posts, setPosts] = useState([]); // 실제 게시글 데이터 const [currentPage, setCurrentPage] = useState(1); const [searchTerm, setSearchTerm] = useState(''); // 입력된 검색어 const [searchQuery, setSearchQuery] = useState(''); // 실제 검색 실행 시의 검색어 - const [loading, setLoading] = useState(false); // 로딩 상태 - - // 게시글 데이터 로드 - const loadPosts = async () => { - try { - setLoading(true); - const data = await getPosts(selectedCategory, searchQuery); // API 호출 - setPosts(data); - } catch (error) { - console.error('게시물 조회 중 오류 발생:', error); - alert('게시물 조회 중 오류가 발생했습니다. 다시 시도해주세요.'); - } finally { - setLoading(false); - } - }; - // 카테고리나 검색어 변경 시 데이터 로드 - useEffect(() => { - loadPosts(); - }, [selectedCategory, searchQuery]); - - // 선택된 카테고리에 따른 게시글 필터링 (status 조건 추가) + // 선택된 카테고리에 따른 게시글 필터링 const categoryFilteredPosts = posts .filter((post) => { if (selectedCategory === 'NOT_APPROVED') { @@ -88,6 +74,10 @@ const PostList: React.FC = ({ // 포스트 클릭 핸들러 const handlePostClick = (communityPostId: number) => { + if (onPostSelect) { + onPostSelect(communityPostId); // 부모 컴포넌트에 선택 이벤트 전달 + } + if (selectedCategory === 'NOT_APPROVED') { navigate(`/admin/post/approval/${communityPostId}`, { state: { communityPostId }, @@ -97,6 +87,11 @@ const PostList: React.FC = ({ } }; + // 특정 포스트의 현재 참여 수 계산 + const getParticipationCount = (postId: number): number => { + return realTimeData[postId]?.participationCount || 0; + }; + return ( @@ -108,9 +103,7 @@ const PostList: React.FC = ({ /> - {loading ? ( - 게시물을 불러오는 중입니다... - ) : categoryFilteredPosts.length === 0 ? ( + {categoryFilteredPosts.length === 0 ? ( 선택된 카테고리에 해당하는 게시글이 없습니다. @@ -124,36 +117,30 @@ const PostList: React.FC = ({ key={post.communityPostId} onClick={() => handlePostClick(post.communityPostId)} > - + {post.title} {post.nickname} - {new Date(post.createdAt).toLocaleString('ko-KR', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - hour12: false, - })} + {formatDateWithOffset(post.createdAt)} - {'~'} - - {new Date(post.closeAt).toLocaleString('ko-KR', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - hour12: false, - })} - + {selectedCategory !== 'NOT_APPROVED' && ( + <> + {'~'} + + {formatDateWithOffset(post.closeAt)} + + + )} - 참여 현황: {post.currentQuantity} / {post.availableNumber} + 참여 현황: {getParticipationCount(post.communityPostId)} /{' '} + {post.availableNumber} {post.description} diff --git a/src/components/pages/admin/ChatRoomManagementPage.tsx b/src/components/pages/admin/ChatRoomManagementPage.tsx index bb336e1..f001134 100644 --- a/src/components/pages/admin/ChatRoomManagementPage.tsx +++ b/src/components/pages/admin/ChatRoomManagementPage.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { useLocation, useNavigate } from 'react-router-dom'; import ChatRoomModal from '../community/modal/ChatRoomModal'; // 기존 채팅 모달 컴포넌트 import Pagination from '../../common/Pagination'; // 기존 페이지네이션 컴포넌트 -import { fetchChatRooms } from '../community/api/chatApi'; +import { fetchAllChatRooms } from '../community/api/chatApi'; import { FaBackspace } from 'react-icons/fa'; import { webSocketService } from '../../../utils/webSocket'; @@ -32,7 +32,7 @@ const ChatRoomManagementPage = () => { useEffect(() => { const fetchData = async () => { try { - const rooms = await fetchChatRooms(); + const rooms = await fetchAllChatRooms(); setChatRooms(rooms); } catch (error) { console.error('Failed to fetch chat rooms:', error); diff --git a/src/components/pages/admin/PostApprovalPage.tsx b/src/components/pages/admin/PostApprovalPage.tsx index c424c2a..4f7ef62 100644 --- a/src/components/pages/admin/PostApprovalPage.tsx +++ b/src/components/pages/admin/PostApprovalPage.tsx @@ -1,12 +1,12 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { useNavigate, useLocation } from 'react-router-dom'; -import { - Post, - updatePostStatus, - fetchPostById, -} from '../community/api/postApi'; +import { fetchPostById } from '../community/api/postApi'; +import { approvePost, rejectPost } from './api/adminApi'; import { FaBackspace, FaAngleLeft, FaAngleRight } from 'react-icons/fa'; +import { Post } from '../../../types/postTypes'; +import { getImageSrc } from '../../../utils/GetImageSrc'; +import { formatDateWithOffset } from '../../../utils/formatDate'; const PostApprovalPage = () => { const location = useLocation(); @@ -18,12 +18,12 @@ const PostApprovalPage = () => { useEffect(() => { const fetchPost = async () => { if (!postId) { - navigate('/admin/posts'); // postId가 없을 경우 관리자 페이지로 리다이렉트 + navigate('/admin/post'); // postId가 없을 경우 관리자 페이지로 리다이렉트 return; } try { const postDetails = await fetchPostById(postId); // 포스트 세부 정보 가져오기 - setPost(postDetails); + setPost(postDetails.communityPost); } catch (error) { console.error('Failed to fetch post details:', error); } @@ -49,7 +49,12 @@ const PostApprovalPage = () => { const handleApprove = async () => { if (!post) return; try { - await updatePostStatus(postId, 'APPROVED'); // 포스트 상태를 APPROVED로 변경 + // 제목에서 '(수정요망)' 제거 + const updatedTitle = post.title.startsWith('(수정요망)') + ? post.title.replace(/^\(수정요망\)\s*/, '') + : post.title; + + await approvePost(postId, updatedTitle); // 포스트 상태를 APPROVED로 변경 alert('게시물이 승인되었습니다.'); navigate('/admin/post'); // 승인 후 관리자 페이지로 리다이렉트 } catch (error) { @@ -61,7 +66,12 @@ const PostApprovalPage = () => { const handleReject = async () => { if (!post) return; try { - await updatePostStatus(postId, 'REJECTED'); // 포스트 상태를 REJECTED로 변경하고 제목 수정 + // 제목에 '(수정요망)' 추가 + const updatedTitle = post.title.startsWith('(수정요망)') + ? post.title // 이미 '(수정요망)'이 있으면 그대로 유지 + : `(수정요망) ${post.title}`; + + await rejectPost(postId, updatedTitle); // 포스트 상태를 REJECTED로 변경 alert('게시물이 거절 처리되었습니다.'); navigate('/admin/post'); // 거절 후 관리자 페이지로 리다이렉트 } catch (error) { @@ -101,7 +111,7 @@ const PostApprovalPage = () => { {`이미지 @@ -153,7 +163,7 @@ const PostApprovalPage = () => { {' '} - {new Date(post.createdAt).toLocaleString()} + {formatDateWithOffset(post.createdAt).toLocaleString()} @@ -161,8 +171,7 @@ const PostApprovalPage = () => { {post.category} - {post.currentQuantity} - {' / '} + {post.availableNumber} diff --git a/src/components/pages/admin/PostManagementPage.tsx b/src/components/pages/admin/PostManagementPage.tsx index 057b1f2..5748e63 100644 --- a/src/components/pages/admin/PostManagementPage.tsx +++ b/src/components/pages/admin/PostManagementPage.tsx @@ -4,31 +4,31 @@ import { useLocation } from 'react-router-dom'; import { ADMIN_CATEGORIES } from './adminCategories'; import CategoryWrapper from '../../../components/common/CategoryWrapper'; import PostList from '../../../components/common/PostList'; -import { Post } from '../community/api/postApi'; -import { fetchAdminPosts } from './api/adminApi'; +import { fetchPendingPosts } from './api/adminApi'; +import { useAtom } from 'jotai'; +import { realTimeDataAtom, selectedPostIdAtom } from '../../../store/postStore'; +import { Post, POST_STATUS, SSEEvent } from '../../../types/postTypes'; const PostManagementPage = () => { const location = useLocation(); const initialCategory = location.state?.selectedCategory || 'NOT_APPROVED'; - const [selectedCategory, setSelectedCategory] = useState(initialCategory); + const [selectedCategory, setSelectedCategory] = + useState(initialCategory); const [posts, setPosts] = useState([]); + const [, setSelectedPostId] = useAtom(selectedPostIdAtom); // 선택된 포스트 ID 설정 함수 + const [realTimeData] = useAtom(realTimeDataAtom); // 실시간 데이터 상태 + + // `realTimeData`를 안전하게 변환 + const formattedRealTimeData: Record = + typeof realTimeData === 'object' && realTimeData !== null + ? (realTimeData as unknown as Record) + : {}; useEffect(() => { const fetchPosts = async () => { try { - const response = await fetchAdminPosts(selectedCategory); - - // '작성글 게시 승인 대기 목록' 카테고리에서는 'NOT_APPROVED' 또는 'REJECTED' 상태의 포스트만 표시 - if (selectedCategory === 'NOT_APPROVED') { - setPosts( - response.filter( - (post) => - post.status === 'NOT_APPROVED' || post.status === 'REJECTED' - ) - ); - } else { - setPosts(response); // 다른 카테고리는 전체 포스트 표시 - } + const response = await fetchPendingPosts(); + setPosts(response); } catch (error) { console.error('Failed to fetch posts:', error); } @@ -37,6 +37,11 @@ const PostManagementPage = () => { fetchPosts(); }, [selectedCategory]); + // 포스트 선택 핸들러 + const handlePostSelect = (postId: number) => { + setSelectedPostId(postId); + }; + return (
@@ -44,19 +49,22 @@ const PostManagementPage = () => { 관리자 페이지
- {' '} setSelectedCategory(id)} - title="게시글 관리" // 제목 변경 + onCategoryChange={(id: string) => + setSelectedCategory(id as POST_STATUS) + } + title="게시글 관리" />
diff --git a/src/components/pages/admin/api/adminApi.ts b/src/components/pages/admin/api/adminApi.ts index 02ab856..f39b7ce 100644 --- a/src/components/pages/admin/api/adminApi.ts +++ b/src/components/pages/admin/api/adminApi.ts @@ -1,18 +1,70 @@ import axiosInstance from '../../../../api/axiosInstance'; -// import { mockCommunityPosts } from '../../../../mocks/communityPosts'; -import { Post } from '../../community/api/postApi'; +import { Post } from '../../../../types/postTypes'; -export const fetchAdminPosts = async ( - selectedCategory: string -): Promise => { +// 관리자용 승인 대기 중인 포스트 목록 조회 +export const fetchPendingPosts = async (): Promise => { try { - // `selectedCategory`를 API 요청의 query parameter로 전달 - const response = await axiosInstance.get('/admin/post', { - params: { category: selectedCategory }, + const response = await axiosInstance.get('/api/admin/post', { + headers: { + Authorization: `Bearer ${localStorage.getItem('jwt')}`, + }, }); + if (response.status !== 200) { + throw new Error('Failed to fetch pending posts'); + } return response.data; } catch (error) { - console.error('Failed to fetch admin posts:', error); + console.error('Error fetching pending posts:', error); + throw error; + } +}; + +// 관리자용 포스트 승인 처리 +export const approvePost = async ( + communityPostId: number, + title: string +): Promise => { + try { + const response = await axiosInstance.patch( + `/api/admin/post/approve/${communityPostId}`, + { title }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('jwt')}`, + }, + } + ); + if (response.status !== 200) { + throw new Error('Failed to approve post'); + } + return response.data; + } catch (error) { + console.error(`Error approving post with ID ${communityPostId}:`, error); + throw error; + } +}; + +// 관리자용 포스트 거절 처리 +export const rejectPost = async ( + communityPostId: number, + title: string +): Promise => { + try { + const response = await axiosInstance.patch( + `/api/admin/post/reject/${communityPostId}`, + { title }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('jwt')}`, + }, + } + ); + if (response.status !== 200) { + throw new Error('Failed to reject post'); + } + return response.data; + } catch (error) { + console.error(`Error rejecting post with ID ${communityPostId}:`, error); throw error; } }; diff --git a/src/components/pages/community/CategoryBasedPostsPage.tsx b/src/components/pages/community/CategoryBasedPostsPage.tsx index 4a6cf12..ef3ed50 100644 --- a/src/components/pages/community/CategoryBasedPostsPage.tsx +++ b/src/components/pages/community/CategoryBasedPostsPage.tsx @@ -1,14 +1,40 @@ import React, { useState } from 'react'; import styled from 'styled-components'; -import { useLocation } from 'react-router-dom'; +import { useAtom } from 'jotai'; +import { + postsAtom, + selectedPostIdAtom, + realTimeDataAtom, +} from '../../../store/postStore'; import CategoryWrapper from '../../common/CategoryWrapper'; import PostList from '../../common/PostList'; import { POST_CATEGORIES } from './postCategories'; +import { Post, SSEEvent } from '../../../types/postTypes'; const CategoryBasedPostsPage = () => { - const location = useLocation(); - const initialCategory = location.state?.selectedCategory || 'LIFESTYLE'; // 전달받은 상태 또는 기본값 - const [selectedCategory, setSelectedCategory] = useState(initialCategory); + const [postsAtomState] = useAtom(postsAtom); // 전체 포스트 상태 + const posts = postsAtomState?.data || []; // Query 결과에서 data 추출 + const [realTimeData] = useAtom(realTimeDataAtom); // 실시간 데이터 상태 + const [selectedCategory, setSelectedCategory] = useState( + POST_CATEGORIES[0].id // 초기 카테고리 설정 + ); + const [, setSelectedPostId] = useAtom(selectedPostIdAtom); // 선택된 포스트 ID 설정 함수 + + // `realTimeData`를 안전하게 변환 + const formattedRealTimeData: Record = + typeof realTimeData === 'object' && realTimeData !== null + ? (realTimeData as unknown as Record) + : {}; + + // 카테고리 변경 핸들러 + const handleCategoryChange = (categoryId: string) => { + setSelectedCategory(categoryId); + }; + + // 포스트 선택 핸들러 + const handlePostSelect = (postId: number) => { + setSelectedPostId(postId); + }; return (
@@ -18,13 +44,16 @@ const CategoryBasedPostsPage = () => { setSelectedCategory(categoryId) // 카테고리 변경 시 상태 업데이트 - } + selectedCategory={selectedCategory} + onCategoryChange={handleCategoryChange} /> - +
diff --git a/src/components/pages/community/PaymentAuthorPage.tsx b/src/components/pages/community/PaymentAuthorPage.tsx index 281fb9b..2fcefdd 100644 --- a/src/components/pages/community/PaymentAuthorPage.tsx +++ b/src/components/pages/community/PaymentAuthorPage.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { useNavigate, useLocation } from 'react-router-dom'; import { VirtualAccountResponse } from './api/paymentApi'; import VirtualAccountModal from './modal/VirtualAccountModal'; +import { getImageSrc } from '../../../utils/GetImageSrc'; const PaymentAuthorPage = () => { const navigate = useNavigate(); @@ -76,7 +77,7 @@ const PaymentAuthorPage = () => { - {'이미지'} + {'이미지'} diff --git a/src/components/pages/community/PaymentCompletePage.tsx b/src/components/pages/community/PaymentCompletePage.tsx index 1ef2b49..486467a 100644 --- a/src/components/pages/community/PaymentCompletePage.tsx +++ b/src/components/pages/community/PaymentCompletePage.tsx @@ -59,8 +59,8 @@ const PaymentCompletePage: React.FC = () => { try { const postData = await fetchPostById(Number(communityPostId)); setPost({ - title: postData.title, - unitAmount: postData.unitAmount, + title: postData.communityPost.title, + unitAmount: postData.communityPost.unitAmount, }); } catch (error) { console.error('게시물 정보 조회 오류:', error); diff --git a/src/components/pages/community/PaymentParticipantPage.tsx b/src/components/pages/community/PaymentParticipantPage.tsx index ba28a18..65b9178 100644 --- a/src/components/pages/community/PaymentParticipantPage.tsx +++ b/src/components/pages/community/PaymentParticipantPage.tsx @@ -3,6 +3,7 @@ import styled from 'styled-components'; import { useNavigate, useLocation } from 'react-router-dom'; import { VirtualAccountResponse } from './api/paymentApi'; import VirtualAccountModal from './modal/VirtualAccountModal'; +import { getImageSrc } from '../../../utils/GetImageSrc'; const PaymentParticipantPage = () => { const navigate = useNavigate(); @@ -65,7 +66,7 @@ const PaymentParticipantPage = () => { - {'이미지'} + {'이미지'} diff --git a/src/components/pages/community/PostCreatePage.tsx b/src/components/pages/community/PostCreatePage.tsx index e9175c1..6c4df2d 100644 --- a/src/components/pages/community/PostCreatePage.tsx +++ b/src/components/pages/community/PostCreatePage.tsx @@ -3,21 +3,24 @@ import styled from 'styled-components'; import { useNavigate, useLocation } from 'react-router-dom'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { - FaCaretDown, - FaPlusCircle, - FaMinusCircle, FaAngleLeft, FaAngleRight, + FaCaretDown, + FaMinusCircle, + FaPlusCircle, } from 'react-icons/fa'; -import { createPost, CreatePostInput } from './api/postApi'; +import { createPost } from './api/postApi'; import CategoryWrapper from '../../common/CategoryWrapper'; import { POST_CATEGORIES } from './postCategories'; +import { CreatePostData } from '../../../types/postTypes'; +import { getImageSrc } from '../../../utils/GetImageSrc'; const PostCreatePage = () => { const navigate = useNavigate(); const location = useLocation(); const queryClient = useQueryClient(); - const defaultCategory = location.state?.selectedCategory || 'LIFESTYLE'; // 이전 페이지에서 전달된 카테고리 + const defaultCategory = + location.state?.selectedCategory || POST_CATEGORIES[0].id; // 이전 페이지에서 전달된 카테고리 const [selectedCategory, setSelectedCategory] = useState(defaultCategory); const [availableNumber, setAvailableNumber] = useState(''); @@ -26,13 +29,13 @@ const PostCreatePage = () => { const [deadline, setDeadline] = useState('마감 기한 '); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); - const [imageUrls, setImageUrls] = useState([]); + const [imageUrls, setImageUrls] = useState>([]); const [currentIndex, setCurrentIndex] = useState(-1); // -1: AddImageButton 상태 const [urlInput, setUrlInput] = useState(''); const [urlError, setUrlError] = useState(false); const createPostMutation = useMutation({ - mutationFn: (postData: CreatePostInput) => createPost(postData), + mutationFn: (postData: CreatePostData) => createPost(postData), onSuccess: () => { // 생성 성공 시 목록 업데이트 queryClient.invalidateQueries({ queryKey: ['postList'] }); @@ -74,33 +77,26 @@ const PostCreatePage = () => { const parsedTotalAmount = parseInt(totalAmount.replace(/,/g, ''), 10); const parsedAvailableNumber = parseInt(availableNumber, 10); - - // 마감 기한 계산 (Long 타입으로 전송할 기간 설정) - const period = - deadline !== '마감 기한' - ? parseInt(deadline.replace(/[^0-9]/g, ''), 10) - : 0; + const period = parseInt(deadline.replace(/[^0-9]/g, ''), 10); if (period <= 0) { alert('마감 기한을 올바르게 설정하세요.'); return; } - const postData: CreatePostInput = { + const postData: CreatePostData = { title: title.trim(), description: description.trim(), - imageUrls, + imageUrls: imageUrls, category: selectedCategory, - currentQuantity: 0, availableNumber: parsedAvailableNumber, totalAmount: parsedTotalAmount, unitAmount: Math.floor(parsedTotalAmount / parsedAvailableNumber), productUrl: urlInput.trim(), - period, // 마감 기한 (Long 타입) - status: 'NOT_APPROVED', + period, }; - createPostMutation.mutate(postData); // React Query Mutation 호출 + createPostMutation.mutate(postData); }; const handleAvailableNumberChange = ( @@ -150,11 +146,10 @@ const PostCreatePage = () => { const handleImageUpload = (e: React.ChangeEvent) => { if (e.target.files) { - const uploadedImages = Array.from(e.target.files).map((file) => - URL.createObjectURL(file) + const uploadedFiles = Array.from(e.target.files).filter( + (file) => file instanceof File ); - setImageUrls((prev) => [...prev, ...uploadedImages]); - setCurrentIndex(imageUrls.length); // 마지막으로 추가된 이미지로 이동 + setImageUrls((prev) => [...prev, ...uploadedFiles]); } }; @@ -249,7 +244,7 @@ const PostCreatePage = () => { ) : ( 이미지 미리보기 @@ -291,7 +286,7 @@ const PostCreatePage = () => { placeholder="상품 관련 URL 주소를 입력해주세요." value={urlInput} onChange={handleUrlChange} - isError={urlError} + $isError={urlError} spellCheck={false} /> @@ -351,7 +346,7 @@ const PostCreatePage = () => { onClick={() => handleDeadlineSelect(`${index + 1}일 `) } - isSelected={deadline === `${index + 1}일 `} + $isSelected={deadline === `${index + 1}일 `} > {index + 1}일 @@ -584,12 +579,12 @@ const UrlInputWrapper = styled.div` border: none; `; -const URLInput = styled.input<{ isError: boolean }>` +const URLInput = styled.input<{ $isError: boolean }>` width: 285px; flex: 1; padding: 10px; background-color: #ececec; - border: 1px solid ${({ isError }) => (isError ? 'red' : '#ccc')}; + border: 1px solid ${({ $isError }) => ($isError ? 'red' : '#ccc')}; border-radius: 5px; `; @@ -719,12 +714,12 @@ const DropdownMenu = styled.div` box-sizing: border-box; `; -const DropdownItem = styled.div<{ isSelected: boolean }>` +const DropdownItem = styled.div<{ $isSelected: boolean }>` padding: 10px; text-align: center; cursor: pointer; - background: ${({ isSelected }) => (isSelected ? '#f0f0f0' : '#fff')}; - font-weight: ${({ isSelected }) => (isSelected ? 'bold' : 'normal')}; + background: ${({ $isSelected }) => ($isSelected ? '#f0f0f0' : '#fff')}; + font-weight: ${({ $isSelected }) => ($isSelected ? 'bold' : 'normal')}; &:hover { background: #eaeaea; diff --git a/src/components/pages/community/PostDetailPage.tsx b/src/components/pages/community/PostDetailPage.tsx index 1e5b6cf..194b485 100644 --- a/src/components/pages/community/PostDetailPage.tsx +++ b/src/components/pages/community/PostDetailPage.tsx @@ -1,283 +1,232 @@ import React, { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; +import { useAtom } from 'jotai'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + selectedPostAtom, + realTimeDataAtom, + selectedPostIdAtom, + joinQuantityAtom, +} from '../../../store/postStore'; +import { currentUserAtom } from '../../../store/userStore'; import styled from 'styled-components'; import { fetchPostById, - deletePostById, joinPost, cancelJoinPost, - addComment, - deleteComment, - updateComment, - Post, - defaultPost, - POST_STATUS, - updatePostStatus, -} from './api/postApi'; -import { - FaBackspace, - FaAngleLeft, - FaAngleRight, - FaPlusCircle, - FaMinusCircle, -} from 'react-icons/fa'; -import { webSocketService } from '../../../utils/webSocket'; - -// 로그인된 사용자의 ID (Mock 처리) -const currentUserId = 'user-00001'; // 실제 구현 시, 인증된 사용자 ID를 받아와야 함 -const currentUserNickname = '사용자 A'; // 실제 구현 시, 인증된 사용자 Nickname을 받아와야 함 - -const PostDetailPage = () => { + deletePostById, +} from '../community/api/postApi'; +import { FaBackspace } from 'react-icons/fa'; +import { POST_STATUS, PostDetailResponse } from '../../../types/postTypes'; +import PostImageSection from './PostDetailPage/PostImageSection'; +import PostDetailsSection from './PostDetailPage/PostDetailsSection'; +import PostCommentsSection from './PostDetailPage/PostCommentsSection'; + +const PostDetailPage: React.FC = () => { const { communityPostId } = useParams<{ communityPostId: string }>(); const navigate = useNavigate(); const queryClient = useQueryClient(); - const communityPostIdNumber = Number(communityPostId); - - if (!communityPostIdNumber) { - alert('잘못된 게시물 ID입니다.'); - navigate('/community'); - return null; - } + const [selectedPost] = useAtom(selectedPostAtom); + const [realTimeData, setRealTimeData] = useAtom(realTimeDataAtom); + const [, setSelectedPostId] = useAtom(selectedPostIdAtom); + const [currentUser] = useAtom(currentUserAtom); + const [joinQuantity] = useAtom(joinQuantityAtom); - const [post, setPost] = useState(defaultPost); - const [quantity, setQuantity] = useState(1); // 기본 최소 수량 - const [isAuthor, setIsAuthor] = useState(false); // 현재 사용자가 작성자인지 여부 - const [currentIndex, setCurrentIndex] = useState(0); + const [quantity, setQuantity] = useState(1); const [remainingTime, setRemainingTime] = useState(''); - const [paymentRemainingTime, setPaymentRemainingTime] = useState(''); // 결제 마감 시간 상태 추가 - const [newCommentContent, setNewCommentContent] = useState(''); // 댓글 입력 필드 - const [editCommentId, setEditCommentId] = useState(null); // 수정 중인 댓글 ID - const [editContent, setEditContent] = useState(''); // 수정 중인 댓글 내용 + const [paymentRemainingTime, setPaymentRemainingTime] = useState(''); + const [currentIndex, setCurrentIndex] = useState(0); + const communityPostIdNumber = Number(communityPostId); const queryKey = ['postDetail', communityPostIdNumber]; + // 게시물 ID 설정 및 초기화 + useEffect(() => { + if (communityPostIdNumber) { + setSelectedPostId(communityPostIdNumber); + } else { + alert('잘못된 게시물 ID입니다.'); + navigate('/community/post'); + } + }, [communityPostIdNumber, setSelectedPostId, navigate]); + // 게시물 데이터 가져오기 - const { - data: fetchedPost, - isLoading, - isError, - } = useQuery({ + const { data, isError } = useQuery({ queryKey, - queryFn: () => fetchPostById(communityPostIdNumber), - enabled: Boolean(communityPostId), + queryFn: async (): Promise => + fetchPostById(communityPostIdNumber), }); - // fetchedPost 상태 처리 및 초기화 useEffect(() => { - if (fetchedPost) { - setPost(fetchedPost); + if (data) { + const quantityToSet = + data.participationStatus === 'JOIN' || + data.participationStatus === 'PAYMENT_STANDBY' + ? (joinQuantity ?? 1) + : 1; + setQuantity(quantityToSet); + } + }, [data, joinQuantity]); + + const isParticipant = selectedPost.data?.participationStatus === 'JOIN'; + const isNotParticipant = !selectedPost.data?.participationStatus; + const isAuthor = selectedPost.data?.communityPost.userId === currentUser?.id; - // 참여자 목록에서 현재 사용자의 수량을 찾아 설정 - const participant = fetchedPost.participants.find( - (p) => p.userId === currentUserId && !p.isCancelled + // SSE 연결 활성화 + useEffect(() => { + if (communityPostIdNumber) { + const eventSource = new EventSource( + `/api/community/post/${communityPostIdNumber}/participants` ); - setQuantity(participant ? participant.quantity : 1); + eventSource.onmessage = (event) => { + const parsedData = JSON.parse(event.data); + setRealTimeData(parsedData); + queryClient.invalidateQueries({ queryKey }); + }; - // 작성자인지 여부 확인 - setIsAuthor(fetchedPost.userId === currentUserId); + eventSource.onerror = () => { + console.error('SSE 연결 오류 발생'); + eventSource.close(); + }; - // 취소한 사용자인 경우 접근 차단 - if ( - fetchedPost.participants.some( - (p) => p.userId === currentUserId && p.isCancelled - ) - ) { - alert('참여를 취소한 게시물에는 접근할 수 없습니다.'); - navigate('/community'); - } + return () => eventSource.close(); } - }, [fetchedPost, currentUserId, navigate]); + }, [communityPostIdNumber, queryClient, setRealTimeData]); + // 오류 처리 useEffect(() => { if (isError) { alert('게시물을 불러오는 데 실패했습니다.'); - navigate('/community'); + navigate('/community/post'); } }, [isError, navigate]); + // 마감 시간 계산 useEffect(() => { - if (communityPostId) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleIncomingData = (data: any) => { - if (data.type === 'STATUS_UPDATE') { - setPost((prev) => ({ ...prev, status: data.status })); - } else if (data.type === 'USER_UPDATE') { - setPost((prev) => ({ - ...prev, - participants: data.participants, - cancelledUsers: data.cancelledUsers, - })); - } + if (selectedPost.data?.communityPost.closeAt) { + const calculateRemainingTime = () => { + const now = Date.now(); + const targetTime = new Date( + selectedPost.data?.communityPost.closeAt || '' + ).getTime(); + const diff = targetTime - now; + if (diff <= 0) return '마감되었습니다.'; + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor( + (diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) + ); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + return `${days}일 ${hours}시간 ${minutes}분 남음`; }; - webSocketService.connect( - handleIncomingData, - () => console.log('WebSocket connected'), - () => console.log('WebSocket disconnected'), - (error: Error) => console.error('WebSocket error:', error) - ); + setRemainingTime(calculateRemainingTime()); + const timer = setInterval(() => { + setRemainingTime(calculateRemainingTime()); + }, 60000); - return () => { - webSocketService.close(); - }; + return () => clearInterval(timer); } - }, [communityPostId]); + }, [selectedPost]); + // 결제 마감 시간 계산 useEffect(() => { - const targetDate = post?.closeAt; - if (!targetDate) return; - - const calculateRemainingTime = (endTime: string) => { - const now = new Date().getTime(); - const targetTime = new Date(endTime).getTime(); - const diff = targetTime - now; - - if (diff <= 0) return '마감되었습니다.'; - - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - const hours = Math.floor( - (diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60) - ); - const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((diff % (1000 * 60)) / 1000); - - return `${days}일 ${hours}시간 ${minutes}분 ${seconds}초 남음`; - }; - - const updateRemainingTime = () => { - const formattedTime = calculateRemainingTime(targetDate); - setRemainingTime(formattedTime); - }; - - updateRemainingTime(); - const timer = setInterval(updateRemainingTime, 1000); - - return () => clearInterval(timer); - }, [post]); - - useEffect(() => { - // 결제 마감 시간 계산 및 업데이트 - if (post.status === POST_STATUS.PAYMENT_STANDBY && post.stateUpdatedAt) { + if ( + selectedPost.data?.communityPost.status === POST_STATUS.PAYMENT_STANDBY && + selectedPost.data.communityPost.paymentDeadline + ) { const countdownTarget = - new Date(post.stateUpdatedAt).getTime() + 12 * 60 * 60 * 1000; // 12시간 후 + new Date(selectedPost.data.communityPost.paymentDeadline).getTime() + + 12 * 60 * 60 * 1000; - const calculateRemainingTime = () => { + const calculatePaymentTime = () => { const now = new Date().getTime(); const diff = countdownTarget - now; if (diff <= 0) return '결제 마감되었습니다.'; - const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); - return `${hours}시간 ${minutes}분 ${seconds}초 남음`; }; - const updateRemainingTime = () => { - const formattedTime = calculateRemainingTime(); - setPaymentRemainingTime(formattedTime); - }; - - updateRemainingTime(); - const timer = setInterval(updateRemainingTime, 1000); + setPaymentRemainingTime(calculatePaymentTime()); + const timer = setInterval(() => { + setPaymentRemainingTime(calculatePaymentTime()); + }, 1000); return () => clearInterval(timer); - } else { - setPaymentRemainingTime(''); } - }, [post]); + }, [selectedPost]); - // 게시물 삭제 + // 게시물 삭제 핸들러 const deletePostMutation = useMutation({ mutationFn: deletePostById, onSuccess: () => { queryClient.invalidateQueries({ queryKey }); alert('게시물이 삭제되었습니다.'); - navigate('/community'); + navigate('/community/post'); }, - onError: (error) => { - console.error(error); + onError: () => { alert('게시물 삭제에 실패했습니다.'); }, }); - // 삭제 핸들러 const handleDelete = () => { if (window.confirm('정말 이 게시물을 삭제하시겠습니까?')) { deletePostMutation.mutate(communityPostIdNumber); } }; - const handleNextImage = () => { - if (post) setCurrentIndex((prev) => (prev + 1) % post.imageUrls.length); - }; - - const handlePreviousImage = () => { - if (post) - setCurrentIndex( - (prev) => (prev - 1 + post.imageUrls.length) % post.imageUrls.length - ); - }; - - const handleDotClick = (index: number) => { - setCurrentIndex(index); - }; - - // 참여하기 - const joinMutation = useMutation({ - mutationFn: () => joinPost(communityPostIdNumber, currentUserId, quantity), - onSuccess: async () => { - queryClient.invalidateQueries({ queryKey }); - alert('참여가 완료되었습니다.'); + interface JoinPostResponse { + message: string; + } - const updatedPost = await fetchPostById(communityPostIdNumber); - if ( - updatedPost.currentQuantity === updatedPost.availableNumber && - updatedPost.status === POST_STATUS.APPROVED - ) { - // 모집이 완료되면 상태를 PAYMENT_STANDBY로 자동 변경 - await updatePostStatus( - communityPostIdNumber, - POST_STATUS.PAYMENT_STANDBY - ); - queryClient.invalidateQueries({ queryKey }); - } + // 참여 핸들러 + const { mutate: joinMutation } = useMutation< + JoinPostResponse, + Error, // 에러 타입 + { communityPostId: number; quantity: number } + >({ + mutationFn: ({ communityPostId, quantity }) => + joinPost(communityPostId, quantity), + onSuccess: () => { + alert('공구 참여가 완료되었습니다.'); + queryClient.invalidateQueries({ queryKey }); // 정확한 queryKey 사용 }, - onError: () => { - alert('참여에 실패했습니다.'); + onError: (error: Error) => { + console.error('공구 참여 실패:', error.message); // 오류 메시지 출력 + alert('공구 참여에 실패했습니다.'); }, }); + // 참여 버튼 핸들러 const handleJoin = () => { - if (post.status === POST_STATUS.APPROVED) { - joinMutation.mutate(); - } else { - alert('현재 참여할 수 없는 상태입니다.'); + if (!quantity) { + alert('수량을 입력해주세요.'); + return; } + + joinMutation({ communityPostId: communityPostIdNumber, quantity }); }; - // 참여 취소 + // 초기 수량 설정 (참여 상태 확인) + useEffect(() => { + if ( + selectedPost.data?.participationStatus === 'JOIN' && + joinQuantity !== null + ) { + setQuantity(joinQuantity); + } + }, [selectedPost.data?.participationStatus, joinQuantity]); + + // 참여 취소 핸들러 const cancelMutation = useMutation({ - mutationFn: () => cancelJoinPost(communityPostIdNumber, currentUserId), - onSuccess: async () => { + mutationFn: () => cancelJoinPost(communityPostIdNumber), + onSuccess: () => { queryClient.invalidateQueries({ queryKey }); alert('참여가 취소되었습니다.'); - - const updatedPost = await fetchPostById(communityPostIdNumber); - if ( - updatedPost.status === POST_STATUS.PAYMENT_STANDBY && - updatedPost.currentQuantity < updatedPost.availableNumber - ) { - // 모집 인원이 줄어들면 다시 APPROVED 상태로 변경 - await updatePostStatus(communityPostIdNumber, POST_STATUS.APPROVED); - queryClient.invalidateQueries({ queryKey }); - } - - navigate('/community'); }, onError: () => { alert('참여 취소에 실패했습니다.'); @@ -285,145 +234,38 @@ const PostDetailPage = () => { }); const handleCancel = () => { - if ( - [POST_STATUS.APPROVED, POST_STATUS.PAYMENT_STANDBY].includes(post.status) - ) { - cancelMutation.mutate(); - } else { - alert('현재 취소할 수 없는 상태입니다.'); - } - }; - - // 진행 취소 핸들러 (포스트 삭제 포함) - const handleCancelPost = () => { - if ( - window.confirm( - '진행자가 공구 진행 취소를 할 경우, 포스트 자체도 삭제됩니다. 정말 취소하시겠습니까?' - ) - ) { - deletePostMutation.mutate(communityPostIdNumber); - } + cancelMutation.mutate(); }; const handleReport = () => { navigate(`/community/post/${communityPostId}/report`); }; + // 수량 변경 const handleQuantityChange = (change: number) => { - if (post) { - const newQuantity = quantity + change; - if ( - newQuantity >= 1 && - newQuantity <= post.availableNumber - post.currentQuantity - ) { - setQuantity(newQuantity); - } - } - }; - - const isParticipant = - post?.participants?.some( - (participant) => - participant.userId === currentUserId && !participant.isCancelled - ) || false; - - // 댓글 작성 - const addCommentMutation = useMutation({ - mutationFn: () => - addComment( - communityPostIdNumber, - currentUserId, - currentUserNickname, - newCommentContent - ), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['postDetail', communityPostIdNumber], - }); - setNewCommentContent(''); - }, - onError: () => { - alert('댓글 작성에 실패했습니다.'); - }, - }); - - const handleAddComment = () => { - if (!newCommentContent.trim()) { - alert('댓글을 입력해주세요.'); - return; - } - if (newCommentContent.length > 300) { - alert('댓글은 최대 300자까지만 입력 가능합니다.'); - return; - } - addCommentMutation.mutate(); - }; - - // 댓글 삭제 - const deleteCommentMutation = useMutation({ - mutationFn: (commentId: string) => - deleteComment(communityPostIdNumber, commentId), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['postDetail', communityPostIdNumber], - }); - }, - onError: () => { - alert('댓글 삭제에 실패했습니다.'); - }, - }); - - const handleDeleteComment = (commentId: string) => { - if (window.confirm('이 댓글을 삭제하시겠습니까?')) { - deleteCommentMutation.mutate(commentId); - } - }; - - // 댓글 수정 - const handleEditComment = (commentId: string, content: string) => { - setEditCommentId(commentId); // 수정할 댓글 ID 설정 - setEditContent(content); // 현재 댓글 내용 불러오기 - }; - - const updateCommentMutation = useMutation({ - mutationFn: () => - updateComment(Number(communityPostId), editCommentId!, editContent), // 수정할 댓글 ID와 새로운 내용 전달 - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: ['postDetail', communityPostId], - }); - setEditCommentId(null); - setEditContent(''); - }, - onError: () => { - alert('댓글 수정에 실패했습니다.'); - }, - }); - - const handleUpdateComment = () => { - if (!editContent.trim()) { - alert('수정할 댓글 내용을 입력하세요.'); - return; - } - if (editContent.length > 300) { - alert('댓글은 최대 300자까지만 입력 가능합니다.'); - return; + const newQuantity = quantity + change; + if ( + newQuantity > 0 && + newQuantity <= selectedPost.data!.communityPost.availableNumber + ) { + setQuantity(newQuantity); } - updateCommentMutation.mutate(); }; // 결제하기 페이지로 이동 const handlePayment = () => { - if (post.status !== POST_STATUS.PAYMENT_STANDBY) { + if ( + selectedPost.data?.participationStatus !== POST_STATUS.PAYMENT_STANDBY + ) { alert('현재 결제할 수 없는 상태입니다.'); return; } const paymentState = { post: { - title: post.title, - unitAmount: post.unitAmount, - imageUrls: post.imageUrls, + title: selectedPost.data.communityPost.title, + unitAmount: selectedPost.data.communityPost.unitAmount, + imageUrls: selectedPost.data.communityPost.imageUrls, }, quantity, }; @@ -443,32 +285,14 @@ const PostDetailPage = () => { // 환불 요청 페이지로 이동 const handleRefund = () => { - // 현재 사용자가 결제를 완료했는지 확인 - const participant = post.participants.find( - (p) => - p.userId === currentUserId && - !p.isCancelled && // 취소되지 않은 상태 - p.isPaymentCompleted // 결제 완료 상태 - ); - - if (participant) { - // 결제를 완료한 사용자만 이동 + if (selectedPost.data?.participationStatus === 'PAYMENT_COMPLETE') { navigate(`/community/post/${communityPostId}/refund`); } else { - alert('환불 요청은 결제를 완료한 이용자만 가능합니다.'); + alert('환불 요청이 불가능한 상태입니다.'); } }; - // 모집 완료 상태에서 참여자만 접근 가능하도록 제한 - useEffect(() => { - if (post.status === POST_STATUS.PAYMENT_STANDBY && !isParticipant) { - alert('모집이 완료되어 더 이상 접근할 수 없습니다.'); - navigate('/community'); - } - }, [post.status, isParticipant, navigate]); - - if (isLoading) return
Loading...
; - if (!post) return
게시물이 없습니다.
; + if (!selectedPost.data?.communityPost) return
게시물이 없습니다.
; return ( @@ -482,7 +306,7 @@ const PostDetailPage = () => { {!isAuthor ? ( 글 신고 - ) : !isParticipant ? ( + ) : isNotParticipant ? ( 글 삭제 ) : ( '' @@ -494,271 +318,49 @@ const PostDetailPage = () => { {/* 이미지 섹션 */} - - - - {post.imageUrls.length > 1 && currentIndex > 0 && ( - - - - )} - - - - {`이미지 - - - - {post.imageUrls.length > 1 && - currentIndex < post.imageUrls.length - 1 && ( - - - - )} - - - - {/* PaginationDots는 이미지 아래에 위치 */} - - {post.imageUrls.length > 1 && ( - - {post.imageUrls.map((_, index) => ( - handleDotClick(index)} - /> - ))} - - )} - - - - - - {post.productUrl} - - - - - - {/* 상세 정보 섹션 */} - - - - {post.title} - - - - - {post.nickname} - - - {' '} - {new Date(post.createdAt).toLocaleString()} - - - - - {post.category} - - - {post.currentQuantity} - {' / '} - {post.availableNumber} - - - - - - {remainingTime} - - {/* 결제 마감 시간이 추가되는 부분 */} - {post?.status === POST_STATUS.PAYMENT_STANDBY && ( - - - {paymentRemainingTime} - - )} - - - - {post.totalAmount.toLocaleString()}{' '} - 원 - - - {post.unitAmount.toLocaleString()}{' '} - 원 - - - - - - {' '} - - {!isParticipant && ( // 참여자가 아닐 때만 수량 변경 가능 - <> - handleQuantityChange(-1)} - /> - {quantity} - handleQuantityChange(1)} - /> - - )} - {isParticipant && {quantity}}{' '} - {/* 참여자일 경우 고정된 수량 표시 */} - - - {isParticipant && ( // 참여자가 된 경우 결제 금액 추가 - - {' '} - - {(quantity * post.unitAmount).toLocaleString()} 원 - - - )} - - {/* 상태에 따라 렌더링되는 액션 버튼 */} - {post && post.status && ( - - {/* 환불 요청 버튼 */} - {post.participants.some( - (p) => - p.userId === currentUserId && - !p.isCancelled && - p.isPaymentCompleted - ) ? ( - - 환불 - - ) : post.status === POST_STATUS.PAYMENT_STANDBY ? ( - // 결제 대기 상태에서는 결제/취소 버튼 - <> - {isParticipant && ( - <> - - 결제 - - - 취소 - - - )} - - ) : post.status === POST_STATUS.APPROVED ? ( - // 승인 완료 상태에서는 참여/취소 버튼 - <> - {!isParticipant ? ( - - 참여 - - ) : isAuthor ? ( - - 취소 - - ) : ( - - 취소 - - )} - - ) : null} - - )} - - - + + + {/* 내용 섹션 */} -