From 26f67fe5e200f8d5a41edb71893ae8d6ed07611c Mon Sep 17 00:00:00 2001 From: woohyun Date: Sun, 9 Aug 2026 23:21:22 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EB=82=B4=EA=B0=80=20=EC=93=B4=20?= =?UTF-8?q?=EA=B8=80/=EB=8C=93=EA=B8=80=20API=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/community.ts | 35 +++++ src/mocks/favorite-disposal.ts | 56 -------- src/mocks/my-page.ts | 13 -- src/mocks/reward-use-history.ts | 40 ------ src/pages/my-page/my-comments-page.tsx | 190 ++++++++++++++++++++----- src/pages/my-page/my-posts-page.tsx | 169 +++++++++++++++++----- src/types/community.ts | 38 +++++ 7 files changed, 357 insertions(+), 184 deletions(-) delete mode 100644 src/mocks/favorite-disposal.ts delete mode 100644 src/mocks/my-page.ts delete mode 100644 src/mocks/reward-use-history.ts create mode 100644 src/types/community.ts diff --git a/src/apis/community.ts b/src/apis/community.ts index dcb0994..2ee23bf 100644 --- a/src/apis/community.ts +++ b/src/apis/community.ts @@ -1,5 +1,10 @@ import api from './api'; +import type { + MyCommentListResponse, + MyCommunityListResponse, +} from '../types/community'; + export const getCommunityList = async () => { const response = await api.get('/api/community'); return response.data; @@ -62,3 +67,33 @@ export const unlikeCommunity = async (communityId: number) => { const response = await api.post(`/api/community/${communityId}/unlike`); return response.data; }; + +// 내가 작성한 게시글 조회 +export const getMyCommunities = async ( + page = 0, + size = 10, +): Promise => { + const response = await api.get('/api/community/me', { + params: { + page, + size, + }, + }); + + return response.data; +}; + +// 내가 작성한 댓글 조회 +export const getMyComments = async ( + page = 0, + size = 10, +): Promise => { + const response = await api.get('/api/community/me/comments', { + params: { + page, + size, + }, + }); + + return response.data; +}; diff --git a/src/mocks/favorite-disposal.ts b/src/mocks/favorite-disposal.ts deleted file mode 100644 index 7613d9a..0000000 --- a/src/mocks/favorite-disposal.ts +++ /dev/null @@ -1,56 +0,0 @@ -export interface FavoriteDisposal { - id: number; - title: string; - createdAt: string; - category: string; - name: string; - content: string; - guide: string[]; - info?: string; -} - -export const MOCK_FAVORITE_DISPOSALS: FavoriteDisposal[] = [ - { - id: 1, - title: '오염된 배달 용기 쉽게 배출하는 법', - createdAt: '2026.06.10', - category: '플라스틱', - name: '투명 페트병', - content: '분리배출이 가능한 품목이에요', - guide: [ - '비닐 라벨을 뜯어 따로 분리해요', - '내용물을 비우고 물로 헹궈요', - '찌그러뜨려 부피를 줄여요', - '투명 페트병 전용함에 배출해요', - ], - info: '라벨, 뚜껑은 비닐/플라스틱으로 따로 버려요', - }, - { - id: 2, - title: '종이팩 쉽게 배출하는 법', - createdAt: '2026.05.02', - category: '종이', - name: '종이팩', - content: '분리배출이 가능한 품목이에요', - guide: [ - '내용물을 모두 비워요', - '물로 가볍게 헹궈요', - '말린 후 펼쳐서 배출해요', - ], - info: '종이류와 구분하여 종이팩 전용 수거함에 배출하세요.', - }, - { - id: 3, - title: '공병 배출하는 법', - createdAt: '2026.04.19', - category: '유리', - name: '공병', - content: '분리배출이 가능한 품목이에요', - guide: [ - '내용물을 모두 비워요', - '병뚜껑을 분리해요', - '이물질을 제거한 후 배출해요', - ], - info: '깨진 유리는 일반 공병과 함께 배출하면 안 됩니다.', - }, -]; \ No newline at end of file diff --git a/src/mocks/my-page.ts b/src/mocks/my-page.ts deleted file mode 100644 index 8bcfcf2..0000000 --- a/src/mocks/my-page.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface MyPageUser { - userId: number; - nickname: string; - profileImageUrl: string | null; - totalPoint: number; -} - -export const MOCK_MY_PAGE_USER: MyPageUser = { - userId: 1, - nickname: '지구', - profileImageUrl: 'BLUE', - totalPoint: 5000, -}; \ No newline at end of file diff --git a/src/mocks/reward-use-history.ts b/src/mocks/reward-use-history.ts deleted file mode 100644 index 0cec36b..0000000 --- a/src/mocks/reward-use-history.ts +++ /dev/null @@ -1,40 +0,0 @@ -export type RewardUseStatus = - | 'SHIPPING' - | 'DELIVERED' - | 'COMPLETED'; - -export interface RewardUseHistoryItem { - id: number; - productName: string; - imageVariant: 'plant' | 'gift-card'; - usedPoint: number; - exchangedAt: string; - status: RewardUseStatus; -} - -export const MOCK_REWARD_USE_HISTORY: RewardUseHistoryItem[] = [ - { - id: 1, - productName: '친환경 수세미 1개입', - imageVariant: 'plant', - usedPoint: 1000, - exchangedAt: '06.24 03:30', - status: 'SHIPPING', - }, - { - id: 2, - productName: '친환경 수세미 1개입', - imageVariant: 'plant', - usedPoint: 1000, - exchangedAt: '06.22 03:30', - status: 'DELIVERED', - }, - { - id: 3, - productName: '기프티콘 5,000원권', - imageVariant: 'gift-card', - usedPoint: 5000, - exchangedAt: '06.20 03:30', - status: 'COMPLETED', - }, -]; \ No newline at end of file diff --git a/src/pages/my-page/my-comments-page.tsx b/src/pages/my-page/my-comments-page.tsx index 1431b2a..da78ba7 100644 --- a/src/pages/my-page/my-comments-page.tsx +++ b/src/pages/my-page/my-comments-page.tsx @@ -1,65 +1,167 @@ import type { MouseEvent } from 'react'; +import { + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; + +import { + deleteComment, + getMyComments, +} from '../../apis/community'; + import LoadingSpinner from '../../components/common/LoadingSpinner'; +import Modal from '../../components/common/Modal'; import Logo from '../../assets/icons/Big-logo.svg?react'; -import Modal from '../../components/common/Modal'; -import { MOCK_MY_COMMENTS } from '../../mocks/my-comment'; +const formatCreatedAt = (createdAt: string) => { + const date = new Date(createdAt); -const MyCommentsPage = () => { - const navigate = useNavigate(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); - const [comments, setComments] = useState(MOCK_MY_COMMENTS); - const [isLoading] = useState(false); - const [deleteCommentId, setDeleteCommentId] = useState(null); + return `${year}.${month}.${day}`; +}; - const isDeleteModalOpen = deleteCommentId !== null; +interface DeleteCommentTarget { + communityId: number; + commentId: number; +} - const handleCommentClick = (commentId: number) => { - navigate(`/community/${commentId}`); +const MyCommentsPage = () => { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const [deleteTarget, setDeleteTarget] = + useState(null); + + const { + data, + isPending, + isError, + } = useQuery({ + queryKey: ['myComments'], + queryFn: () => getMyComments(0, 10), + }); + + const comments = data?.result?.items ?? []; + + const deleteMutation = useMutation({ + mutationFn: ({ + communityId, + commentId, + }: DeleteCommentTarget) => + deleteComment( + communityId, + commentId, + ), + + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['myComments'], + }); + + setDeleteTarget(null); + }, + }); + + const isDeleteModalOpen = + deleteTarget !== null; + + const handleCommentClick = ( + communityId: number, + ) => { + navigate(`/community/${communityId}`); }; - const handleDeleteButtonClick = (event: MouseEvent, commentId: number) => { + const handleDeleteButtonClick = ( + event: MouseEvent, + communityId: number, + commentId: number, + ) => { event.stopPropagation(); - setDeleteCommentId(commentId); + + setDeleteTarget({ + communityId, + commentId, + }); }; const handleDeleteModalClose = () => { - setDeleteCommentId(null); + if (deleteMutation.isPending) return; + + setDeleteTarget(null); }; const handleDeleteConfirm = () => { - if (deleteCommentId === null) return; - - setComments((prevComments) => prevComments.filter((comment) => comment.id !== deleteCommentId)); - - setDeleteCommentId(null); + if ( + deleteTarget === null || + deleteMutation.isPending + ) { + return; + } + + deleteMutation.mutate(deleteTarget); }; - if (isLoading) { - return ( -
- -
- ); -} + if (isPending) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+ 작성한 댓글을 불러오지 못했어요. +
+ ); + } return ( -
+
{comments.length > 0 ? (
{comments.map((comment) => ( -
handleCommentClick(comment.id)} className='h-[81px] w-full cursor-pointer rounded-[16px] bg-white px-[16px] py-[16px] shadow-[0_3px_12px_rgba(0,0,0,0.08)]'> +
+ handleCommentClick( + comment.communityId, + ) + } + className='h-[81px] w-full cursor-pointer rounded-[16px] bg-white px-[16px] py-[16px] shadow-[0_3px_12px_rgba(0,0,0,0.08)]' + >
-

{comment.content}

+

+ {comment.content} +

- - -
@@ -69,17 +171,29 @@ const MyCommentsPage = () => {
) : (
- -

- 아직 작성한 댓글이 없어요 -

-
+ + +

+ 아직 작성한 댓글이 없어요 +

+
)} - +
); }; -export default MyCommentsPage; +export default MyCommentsPage; \ No newline at end of file diff --git a/src/pages/my-page/my-posts-page.tsx b/src/pages/my-page/my-posts-page.tsx index af17f52..d49a58c 100644 --- a/src/pages/my-page/my-posts-page.tsx +++ b/src/pages/my-page/my-posts-page.tsx @@ -1,65 +1,148 @@ import type { MouseEvent } from 'react'; +import { + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; + import LoadingSpinner from '../../components/common/LoadingSpinner'; +import Modal from '../../components/common/Modal'; import Logo from '../../assets/icons/Big-logo.svg?react'; -import Modal from '../../components/common/Modal'; -import { MOCK_MY_POSTS } from '../../mocks/my-post'; +import { + deleteCommunity, + getMyCommunities, +} from '../../apis/community'; -const MyPostsPage = () => { - const navigate = useNavigate(); +const formatCreatedAt = (createdAt: string) => { + const date = new Date(createdAt); - const [posts, setPosts] = useState(MOCK_MY_POSTS); - const [isLoading] = useState(false); - const [deletePostId, setDeletePostId] = useState(null); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); - const isDeleteModalOpen = deletePostId !== null; + return `${year}.${month}.${day}`; +}; - const handlePostClick = (postId: number) => { - navigate(`/community/${postId}`); +const MyPostsPage = () => { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const [deletePostId, setDeletePostId] = + useState(null); + + const { + data, + isPending, + isError, + } = useQuery({ + queryKey: ['myCommunities'], + queryFn: () => getMyCommunities(0, 10), + }); + + const posts = data?.result?.items ?? []; + + const deleteMutation = useMutation({ + mutationFn: deleteCommunity, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: ['myCommunities'], + }); + + setDeletePostId(null); + }, + }); + + const isDeleteModalOpen = + deletePostId !== null; + + const handlePostClick = ( + communityId: number, + ) => { + navigate(`/community/${communityId}`); }; - const handleDeleteButtonClick = (event: MouseEvent, postId: number) => { + const handleDeleteButtonClick = ( + event: MouseEvent, + communityId: number, + ) => { event.stopPropagation(); - setDeletePostId(postId); + setDeletePostId(communityId); }; const handleDeleteModalClose = () => { + if (deleteMutation.isPending) return; + setDeletePostId(null); }; const handleDeleteConfirm = () => { - if (deletePostId === null) return; - - setPosts((prevPosts) => prevPosts.filter((post) => post.id !== deletePostId)); - - setDeletePostId(null); + if ( + deletePostId === null || + deleteMutation.isPending + ) { + return; + } + + deleteMutation.mutate(deletePostId); }; - if (isLoading) { - return ( -
- -
- ); -} + if (isPending) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+ 작성한 게시글을 불러오지 못했어요. +
+ ); + } return ( -
+
{posts.length > 0 ? (
{posts.map((post) => ( -
handlePostClick(post.id)} className='h-[81px] w-full cursor-pointer rounded-[16px] bg-white px-[16px] py-[16px] shadow-[0_3px_12px_rgba(0,0,0,0.08)]'> +
+ handlePostClick( + post.communityId, + ) + } + className='h-[81px] w-full cursor-pointer rounded-[16px] bg-white px-[16px] py-[16px] shadow-[0_3px_12px_rgba(0,0,0,0.08)]' + >
-

{post.title}

+

+ {post.title} +

- - -
@@ -69,17 +152,29 @@ const MyPostsPage = () => {
) : (
- -

- 아직 작성한 게시글이 없어요 -

-
+ + +

+ 아직 작성한 게시글이 없어요 +

+
)} - +
); }; -export default MyPostsPage; +export default MyPostsPage; \ No newline at end of file diff --git a/src/types/community.ts b/src/types/community.ts new file mode 100644 index 0000000..1d96fb0 --- /dev/null +++ b/src/types/community.ts @@ -0,0 +1,38 @@ +import type { CommonResponse } from './common'; + +export interface MyCommunityItem { + communityId: number; + title: string; + preview: string; + category: string; + imageUrl: string | null; + numComments: number; + numLikes: number; + createdAt: string; +} + +export interface MyCommentItem { + commentId: number; + content: string; + createdAt: string; + communityId: number; + communityTitle: string; + communityCategory: string; + communityImageUrl: string | null; +} + +export interface CommunityPageResult { + items: T[]; + page: number; + size: number; + hasNext: boolean; +} + +export type MyCommunityListResponse = CommonResponse< + CommunityPageResult +>; + +export type MyCommentListResponse = CommonResponse< + CommunityPageResult +>; +