diff --git a/frontend/src/api/question-list/getQuestionList.ts b/frontend/src/api/question-list/getQuestionList.ts index 8bdd78ec..95560747 100644 --- a/frontend/src/api/question-list/getQuestionList.ts +++ b/frontend/src/api/question-list/getQuestionList.ts @@ -1,12 +1,17 @@ -import axios from "axios"; +import { api } from "@/api/config/axios.ts"; +import type { QuestionList } from "@/pages/QuestionListPage/types/QuestionList"; interface QuestionListProps { - page: number; - limit: number; + page?: number; + limit?: number; + categoryName?: string; } -export const getQuestionList = async ({ page, limit }: QuestionListProps) => { - const response = await axios.get("/api/question-list", { +export const getQuestionList = async ({ + page, + limit, +}: QuestionListProps): Promise => { + const response = await api.get("/api/question-list", { params: { page, limit, @@ -15,3 +20,23 @@ export const getQuestionList = async ({ page, limit }: QuestionListProps) => { return response.data.data.allQuestionLists; }; + +export const getQuestionListWithCategory = async ({ + categoryName, + page, + limit, +}: QuestionListProps): Promise => { + const response = await api.post( + `/api/question-list/category`, + { + categoryName, + }, + { + params: { + page, + limit, + }, + } + ); + return response.data.data.allQuestionLists; +}; diff --git a/frontend/src/components/common/Button/index.tsx b/frontend/src/components/common/Button/index.tsx index 66b008cf..fb188af2 100644 --- a/frontend/src/components/common/Button/index.tsx +++ b/frontend/src/components/common/Button/index.tsx @@ -15,7 +15,7 @@ const Button = ({ text, type, icon: Icon, onClick }: ButtonProps) => { return (
- +
+ 작성자 {question.username} • {question.contents.length}개의 질문 - +
{question.usage} diff --git a/frontend/src/hooks/api/useGetQuestionList.ts b/frontend/src/hooks/api/useGetQuestionList.ts index 4968950d..62fb4e55 100644 --- a/frontend/src/hooks/api/useGetQuestionList.ts +++ b/frontend/src/hooks/api/useGetQuestionList.ts @@ -1,22 +1,30 @@ -import { getQuestionList } from "@/api/question-list/getQuestionList"; +import { + getQuestionList, + getQuestionListWithCategory, +} from "@/api/question-list/getQuestionList"; import { useQuery } from "@tanstack/react-query"; interface UseGetQuestionListProps { - page: number; - limit: number; + page?: number; + limit?: number; + category: string; } -export const useCreateQuestionList = ({ +export const useQuestionList = ({ page, limit, + category = "전체", }: UseGetQuestionListProps) => { const { data, isLoading, error } = useQuery({ - queryKey: ["questions", page, limit], - queryFn: () => getQuestionList({ page, limit }), + queryKey: ["questions", page, limit, category], + queryFn: () => + category !== "전체" + ? getQuestionListWithCategory({ categoryName: category, page, limit }) + : getQuestionList({ page, limit }), }); return { - questions: data, + data, isLoading, error, }; diff --git a/frontend/src/pages/CreateQuestionPage.tsx b/frontend/src/pages/CreateQuestionPage.tsx index 2da7d5f7..55dc7ec2 100644 --- a/frontend/src/pages/CreateQuestionPage.tsx +++ b/frontend/src/pages/CreateQuestionPage.tsx @@ -1,23 +1,26 @@ import QuestionForm from "@/components/questions/create/QuestionForm"; import { IoArrowBackSharp } from "react-icons/io5"; import { Link } from "react-router-dom"; +import SidebarPageLayout from "@components/layout/SidebarPageLayout.tsx"; const CreateQuestionPage = () => { return ( -
- - - 면접 리스트로 돌아가기 - -

새로운 면접 질문 리스트 만들기

-

- 면접 스터디를 위한 새로운 질문지를 생성합니다. -

- -
+ +
+ + + 면접 리스트로 돌아가기 + +

새로운 면접 질문 리스트 만들기

+

+ 면접 스터디를 위한 새로운 질문지를 생성합니다. +

+ +
+
); }; diff --git a/frontend/src/pages/QuestionDetailPage.tsx b/frontend/src/pages/QuestionDetailPage.tsx deleted file mode 100644 index 9bbab5c2..00000000 --- a/frontend/src/pages/QuestionDetailPage.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useNavigate, useParams } from "react-router-dom"; -import Sidebar from "@/components/common/Sidebar"; -import { sectionWithSidebar } from "@/constants/LayoutConstant.ts"; -import QuestionTitle from "@/components/questions/detail/QuestionTitle.tsx"; -import QuestionList from "@/components/questions/detail/QuestionList.tsx"; -import { useGetQuestionContent } from "@/hooks/api/useGetQuestionContent"; -import ButtonSection from "@/components/questions/detail/ButtonSection.tsx"; -import { useEffect } from "react"; - -const QuestionDetailPage = () => { - const navigate = useNavigate(); - const { questionId } = useParams(); - - const { - data: question, - isLoading, - error, - } = useGetQuestionContent(Number(questionId!)); - - useEffect(() => { - if (!questionId) { - navigate("/questions"); - } - }, [questionId, navigate]); - - if (isLoading) return
로딩 중
; - if (error) return
에러가 발생
; - if (!question) return null; - - return ( -
- -
-
- - - -
-
-
- ); -}; - -export default QuestionDetailPage; diff --git a/frontend/src/pages/QuestionDetailPage/QuestionDetailPage.tsx b/frontend/src/pages/QuestionDetailPage/QuestionDetailPage.tsx new file mode 100644 index 00000000..d7fc510e --- /dev/null +++ b/frontend/src/pages/QuestionDetailPage/QuestionDetailPage.tsx @@ -0,0 +1,79 @@ +import { useNavigate, useParams } from "react-router-dom"; +import QuestionTitle from "@components/questions/detail/QuestionTitle.tsx"; +import QuestionList from "@components/questions/detail/QuestionList.tsx"; +import { useGetQuestionContent } from "@hooks/api/useGetQuestionContent.ts"; +import ButtonSection from "@components/questions/detail/ButtonSection.tsx"; +import { useEffect, useState } from "react"; +import SidebarPageLayout from "@components/layout/SidebarPageLayout.tsx"; +import { + deleteScrapQuestionList, + postScrapQuestionList, +} from "@/pages/QuestionDetailPage/api/scrapAPI.ts"; +import useToast from "@hooks/useToast.ts"; +import ErrorBlock from "@components/common/Error/ErrorBlock.tsx"; +import LoadingIndicator from "@components/common/LoadingIndicator.tsx"; + +const QuestionDetailPage = () => { + const navigate = useNavigate(); + const { questionId } = useParams(); + const [isScrapped, setIsScrapped] = useState(false); + // TODO: isScrapped 상태를 서버에서 가져오는 로직이 추가되면 해당 로직을 추가 + const toast = useToast(); + + const { + data: question, + isLoading, + error, + } = useGetQuestionContent(Number(questionId!)); + + useEffect(() => { + if (!questionId) { + navigate("/questions"); + } + }, [questionId, navigate]); + + const shareQuestionList = () => { + if (question) { + navigator.clipboard.writeText(window.location.href); + toast.success(`${question.title} 질문지가 클립보드에 복사되었습니다.`); + } + }; + + return ( + +
+
+ + + + + {question && ( + { + if (await postScrapQuestionList(questionId!)) { + setIsScrapped(true); + } + }} + unScrapQuestionList={async () => { + if (await deleteScrapQuestionList(questionId!)) { + setIsScrapped(false); + } + }} + shareQuestionList={shareQuestionList} + /> + )} +
+
+
+ ); +}; + +export default QuestionDetailPage; diff --git a/frontend/src/pages/QuestionDetailPage/api/scrapAPI.ts b/frontend/src/pages/QuestionDetailPage/api/scrapAPI.ts new file mode 100644 index 00000000..ac0edeb1 --- /dev/null +++ b/frontend/src/pages/QuestionDetailPage/api/scrapAPI.ts @@ -0,0 +1,13 @@ +import { api } from "@/api/config/axios.ts"; + +export const postScrapQuestionList = async (id: string) => { + const response = await api.post("/api/question-list/scrap", { + questionListId: id, + }); + return response.data.success; +}; + +export const deleteScrapQuestionList = async (id: string) => { + const response = await api.delete(`/api/question-list/scrap/${id}`); + return response.data.success; +}; diff --git a/frontend/src/pages/QuestionListPage.tsx b/frontend/src/pages/QuestionListPage.tsx deleted file mode 100644 index 4d940c33..00000000 --- a/frontend/src/pages/QuestionListPage.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import Sidebar from "@components/common/Sidebar.tsx"; -import SearchBar from "@components/common/SearchBar.tsx"; -import QuestionsPreviewCard from "@components/questions/QuestionsPreviewCard.tsx"; -import Select from "@components/common/Select.tsx"; -import { useEffect, useState } from "react"; -import LoadingIndicator from "@components/common/LoadingIndicator.tsx"; -import { IoMdAdd } from "react-icons/io"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import axios from "axios"; -import CreateButton from "@components/common/CreateButton.tsx"; -import { options } from "@/constants/CategoryData.ts"; - -interface QuestionList { - id: number; - title: string; - usage: number; - isStarred?: boolean; - questionCount: number; - categoryNames: string[]; -} - -const QuestionList = () => { - const [questionList, setQuestionList] = useState([]); - const [questionLoading, setQuestionLoading] = useState(true); - const navigate = useNavigate(); - const [selectedCategory, setSelectedCategory] = useState("전체"); - const [searchParams, setSearchParams] = useSearchParams(); - - useEffect(() => { - getQuestionList(selectedCategory); - if (selectedCategory !== "전체") { - console.log("selectedCategory", selectedCategory); - setSearchParams({ category: selectedCategory }); - } - }, [selectedCategory]); - - useEffect(() => { - if (searchParams.get("category")) { - setSelectedCategory(searchParams.get("category") ?? "전체"); - } - }, [searchParams]); - - const getQuestionList = async (category?: string) => { - try { - const response = - category !== "전체" - ? await axios.post(`/api/question-list/category`, { - categoryName: category, - }) - : await axios.get("/api/question-list"); - const data = response.data.data.allQuestionLists ?? []; - setQuestionList(data); - setQuestionLoading(false); - } catch (error) { - console.error("질문지 리스트 불러오기 실패", error); - setQuestionList([]); - } - }; - - const handleNavigateDetail = (id: number) => { - navigate(`/questions/${id}`); - }; - - return ( -
- -
-
-

- 질문지 목록 -

-
- - + +
+
+ + + +
+
+ ); +}; + +export default QuestionListPage; diff --git a/frontend/src/pages/QuestionListPage/hooks/useCategory.ts b/frontend/src/pages/QuestionListPage/hooks/useCategory.ts new file mode 100644 index 00000000..b9a4d9b5 --- /dev/null +++ b/frontend/src/pages/QuestionListPage/hooks/useCategory.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; + +const useCategory = () => { + const [selectedCategory, setSelectedCategory] = useState("전체"); + const [searchParams, setSearchParams] = useSearchParams(); + + useEffect(() => { + if (selectedCategory !== "전체") { + setSearchParams({ category: selectedCategory }); + } else { + setSearchParams({}); + } + }, [selectedCategory]); + + useEffect(() => { + if (searchParams.get("category")) { + setSelectedCategory(searchParams.get("category") ?? "전체"); + } + }, [searchParams]); + + return { + selectedCategory, + setSelectedCategory, + }; +}; + +export default useCategory; diff --git a/frontend/src/pages/QuestionListPage/types/QuestionList.d.ts b/frontend/src/pages/QuestionListPage/types/QuestionList.d.ts new file mode 100644 index 00000000..19578230 --- /dev/null +++ b/frontend/src/pages/QuestionListPage/types/QuestionList.d.ts @@ -0,0 +1,8 @@ +export interface QuestionList { + id: number; + title: string; + usage: number; + isStarred?: boolean; + questionCount: number; + categoryNames: string[]; +} diff --git a/frontend/src/pages/QuestionListPage/view/QuestionsPreviewList.tsx b/frontend/src/pages/QuestionListPage/view/QuestionsPreviewList.tsx new file mode 100644 index 00000000..67a0025b --- /dev/null +++ b/frontend/src/pages/QuestionListPage/view/QuestionsPreviewList.tsx @@ -0,0 +1,37 @@ +import QuestionsPreviewCard from "@components/questions/QuestionsPreviewCard.tsx"; +import type { QuestionList } from "@/pages/QuestionListPage/types/QuestionList.ts"; + +interface QuestionListProps { + questionList?: QuestionList[]; + questionLoading?: boolean; +} + +const QuestionsPreviewList = ({ + questionList, + questionLoading, +}: QuestionListProps) => { + return ( +
+ {questionList && + questionList.map((list) => ( + + ))} + + {!questionLoading && questionList?.length === 0 && ( +
+ 이런! 아직 질문지가 없습니다! 처음으로 생성해보시는 것은 어떤가요? ☃️ +
+ )} +
+ ); +}; + +export default QuestionsPreviewList; diff --git a/frontend/src/pages/SessionListPage/view/list/SessionList.tsx b/frontend/src/pages/SessionListPage/view/list/SessionList.tsx index 551079dd..b0f2f0f4 100644 --- a/frontend/src/pages/SessionListPage/view/list/SessionList.tsx +++ b/frontend/src/pages/SessionListPage/view/list/SessionList.tsx @@ -16,7 +16,6 @@ const SessionList = ({ sessionList, }: SessionListProps) => { const toast = useToast(); - console.log(sessionList); const renderSessionList = () => { return sessionList.map((session) => { return ( diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 35969791..235f4639 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -1,12 +1,12 @@ import App from "./App.tsx"; import CreateQuestionPage from "./pages/CreateQuestionPage.tsx"; import CreateSessionPage from "./pages/CreateSessionPage.tsx"; -import QuestionDetailPage from "./pages/QuestionDetailPage.tsx"; +import QuestionDetailPage from "./pages/QuestionDetailPage/QuestionDetailPage.tsx"; import SessionListPage from "./pages/SessionListPage/SessionListPage.tsx"; import SessionPage from "./pages/SessionPage"; import ErrorPage from "@/pages/ErrorPage.tsx"; import LoginPage from "@/pages/Login/LoginPage.tsx"; -import QuestionListPage from "@/pages/QuestionListPage.tsx"; +import QuestionListPage from "@/pages/QuestionListPage/QuestionListPage.tsx"; import AuthCallbackPage from "@/pages/Login/AuthCallbackPage.tsx"; import MyPage from "@/pages/MyPage/index.tsx"; import ProtectedRouteLayout from "@components/layout/ProtectedRouteLayout.tsx";