From 49753059a7d8219d6747080cb3c2dc9db40635ec Mon Sep 17 00:00:00 2001 From: Lseojeong Date: Thu, 5 Jun 2025 17:43:43 +0900 Subject: [PATCH 01/13] =?UTF-8?q?Feat=20:=20=EA=B8=80=EB=A1=9C=EB=B2=8C=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=ED=8E=98=EC=9D=B4=EC=A7=80=20api=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/ActivitiesApi.js | 44 +++- src/api/IssueApi.js | 181 +++++++++++++ src/components/activity/ActivityCard.jsx | 2 +- src/components/issue/IssueCard.jsx | 21 +- src/components/my/BookmarkList.jsx | 233 +++++++++------- src/pages/ActivityPage.jsx | 4 +- src/pages/issue/GlobalIssueDetailPage.jsx | 308 ++++++++++++---------- src/pages/issue/GlobalIssuePage.jsx | 168 +++++++----- src/pages/issue/MoreDetailPage.jsx | 140 ---------- src/query/useActivities.js | 119 +++++---- src/query/useIssues.js | 96 +++++++ src/routes/Route.jsx | 2 - src/store/activityStore.js | 98 ++++++- 13 files changed, 892 insertions(+), 524 deletions(-) create mode 100644 src/api/IssueApi.js delete mode 100644 src/pages/issue/MoreDetailPage.jsx create mode 100644 src/query/useIssues.js diff --git a/src/api/ActivitiesApi.js b/src/api/ActivitiesApi.js index b581a7b..aa208bc 100644 --- a/src/api/ActivitiesApi.js +++ b/src/api/ActivitiesApi.js @@ -3,7 +3,7 @@ import api from './axiosInstance'; // 키워드 및 타입 매핑 const KEYWORD_MAP = { '환경': 'Environment', - '사람과사회': 'PeopleAndSociety', + '사람과 사회': 'PeopleAndSociety', '경제': 'Economy', '기술': 'Technology' }; @@ -17,7 +17,7 @@ const ACTIVITY_TYPE_MAP = { const REVERSE_KEYWORD_MAP = { 'Environment': '환경', - 'PeopleAndSociety': '사람과사회', + 'PeopleAndSociety': '사람과 사회', 'Economy': '경제', 'Technology': '기술' }; @@ -30,14 +30,14 @@ const REVERSE_TYPE_MAP = { }; class ActivitiesApi { - // 전체/필터 활동 목록 조회 (기존) + // 전체/필터 활동 목록 조회 (다중 필터 지원) async getActivities(params = {}) { try { const { page = 0, fieldFilter, typeFilter } = params; const queryParams = new URLSearchParams(); queryParams.append('page', page.toString()); - // 키워드 필터 추가 + // 키워드 필터 추가 (전체가 아닌 경우) if (fieldFilter && fieldFilter !== '전체') { const mappedKeyword = KEYWORD_MAP[fieldFilter]; if (mappedKeyword) { @@ -45,7 +45,7 @@ class ActivitiesApi { } } - // 활동 타입 필터 추가 (쿼리 방식) + // 활동 타입 필터 추가 (전체가 아닌 경우) if (typeFilter && typeFilter !== '전체') { const mappedType = ACTIVITY_TYPE_MAP[typeFilter]; if (mappedType) { @@ -122,6 +122,27 @@ class ActivitiesApi { } } + // 키워드별 활동 4개 조회 (마감 기한 내) +async getActivitiesByKeywordLimited(keyword) { + try { + const mappedKeyword = KEYWORD_MAP[keyword] || keyword; + + const response = await api.get(`/activities/keyword/${mappedKeyword}`); + if (!response.data.isSuccess) { + throw new Error(response.data.message || '키워드별 활동 조회에 실패했습니다.'); + } + + const activities = response.data.result; + const transformedActivities = activities.map(this.transformActivity); + + return transformedActivities; + } catch (error) { + console.error('키워드별 활동 4개 조회 실패:', error); + throw this.handleApiError(error); + } +} + + // 북마크 토글 async toggleBookmark(activityId) { try { @@ -143,7 +164,14 @@ class ActivitiesApi { if (!response.data.isSuccess) { throw new Error(response.data.message || '북마크한 활동글 조회에 실패했습니다.'); } - return response.data.result; + + const result = response.data.result; + const transformedContent = result.content.map(this.transformActivity); + + return { + ...result, + content: transformedContent + }; } catch (error) { console.error('북마크한 활동글 조회 실패:', error); throw this.handleApiError(error); @@ -154,10 +182,12 @@ class ActivitiesApi { transformActivity = (activity) => { const koreanKeyword = REVERSE_KEYWORD_MAP[activity.keyword] || activity.keyword; const koreanType = REVERSE_TYPE_MAP[activity.activityType] || activity.activityType; + const formatDate = (dateString) => { if (!dateString) return ''; return dateString.split('T')[0].replace(/-/g, '.'); }; + const now = new Date(); const endDate = new Date(activity.endDate); const isClosed = endDate < now; @@ -181,6 +211,7 @@ class ActivitiesApi { }; }; + // API 에러 처리 handleApiError = (error) => { if (error.response) { const status = error.response.status; @@ -203,3 +234,4 @@ class ActivitiesApi { const activitiesApi = new ActivitiesApi(); export default activitiesApi; +export { KEYWORD_MAP, ACTIVITY_TYPE_MAP, REVERSE_KEYWORD_MAP, REVERSE_TYPE_MAP }; diff --git a/src/api/IssueApi.js b/src/api/IssueApi.js new file mode 100644 index 0000000..32e5108 --- /dev/null +++ b/src/api/IssueApi.js @@ -0,0 +1,181 @@ +// api/IssuesApi.js +import api from './axiosInstance'; + +const KEYWORD_MAP = { + '환경': 'Environment', + '사람과 사회': 'PeopleAndSociety', + '경제': 'Economy', + '기술': 'Technology' +}; + +const REVERSE_KEYWORD_MAP = { + 'Environment': '환경', + 'PeopleAndSociety': '사람과 사회', + 'Economy': '경제', + 'Technology': '기술' +}; + +class IssuesApi { + // 전체 이슈 조회 + async getIssues(page = 0) { + try { + const response = await api.get(`/issues?page=${page}`); + if (!response.data.isSuccess) { + throw new Error(response.data.message || 'API 요청이 실패했습니다.'); + } + + const result = response.data.result; + const transformedContent = result.content.map(this.transformIssue); + + return { + ...result, + content: transformedContent + }; + } catch (error) { + console.error('이슈 목록 조회 실패:', error); + throw this.handleApiError(error); + } + } + + async getIssuesByKeyword({ keyword, page = 0 }) { + try { + const mappedKeyword = KEYWORD_MAP[keyword] || keyword; + + const response = await api.get(`/issues/keyword/${mappedKeyword}?page=${page}`); + if (!response.data.isSuccess) { + throw new Error(response.data.message || '키워드별 이슈 조회에 실패했습니다.'); + } + + const result = response.data.result; + const transformedContent = result.content.map(this.transformIssue); + + return { + ...result, + content: transformedContent + }; + } catch (error) { + console.error('키워드별 이슈 조회 실패:', error); + throw this.handleApiError(error); + } + } + + // 특정 이슈 상세 조회 +async getIssueDetail(issueId) { + try { + const response = await api.get(`/issues/${issueId}`); + if (!response.data.isSuccess) { + throw new Error(response.data.message || '이슈 상세 조회에 실패했습니다.'); + } + + const issue = response.data.result; + const koreanKeyword = REVERSE_KEYWORD_MAP[issue.keyword] || issue.keyword; + + return { + id: issue.id, + title: issue.title, + content: issue.content, + issueDate: issue.issueDate, + siteUrl: issue.siteUrl, + imageUrl: issue.imageUrl || '/assets/images/main/ic_NoImage.png', + keyword: koreanKeyword, + category: `#${koreanKeyword}`, + bookmarked: issue.bookmarked || false + }; + } catch (error) { + console.error('이슈 상세 조회 실패:', error); + throw this.handleApiError(error); + } +} + + // 이슈 북마크 토글 + async toggleIssueBookmark(issueId) { + try { + const response = await api.post(`/issues/${issueId}/bookmark`); + if (!response.data.isSuccess) { + throw new Error(response.data.message || '북마크 처리에 실패했습니다.'); + } + return response.data.result; + } catch (error) { + console.error('이슈 북마크 토글 실패:', error); + throw this.handleApiError(error); + } + } + +// 북마크된 이슈 목록 조회 +async getBookmarkedIssues(page = 0) { + try { + const response = await api.get(`/profile/issues/bookmark?page=${page}`); + if (response.data.isSuccess) { + return { + ...response.data.result, + content: response.data.result.content.map(issue => { + const koreanKeyword = REVERSE_KEYWORD_MAP[issue.keyword] || issue.keyword; + + return { + id: issue.issueId, + title: issue.title, + category: `#${koreanKeyword}`, + date: issue.issueDate, + bookmarkId: issue.bookmarkId + }; + }) + }; + } + throw new Error(response.data.message || '북마크한 이슈 조회에 실패했습니다.'); + } catch (error) { + console.error('북마크한 이슈 조회 에러:', error); + throw error; + } +} + + // 북마크된 이슈 전용 변환 메서드 추가 + transformBookmarkedIssue = (bookmark) => { + const koreanKeyword = REVERSE_KEYWORD_MAP[bookmark.keyword] || bookmark.keyword; + + return { + bookmarkId: bookmark.bookmarkId, + id: bookmark.issueId, + title: bookmark.title, + category: `#${koreanKeyword}`, + keyword: koreanKeyword, + issueDate: bookmark.issueDate, + bookmarked: true + }; + }; + transformIssue = (issue) => { + const koreanKeyword = REVERSE_KEYWORD_MAP[issue.keyword] || issue.keyword; + + return { + id: issue.id, + title: issue.title, + category: `#${koreanKeyword}`, + keyword: koreanKeyword, + thumbnailUrl: issue.imageUrl || '/assets/images/main/ic_NoImage.png', + bookmarked: issue.bookmarked || false + }; + }; + + handleApiError = (error) => { + if (error.response) { + const status = error.response.status; + const message = error.response.data?.message || error.message; + switch (status) { + case 400: return new Error('잘못된 요청입니다.'); + case 401: return new Error('로그인이 필요합니다.'); + case 403: return new Error('접근 권한이 없습니다.'); + case 404: return new Error('요청한 데이터를 찾을 수 없습니다.'); + case 500: return new Error('서버 오류가 발생했습니다.'); + default: return new Error(message); + } + } else if (error.request) { + return new Error('네트워크 연결을 확인해주세요.'); + } else { + return new Error(error.message || '알 수 없는 오류가 발생했습니다.'); + } + }; +} + + + +const issuesApi = new IssuesApi(); +export default issuesApi; diff --git a/src/components/activity/ActivityCard.jsx b/src/components/activity/ActivityCard.jsx index c64a62c..170c910 100644 --- a/src/components/activity/ActivityCard.jsx +++ b/src/components/activity/ActivityCard.jsx @@ -9,7 +9,7 @@ export default function ActivityCard({ tags, image, date, - bookmarked, + bookmarked = false, onToggle, isClosed, siteUrl diff --git a/src/components/issue/IssueCard.jsx b/src/components/issue/IssueCard.jsx index 3b7bdc2..49b5a30 100644 --- a/src/components/issue/IssueCard.jsx +++ b/src/components/issue/IssueCard.jsx @@ -20,7 +20,6 @@ export default function IssueCard({ title, tag, image, bookmarked, onToggle, onC ); } - const Card = styled.div` width: 330px; height: 430px; @@ -31,22 +30,22 @@ const Card = styled.div` display: flex; flex-direction: column; justify-content: flex-start; - overflow: visible; .issue-title { - font-size: 24px; + font-size: 23px; font-weight: bold; - margin-top: 40px; + margin-top: 50px; font-family: NotoSansCustom; padding: 0 20px; word-break: keep-all; - - height: 68px; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; + height: 68px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-height: 1.2em; + max-height: 2.4em; } .issue-tag { diff --git a/src/components/my/BookmarkList.jsx b/src/components/my/BookmarkList.jsx index a9031a5..fd590c4 100644 --- a/src/components/my/BookmarkList.jsx +++ b/src/components/my/BookmarkList.jsx @@ -1,44 +1,46 @@ -// src/components/mypage/BookmarkList.jsx - -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; -import usePagination from '../../hooks/usePagination'; import Pagination from '../common/Pagination'; import bookmarkIcon from '../../assets/images/common/BookmarkFilledButton.png'; import { useNavigate } from 'react-router-dom'; import activitiesApi from '../../api/ActivitiesApi'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useBookmarkedIssues, useToggleIssueBookmark } from '../../query/useIssues'; +import { formatDate } from '../../utils/formatDate'; // 쿼리 키 상수 정의 const QUERY_KEYS = { ACTIVITIES: 'activities', - BOOKMARKED_ACTIVITIES: 'bookmarkedActivities' + BOOKMARKED_ACTIVITIES: 'bookmarkedActivities', + BOOKMARKED_ISSUES: 'bookmarkedIssues' }; - export default function BookmarkList() { const [selectedType, setSelectedType] = useState('issue'); const [currentPage, setCurrentPage] = useState(0); const navigate = useNavigate(); const queryClient = useQueryClient(); - const { data: bookmarkedActivities, isLoading, error } = useQuery({ + // 북마크된 이슈 조회 + const { data: bookmarkedIssues, isLoading: issuesLoading, error: issuesError } = useBookmarkedIssues(currentPage); + + // 북마크된 활동 조회 + const { data: bookmarkedActivities, isLoading: activitiesLoading, error: activitiesError } = useQuery({ queryKey: [QUERY_KEYS.BOOKMARKED_ACTIVITIES, currentPage], queryFn: () => activitiesApi.getBookmarkedActivities(currentPage), enabled: selectedType === 'activity' }); - const toggleBookmarkMutation = useMutation({ + // 이슈 북마크 토글 + const toggleIssueBookmark = useToggleIssueBookmark(); + + // 활동 북마크 토글 + const toggleActivityBookmark = useMutation({ mutationFn: (activityId) => activitiesApi.toggleBookmark(activityId), onMutate: async (activityId) => { - // 진행 중인 모든 관련 쿼리 취소 - await queryClient.cancelQueries({ queryKey: [QUERY_KEYS.ACTIVITIES] }); await queryClient.cancelQueries({ queryKey: [QUERY_KEYS.BOOKMARKED_ACTIVITIES] }); - - // 이전 데이터 저장 const previousData = queryClient.getQueryData([QUERY_KEYS.BOOKMARKED_ACTIVITIES, currentPage]); - // 낙관적 업데이트 queryClient.setQueryData([QUERY_KEYS.BOOKMARKED_ACTIVITIES, currentPage], (old) => { if (!old) return old; return { @@ -51,36 +53,52 @@ export default function BookmarkList() { return { previousData }; }, onError: (err, activityId, context) => { - // 에러 발생 시 이전 데이터로 롤백 if (context?.previousData) { queryClient.setQueryData([QUERY_KEYS.BOOKMARKED_ACTIVITIES, currentPage], context.previousData); } }, onSettled: () => { - // 성공/실패 관계없이 모든 관련 쿼리 무효화 - queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.ACTIVITIES] }); queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.BOOKMARKED_ACTIVITIES] }); } }); - const handleTitleClick = (activity) => { - if (selectedType === 'issue') { - navigate('/global-issue-detail', { - state: { - label: `#${activity.keyword}`, - title: activity.name, - }, - }); + const handleIssueClick = (issue) => { + navigate(`/global-issue/${issue.id}`, { + state: { + label: issue.category, + title: issue.title + } + }); + }; + + const handleActivityClick = (activity) => { + const url = activity.siteUrl; + if (url) { + try { + new URL(url); + window.open(url, '_blank', 'noopener,noreferrer'); + } catch (error) { + console.error('잘못된 URL입니다:', url); + alert('올바르지 않은 링크입니다.'); + } } else { - navigate(`/more-detail?query=${encodeURIComponent(activity.keyword)}`); + alert('해당 활동의 링크 정보가 없습니다.'); } }; - const handleBookmarkToggle = (e, activityId) => { + const handleBookmarkToggle = (e, id, type) => { e.stopPropagation(); - toggleBookmarkMutation.mutate(activityId); + if (type === 'issue') { + toggleIssueBookmark.mutate(id); + } else { + toggleActivityBookmark.mutate(id); + } }; + const isLoading = selectedType === 'issue' ? issuesLoading : activitiesLoading; + const error = selectedType === 'issue' ? issuesError : activitiesError; + const data = selectedType === 'issue' ? bookmarkedIssues : bookmarkedActivities; + return ( @@ -98,63 +116,81 @@ export default function BookmarkList() { - {selectedType === 'activity' ? ( - isLoading ? ( - 로딩 중... - ) : error ? ( - 데이터를 불러오는데 실패했습니다. - ) : bookmarkedActivities?.content.length === 0 ? ( - 북마크한 활동이 없습니다. - ) : ( - <> - - - - - - - - - - - - - {bookmarkedActivities?.content.map((activity, idx) => ( - - + {isLoading ? ( + 로딩 중... + ) : error ? ( + 데이터를 불러오는데 실패했습니다. + ) : data?.content.length === 0 ? ( + 북마크한 {selectedType === 'issue' ? '이슈' : '활동'}가 없습니다. + ) : ( + <> + +
No.제목카테고리날짜북마크
{bookmarkedActivities.pageable.offset + idx + 1}
+ + + + + + {selectedType === 'activity' && } + {selectedType === 'issue' && } + + + + + {selectedType === 'issue' ? ( + data?.content.map((issue, idx) => ( + handleIssueClick(issue)}> + + + - + + )) + ) : ( + data?.content.map((activity, idx) => ( + handleActivityClick(activity)}> + + + + - ))} - -
No.제목카테고리마감 날짜날짜북마크
{data.pageable.offset + idx + 1}{issue.title} - handleTitleClick(activity)}> - {activity.name} - + + {issue.category} + {formatDate(issue.date)} - #{activity.keyword} + handleBookmarkToggle(e, issue.id, 'issue')} + /> {new Date(activity.startDate).toLocaleDateString()}
{data.pageable.offset + idx + 1}{activity.name} + + #{activity.keyword} + #{activity.activityType} + + {formatDate(activity.endDate)} handleBookmarkToggle(e, activity.activityId)} + onClick={(e) => handleBookmarkToggle(e, activity.activityId, 'activity')} />
-
- - - setCurrentPage(page - 1)} - /> - - - ) - ) : ( - 글로벌 이슈 북마크는 준비 중입니다. + )) + )} + + + + + + setCurrentPage(page - 1)} + /> + + )}
); @@ -188,7 +224,6 @@ const TabButton = styled.button` transition: all 0.2s ease-in-out; `; -// 테이블 컨테이너: 폭 고정, 가운데 정렬, 상·하 테두리 const TableContainer = styled.div` width: 900px; max-width: 100%; @@ -198,7 +233,6 @@ const TableContainer = styled.div` overflow-x: auto; `; -// 테이블: 고정 레이아웃, 셀 크기·간격 고정 const Table = styled.table` width: 100%; border-collapse: collapse; @@ -233,34 +267,32 @@ const Table = styled.table` th:nth-child(4), td:nth-child(4) { width: 15%; } th:nth-child(5), td:nth-child(5) { width: 10%; } - tr:nth-child(even) { - background-color: #f7faff; - } -`; + tr { + transition: background-color 0.2s ease; + cursor: pointer; -const TitleLink = styled.span` - color: #000; - font-weight: 500; - cursor: pointer; + &:hover { + background-color: #f0f5ff; + } + } - &:hover { - opacity: 0.8; + tr:nth-child(even) { + background-color: #f7faff; } `; -const CategoryTag = styled.div` - display: inline-block; - padding: 4px 12px; - border-radius: 999px; - font-size: 13px; - color: #34a853; - background: #f6faff; - border: 1px solid #34a853; +const CategoryContainer = styled.div` + display: flex; + justify-content: center; `; const ActivityTag = styled.span` color: #235ba9; font-weight: 500; + font-size: 13px; + padding: 2px 6px; + border-radius: 12px; + display: inline-block; `; const EmptyMessage = styled.div` @@ -271,15 +303,20 @@ const EmptyMessage = styled.div` `; const BookmarkIcon = styled.img` - width: 30px; - height: 30px; + width: 24px; + height: 24px; + cursor: pointer; + transition: transform 0.2s; + + &:hover { + transform: scale(1.1); + } `; const PaginationWrapper = styled.div` - margin-top: 24px; display: flex; justify-content: center; - width: 100%; + margin-top: 20px; `; const LoadingMessage = styled.div` @@ -294,4 +331,4 @@ const ErrorMessage = styled.div` text-align: center; color: #ff4d4f; font-size: 15px; -`; \ No newline at end of file +`; diff --git a/src/pages/ActivityPage.jsx b/src/pages/ActivityPage.jsx index fbc067b..7302809 100644 --- a/src/pages/ActivityPage.jsx +++ b/src/pages/ActivityPage.jsx @@ -10,7 +10,7 @@ import CustomDropdown from '../components/common/CustomDropdown'; import { useActivityStore } from '../store/activityStore'; import { useActivities, useToggleBookmark } from '../query/useActivities'; -const fieldOptions = ["전체", "경제", "환경", "사람과사회", "기술"]; +const fieldOptions = ["전체", "경제", "환경", "사람과 사회", "기술"]; const typeOptions = ["전체", "공모전", "봉사활동", "인턴십", "서포터즈"]; export default function ActivityPage() { @@ -103,7 +103,7 @@ export default function ActivityPage() { bookmarked={activity.bookmarked} onToggle={() => handleToggleBookmark(activity.id)} isClosed={activity.isClosed} - siteUrl={activity.siteUrl} + siteUrl={activity.siteUrl || 'https://naver.com'} /> ))} {Array.from({ length: 4 - (activities.length % 4) }).map((_, idx) => ( diff --git a/src/pages/issue/GlobalIssueDetailPage.jsx b/src/pages/issue/GlobalIssueDetailPage.jsx index 7271791..77ad8fb 100644 --- a/src/pages/issue/GlobalIssueDetailPage.jsx +++ b/src/pages/issue/GlobalIssueDetailPage.jsx @@ -1,143 +1,138 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import styled from 'styled-components'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import MainNav from '../../layout/MainNav'; import Footer from '../../layout/Footer'; -import ActivityCard from '../../components/activity/ActivityCard'; -import issueCardSample from '../../assets/images/issue/ic_IssueCardSample.png'; import linkIcon from '../../assets/images/issue/ic_Link.png'; -import bookmarkButton from '../../assets/images/common/BookmarkButton.png'; -import bookmarkFilledButton from '../../assets/images/common/BookmarkFilledButton.png'; - -const dummyActivities = [ - { - id: 1, - title: '제 22회 한국 경제 논문 공모전', - tags: ['#경제', '#공모전'], - date: '2025.04.15~2025.04.20', - image: issueCardSample, - bookmarked: false, - }, - { - id: 2, - title: '경제 공모전', - tags: ['#경제', '#공모전'], - date: '2025.04.15~2025.04.20', - image: issueCardSample, - bookmarked: false, - }, - { - id: 3, - title: '경제 봉사활동', - tags: ['#경제', '#봉사활동'], - date: '2025.04.15~2025.04.20', - image: issueCardSample, - bookmarked: false, - }, - { - id: 4, - title: '경제 서포터즈', - tags: ['#경제', '#서포터즈'], - date: '2025.04.15~2025.04.20', - image: issueCardSample, - bookmarked: false, - }, - { - id: 5, - title: '환경 서포터즈', - tags: ['#환경', '#서포터즈'], - date: '2025.04.15~2025.04.20', - image: issueCardSample, - bookmarked: false, - }, - { - id: 6, - title: '사람과 사회 서포터즈', - tags: ['#사람과 사회', '#서포터즈'], - date: '2025.04.15~2025.04.20', - image: issueCardSample, - bookmarked: false, - }, -]; - +import BookmarkButtonIcon from '../../assets/images/common/BookmarkButton.png'; +import BookmarkFilledIcon from '../../assets/images/common/BookmarkFilledButton.png'; +import { useIssueDetail, useToggleIssueBookmark } from '../../query/useIssues'; +import { useActivitiesByKeywordLimited } from '../../query/useActivities'; +import ActivityCard from '../../components/activity/ActivityCard'; +import { formatDate } from '../../utils/formatDate'; export default function GlobalIssueDetailPage() { - useEffect(() => { - window.scrollTo({ top: 0, left: 0 });}, []); - const location = useLocation(); + const { id } = useParams(); const navigate = useNavigate(); - const { label, title } = location.state || {}; - const [bookmarked, setBookmarked] = useState(false); - const toggleBookmark = () => setBookmarked((prev) => !prev); + const { data: issue, isLoading, error } = useIssueDetail(id); + const toggleBookmark = useToggleIssueBookmark(); - const filteredActivities = dummyActivities.filter((activity) => - activity.tags.includes(label) ); + // 추천 활동 조회 추가 + const { data: recommendedActivities, isLoading: activitiesLoading } = useActivitiesByKeywordLimited( + issue?.keyword, // 이슈의 키워드로 관련 활동 조회 + { + enabled: !!issue?.keyword // issue가 로드된 후에만 실행 + } + ); + + const handleBookmarkToggle = () => { + toggleBookmark.mutate(id); + }; + + if (isLoading) { + return ( + + + +

이슈를 불러오는 중...

+
+