diff --git a/src/apis/main.api.ts b/src/apis/main.api.ts new file mode 100644 index 0000000..b7f0e5f --- /dev/null +++ b/src/apis/main.api.ts @@ -0,0 +1,45 @@ +import { mainPosts, mainTags } from '@/types/main.model'; +import { Pagination } from '@/types/pagination.model'; +import { httpClient } from './http.api'; + +export interface FetchMainDataParams { + selectedTags: number[]; + currentPage: number; + limit: number; +} + +export interface FetchMainDataResponse { + posts: mainPosts[]; + tags: mainTags[]; + pagination: Pagination; + filteredPosts: mainPosts[]; +} + + +// 기본 및 태그별 게시물 요청 +export const fetchMainData = async ({ selectedTags, currentPage, limit }: FetchMainDataParams): Promise => { + try { + // 기본 게시글 데이터 요청 + const responseMainData = await httpClient.get('/api/main', { + params: { page: currentPage, limit } + }); + + // 태그별 필터링된 게시글 데이터 요청 (선택된 태그가 있을 때만) + let filteredPosts: mainPosts[] = []; + if (selectedTags.length > 0) { + const params = new URLSearchParams(); + selectedTags.forEach(tag => params.append('tags', tag.toString())); + const responsePostsByTags = await httpClient.get('/api/main/tags', { params }); + filteredPosts = responsePostsByTags.data; + } + + return { + posts: responseMainData.data.posts, // 기본 게시글 + tags: responseMainData.data.tags, // 태그 데이터 + pagination: responseMainData.data.pagination, // 페이지네이션 정보 + filteredPosts, // 선택된 태그에 따라 필터링된 게시글 (없으면 빈 배열) + }; + } catch { + throw new Error("Failed to fetch data"); + } +} \ No newline at end of file diff --git a/src/apis/maindata.api.ts b/src/apis/maindata.api.ts deleted file mode 100644 index 63b8e83..0000000 --- a/src/apis/maindata.api.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { httpClient } from './http.api'; -import { mainData } from '@/types/main.model'; - -export const fetchMainData = async () => { - const response = await httpClient.get('/api/main'); - try { - return response.data; - } catch { - throw Error; - } -}; diff --git a/src/components/main-page/MainTagList.tsx b/src/components/main-page/MainTagList.tsx deleted file mode 100644 index f4dabe0..0000000 --- a/src/components/main-page/MainTagList.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useMainTags } from '@/hooks/useMainTags'; -import { useSearchParams } from 'react-router'; -import styled from 'styled-components'; - -const MainTagList = () => { - const { maintags } = useMainTags(); - const [searchParams, setSearchParams] = useSearchParams(); - - const handleTags = (id: number | null) => { - const newSearchParams = new URLSearchParams(searchParams); - if (id === null) { - newSearchParams.delete('id'); - } else { - newSearchParams.set('id', id.toString()); - } - setSearchParams(newSearchParams); - }; - - return ( - - {maintags.map((tag) => ( - handleTags(tag.id)} - > - {tag.name} - - ))} - - ); -}; - -const MainTagListContainer = styled.div` - display: flex; - justify-content: space-evenly; - align-items: center; -`; - -const TagButton = styled.button` - border-radius: 30px; - font-size: 1.1rem; - background-color: transparent; - color: #32c040; - border: 1px solid #32c040; - padding: 8px 20px; - text-decoration: none; - cursor: pointer; - transition: all 0.12s ease-in-out; - - &:hover { - background-color: rgba(49, 191, 63, 0.23); - } - - &.active { - background-color: rgba(49, 191, 63, 0.23); - } -`; - -export default MainTagList; diff --git a/src/components/main-page/Pagination.tsx b/src/components/main-page/Pagination.tsx new file mode 100644 index 0000000..8e0e994 --- /dev/null +++ b/src/components/main-page/Pagination.tsx @@ -0,0 +1,67 @@ +import styled from 'styled-components'; +import { useSearchParams } from 'react-router'; +import { Pagination as IPagination } from '@/types/pagination.model'; + +interface Props { + pagination?: IPagination; +} + +const Pagination = ({pagination} : Props) => { + const [searchParams, setSearchParams] = useSearchParams(); + const totalCount = pagination?.totalCount; + const page = pagination?.page; + const pages: number = Math.ceil(totalCount / 20); + + const handleClickPage = (page: number) => { + const newSearchParams = new URLSearchParams(searchParams); + + newSearchParams.set('page', page.toString()); + + setSearchParams(newSearchParams); + } + return ( + + { + pages > 0 && ( +
    + { + Array(pages).fill(0).map((_, index) => ( +
  1. + +
  2. + )) + } +
