diff --git a/src/Friends/Friends.tsx b/src/Friends/Friends.tsx new file mode 100644 index 0000000..d71964d --- /dev/null +++ b/src/Friends/Friends.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Box, Heading, ChakraProvider } from "@chakra-ui/react"; + +const Friends: React.FC = () => { + return ( + + + + + 친구 관리 페이지입니다. + + + + + ); +}; + +export default Friends; diff --git a/src/Main/components/NavBar.tsx b/src/Main/components/NavBar.tsx index a199c1c..77b0bfa 100644 --- a/src/Main/components/NavBar.tsx +++ b/src/Main/components/NavBar.tsx @@ -9,6 +9,7 @@ import { IconButton, } from "@chakra-ui/react"; import { HamburgerIcon } from "@chakra-ui/icons"; +import { useNavigate } from "react-router-dom"; // React Router의 useNavigate 가져오기 import api from "../../api/interceptor"; interface NavBarProps { @@ -23,6 +24,7 @@ const NavBar = ({ handleLogout, }: NavBarProps): JSX.Element => { const [userName, setUserName] = useState(""); + const navigate = useNavigate(); // useNavigate 훅 초기화 useEffect(() => { const fetchUserName = async () => { @@ -91,9 +93,13 @@ const NavBar = ({ icon={} variant="outline" /> - + {/* 사용자 이름 표시 */} {userName ? `${userName} 님` : "사용자 님"} + navigate("/mypage")}>나의 콘텐츠 + navigate("/friends")}>친구 관리 로그아웃 diff --git a/src/Main/components/RecommendedContents.tsx b/src/Main/components/RecommendedContents.tsx index 1feaf43..6810ecf 100644 --- a/src/Main/components/RecommendedContents.tsx +++ b/src/Main/components/RecommendedContents.tsx @@ -55,15 +55,12 @@ function RecommendedContents(): JSX.Element { const fetchAllContents = async () => { setIsLoading(true); try { - // 많이 시청한 콘텐츠 TOP 10 가져오기 const watchResponse = await api.get("/api/watch/top"); setWatchTop(watchResponse.data || []); - // 좋아요한 콘텐츠 TOP 10 가져오기 const likeResponse = await api.get("/api/like/top"); setLikeTop(likeResponse.data || []); - // 추천 콘텐츠 가져오기 const recommendResponse = await api.get("/api/recommend/10"); const fetchedCategories = Object.keys(categoryLabels).map( (category) => ({ @@ -98,11 +95,11 @@ function RecommendedContents(): JSX.Element { const renderContentGrid = ( items: { content: Content; count?: number }[], label: string, - countLabel?: string + countLabel?: "likeCount" | "watchCount" ) => ( - + 포스터가 없습니다. @@ -190,9 +191,11 @@ function RecommendedContents(): JSX.Element { > {content.title} - {countLabel && ( + {countLabel && count !== undefined && ( - {`${countLabel}: ${count}`} + {countLabel === "likeCount" + ? `${count}명이 좋아해요💕` + : `${count}명이 시청했어요✨`} )} @@ -200,7 +203,11 @@ function RecommendedContents(): JSX.Element { ) : ( - + 충분한 데이터가 쌓이지 않았습니다. )} @@ -237,7 +244,7 @@ function RecommendedContents(): JSX.Element { count: item.watchCount, })), "🔥 서비스 이용자들이 많이 시청한 콘텐츠 TOP 10", - "시청 횟수" + "watchCount" )} {/* 좋아요한 콘텐츠 TOP 10 */} @@ -247,7 +254,7 @@ function RecommendedContents(): JSX.Element { count: item.likeCount, })), "❤️ 서비스 이용자들이 좋아한 콘텐츠 TOP 10", - "좋아요 수" + "likeCount" )} {/* 기존 추천 콘텐츠 */} diff --git a/src/MyPage/MyPage.tsx b/src/MyPage/MyPage.tsx new file mode 100644 index 0000000..44b1369 --- /dev/null +++ b/src/MyPage/MyPage.tsx @@ -0,0 +1,397 @@ +import { useState, useEffect } from "react"; +import { + Box, + Grid, + Text, + Button, + IconButton, + Image, + Spinner, + Flex, + useToast, + Divider, +} from "@chakra-ui/react"; +import { ArrowBackIcon, CloseIcon } from "@chakra-ui/icons"; +import { useNavigate } from "react-router-dom"; +import api from "../api/interceptor"; +import DetailModal from "../Main/components/DetailModal"; + +interface Content { + id: number; + showId: string; + type: string; + title: string; + director: string; + cast: string; + country: string; + dateAdded: string; + releaseYear: string; + rating: string; + duration: string; + listedIn: string; + description: string; + posterPath?: string; +} + +interface WatchRecord { + watchedDateTime: string; + content: Content; +} + +function MyContentPage(): JSX.Element { + const [contents, setContents] = useState([]); + const [watchRecords, setWatchRecords] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [selectedContent, setSelectedContent] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [currentTab, setCurrentTab] = useState<"like" | "dislike" | "watch">( + "like" + ); + + const navigate = useNavigate(); + const toast = useToast(); + + const fetchContents = async (type: "like" | "dislike" | "watch") => { + setIsLoading(true); + try { + let response; + if (type === "like") { + response = await api.get("/api/like/true"); + setContents( + response.data.map((item: { content: Content }) => item.content) + ); + } else if (type === "dislike") { + response = await api.get("/api/like/false"); + setContents( + response.data.map((item: { content: Content }) => item.content) + ); + } else { + response = await api.get("/api/watch"); + setWatchRecords(response.data || []); + } + } catch (error) { + toast({ + title: "데이터를 불러오는 중 오류 발생", + description: "데이터를 가져오는 데 실패했습니다.", + status: "error", + duration: 3000, + isClosable: true, + position: "top", // Toast 위치 설정 + }); + } finally { + setIsLoading(false); + } + }; + + const deleteContent = async (id: number) => { + try { + if (currentTab === "like" || currentTab === "dislike") { + await api.delete(`/api/like/${id}`); + } else { + await api.delete(`/api/watch/${id}`); + } + toast({ + title: "삭제 성공", + description: "콘텐츠가 성공적으로 삭제되었습니다.", + status: "success", + duration: 3000, + isClosable: true, + position: "top", // Toast 위치 설정 + }); + fetchContents(currentTab); + } catch (error) { + toast({ + title: "삭제 실패", + description: "콘텐츠 삭제 중 오류가 발생했습니다.", + status: "error", + duration: 3000, + isClosable: true, + position: "top", // Toast 위치 설정 + }); + } + }; + + const handleCardClick = (content: Content) => { + setSelectedContent(content); + setIsModalOpen(true); + }; + + const closeModal = () => { + setSelectedContent(null); + setIsModalOpen(false); + }; + + const groupByDate = (records: WatchRecord[]) => { + const grouped: { [key: string]: Content[] } = {}; + records.forEach((record) => { + const date = new Date(record.watchedDateTime).toLocaleDateString(); + if (!grouped[date]) { + grouped[date] = []; + } + grouped[date].push(record.content); + }); + return grouped; + }; + + const renderWatchRecords = () => { + const groupedRecords = groupByDate(watchRecords); + return Object.entries(groupedRecords).map(([date, contents]) => ( + + + {date} + + + {contents.map((content) => ( + handleCardClick(content)} + > + {content.posterPath ? ( + {`${content.title} + ) : ( + + + 포스터 없음 + + + )} + + {content.title} + + } + position="absolute" + top="0.5rem" + right="0.5rem" + size="sm" + color="red.500" + bg="none" // 배경 제거 + _hover={{ bg: "none", color: "red.700" }} // Hover 스타일 + onClick={(e) => { + e.stopPropagation(); + deleteContent(content.id); + }} + /> + + ))} + + + + )); + }; + + useEffect(() => { + fetchContents("like"); + }, []); + + return ( + + + } + position="absolute" // 절대 위치 지정 + top="0" // 화면의 위쪽 경계에 맞춤 + left="0" // 화면의 왼쪽 경계에 맞춤 + mr={"1rem"} // 우측 마진 설정 + onClick={() => navigate("/main")} + /> + + + + + + + + + {isLoading ? ( + + + + ) : currentTab === "watch" ? ( + renderWatchRecords() + ) : ( + + {contents.map((content) => ( + handleCardClick(content)} + > + {content.posterPath ? ( + {`${content.title} + ) : ( + + + 포스터 없음 + + + )} + + {content.title} + + } + position="absolute" + top="0.1rem" + right="0.1rem" + size="sm" + color="red.500" + bg="none" // 배경 제거 + _hover={{ bg: "none", color: "red.700" }} // Hover 스타일 + onClick={(e) => { + e.stopPropagation(); + deleteContent(content.id); + }} + /> + + ))} + + )} + + + {selectedContent && ( + + )} + + ); +} + +export default MyContentPage; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index d107a5e..9be8715 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -2,6 +2,8 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; // 라 import MainPage from "../../src/Main/MainPage"; // 메인 페이지 컴포넌트 가져오기 import StartPage from "../Start/StartPage"; import RedirectPage from "../Start/Redirection"; +import MyPage from "../MyPage/MyPage"; +import Friends from "../Friends/Friends"; import { RouterPath } from "./path"; // 경로 상수 가져오기 // 라우터 정의 @@ -18,6 +20,14 @@ const router = createBrowserRouter([ path: RouterPath.main, // 메인 페이지 경로 element: , // 메인 페이지를 직접 렌더링 }, + { + path: RouterPath.mypage, // 마이페이지 경로 + element: , // 마이페이지를 직접 렌더링 + }, + { + path: RouterPath.friends, // 친구 페이지 경로 + element: , // 친구 페이지를 직접 렌더링 + }, ]); // 라우터를 렌더링하는 컴포넌트 diff --git a/src/routes/path.ts b/src/routes/path.ts index d629d24..c32e2a3 100644 --- a/src/routes/path.ts +++ b/src/routes/path.ts @@ -2,4 +2,6 @@ export const RouterPath = { root: "/", main: "/main", // 메인 페이지 rediretcion: "/redirection", // 카카오 로그인 리다이렉션 페이지 + mypage: "/mypage", // 마이페이지 + friends: "/friends", // 친구 페이지 };