From dca2178910d2be8074d894b6d04b89e80a321874 Mon Sep 17 00:00:00 2001 From: woojeong Date: Tue, 23 Jan 2024 00:03:22 +0900 Subject: [PATCH] Feat/content page data fetch (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bug: Content.header section margin -> 0 * cors 테스트 * Style: 콤마찍는거 제거 * feat: content api 구현 * feat: contentPage에서 fetch후 컴포넌트에 props로 주입 * feat: Contet.Header 데이터 * feat: Content.Cast 데이터 props로 주입 * feat: Content.panel 데이터 props로 주입 및 MyCommentBox 컴포넌트 분리 * feat: MyCommentBox 컴포넌트 구현 * feat: 모달 컴포넌트 구현 * feat: 코멘트, 대댓글 작성 모달 구현 * feat: Content.Comment 데이터 props로 주입 * feat: 코멘트 api구현 * feat: 평점 매기기 api연결 * feat: 코멘트 리스트 페이지 데이터 fetch 구현 * feat : 별점 api 서비스 구현 * feat : 코멘트 작성/수정/삭제 기능 추가 * feat : user subpage의 유저가 평가한 영화 리스트 데이터 패칭 추가 --------- Co-authored-by: suhyeonOh --- .prettierrc | 1 - src/apis/auth.ts | 17 +- src/apis/comment.ts | 83 +++++++ src/apis/content.ts | 76 ++++++ src/apis/user.ts | 36 +-- src/components/CommentCard.tsx | 61 +++-- src/components/CommentInfo.tsx | 43 ++-- src/components/Content.module.scss | 106 +------- src/components/Content.tsx | 229 +++++++++--------- src/components/ContentList.tsx | 2 +- src/components/Modal.module.scss | 14 ++ src/components/Modal.tsx | 21 ++ src/components/MyCommentBox.module.scss | 105 ++++++++ src/components/MyCommentBox.tsx | 84 +++++++ src/components/StarRating.tsx | 72 ++++-- src/components/WritingModal.module.scss | 93 +++++++ src/components/WritingModal.tsx | 132 ++++++++++ src/components/user/DefaultMovieList.tsx | 40 +-- src/pages/CommentListPage.tsx | 105 ++++---- src/pages/CommentPage.tsx | 67 ++--- src/pages/ContentPage.tsx | 35 ++- src/pages/user/UserLikesCommentListPage.tsx | 33 +-- src/pages/user/UserRatingsPage.tsx | 23 +- src/pages/user/UserWrittenCommentListPage.tsx | 64 +++-- src/tmp.ts | 191 +++++++++++++++ src/type.ts | 117 ++++++++- src/utils/snackToCamel.tsx | 20 ++ 27 files changed, 1366 insertions(+), 504 deletions(-) create mode 100644 src/apis/comment.ts create mode 100644 src/apis/content.ts create mode 100644 src/components/Modal.module.scss create mode 100644 src/components/Modal.tsx create mode 100644 src/components/MyCommentBox.module.scss create mode 100644 src/components/MyCommentBox.tsx create mode 100644 src/components/WritingModal.module.scss create mode 100644 src/components/WritingModal.tsx create mode 100644 src/tmp.ts create mode 100644 src/utils/snackToCamel.tsx diff --git a/.prettierrc b/.prettierrc index 97fcc09..9f47b0b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,6 +3,5 @@ "semi": true, "useTabs": false, "tabWidth": 2, - "trailingComma": "all", "printWidth": 80 } diff --git a/src/apis/auth.ts b/src/apis/auth.ts index 85ca60c..64f2b5f 100644 --- a/src/apis/auth.ts +++ b/src/apis/auth.ts @@ -1,4 +1,3 @@ - // import { BASE_API_URL } from "./const"; import { BASE_API_AUTH_URL } from "./const"; @@ -6,9 +5,9 @@ export async function postSignup( nickname: string, username: string, password1: string, - password2: string, + password2: string ) { - return fetch(`${BASE_API_AUTH_URL}/auth/register/`, { + return fetch(`${BASE_API_AUTH_URL}/auth/token/register/`, { method: "POST", headers: { "Content-Type": "application/json", @@ -18,9 +17,7 @@ export async function postSignup( } export async function postLogin(username: string, password: string) { - return fetch(`${BASE_API_AUTH_URL}/auth/token/`, { - method: "POST", headers: { "Content-Type": "application/json", @@ -34,9 +31,7 @@ export async function postLogin(username: string, password: string) { } export async function getMyUserData(accessToken: string) { - return fetch(`${BASE_API_AUTH_URL}/users/mypage/`, { - method: "GET", headers: { Authorization: `Bearer ${accessToken}`, @@ -46,24 +41,19 @@ export async function getMyUserData(accessToken: string) { // 리프레시 토큰 및 엑세스 토큰을 갱신. 자동로그인을 위해 사용 export async function postNewToken() { - return fetch(`${BASE_API_AUTH_URL}/auth/token/refresh/new/`, { - method: "POST", credentials: "include", }); } export async function postLogout() { - return fetch(`${BASE_API_AUTH_URL}/auth/token/logout/`, { - method: "POST", credentials: "include", }); } - export async function getKakaoAutoCallback(code: string | null) { return fetch(`${BASE_API_AUTH_URL}/auth/kakao/login?code=${code}`); } @@ -71,14 +61,12 @@ export async function getKakaoAutoCallback(code: string | null) { // 카카오 회원탈퇴 기능이 구현되어 있는 엔드포인트이나, 기본 탈퇴 기능도 구현되어 있어서 이 api만 사용 export async function deleteWithDrawalUser(accessToken: string) { return fetch(`${BASE_API_AUTH_URL}/users/mypage/delete/kakao_unlink/`, { - method: "DELETE", credentials: "include", headers: { Authorization: `Bearer ${accessToken}`, }, }); - } /* @@ -91,4 +79,3 @@ export async function deleteWithDrawalUser(accessToken: string) { }, }); }*/ - diff --git a/src/apis/comment.ts b/src/apis/comment.ts new file mode 100644 index 0000000..29fba9c --- /dev/null +++ b/src/apis/comment.ts @@ -0,0 +1,83 @@ +import { BASE_API_URL } from "./const"; + +// 아직 영화리스트가 없어서 받아보기 어려움 +// type은 추후에 enum으로 수정 +export async function getCommentListRequest(movieCD: string) { + return fetch(`${BASE_API_URL}/contents/${movieCD}/comments`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); +} + +export async function createCommentRequest( + movieCD: string, + accessToken: string, + content: string, + has_spoiler: boolean +) { + return fetch(`${BASE_API_URL}/contents/${movieCD}/comments/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + accessToken, + }, + body: JSON.stringify({ + content, + has_spoiler: has_spoiler, + }), + }); +} + +//특정 코멘트 아이디의 코멘트를 알 수 있다. +export async function getCommentRequest(commentId: number) { + return fetch(`${BASE_API_URL}/comments/${commentId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); +} + +export async function updateCommentRequest( + commentId: number, + accessToken: string, + content: string, + hasSpoiler: boolean +) { + return fetch(`${BASE_API_URL}/comments/${commentId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + accessToken, + }, + credentials: "include", + body: JSON.stringify({ + content, + has_spoiler: hasSpoiler, + }), + }); +} + +export async function deleteCommentRequest(id: number, accessToken: string) { + return fetch(`${BASE_API_URL}/comments/${id}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + accessToken, + }, + credentials: "include", + }); +} + +export async function getCommentReplies(commentId: number) { + return fetch(`${BASE_API_URL}/comments/${commentId}/replies`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); +} diff --git a/src/apis/content.ts b/src/apis/content.ts new file mode 100644 index 0000000..92b1619 --- /dev/null +++ b/src/apis/content.ts @@ -0,0 +1,76 @@ +import { BASE_API_URL } from "./const"; + +// 아직 영화리스트가 없어서 받아보기 어려움 +// type은 추후에 enum으로 수정 +export async function getContentListRequest(order: string) { + return fetch(`${BASE_API_URL}/contents?order=${order}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }); +} + +export async function getContentRequest(movieCD: string, accessToken?: string) { + const headers: HeadersInit = accessToken + ? { + "Content-Type": "application/json", + Authorization: "Bearer " + accessToken, + } + : { + "Content-Type": "application/json", + }; + return fetch(`${BASE_API_URL}/contents/${movieCD}`, { + method: "GET", + headers, + // credentials: "include", + }); +} + +export async function createRatingRequest( + movieCD: string, + rate: number, + accessToken: string +) { + return fetch(`${BASE_API_URL}/contents/${movieCD}/rate`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + accessToken, + }, + credentials: "include", + body: JSON.stringify({ + rate, + }), + }); +} + +export async function updateRatingRequest( + rateId: number, + rate: number, + accessToken: string +) { + return fetch(`${BASE_API_URL}/contents/rates/${rateId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + accessToken, + }, + credentials: "include", + body: JSON.stringify({ + rate, + }), + }); +} + +export async function deleteRatingRequest(rateId: number, accessToken: string) { + return fetch(`${BASE_API_URL}/contents/rates/${rateId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + accessToken, + }, + credentials: "include", + }); +} diff --git a/src/apis/user.ts b/src/apis/user.ts index c2ea6ba..2ea12b1 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -1,25 +1,25 @@ // 모두가 접근 가능한 유저페이지에 대한 api import { BASE_API_URL } from "./const"; -export async function getUserDetail(id: number) { - return fetch(`${BASE_API_URL}/users/${id}/`); +export async function getUserDetail(userId: number) { + return fetch(`${BASE_API_URL}/users/${userId}/`); } -export async function getFollowingList(id: number) { - return fetch(`${BASE_API_URL}/users/${id}/followings/`); +export async function getFollowingList(userId: number) { + return fetch(`${BASE_API_URL}/users/${userId}/followings/`); } -export async function getFollowerList(id: number) { - return fetch(`${BASE_API_URL}/users/${id}/followers/`); +export async function getFollowerList(userId: number) { + return fetch(`${BASE_API_URL}/users/${userId}/followers/`); } -export async function postAddFollow(accessToken: string, id: number) { +export async function postAddFollow(accessToken: string, userId: number) { return fetch(`${BASE_API_URL}/users/add/follow/`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, - body: JSON.stringify({ user_id: id }), + body: JSON.stringify({ user_id: userId }), }); } @@ -41,24 +41,24 @@ export async function getUserLikesComments(id: number) { } export async function getUserWrittenComments( - id: number, - query: "like" | "created" | "high-rating" | "low-rating" | undefined, + userId: number, + query?: "like" | "created" | "high-rating" | "low-rating" ) { if (query === undefined) { - return fetch(`${BASE_API_URL}/users/${id}/comments/`); + return fetch(`${BASE_API_URL}/users/${userId}/comments/`); } - return fetch(`${BASE_API_URL}/users/${id}/comments/?order=${query}`); + return fetch(`${BASE_API_URL}/users/${userId}/comments/?order=${query}`); } -export async function getUserRatings(id: number) { - return fetch(`${BASE_API_URL}/users/${id}/ratings/`); +export async function getUserRatings(userId: number) { + return fetch(`${BASE_API_URL}/users/${userId}/ratings/`); } -export async function getUserDoings(id: number) { - return fetch(`${BASE_API_URL}/users/${id}/movies/watching/`); +export async function getUserDoings(userId: number) { + return fetch(`${BASE_API_URL}/users/${userId}/movies/watching/`); } -export async function getUserWishes(id: number) { - return fetch(`${BASE_API_URL}/users/${id}/movies/want_to_watch/`); +export async function getUserWishes(userId: number) { + return fetch(`${BASE_API_URL}/users/${userId}/movies/want_to_watch/`); } /* diff --git a/src/components/CommentCard.tsx b/src/components/CommentCard.tsx index e039e17..06f4f6e 100644 --- a/src/components/CommentCard.tsx +++ b/src/components/CommentCard.tsx @@ -1,51 +1,50 @@ -import styles from "./CommentCard.module.scss"; +import { CommentType, CommentInUserPageType } from "../type"; -type CommentCardProp = { - comment: { - user: { - img: string; - name: string; - }; - reviewRating: number; - text: string; - likeCount: number; - subcommentCount: number; - }; -}; +import styles from "./CommentCard.module.scss"; +import profileDefault from "../assets/user_default.jpg"; +import { Link } from "react-router-dom"; -export default function CommentCard({ comment }: CommentCardProp) { +export default function CommentCard({ + comment, +}: { + comment: CommentType | CommentInUserPageType; +}) { return (
  • - - - {comment.user.name} - -
    - - - - {comment.reviewRating} -
    + + + {comment.created_by.nickname} + + {comment.rating && ( +
    + + + + {comment.rating} +
    + )}
    - {comment.text} + + {comment.content} +
    - {comment.likeCount} + {/* {comment.like_count}*/} - {comment.subcommentCount} + {"replyCount"}
    diff --git a/src/components/CommentInfo.tsx b/src/components/CommentInfo.tsx index 511c564..324de6d 100644 --- a/src/components/CommentInfo.tsx +++ b/src/components/CommentInfo.tsx @@ -4,6 +4,7 @@ import styles from "./CommentInfo.module.scss"; import { MovieType } from "./ContentList"; import ReplyList from "./ReplyList"; import elapsedTime from "../utils/elapsedTime"; +import { CommentType } from "../type"; export type ReplyType = { userName: string; @@ -13,18 +14,6 @@ export type ReplyType = { liked: boolean; }; -type CommentType = { - userName: string; - movie: MovieType; - rating: number; - content: string; - date: Date; - likes: number; - liked: boolean; - replyNumber: number; - replies: ReplyType[]; -}; - function CommentHeader({ userName, movie, @@ -158,19 +147,35 @@ function LikeReplyBar({ liked }: { liked: boolean }) { ); } +/* +{ + id: number; + created_by: { + id: number; + nickname: string; + profile_photo: string | null; + }; + rating: null | string; + like_count: number; + content: string; + has_spoiler: boolean; + created_at: string; + updated_at: string; +*/ export default function CommentInfo({ comment }: { comment: CommentType }) { return (
    - - - - - + {" "} + + + + */}
    ); } diff --git a/src/components/Content.module.scss b/src/components/Content.module.scss index dd4505c..aabffd3 100644 --- a/src/components/Content.module.scss +++ b/src/components/Content.module.scss @@ -57,9 +57,7 @@ width: 280px; height: 400px; margin-bottom: 30px; - transition: - width 0.3s, - height 0.3s; + transition: width 0.3s, height 0.3s; @media (max-width: #{$medium-screen + 100}) { width: 200px; @@ -184,109 +182,7 @@ } } } - .userCommentCon { - width: 100%; - border-top: 1px solid $light-gray; - padding: 20px 0; - - h3 { - margin-bottom: 10px; - line-height: 12px; - font-size: 12px; - font-weight: 500; - } - .userCommentBox { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - border: solid 1px $light-gray; - border-radius: 6px; - padding: 12px 20px; - background-color: $white; - - @media (max-width: $small-screen) { - flex-direction: column; - } - - .userCommentText { - width: 100%; - margin-right: 20px; - line-height: 20px; - font-size: 15px; - font-weight: 400; - color: $light-black; - @media (max-width: $small-screen) { - text-align: center; - } - } - .commentCreateBtn { - width: 256px; - height: 40px; - border: 1px solid $light-gray; - border-radius: 6px; - background-color: inherit; - font-size: 17px; - font-weight: 500; - text-align: center; - color: $dark-black; - cursor: pointer; - - @media (max-width: $small-screen) { - margin-top: 10px; - } - } - .userImage { - width: 56px; - height: 56px; - margin-right: 20px; - border: 1px solid $extra-light-gray; - border-radius: 50%; - - @media (max-width: $small-screen) { - margin-bottom: 10px; - } - } - .commentBtnBox { - display: flex; - align-items: center; - - button { - display: flex; - align-items: center; - border-style: none; - padding: 16px 0 13px 0; - line-height: 16px; - font-size: 12px; - font-weight: 400; - color: $medium-gray; - background-color: inherit; - white-space: nowrap; - cursor: pointer; - - img { - width: 18px; - height: 18px; - margin-right: 4px; - } - } - .virtualLineBox { - display: flex; - justify-content: center; - align-items: center; - - width: 40px; - height: 100%; - .virtualLine { - width: 1px; - height: 8px; - background-color: $medium-gray; - } - } - } - } - } .overviewBox { min-height: 150px; padding: 20px 0; diff --git a/src/components/Content.tsx b/src/components/Content.tsx index bf31038..9f8af3a 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,79 +1,89 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import styles from "./Content.module.scss"; import { Carousel } from "./Carousel"; import profileDefault from "../assets/user_default.jpg"; import StarRating from "./StarRating"; import CommentCard from "./CommentCard"; +import { CommentsResType, CommentType, ContentType } from "../type"; +// import { convertKeysToCamelCase } from "../utils/snackToCamel"; +import { Link } from "react-router-dom"; +import { defaultResponseHandler } from "../apis/custom"; +import { + createCommentRequest, + getCommentListRequest, + getCommentRequest, + updateCommentRequest, +} from "../apis/comment"; +import MyCommentBox from "./MyCommentBox"; +import WritingModal from "./WritingModal"; +import { useAuthContext } from "../contexts/authContext"; -function ContentHeader() { - // 임시 - const content = { - backGroundImg: - "https://an2-img.amz.wtchn.net/image/v2/Oc4xhq9o4_2YVAmvyR8MFw.jpg?jwt=ZXlKaGJHY2lPaUpJVXpJMU5pSjkuZXlKdmNIUnpJanBiSW1SZk1Ua3lNSGd4TURnd2NUZ3dJbDBzSW5BaU9pSXZkakl2YzNSdmNtVXZhVzFoWjJVdk1UWTNNVFF4T0RJM01UUTBNelE1Tmpnek5pSjkuRlNwNURGdzlQenhIN1BaSmhwWEwweElXR1Z2NUlZYUxjX2dBcWh6X2ZGaw", - title: "오펜하이머", - titleEn: "Oppenheimer", - genre: "2023 스릴러", - runningTime: "3시간 00분", - }; - +function ContentHeader({ content }: { content: ContentType }) { const backGroundStyle = { - backgroundImage: `url(${content.backGroundImg})`, + backgroundImage: `url(https://an2-ast.amz.wtchn.net/ayg/images/asContentFallbackVideo1.207fd38cdebff49ccde8.jpg)`, backgroundSize: "cover", backgroundPosition: "center", }; + const genres = content.genres.map((item) => item.genre).join("/"); + const hours = Math.floor(content.runtime / 60); + const remainingMinutes = content.runtime % 60; + const runtime = + hours > 0 ? `${hours}시간 ${remainingMinutes}분` : `${remainingMinutes}분`; + return (
    -
    -

    {content.title}

    -
    {content.titleEn}
    -
    {content.genre}
    -
    {content.runningTime}
    -
    + {content && ( +
    +

    {content.titleKo}

    +
    {content.titleOriginal}
    +
    {`${content.releaseDate} · ${genres} · ${content.prodCountry}`}
    +
    {runtime}
    +
    + )}
    ); } -function ContentPanel() { - const content = { - posterSrc: - "https://an2-img.amz.wtchn.net/image/v2/TnfLooyFVulaY3fZhLMNIw.jpg?jwt=ZXlKaGJHY2lPaUpJVXpJMU5pSjkuZXlKdmNIUnpJanBiSW1SZk5Ea3dlRGN3TUhFNE1DSmRMQ0p3SWpvaUwzWXlMM04wYjNKbEwybHRZV2RsTHpFMk9UQTFNRFk1TWpBNU9ERTVNamt5TkRNaWZRLmdfM0lvai1JZGVCbjVFRXhYQ3VFODMwdEN4MnNEa2JKbXV4VEk3QlJPYVE", - avgRating: 4.0, - ratingCount: 1000, - overView: ` - “나는 이제 죽음이요, 세상의 파괴자가 되었다.” - - 세상을 구하기 위해 세상을 파괴할 지도 모르는 선택을 해야 하는 천재 과학자의 핵개발 프로젝트. - `, - }; - const user = { - name: "WOOJIN", - reviewedRating: 2.5, - comment: "오펜하이머는 얼마나 좋았을까...", - }; - // 추후에 hook으로 수정 - const [rating, setRating] = useState(2 * user.reviewedRating); +function ContentPanel({ + content, + setContent, +}: { + content: ContentType; + setContent: (content: ContentType) => void; +}) { + const [currentModal, setCurrentModal] = useState< + "updateComment" | "createComment" | null + >(null); + + const { accessToken } = useAuthContext(); return (
    - 영화 포스터 + 영화 포스터
    평점 그래프
    보고싶어요
  • -
  • +
  • { + // mycommentapi가 있어야함 + + setCurrentModal("createComment"); + }} + >
  • -
    -
    -
    - 대단한 작품이군요! {user.name} 님의 감동을 글로 남겨보세요 -
    - -
    -
    -
    -

    내가 쓴 코멘트

    -
    - - {user.comment} -
    - -
    -
    -
    - -
    -
    -
    -
    {content.overView}
    + { + setCurrentModal(null); + }} + openModal={setCurrentModal} + content={content} + setContent={setContent} + /> + {/* 아직 코멘트 받아오는 api가 없음 */} +
    {content.plot}
    + {currentModal && accessToken && ( + + )} ); } -function ContentCast() { - // 임시 - const cast = { - img: "https://an2-img.amz.wtchn.net/image/v2/MsLKK8t7W1kQuIr3SztdjQ.jpg?jwt=ZXlKaGJHY2lPaUpJVXpJMU5pSjkuZXlKdmNIUnpJanBiSW1SZk1qUXdlREkwTUNKZExDSndJam9pTDNZeEwzQmxiM0JzWlM5dFpXUnBkVzB2WWpKa016TTRZMll4TmpsbE5qSXpPRGsxTVdRdWFuQm5JbjAuVWJRSHJiR0d0Q2V3WjFvdlFPdEk1ZE5wUnppUHNoc2tCcDE0ZDJGQjhGTQ", - name: "크리스토퍼 놀란", - role: "출연", - }; - const castList = [ - { img: profileDefault, name: "크리스토퍼 놀란", role: "감독" }, - ]; - for (let i = 0; i < 23; i++) castList.push(cast); - +function ContentCast({ content }: { content: ContentType }) { return (

    출연/제작

    @@ -175,39 +174,43 @@ function ContentCast() { ); } -function ContentComments() { - const comment = { - user: { - img: profileDefault, - name: "이동진", - }, - reviewRating: 4.5, - text: `줄리어스 로버트 오펜하이머.. -줄리어스 로버트 오펜하이머.. -자기 이야기가 영화로 만들어진다니. -로버트는 얼마나 좋았을까. - `, - likeCount: 200, - subcommentCount: 1000, - }; - const comments = []; +function ContentComments({ content }: { content: ContentType }) { + const [comments, setComments] = useState([]); + + useEffect(() => { + getCommentListRequest(content.movieCD) + .then(defaultResponseHandler) + .then((data: CommentsResType) => { + const commentsResponse = data; + const comments = commentsResponse.results; + + const repComment = + comments.length <= 4 ? comments : comments.slice(0, 4); + console.log(repComment); + setComments(repComment); + }) + .catch(() => alert("잘못된 요청입니다")); + }, [content.movieCD]); - for (let i = 0; i < 8; i++) { - comments.push(comment); - } return (

    코멘트 10000+

    - 더보기 + + 더보기 +
      - {comments.map((comment) => ( - - ))} + {comments.map((comment) => { + console.log(comment); + return ; + })}
    diff --git a/src/components/ContentList.tsx b/src/components/ContentList.tsx index 5d1fe94..3d02121 100644 --- a/src/components/ContentList.tsx +++ b/src/components/ContentList.tsx @@ -18,7 +18,7 @@ export type ContentListProps = { function ContentCell(content: MovieType, rank: number) { return (
  • - +
    {rank}
    diff --git a/src/components/Modal.module.scss b/src/components/Modal.module.scss new file mode 100644 index 0000000..83409c3 --- /dev/null +++ b/src/components/Modal.module.scss @@ -0,0 +1,14 @@ +@import "../utils/common.module.scss"; + +.modalContainer { + z-index: 100; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx new file mode 100644 index 0000000..acf5aab --- /dev/null +++ b/src/components/Modal.tsx @@ -0,0 +1,21 @@ +import { ReactNode } from "react"; +import styles from "./Modal.module.scss"; + +type ModalProps = { + onClose: () => void; + children: ReactNode; +}; + +export default function Modal({ onClose, children }: ModalProps) { + const handleClickOutside = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + return ( +
    + {children} +
    + ); +} + \ No newline at end of file diff --git a/src/components/MyCommentBox.module.scss b/src/components/MyCommentBox.module.scss new file mode 100644 index 0000000..e4f8443 --- /dev/null +++ b/src/components/MyCommentBox.module.scss @@ -0,0 +1,105 @@ +@import "../utils/common.module.scss"; + +.commentCon { + width: 100%; + border-top: 1px solid $light-gray; + padding: 20px 0; + + h3 { + margin-bottom: 10px; + line-height: 12px; + font-size: 12px; + font-weight: 500; + } + .commentBox { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + border: solid 1px $light-gray; + border-radius: 6px; + padding: 12px 20px; + background-color: $white; + + @media (max-width: $small-screen) { + flex-direction: column; + } + + .commentText { + width: 100%; + margin-right: 20px; + line-height: 20px; + font-size: 15px; + font-weight: 400; + color: $light-black; + + @media (max-width: $small-screen) { + text-align: center; + } + } + .commentCreateBtn { + width: 256px; + height: 40px; + border: 1px solid $light-gray; + border-radius: 6px; + background-color: inherit; + font-size: 17px; + font-weight: 500; + text-align: center; + color: $dark-black; + cursor: pointer; + + @media (max-width: $small-screen) { + margin-top: 10px; + } + } + .userImage { + width: 56px; + height: 56px; + margin-right: 20px; + border: 1px solid $extra-light-gray; + border-radius: 50%; + + @media (max-width: $small-screen) { + margin-bottom: 10px; + } + } + .commentBtnBox { + display: flex; + align-items: center; + + button { + display: flex; + align-items: center; + border-style: none; + padding: 16px 0 13px 0; + line-height: 16px; + font-size: 12px; + font-weight: 400; + color: $medium-gray; + background-color: inherit; + white-space: nowrap; + cursor: pointer; + + img { + width: 18px; + height: 18px; + margin-right: 4px; + } + } + .virtualLineBox { + display: flex; + justify-content: center; + align-items: center; + + width: 40px; + height: 100%; + .virtualLine { + width: 1px; + height: 8px; + background-color: $medium-gray; + } + } + } + } +} diff --git a/src/components/MyCommentBox.tsx b/src/components/MyCommentBox.tsx new file mode 100644 index 0000000..45559a7 --- /dev/null +++ b/src/components/MyCommentBox.tsx @@ -0,0 +1,84 @@ +import { useAuthContext } from "../contexts/authContext"; +import styles from "./MyCommentBox.module.scss"; +import profileDefault from "../assets/user_default.jpg"; +import { Link } from "react-router-dom"; +import { ContentType, MyCommentType } from "../type"; +import { deleteCommentRequest } from "../apis/comment"; +import { defaultResponseHandler } from "../apis/custom"; + +export default function MyCommentBox({ + openModal, + content, + setContent, + closeModal, +}: { + openModal: (type: "updateComment" | "createComment") => void; + closeModal: () => void; + content: ContentType; + setContent: (content: ContentType) => void; +}) { + const { isLogined, myUserData, accessToken } = useAuthContext(); + const my_comment = content.my_comment; + return ( + isLogined && ( + <> + {my_comment ? ( +
    +

    내가 쓴 코멘트

    +
    + + + {my_comment.my_comment} + +
    + +
    +
    +
    + +
    +
    +
    + ) : ( +
    +
    +
    + 대단한 작품이군요! {myUserData?.nickname} 님의 감동을 글로 + 남겨보세요 +
    + +
    +
    + )} + + ) + ); +} diff --git a/src/components/StarRating.tsx b/src/components/StarRating.tsx index f7c9044..08bb4e4 100644 --- a/src/components/StarRating.tsx +++ b/src/components/StarRating.tsx @@ -1,5 +1,13 @@ import { useState } from "react"; import styles from "./StarRating.module.scss"; +import { RateType } from "../type"; +import { + createRatingRequest, + deleteRatingRequest, + updateRatingRequest, +} from "../apis/content"; +import { defaultResponseHandler } from "../apis/custom"; +import { useAuthContext } from "../contexts/authContext"; type StarProps = { fill: "full" | "half" | "empty"; @@ -19,9 +27,9 @@ function Star(props: StarProps) {
    onMouseEnterStar(rating + 1)} + onMouseOver={() => onMouseEnterStar(rating + 0.5)} onMouseLeave={onMouseLeaveStar} - onClick={() => onClickStar(rating + 1)} + onClick={() => onClickStar(rating + 0.5)} > @@ -29,9 +37,9 @@ function Star(props: StarProps) {
    onMouseEnterStar(rating + 2)} + onMouseOver={() => onMouseEnterStar(rating + 1)} onMouseLeave={onMouseLeaveStar} - onClick={() => onClickStar(rating + 2)} + onClick={() => onClickStar(rating + 1)} > @@ -42,42 +50,58 @@ function Star(props: StarProps) { } type StarRatingProps = { - rating: number; - setRating: (rating: number) => void; + my_rate: RateType | null; + movieCD: string; }; -export default function StarRating({ - rating: reviewedRating, - setRating, -}: StarRatingProps) { - const [selectedStars, setSelectedStars] = useState(reviewedRating); +export default function StarRating({ my_rate, movieCD }: StarRatingProps) { + console.log("my_Rate :", my_rate); + const [savedRating, setSavedRating] = useState(my_rate ? my_rate.my_rate : 0); + const [selectedRating, setSelectedRating] = useState(savedRating); + const { isLogined, accessToken } = useAuthContext(); - const onMouseEnterStar = (seletedRating: number) => { - setSelectedStars(seletedRating); + const onMouseEnterStar = (rating: number) => { + setSelectedRating(rating); }; const onMouseLeaveStar = () => { - setSelectedStars(reviewedRating); + setSelectedRating(savedRating); }; - const onClickStar = (seletedRating: number) => { - // - console.log(`${seletedRating / 2}점 평가하기`); - setRating(seletedRating); + const onClickStar = (rating: number) => { + if (!isLogined) { + // loginModal; + } else { + console.log("clicked rating: ", rating); + console.log("accessToken", accessToken); + (my_rate + ? rating === my_rate.my_rate + ? deleteRatingRequest(my_rate.id, accessToken ?? "") + : updateRatingRequest(my_rate.id, rating, accessToken ?? "") + : createRatingRequest(movieCD, rating, accessToken ?? "") + ) + .then(() => { + return setSavedRating( + my_rate && rating === my_rate.my_rate ? 0 : rating + ); + }) + .catch((e) => console.log(e)); + } }; const stars: ("full" | "half" | "empty")[] = [ - selectedStars >= 2 ? "full" : selectedStars === 1 ? "half" : "empty", - selectedStars >= 4 ? "full" : selectedStars === 3 ? "half" : "empty", - selectedStars >= 6 ? "full" : selectedStars === 5 ? "half" : "empty", - selectedStars >= 8 ? "full" : selectedStars === 7 ? "half" : "empty", - selectedStars >= 10 ? "full" : selectedStars === 9 ? "half" : "empty", + selectedRating >= 1 ? "full" : selectedRating === 0.5 ? "half" : "empty", + selectedRating >= 2 ? "full" : selectedRating === 1.5 ? "half" : "empty", + selectedRating >= 3 ? "full" : selectedRating === 2.5 ? "half" : "empty", + selectedRating >= 4 ? "full" : selectedRating === 3.5 ? "half" : "empty", + selectedRating >= 5 ? "full" : selectedRating === 4.5 ? "half" : "empty", ]; return (
    {stars.map((star, idx) => ( void; + setContent: (content: ContentType) => void; +}; + +export default function WritingModal(props: WritingModalProps) { + const { accessToken } = useAuthContext(); + const { type, title, content, currentModal, setCurrentModal, setContent } = + props; + const { my_comment, movieCD } = content; + + const [commentInput, setCommentInput] = useState( + my_comment === null ? "" : my_comment.my_comment + ); //comment는 기존에 썼던 코멘트가 있을 경우에만 존재 + const [hasSpoiler, setHasSpoiler] = useState( + my_comment === null ? false : my_comment.has_spoiler + ); + return ( + { + setCurrentModal(null); + }} + > +
    +
    +

    {title}

    + +
    +
    +
    { + e.preventDefault(); + console.log(movieCD, accessToken, commentInput, !hasSpoiler); + currentModal === "createComment" && + accessToken && + createCommentRequest( + movieCD, + accessToken, + commentInput, + hasSpoiler + ) + .then(defaultResponseHandler) + .then((data: CommentType) => { + setContent({ + ...content, + my_comment: { + my_comment: data.content, + has_spoiler: data.has_spoiler, + id: data.id, + }, + }); + setCurrentModal(null); + }); + currentModal === "updateComment" && + accessToken && + my_comment && + updateCommentRequest( + my_comment?.id, + accessToken, + commentInput, + hasSpoiler + ) + .then(defaultResponseHandler) + .then((data: CommentType) => { + setContent({ + ...content, + my_comment: { + my_comment: data.content, + has_spoiler: data.has_spoiler, + id: data.id, + }, + }); + setCurrentModal(null); + }); + }} + > +