+ ) + } +
+ ) +} + +const PaginationStyle = styled.div` + display: flex; + justify-content: start; + align-items: center; + padding: 24px; + + ol { + list-style: none; + display: flex; + gap: 8px; + padding: 0; + margin: 0; + } + + button { + border: 1px solid #ccc; + border-radius: 5px; + padding: 5px 6px; + cursor: pointer; + + &.active { + background-color: #deffe2; + } + } +`; + +export default Pagination \ No newline at end of file diff --git a/src/components/main-page/QuestionBox.tsx b/src/components/main-page/QuestionBox.tsx index 664caf0..e64ffae 100644 --- a/src/components/main-page/QuestionBox.tsx +++ b/src/components/main-page/QuestionBox.tsx @@ -1,10 +1,8 @@ -import { useEffect, useState } from 'react'; import styled from 'styled-components'; import QuestionBody from '../ui/molecules/mainpage-molecule/QuestionBody'; import QuestionHeader from '../ui/molecules/mainpage-molecule/QuestionHeader'; import QuestionTag from '../ui/atoms/mainpage-atom/QuesitonTag'; import QuestionBottom from '../ui/molecules/mainpage-molecule/QuestionBottom'; -import { fetchMainData } from '@/apis/maindata.api'; import { mainPosts } from '@/types/main.model'; const QuestionBoxContainer = styled.div` @@ -18,32 +16,38 @@ const QuestionItem = styled.div` margin: 10px; `; -function QuestionBox() { - const [posts, setPosts] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); +interface Props { + posts?: mainPosts[]; + isLoading: boolean; + error?: unknown; +} + +function QuestionBox({posts = [], isLoading, error} : Props) { + // const [posts, setPosts] = useState([]); + // const [loading, setLoading] = useState(true); + // const [error, setError] = useState(null); - useEffect(() => { - const loadPosts = async () => { - try { - const data = await fetchMainData(); - setPosts(data.posts); - } catch (err) { - setError('데이터를 불러오는 중 오류가 발생했습니다.'); - } finally { - setLoading(false); - } - }; + // useEffect(() => { + // const loadPosts = async () => { + // try { + // const data = await fetchMainData(); + // setPosts(data.posts); + // } catch (err) { + // setError('데이터를 불러오는 중 오류가 발생했습니다.'); + // } finally { + // setLoading(false); + // } + // }; - loadPosts(); - }, []); + // loadPosts(); + // }, []); - if (loading) return
데이터를 불러오는 중...
; - if (error) return
{error}
; + if (isLoading) return
데이터를 불러오는 중...
; + if (error as boolean) return
데이터를 불러오는 중 오류가 발생했습니다.
; return ( - {posts.map((post) => ( + {Array.isArray(posts) && posts.map((post) => ( diff --git a/src/components/main-page/SearchInput.tsx b/src/components/main-page/SearchInput.tsx index 8072832..8184914 100644 --- a/src/components/main-page/SearchInput.tsx +++ b/src/components/main-page/SearchInput.tsx @@ -19,11 +19,11 @@ const SearchInputStyle = styled.div` margin: 0 auto; display: flex; justify-content: space-between; - margin-bottom: 20px; - padding: 8px 20px; + margin-bottom: 18px; + padding: 8px 15px; border: 1px solid #727272; border-radius: 30px; - width: 70%; + width: 80%; input { width: 95%; diff --git a/src/components/main-page/TagList.tsx b/src/components/main-page/TagList.tsx new file mode 100644 index 0000000..c26d52f --- /dev/null +++ b/src/components/main-page/TagList.tsx @@ -0,0 +1,54 @@ +import { mainTags } from '@/types/main.model'; +import styled from 'styled-components'; + +interface TagsProps { + tags?: mainTags[] | null; + selectedTags: number[]; + onTagToggle: (tagId: number) => void; +} + +const TagList = ({ tags, selectedTags, onTagToggle }: TagsProps) => { + console.log(selectedTags); + return ( + + {tags?.map((tag) => ( + onTagToggle(tag.id as number)} + selected={selectedTags.includes(tag.id as number)} + > + {tag.name} + + ))} + + ); +}; + +const TagListContainer = styled.div` + display: flex; + position: relative; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 5px; + + } +`; + +interface TagButtonProps { + selected: boolean; +} + +const TagButton = styled.button` + display: block; + border-radius: 30px; + font-size: 1rem; + background-color: ${({ selected }) => (selected ? 'rgba(49, 191, 63, 0.23)' : 'transparent')}; + color: #32c040; + border: 1px solid #32c040; + padding: 8px 15px; + text-decoration: none; + cursor: pointer; + transition: all 0.12s ease-in-out; +`; +export default TagList; \ No newline at end of file diff --git a/src/hooks/useMainTags.ts b/src/hooks/useMainTags.ts deleted file mode 100644 index 795a41b..0000000 --- a/src/hooks/useMainTags.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { fetchMainData } from '@/apis/maindata.api'; -import { mainTags } from '@/types/main.model'; -import { useEffect, useState } from 'react'; -import { useLocation } from 'react-router'; - -export const useMainTags = () => { - const location = useLocation(); - const [maintags, setMaintags] = useState([]); - - const setActive = () => { - const params = new URLSearchParams(location.search); - if (params.get('id')) { - setMaintags((prev) => { - return prev.map((item) => { - return { ...item, isActive: item.id === Number(params.get('id')) }; - }); - }); - } else { - setMaintags((prev) => { - return prev.map((item) => { - return { ...item, isActive: false }; - }); - }); - } - }; - - useEffect(() => { - fetchMainData().then((data) => { - if (!data.tags) return; - const tagsAll = [ - { - id: null, - name: '전체', - }, - ...data.tags, - ]; - - setMaintags(tagsAll); - setActive(); - }); - }, []); - - useEffect(() => { - setActive(); - }, [location.search]); - - return { maintags }; -}; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 86020db..f76e024 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,15 +1,101 @@ -import MainTagList from '@/components/main-page/MainTagList'; -import SearchInput from '@/components/main-page/SearchInput'; +import { useEffect, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; import QuestionBox from '@/components/main-page/QuestionBox'; +import TagList from '@/components/main-page/TagList'; +import { useNavigate } from 'react-router'; +import { fetchMainData } from '@/apis/main.api'; +import SearchInput from '@/components/main-page/SearchInput'; +import Pagination from '@/components/main-page/Pagination'; +import { Pagination as IPagination} from '@/types/pagination.model'; +import { mainTags } from '@/types/main.model'; const HomePage = () => { + const [selectedTags, setSelectedTags] = useState([]); + const [hometags, setHomeTags] = useState([]); + const [homepagination, setHomePagination] = useState(); + const navigate = useNavigate(); + + const { data, isLoading, error } = useQuery({ + queryKey: ['mainData', selectedTags], + queryFn: () => + fetchMainData({ + selectedTags, + currentPage: 1, + limit: 20, + }), + enabled: true, // 데이터 fetch 실행!!! + }); + + const { posts, tags:fetchedTags, pagination:fetchedPagination, filteredPosts } = data ?? {}; + + // 태그, 페이지네이션 상태로 관리 + useEffect(() => { + if (fetchedTags) { + setHomeTags(fetchedTags); + } + if (fetchedPagination) { + setHomePagination(fetchedPagination); + } + }, [fetchedTags, fetchedPagination]); + + // url 달라질때 태그들 상태도 달라지게 요청 + useEffect(() => { + const params = new URLSearchParams(location.search); + const tagsFromURL = params.getAll('tags').map((tag) => Number(tag)); + setSelectedTags(tagsFromURL); + + }, [location.search]); + + // 새로고침 시 '/'로 이동 (전체 데이터 뿌림) + useEffect(() => { + if (location.search) { + navigate('/', { replace: true }); + } + }, []); + + + // 태그 도글 + const toggleTag = (tagId: number) => { + setSelectedTags((prev) => { + const updatedTags = prev.includes(tagId) + ? prev.filter((id) => id !== tagId) // 이미 선택된 태그를 클릭하면 제거 + : [...prev, tagId]; // 새 태그를 클릭하면 추가 + + //선택된 태그에 따라 URL 쿼리 파라미터 업데이트 + if (updatedTags.length > 0) { + navigate({ + search: `?tags=${updatedTags.join('&tags=')}`, + }); + } else { + navigate({ + search: '', + }); + } + + return updatedTags; + }); + }; + + // 필터링된 게시글을 가져오고, 없으면 기본 게시글 사용 + const postsToDisplay = selectedTags.length > 0 ? filteredPosts : posts; + + if (isLoading) return

Loading...

; + if (error) { + return

Error occurred: {error instanceof Error ? error.message : 'An unknown error occurred'}

; + } + return (
- - + { + hometags && ( + + ) + } + +
); }; -export default HomePage; +export default HomePage; \ No newline at end of file diff --git a/src/types/main.model.ts b/src/types/main.model.ts index c3ae25c..6075175 100644 --- a/src/types/main.model.ts +++ b/src/types/main.model.ts @@ -1,10 +1,8 @@ -export interface mainUsers { - users: string | null; -} +import { Pagination } from './pagination.model'; + export interface mainTags { id: number | null; name: string; - isActive?: boolean; } export interface mainPosts { id: number; @@ -18,8 +16,9 @@ export interface mainPosts { view: number; tags: string | null; } + export interface mainData { - users: mainUsers; tags: mainTags[]; posts: mainPosts[]; -} + pagination: Pagination; +} \ No newline at end of file diff --git a/src/types/pagination.model.ts b/src/types/pagination.model.ts new file mode 100644 index 0000000..7c9b76a --- /dev/null +++ b/src/types/pagination.model.ts @@ -0,0 +1,6 @@ +export interface Pagination { + limit: number; + page: number; + totalCount: number; + totalPages: number; +} \ No newline at end of file