diff --git a/src/api/activitiesService.ts b/src/api/activitiesService.ts new file mode 100644 index 0000000..2cbf5ea --- /dev/null +++ b/src/api/activitiesService.ts @@ -0,0 +1,94 @@ +import { instance } from './client'; +import { ENDPOINTS } from './apiConfig'; + +interface ActivitiesResponse { + code: number; + message: string; + data: ActivitiesData; +} + +interface ActivitiesData { + totalPages: number; + isLastPage: boolean; + totalActivities: number; + activities: Activity[]; +} + +interface Activity { + id: number; + title: string; + organization: string; + imageUrl: string; + dDay: number; +} + +interface DetailActivitiesResponse { + code: number; + message: string; + data: ActivityDetail; +} + +interface ActivityDetail { + id: number; + title: string; + organization: string; + corporateType: string; + participate: string; + startDate: string; + endDate: string; + period: string; + recruitment: string; + area: string; + preferredSkills: string; + homepageUrl: string; + activityBenefit: string; + activityField: string; + bonusBenefit: string; + description: string; + imageUrl: string; +} + +export interface ActivitiesQuery { + keyword?: null | string; + before?: boolean; + during?: boolean; + closed?: boolean; + orderBy?: 'latest' | 'd-day'; + page?: number; + pageSize?: number; +} + +class ActivitiesService { + private endpoint = ENDPOINTS.activities; + + getAll = async ({ + keyword = null, + before = true, + during = true, + closed = false, + orderBy = 'latest', + page = 1, + pageSize = 10, + }: ActivitiesQuery) => { + console.log(this.endpoint); + if (keyword === '' || keyword === null) { + const response = await instance.get( + `${this.endpoint}?before=${before}&during=${during}&closed=${closed}&orderBy=${orderBy}&page=${page}&pageSize=${pageSize}` + ); + return response.data.data; + } else { + const response = await instance.get( + `${this.endpoint}?keyword=${keyword}&before=${before}&during=${during}&closed=${closed}&orderBy=${orderBy}&page=${page}&pageSize=${pageSize}` + ); + return response.data.data; + } + }; + + getOne = async (id: number) => { + const response = await instance.get(`${this.endpoint}/${id}`); + + return response.data.data; + }; +} + +export default ActivitiesService; diff --git a/src/api/apiConfig.ts b/src/api/apiConfig.ts index 4b7c8f9..6fe7c80 100644 --- a/src/api/apiConfig.ts +++ b/src/api/apiConfig.ts @@ -13,4 +13,6 @@ export const ENDPOINTS = { resume: `${API_VERSION}/resumes`, studies: `${API_VERSION}/studies`, categories: `${API_VERSION}/categories`, + activities: `${API_VERSION}/activities`, + competitions: `${API_VERSION}/competitions`, }; diff --git a/src/api/competitionsService.ts b/src/api/competitionsService.ts new file mode 100644 index 0000000..bb18d7b --- /dev/null +++ b/src/api/competitionsService.ts @@ -0,0 +1,89 @@ +import { instance } from './client'; +import { ENDPOINTS } from './apiConfig'; + +interface CompetitionApiResponse { + code: number; + message: string; + data: CompetitionsData; +} + +interface CompetitionsData { + totalPages: number; + isLastPage: boolean; + totalCompetitions: number; + competitions: Competition[]; +} + +interface Competition { + id: number; + title: string; + organization: string; + imageUrl: string; + dDay: number; +} + +interface CompetitionDetailApiResponse { + code: number; + message: string; + data: CompetitionDetail; +} + +interface CompetitionDetail { + id: number; + title: string; + organization: string; + corporateType: string; + participate: string; + awardScale: string; + startDate: string; + endDate: string; + homepageUrl: string; + activityBenefit: string; + bonusBenefit: string; + description: string; + imageUrl: string; +} + +export interface CompetitionsQuery { + keyword?: null | string; + before?: boolean; + continues?: boolean; + after?: boolean; + orderBy?: 'latest' | 'd-day'; + page?: number; + pageSize?: number; +} + +class CompetitionsService { + private endpoint = ENDPOINTS.competitions; + + getAll = async ({ + keyword = null, + before = true, + continues = true, + after = false, + orderBy = 'latest', + page = 1, + pageSize = 10, + }: CompetitionsQuery) => { + if (keyword === '' || keyword === null) { + const response = await instance.get( + `${this.endpoint}?before=${before}&continue=${continues}&after=${after}&orderBy=${orderBy}&page=${page}&pageSize=${pageSize}` + ); + return response.data.data; + } else { + const response = await instance.get( + `${this.endpoint}?keyword=${keyword}&before=${before}&continue=${continues}&after=${after}&orderBy=${orderBy}&page=${page}&pageSize=${pageSize}` + ); + return response.data.data; + } + }; + + getOne = async (id: number) => { + const response = await instance.get(`${this.endpoint}/${id}`); + + return response.data.data; + }; +} + +export default CompetitionsService; diff --git a/src/api/reviewService.ts b/src/api/reviewService.ts index 05e4e43..61367de 100644 --- a/src/api/reviewService.ts +++ b/src/api/reviewService.ts @@ -11,10 +11,19 @@ export interface ReviewPostData { content: string; } +export interface ReviewEditData extends ReviewPostData { + authorNickname: string; +} + export interface ReviewPostResponse { code: number; message: string; - data: ReviewPostData | null; + data: ReviewPostData; +} +export interface ReviewEditResponse { + code: number; + message: string; + data: ReviewEditData | null; } export type ReviewRequest = { @@ -28,7 +37,15 @@ export type ReviewRequest = { export interface SummaryResponse { code: number; message: string; - data: Summary[]; + data: SummaryData; +} + +interface SummaryData { + currentPage: 1; + totalPages: 1; + currentElements: 2; + totalElements: 2; + reviews: Summary[]; } export interface Summary { @@ -37,32 +54,76 @@ export interface Summary { totalReviews: number; } +export interface ReviewDetail { + id: number; + authorNickname: string; + bootcamp: string; + title: string; + goodtags: string[]; + badtags: string[]; + rating: number; + content: string; +} + +export interface ReviewDetailResponseData { + currentPage: number; + totalPages: number; + currentElements: number; + totalElements: number; + reviews: ReviewDetail[]; +} + +export interface ReviewDetailResponse { + code: number; + message: string; + data: ReviewDetailResponseData; +} + class ReviewService { private endpoint = ENDPOINTS.reviews; post = async (review: ReviewRequest) => { - const response = await instance.post<{ data: ReviewPostResponse }>(this.endpoint, review); + const response = await instance.post(this.endpoint, review); return response.data.data; }; delete = async (id: number) => { - const response = await instance.post<{ data: ReviewPostResponse }>(`${this.endpoint}/${id}`); + const response = await instance.post<{ + code: number; + message: string; + data: null; + }>(`${this.endpoint}/${id}`); return response.data.data; }; edit = async (review: ReviewRequest, id: number) => { - const response = await instance.put<{ data: ReviewPostResponse }>( - `${this.endpoint}/${id}`, - review + const response = await instance.put(`${this.endpoint}/${id}`, review); + + return response.data.data; + }; + + summary = async ({ page = 1, size = 6 }: { page?: number; size?: number }) => { + const response = await instance.get( + `${this.endpoint}/summary?page=${page}&size=${size}` ); return response.data.data; }; - summary = async () => { - const response = await instance.put<{ data: SummaryResponse }>(`${this.endpoint}/summary`); + detail = async ({ + sortBy = 'createdAt', + bootcamp, + page = 1, + }: { + sortBy?: 'rating' | 'createdAt'; + bootcamp: string; + page?: number; + }) => { + const response = await instance.get( + `${this.endpoint}?sortBy=${sortBy}&bootcamp=${bootcamp}&page=${page}` + ); return response.data.data; }; diff --git a/src/hooks/activitiesData/query/useActivitiesData.ts b/src/hooks/activitiesData/query/useActivitiesData.ts new file mode 100644 index 0000000..981e260 --- /dev/null +++ b/src/hooks/activitiesData/query/useActivitiesData.ts @@ -0,0 +1,24 @@ +import ActivitiesService, { type ActivitiesQuery } from '@/api/activitiesService'; +import { useQuery } from '@tanstack/react-query'; + +const useActivitiesData = (query: ActivitiesQuery) => { + const activitiesService = new ActivitiesService(); + + return useQuery({ + queryKey: [ + 'activities', + query.before, + query.closed, + query.during, + query.keyword, + query.orderBy, + query.page, + query.pageSize, + ], + queryFn: () => { + return activitiesService.getAll(query); + }, + }); +}; + +export default useActivitiesData; diff --git a/src/hooks/activitiesData/query/useActivityData.ts b/src/hooks/activitiesData/query/useActivityData.ts new file mode 100644 index 0000000..8650770 --- /dev/null +++ b/src/hooks/activitiesData/query/useActivityData.ts @@ -0,0 +1,16 @@ +import ActivitiesService from '@/api/activitiesService'; +import { useQuery } from '@tanstack/react-query'; + +const useActivitiyData = ({ id, start }: { id: number; start: boolean }) => { + const activitiesService = new ActivitiesService(); + + return useQuery({ + queryKey: ['activitiy', id], + queryFn: () => { + return activitiesService.getOne(id); + }, + enabled: start, + }); +}; + +export default useActivitiyData; diff --git a/src/hooks/competitionsData/query/useAllCompetitionData.ts b/src/hooks/competitionsData/query/useAllCompetitionData.ts new file mode 100644 index 0000000..8700db4 --- /dev/null +++ b/src/hooks/competitionsData/query/useAllCompetitionData.ts @@ -0,0 +1,24 @@ +import CompetitionsService, { type CompetitionsQuery } from '@/api/competitionsService'; +import { useQuery } from '@tanstack/react-query'; + +const useAllCompetitionData = (query: CompetitionsQuery) => { + const competitionsService = new CompetitionsService(); + + return useQuery({ + queryKey: [ + 'competitions', + query.before, + query.continues, + query.after, + query.keyword, + query.orderBy, + query.page, + query.pageSize, + ], + queryFn: () => { + return competitionsService.getAll(query); + }, + }); +}; + +export default useAllCompetitionData; diff --git a/src/hooks/competitionsData/query/useCompetitionData.ts b/src/hooks/competitionsData/query/useCompetitionData.ts new file mode 100644 index 0000000..c5eb020 --- /dev/null +++ b/src/hooks/competitionsData/query/useCompetitionData.ts @@ -0,0 +1,16 @@ +import CompetitionsService from '@/api/competitionsService'; +import { useQuery } from '@tanstack/react-query'; + +const useCompetitionData = ({ id, start }: { id: number; start: boolean }) => { + const competitionsService = new CompetitionsService(); + + return useQuery({ + queryKey: ['competition', id], + queryFn: () => { + return competitionsService.getOne(id); + }, + enabled: start, + }); +}; + +export default useCompetitionData; diff --git a/src/hooks/reviewData/mutation/usePostReview.ts b/src/hooks/reviewData/mutation/usePostReview.ts new file mode 100644 index 0000000..1c3b700 --- /dev/null +++ b/src/hooks/reviewData/mutation/usePostReview.ts @@ -0,0 +1,26 @@ +import type { ReviewPostData, ReviewRequest } from '@/api/reviewService'; +import ReviewService from '@/api/reviewService'; +import { useMutation } from '@tanstack/react-query'; +import { isAxiosError } from 'axios'; +import toast from 'react-hot-toast'; +import { useNavigate } from 'react-router-dom'; + +const reviewService = new ReviewService(); + +const usePostReview = () => { + const navigate = useNavigate(); + return useMutation({ + mutationFn: (newReview) => reviewService.post(newReview), + onSuccess: (data) => { + console.log(data); + navigate('review'); + }, + onError: (error) => { + if (isAxiosError(error)) { + toast.error(error.response?.data.message); + } + }, + }); +}; + +export default usePostReview; diff --git a/src/hooks/reviewData/query/useReviewDetailData.ts b/src/hooks/reviewData/query/useReviewDetailData.ts new file mode 100644 index 0000000..a4a5514 --- /dev/null +++ b/src/hooks/reviewData/query/useReviewDetailData.ts @@ -0,0 +1,20 @@ +import ReviewService, { type ReviewDetailResponseData } from '@/api/reviewService'; +import { useInfiniteQuery } from '@tanstack/react-query'; + +const reviewService = new ReviewService(); + +const useReviewDetailData = (query: { sortBy?: 'rating' | 'createdAt'; bootcamp: string }) => { + return useInfiniteQuery({ + queryKey: ['review', query.bootcamp], + queryFn: ({ pageParam = 1 }) => reviewService.detail({ ...query, page: pageParam as number }), + getNextPageParam: (lastPage) => { + if (lastPage.currentPage < lastPage.totalPages) { + return lastPage.currentPage + 1; + } + return undefined; + }, + initialPageParam: 1, + }); +}; + +export default useReviewDetailData; diff --git a/src/hooks/reviewData/query/useSummaryData.ts b/src/hooks/reviewData/query/useSummaryData.ts index 11afc7b..ebf0f3b 100644 --- a/src/hooks/reviewData/query/useSummaryData.ts +++ b/src/hooks/reviewData/query/useSummaryData.ts @@ -1,11 +1,13 @@ import { useQuery } from '@tanstack/react-query'; import ReviewService from '@/api/reviewService'; -const useSummaryData = () => { +const useSummaryData = (query: { page?: number; size?: number }) => { const reviewService = new ReviewService(); return useQuery({ - queryKey: ['review', 'summary'], - queryFn: reviewService.summary, + queryKey: ['review', 'summary', query.page, query.size], + queryFn: () => { + return reviewService.summary(query); + }, }); }; diff --git a/src/hooks/useInview.ts b/src/hooks/useInview.ts new file mode 100644 index 0000000..bd1a162 --- /dev/null +++ b/src/hooks/useInview.ts @@ -0,0 +1,27 @@ +import { useEffect, useState, useRef } from 'react'; + +const useInView = (options?: IntersectionObserverInit) => { + const [inView, setInView] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver(([entry]) => { + setInView(entry.isIntersecting); + }, options); + + const currentRef = ref.current; + if (currentRef) { + observer.observe(currentRef); + } + + return () => { + if (currentRef) { + observer.unobserve(currentRef); + } + }; + }, [options]); + + return [ref, inView] as const; +}; + +export default useInView; diff --git a/src/pages/activity/components/Activity.tsx b/src/pages/activity/components/Activity.tsx new file mode 100644 index 0000000..ed4859d --- /dev/null +++ b/src/pages/activity/components/Activity.tsx @@ -0,0 +1,180 @@ +import { type ActivitiesQuery } from '@/api/activitiesService'; +import Divider from '@/components/atoms/Divider'; +import useActivitiesData from '@/hooks/activitiesData/query/useActivitiesData'; +import { useState } from 'react'; +import { GrNext, GrPrevious } from 'react-icons/gr'; +import { IoIosSearch } from 'react-icons/io'; +import { useNavigate } from 'react-router-dom'; +import styles from '../index.module.scss'; +import Item from './Item'; +import StatusBadge from './StatusBadge'; +import ActivitySkeleton from './ActivitySkeleton'; + +const Activity = () => { + const navigate = useNavigate(); + + const [activitiesQuery, setActivitiesQuery] = useState({ + keyword: '', + before: true, + during: true, + closed: false, + orderBy: 'latest', + page: 1, + pageSize: 6, + }); + + const { data: activities, isLoading } = useActivitiesData(activitiesQuery); + + const [currentPage, setCurrentPage] = useState(1); + const [currentPageList, setCurrentPageList] = useState(0); + + const updateActivitiesQuery = (updates: Partial) => { + setActivitiesQuery((prev) => ({ + ...prev, + ...updates, + })); + }; + + return ( +
+ {/* 필터링 검색 바 */} +
+
+ { + updateActivitiesQuery({ before: !activitiesQuery.before }); + }} + /> + { + updateActivitiesQuery({ during: !activitiesQuery.during }); + }} + /> + { + updateActivitiesQuery({ closed: !activitiesQuery.closed }); + }} + /> +
+
+
+ { + updateActivitiesQuery({ keyword: e.target.value }); + }} + /> + + + +
+
+ + +
+
+
+ + {/* 아이템 리스트 */} + + {isLoading && ( +
    + + + + + + +
+ )} + {activities?.activities.length === 0 ? ( +
등록된 대외 활동이 없습니다.
+ ) : ( +
    + {activities?.activities.map((item, index) => { + return ( + { + navigate(`/activity/${index}?t=activities`); + }} + /> + ); + })} +
+ )} + +
+ + + + {/** * + * TODO: 리스트 아이템 개수 연결 + */} + {activities?.totalPages && + Array.from({ length: activities?.totalPages }, (_, i) => i + 1) + .slice(currentPageList * 5, currentPageList * 5 + 5) + .map((number, index) => { + return ( + + ); + })} + + +
+
+ ); +}; + +export default Activity; diff --git a/src/pages/activity/components/ActivitySkeleton.tsx b/src/pages/activity/components/ActivitySkeleton.tsx new file mode 100644 index 0000000..8729cec --- /dev/null +++ b/src/pages/activity/components/ActivitySkeleton.tsx @@ -0,0 +1,16 @@ +import styles from './Item.module.scss'; +import LoadingSkeleton from '@/components/skeletons'; + +const ActivitySkeleton = () => { + return ( +
  • + +
  • + ); +}; + +export default ActivitySkeleton; diff --git a/src/pages/activity/components/Competition.tsx b/src/pages/activity/components/Competition.tsx new file mode 100644 index 0000000..1dfe280 --- /dev/null +++ b/src/pages/activity/components/Competition.tsx @@ -0,0 +1,181 @@ +import { type CompetitionsQuery } from '@/api/competitionsService'; +import Divider from '@/components/atoms/Divider'; +import useAllCompetitionData from '@/hooks/competitionsData/query/useAllCompetitionData'; +import { useState } from 'react'; +import { GrNext, GrPrevious } from 'react-icons/gr'; +import { IoIosSearch } from 'react-icons/io'; +import { useNavigate } from 'react-router-dom'; +import styles from '../index.module.scss'; +import Item from './Item'; +import StatusBadge from './StatusBadge'; +import ActivitySkeleton from './ActivitySkeleton'; + +const Competition = () => { + const navigate = useNavigate(); + + const [competitionsQuery, setCompetitionsQuery] = useState({ + keyword: '', + before: true, + continues: true, + after: false, + orderBy: 'latest', + page: 1, + pageSize: 6, + }); + + const { data: competitions, isLoading } = useAllCompetitionData(competitionsQuery); + + const [currentPage, setCurrentPage] = useState(1); + const [currentPageList, setCurrentPageList] = useState(0); + + const updateCompetitionsQuery = (updates: Partial) => { + setCompetitionsQuery((prev) => ({ + ...prev, + ...updates, + })); + }; + + return ( +
    + {/* 필터링 검색 바 */} +
    +
    + { + updateCompetitionsQuery({ before: !competitionsQuery.before }); + }} + /> + { + updateCompetitionsQuery({ continues: !competitionsQuery.continues }); + }} + /> + { + updateCompetitionsQuery({ after: !competitionsQuery.after }); + }} + /> +
    +
    +
    + { + updateCompetitionsQuery({ keyword: e.target.value }); + }} + /> + + + +
    +
    + + +
    +
    +
    + + {/* 아이템 리스트 */} + + {isLoading && ( +
      + + + + + + +
    + )} + {competitions?.competitions.length === 0 ? ( +
    등록된 공모전이 없습니다.
    + ) : ( +
      + {competitions?.competitions.map((item, index) => { + return ( + { + navigate(`/activity/${index}?t=competitions`); + }} + /> + ); + })} +
    + )} + +
    + + + + {/** * + * TODO: 리스트 아이템 개수 연결 + */} + {competitions?.totalPages && + Array.from({ length: competitions?.totalPages }, (_, i) => i + 1) + .slice(currentPageList * 5, currentPageList * 5 + 5) + .map((number, index) => { + return ( + + ); + })} + + + +
    +
    + ); +}; + +export default Competition; diff --git a/src/pages/activity/components/Item.module.scss b/src/pages/activity/components/Item.module.scss index 855deec..d5fbd11 100644 --- a/src/pages/activity/components/Item.module.scss +++ b/src/pages/activity/components/Item.module.scss @@ -3,6 +3,7 @@ .list { width: 33.333%; + list-style: none; @include m.mobile { width: 50%; @@ -35,6 +36,7 @@ overflow: hidden; height: 200px; + width: 100%; & > img { display: block; @@ -45,7 +47,7 @@ } } & > div:nth-of-type(2) { - font-size: 1.2rem; + font-size: 1rem; width: 90%; text-align: left; @@ -61,7 +63,7 @@ justify-content: space-between; color: v.$gray; - font-size: 0.9rem; + font-size: 0.7rem; width: 100%; diff --git a/src/pages/activity/index.tsx b/src/pages/activity/index.tsx index 87b2e29..bc37064 100644 --- a/src/pages/activity/index.tsx +++ b/src/pages/activity/index.tsx @@ -1,155 +1,20 @@ -import Divider from '@/components/atoms/Divider'; import { useState } from 'react'; -import { GrNext, GrPrevious } from 'react-icons/gr'; -import { IoIosSearch } from 'react-icons/io'; -import Item from './components/Item'; -import StatusBadge from './components/StatusBadge'; +import Activity from './components/Activity'; +import Competition from './components/Competition'; import TabTitle from './components/TabTitle'; -import { listARes, listBRes } from './constants'; import styles from './index.module.scss'; const ActivityPage = () => { const [currentTab, setCurrentTab] = useState(0); - const [currentPage, setCurrentPage] = useState(1); - const [currentPageList, setCurrentPageList] = useState(0); - - const [order, setOrder] = useState<'latest' | 'd-day'>('latest'); - - const [filter, setFilter] = useState([ - { title: '모집 전', active: false }, - { title: '모집 중', active: false }, - { title: '모집 마감', active: false }, - ]); - - const filteringItem = (index: number) => { - setFilter((prev) => - prev.map((item, i) => (i === index ? { ...item, active: !item.active } : item)) - ); - }; return (
    - {/* 헤더 */}
    setCurrentTab(0)} /> setCurrentTab(1)} />
    - {/* 필터링 검색 바 */} -
    -
    - {filter.map((item, index) => { - return ( - filteringItem(index)} - /> - ); - })} -
    -
    -
    - - - - -
    -
    - - -
    -
    -
    - - {/* 아이템 리스트 */} -
      - {currentTab === 0 && - listARes.data.competitions.map((item) => { - return ( - - ); - })} - - {currentTab === 1 && - listBRes.data.activities.map((item) => { - return ( - - ); - })} -
    -
    - - - - {/** * - * TODO: 리스트 아이템 개수 연결 - */} - {Array.from({ length: 49 }, (_, i) => i + 1) - .slice(currentPageList * 5, currentPageList * 5 + 5) - .map((number, index) => { - return ( - - ); - })} - - -
    + {currentTab === 0 && } + {currentTab === 1 && }
    ); }; diff --git a/src/pages/activityDetail/index.module.scss b/src/pages/activityDetail/index.module.scss index 3b44aff..c499bf7 100644 --- a/src/pages/activityDetail/index.module.scss +++ b/src/pages/activityDetail/index.module.scss @@ -31,9 +31,16 @@ @include m.tablet { max-width: 680px; + + .title { + font-size: 1.3rem; + } } @include m.mobile { max-width: 680px; + .title { + font-size: 1rem; + } } } @@ -122,5 +129,11 @@ .content { & > img { width: 100%; + + margin-bottom: 1rem; } } + +.detailLink:hover { + text-decoration: underline; +} diff --git a/src/pages/activityDetail/index.tsx b/src/pages/activityDetail/index.tsx index fda0557..017d52b 100644 --- a/src/pages/activityDetail/index.tsx +++ b/src/pages/activityDetail/index.tsx @@ -1,18 +1,27 @@ -import { useLocation } from 'react-router-dom'; -import styles from './index.module.scss'; -import { itemADetail, itemBDetail } from '../activity/constants'; import Divider from '@/components/atoms/Divider'; +import useActivitiyData from '@/hooks/activitiesData/query/useActivityData'; +import useCompetitionData from '@/hooks/competitionsData/query/useCompetitionData'; +import { useLocation, useParams } from 'react-router-dom'; +import styles from './index.module.scss'; const ActivityDetailPage = () => { - // const { id } = useParams(); + const { id } = useParams(); const location = useLocation(); const queryParams = new URLSearchParams(location.search); const tabName = queryParams.get('t'); + const { data: competition } = useCompetitionData({ + id: Number(id), + start: tabName === 'competitions', + }); + const { data: activitiy } = useActivitiyData({ + id: Number(id), + start: tabName === 'activities', + }); return (
    - {tabName === 'competitions' ? itemADetail.data.title : itemBDetail.data.title} + {tabName === 'competitions' ? competition?.title : activitiy?.title}
    @@ -26,45 +35,43 @@ const ActivityDetailPage = () => { {tabName === 'competitions' ? '공모전 주최' : '대외 활동 주최'} {tabName === 'competitions' - ? itemADetail.data.organization || '' - : itemBDetail.data.organization} + ? competition?.organization || '' + : activitiy?.organization}
    기업 형태 {tabName === 'competitions' - ? itemADetail.data.corporate_type || '' - : itemBDetail.data.corporate_type} + ? competition?.corporateType || '' + : activitiy?.corporateType}
    참여 대상 - {tabName === 'competitions' - ? itemADetail.data.participate || '' - : itemBDetail.data.participate} + {tabName === 'competitions' ? competition?.participate || '' : activitiy?.participate}
    접수 기간 {tabName === 'competitions' - ? `${itemADetail.data.start_date || ''} ~ ${itemADetail.data.end_date || ''}` || '' - : `${itemBDetail.data.start_date || ''} ~ ${itemBDetail.data.end_date || ''}`} + ? `${competition?.startDate || ''} ~ ${competition?.endDate || ''}` || '' + : `${activitiy?.startDate || ''} ~ ${activitiy?.endDate || ''}`}
    {tabName === 'activities' && (
    활동 기간 - {itemBDetail.data.period} + {activitiy?.period}
    )} {tabName === 'activities' && (
    모집 인원 - {itemBDetail.data.recruitment}명 + {activitiy?.recruitment}명
    )}
    @@ -72,26 +79,26 @@ const ActivityDetailPage = () => { {tabName === 'competitions' && (
    시상 규모 - {itemADetail.data.activity_benefit || ''} + {competition?.activityBenefit || ''}
    )} {tabName === 'activities' && (
    활동 지역 - {itemBDetail.data.area} + {activitiy?.area}
    )} {tabName === 'activities' && (
    우대 역량 - {itemBDetail.data.preferred_skills} + {activitiy?.preferredSkills}
    )} {tabName === 'activities' && (
    활동 분야 - {itemBDetail.data.activity_field} + {activitiy?.activityField}
    )} @@ -99,16 +106,16 @@ const ActivityDetailPage = () => { 활동 혜택 {tabName === 'competitions' - ? itemADetail.data.activity_benefit || '' - : itemBDetail.data.activity_benefit} + ? competition?.activityBenefit || '' + : activitiy?.activityBenefit}
    추가 혜택 {tabName === 'competitions' - ? itemADetail.data.bonus_benefit || '' - : itemBDetail.data.bonus_benefit} + ? competition?.bonusBenefit || '' + : activitiy?.bonusBenefit}
    @@ -134,12 +140,10 @@ const ActivityDetailPage = () => {
    detail -
    - {tabName === 'competitions' ? itemADetail.data.description : itemBDetail.data.description} -
    +
    {tabName === 'competitions' ? competition?.description : activitiy?.description}
    ); diff --git a/src/pages/campReviewList/components/BootcampList.tsx b/src/pages/campReviewList/components/BootcampList.tsx index 876d6a9..2147d84 100644 --- a/src/pages/campReviewList/components/BootcampList.tsx +++ b/src/pages/campReviewList/components/BootcampList.tsx @@ -1,40 +1,34 @@ import Button from '@/components/atoms/button/Button'; +import useSummaryData from '@/hooks/reviewData/query/useSummaryData'; import { currentReviewState } from '@/recoil/currentReviewState'; import { useNavigate } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import styles from './BootcampList.module.scss'; +const reviewQuery = { + page: 1, + size: 6, +}; + const BootcampList = () => { + const { data: summary, isError } = useSummaryData(reviewQuery); + const setRating = useSetRecoilState(currentReviewState); const navigate = useNavigate(); - const list = [ - { - bootcamp: '42 Seoul', - averageRating: 4.2, - totalReviews: 20, - }, - { - bootcamp: '우아한테크코스', - averageRating: 4.0, - totalReviews: 100, - }, - { - bootcamp: '패스트캠퍼스', - averageRating: 1.2, - totalReviews: 1, - }, - ]; const handleClick = (name: string, rating: number, total: number) => { setRating({ averageRating: rating, totalReviews: total }); navigate(`/review/detail?n=${name}`); }; + if (isError) return
    잠시 후 다시 시도해주세요.
    ; + if (!summary) return
    등록 된 리뷰가 없습니다.
    ; + return (
      - {list.map((bootcamp, index) => { + {summary?.reviews.map((bootcamp, index) => { return (
    • diff --git a/src/pages/reviewDetailList/ReviewList.tsx b/src/pages/reviewDetailList/ReviewList.tsx index dfd63ee..0a4025f 100644 --- a/src/pages/reviewDetailList/ReviewList.tsx +++ b/src/pages/reviewDetailList/ReviewList.tsx @@ -1,44 +1,36 @@ +import useReviewDetailData from '@/hooks/reviewData/query/useReviewDetailData'; +import useInView from '@/hooks/useInview'; +import { useEffect } from 'react'; import styles from './ReviewList.module.scss'; -const data = [ - { - id: 7, - authorNickname: 'nickcname1', - bootcamp: '야놀자x패스트캠퍼스 부트캠프', - title: '후기 수정', - goodtags: ['강의가 좋아요'], - badtags: ['피드백이 느려요'], - rating: 5, - content: '내용 수정', - }, - { - id: 6, - authorNickname: 'nickcname2', - bootcamp: '야놀자x패스트캠퍼스 부트캠프', - title: 'test 부트캠프 리뷰', - goodtags: ['친절해요', '강의가 좋아요'], - badtags: ['불친절해요', '피드백이 느려요'], - rating: 3, - content: '뭐야', - }, - { - id: 5, - authorNickname: 'nickcname3', - bootcamp: '야놀자x패스트캠퍼스 부트캠프', - title: '후기 수정111', - goodtags: ['친절해요'], - badtags: ['불친절해요'], - rating: 2, - content: '훈련장려금 언제나와???', - }, -]; +const ReviewList = ({ bootcampTitle }: { bootcampTitle: string }) => { + const { + data: reviewDetail, + fetchNextPage, + hasNextPage, + } = useReviewDetailData({ + sortBy: 'createdAt', + bootcamp: bootcampTitle as string, + }); + + const [ref, inView] = useInView({ + rootMargin: '100px', + }); + + // Fetch the next page when the bottom of the list comes into view + useEffect(() => { + if (inView && hasNextPage) { + fetchNextPage(); + } + }, [inView, fetchNextPage, hasNextPage]); + + console.log(reviewDetail); -const ReviewList = () => { return (
        - {data.map((review) => { - return ( + {reviewDetail?.pages.map((page) => + page.reviews.map((review) => (
      • {review.bootcamp} @@ -65,9 +57,10 @@ const ReviewList = () => {
        {review.content}
      • - ); - })} + )) + )}
      +
      ); }; diff --git a/src/pages/reviewDetailList/index.tsx b/src/pages/reviewDetailList/index.tsx index eae28c1..1e1652a 100644 --- a/src/pages/reviewDetailList/index.tsx +++ b/src/pages/reviewDetailList/index.tsx @@ -1,17 +1,18 @@ import { currentReviewState } from '@/recoil/currentReviewState'; import { useEffect, useRef, useState } from 'react'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; import ReviewList from './ReviewList'; import styles from './index.module.scss'; const ReviewDetailListPage = () => { - const review = useRecoilValue(currentReviewState); - + const navigate = useNavigate(); const location = useLocation(); const queryParams = new URLSearchParams(location.search); const bootcampTitle = queryParams.get('n'); + const review = useRecoilValue(currentReviewState); + const [moreView, setMoreView] = useState(false); const [viewButton, setViewButtin] = useState(false); @@ -40,6 +41,11 @@ const ReviewDetailListPage = () => { } }; + if (!bootcampTitle) { + navigate('/review'); + return; + } + return (

      {bootcampTitle}

      @@ -84,7 +90,7 @@ const ReviewDetailListPage = () => { )}
      - +
      ); }; diff --git a/src/pages/writeReview/constants.ts b/src/pages/writeReview/constants.ts index e9b1b31..e2540d1 100644 --- a/src/pages/writeReview/constants.ts +++ b/src/pages/writeReview/constants.ts @@ -1,60 +1,60 @@ export const goodTags = [ { - id: 0, + id: 1, title: '체계적인 커리큘럼', type: 'good', icon: '🌱', }, { - id: 1, + id: 2, title: '퀄리티 있는 강의', type: 'good', icon: '🧑‍💻', }, { - id: 2, + id: 3, title: '다양한 강의', type: 'good', icon: '🎞️', }, { - id: 3, + id: 4, title: '빠른 피드백', type: 'good', icon: '🚗', }, { - id: 4, + id: 5, title: '다양한 프로젝트 경험', type: 'good', icon: '📚', }, { - id: 5, + id: 6, title: '다양한 협업 경험', type: 'good', icon: '⚒️', }, { - id: 6, + id: 7, title: '훌륭한 강사진', type: 'good', icon: '🕺', }, { - id: 7, + id: 8, title: '오프라인', type: 'good', icon: '🔈', }, { - id: 8, + id: 9, title: '온라인', type: 'good', icon: '🔊', }, { - id: 9, + id: 10, title: '좋은 복지', type: 'good', icon: '🎁', @@ -63,61 +63,61 @@ export const goodTags = [ export const badTags = [ { - id: 10, + id: 11, title: '아쉬운 커리큘럼', type: 'bad', icon: '😒', }, { - id: 11, + id: 12, title: '퀄리티 낮은 강의', type: 'bad', icon: '😒', }, { - id: 12, + id: 13, title: '적은 강의', type: 'bad', icon: '😒', }, { - id: 13, + id: 14, title: '느린 피드백', type: 'bad', icon: '😒', }, { - id: 14, + id: 15, title: '한정적인 프로젝트', type: 'bad', icon: '😒', }, { - id: 15, + id: 16, title: '커리어 컨설팅 부족', type: 'bad', icon: '😒', }, { - id: 16, + id: 17, title: '아쉬운 강사진', type: 'bad', icon: '😒', }, { - id: 17, + id: 18, title: '부족한 혜택', type: 'bad', icon: '😒', }, { - id: 18, + id: 19, title: '오프라인', type: 'bad', icon: '😒', }, { - id: 19, + id: 20, title: '온라인', type: 'bad', icon: '😒', diff --git a/src/pages/writeReview/index.tsx b/src/pages/writeReview/index.tsx index cc21c98..b679cd7 100644 --- a/src/pages/writeReview/index.tsx +++ b/src/pages/writeReview/index.tsx @@ -1,14 +1,14 @@ +import { ReactComponent as PenSquare } from '@/assets/icons/pen_square.svg'; import Button from '@/components/atoms/button/Button'; import Input from '@/components/atoms/input'; +import usePostReview from '@/hooks/reviewData/mutation/usePostReview'; import { reviewFormState } from '@/recoil/reviewFormState'; +import { useState } from 'react'; import { useRecoilValue } from 'recoil'; import SelectTag from './components/SelectTag'; import SetRating from './components/SetRating'; import styles from './index.module.scss'; -import { ReactComponent as PenSquare } from '@/assets/icons/pen_square.svg'; -import { useState } from 'react'; - export interface Tag { id: number; title: string; @@ -17,6 +17,7 @@ export interface Tag { } const WriteReviewPage = () => { + const { mutate: postReview } = usePostReview(); const review = useRecoilValue(reviewFormState); const [titleInput, setTitleInput] = useState(''); const [contentInput, setContentInput] = useState(''); @@ -24,13 +25,14 @@ const WriteReviewPage = () => { const handleSubmit = () => { const data = { title: titleInput, - goodTags: review.goodTags, - badTags: review.badTags, + goodtags: review.goodTags, + badtags: review.badTags, rating: review.rating, content: contentInput, }; - console.log(data); + postReview(data); + // console.log(data); }; return (