diff --git a/src/apis/user.ts b/src/apis/user.ts index 7d3cbf3..268f1e7 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -5,6 +5,39 @@ export async function getUserDetail(userId: number) { return fetch(`${BASE_API_URL}/users/${userId}/`); } +export async function putUserUpdate( + nickname: string, + bio: string, + accessToken: string +) { + return fetch(`${BASE_API_URL}/users/mypage/`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ nickname, bio }), + }); +} + +export async function putUserPhotoUpdate( + backgroundPhoto: File | null, + profilePhoto: File | null, + accessToken: string +) { + const formData = new FormData(); + backgroundPhoto && formData.append("background_photo", backgroundPhoto); + profilePhoto && formData.append("profile_photo", profilePhoto); + + return fetch(`${BASE_API_URL}/users/mypage/image_update/`, { + method: "PUT", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + body: formData, + }); +} + export async function getFollowingList(userId: number) { return fetch(`${BASE_API_URL}/users/${userId}/followings/`); } @@ -66,7 +99,7 @@ export async function getUserRatingMovies( query: { order?: "high-rating" | "low-rating" | "created"; rate?: number; - }, + } ) { const result = Object.entries(query) .map(([key, value]) => `${key}=${value}`) @@ -84,7 +117,7 @@ export async function getUserWantToWatch(userId: number) { export async function postCreateWatchingState( movieCD: string, accessToken: string, - user_state: "watching" | "want_to_watch" | "not_interested" | null, + user_state: "watching" | "want_to_watch" | "not_interested" | null ) { return fetch(`${BASE_API_URL}/contents/${movieCD}/state`, { method: "POST", @@ -102,7 +135,7 @@ export async function postCreateWatchingState( export async function putUpdateWatchingState( state_id: number, accessToken: string, - user_state: "watching" | "want_to_watch" | "not_interested" | null, + user_state: "watching" | "want_to_watch" | "not_interested" | null ) { return fetch(`${BASE_API_URL}/contents/states/${state_id}`, { method: "PUT", @@ -118,7 +151,7 @@ export async function putUpdateWatchingState( export async function deleteWatchingState( state_id: number, - accessToken: string, + accessToken: string ) { return fetch(`${BASE_API_URL}/contents/states/${state_id}`, { method: "DELETE", diff --git a/src/components/CommentInfo.tsx b/src/components/CommentInfo.tsx index e7a82d0..97d3df8 100644 --- a/src/components/CommentInfo.tsx +++ b/src/components/CommentInfo.tsx @@ -1,7 +1,6 @@ +import profileDefault from "../assets/user_default.jpg"; import { Link, useOutletContext, useParams } from "react-router-dom"; -import userImage from "../assets/user_default.jpg"; import styles from "./CommentInfo.module.scss"; - import ReplyList from "./ReplyList"; import elapsedTime from "../utils/elapsedTime"; import { CommentType } from "../type"; @@ -30,7 +29,10 @@ function CommentHeader({ comment }: { comment: CommentType }) { title={created_by.nickname} >
- {created_by.nickname + {created_by.nickname
{created_by.nickname}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index fa81ead..b90b45b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,7 +1,7 @@ import { CurrentModalType } from "../pages/Layout"; import Logo from "../assets/logo.svg"; import WhiteLogo from "../assets/logo_white.svg"; -import UserImage from "../assets/user_default.jpg"; +import profileDefault from "../assets/user_default.jpg"; import styles from "./Header.module.scss"; import SearchBar from "./SearchBar"; import searchSmall from "../assets/searchSmall.svg"; @@ -113,7 +113,7 @@ export default function Header({ setCurrentModal }: HeaderProps) {
  • - +
  • diff --git a/src/components/MyCommentBox.tsx b/src/components/MyCommentBox.tsx index c7969aa..222f962 100644 --- a/src/components/MyCommentBox.tsx +++ b/src/components/MyCommentBox.tsx @@ -37,7 +37,11 @@ export default function MyCommentBox({

    내가 쓴 코멘트

    - +
    - {reply.created_by.nickname + {reply.created_by.nickname
    diff --git a/src/components/SearchUserList.tsx b/src/components/SearchUserList.tsx index 9cb8503..30750b7 100644 --- a/src/components/SearchUserList.tsx +++ b/src/components/SearchUserList.tsx @@ -39,8 +39,8 @@ export default function SearchUserList({
    {user.nickname
    diff --git a/src/components/user/User.module.scss b/src/components/user/User.module.scss index 240cbfa..3559150 100644 --- a/src/components/user/User.module.scss +++ b/src/components/user/User.module.scss @@ -24,8 +24,16 @@ } } .profileSection { + position: relative; border-radius: 6px 6px 0 0; + .backgroundPhoto { + position: absolute; + top: 0; + width: 100%; + height: 35%; + } + .setBttnBox { position: relative; height: 70px; @@ -42,6 +50,9 @@ background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxnIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCI+CiAgICAgICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIvPgogICAgICAgIDxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgZD0iTTE5LjQzIDEyLjk4Yy4wNC0uMzIuMDctLjY0LjA3LS45OCAwLS4zNC0uMDMtLjY2LS4wNy0uOThsMi4xMS0xLjY1Yy4xOS0uMTUuMjQtLjQyLjEyLS42NGwtMi0zLjQ2Yy0uMTItLjIyLS4zOS0uMy0uNjEtLjIybC0yLjQ5IDFjLS41Mi0uNC0xLjA4LS43My0xLjY5LS45OGwtLjM4LTIuNjVBLjQ4OC40ODggMCAwIDAgMTQgMmgtNGMtLjI1IDAtLjQ2LjE4LS40OS40MmwtLjM4IDIuNjVjLS42MS4yNS0xLjE3LjU5LTEuNjkuOThsLTIuNDktMWMtLjIzLS4wOS0uNDkgMC0uNjEuMjJsLTIgMy40NmMtLjEzLjIyLS4wNy40OS4xMi42NGwyLjExIDEuNjVjLS4wNC4zMi0uMDcuNjUtLjA3Ljk4IDAgLjMzLjAzLjY2LjA3Ljk4bC0yLjExIDEuNjVjLS4xOS4xNS0uMjQuNDItLjEyLjY0bDIgMy40NmMuMTIuMjIuMzkuMy42MS4yMmwyLjQ5LTFjLjUyLjQgMS4wOC43MyAxLjY5Ljk4bC4zOCAyLjY1Yy4wMy4yNC4yNC40Mi40OS40Mmg0Yy4yNSAwIC40Ni0uMTguNDktLjQybC4zOC0yLjY1Yy42MS0uMjUgMS4xNy0uNTkgMS42OS0uOThsMi40OSAxYy4yMy4wOS40OSAwIC42MS0uMjJsMi0zLjQ2Yy4xMi0uMjIuMDctLjQ5LS4xMi0uNjRsLTIuMTEtMS42NXpNMTIgMTUuNWMtMS45MyAwLTMuNS0xLjU3LTMuNS0zLjVzMS41Ny0zLjUgMy41LTMuNSAzLjUgMS41NyAzLjUgMy41LTEuNTcgMy41LTMuNSAzLjV6Ii8+CiAgICA8L2c+Cjwvc3ZnPgo="); } .profileInfoBox { + position: inherit; + display: flex; + flex-direction: column; padding: 15px 15px 30px 15px; .profilePhoto { @@ -49,8 +60,6 @@ width: 135px; height: 135px; border-radius: 50%; - background: url("https://an2-glx.amz.wtchn.net/assets/default/user/photo_file_name_large-ab0a7f6a92a282859192ba17dd4822023e22273e168c2daf05795e5171e66446.jpg") - center center / cover no-repeat; border: 1px solid rgb(227, 227, 227); margin-bottom: 8px; } @@ -84,6 +93,19 @@ font-weight: bold; } } + .userEditBttn { + width: 100%; + height: 40px; + border: none; + border-radius: 4px; + font-size: 16px; + font-weight: 500; + margin-top: 14px; + cursor: pointer; + color: rgb(41, 42, 50); + background-color: white; + border: 1px solid rgb(212, 212, 212); + } .followBttn { width: 100%; height: 40px; diff --git a/src/components/user/User.tsx b/src/components/user/User.tsx index e289497..8ddcb85 100644 --- a/src/components/user/User.tsx +++ b/src/components/user/User.tsx @@ -12,6 +12,8 @@ import { FollowListType } from "../../type"; import { getFollowingList } from "../../apis/user"; import { postAddFollow, postUnFollow } from "../../apis/user"; import useChangeTitle from "../../hooks/useChangeTitle"; +import profileDefault from "../../assets/user_default.jpg"; + export default function User() { const { setCurrentModal } = useOutletContext(); const { myUserData, accessToken } = useAuthContext(); @@ -77,7 +79,7 @@ export default function User() { .finally(() => { setPageUserLoading(false); }); - }, [pageUserId]); + }, [pageUserId, myUserData]); // 유저데이터에 팔로잉 리스트가 없어서 추가로 가져와야 함 @@ -110,12 +112,23 @@ export default function User() { scrollToTop(); }, []); + const backgoundStyle = { + backgroundImage: `url(${pageUser?.background_photo ?? ""})`, + backgroundSize: "cover", + backgroundRepeat: "no-repeat", + backgroundPosition: "center", + }; + return (
    {!loading && pageUser && ( <> {/* profile section. user 기본 정보와 평가&코멘트 탭을 포함하는 섹션 */}
    +
    -
    +

    {pageUser.nickname}

    {pageUser.username}

    @@ -137,7 +153,14 @@ export default function User() { 팔로잉 {pageUser.following_count}
    - {pageMode !== "myPage" && ( + {pageMode === "myPage" ? ( + + ) : ( +

    프로필 변경

    + + +
    + + +
    +
    + + +
    +
    + + +
    + + + ); +} diff --git a/src/hooks/useUserEdit.ts b/src/hooks/useUserEdit.ts new file mode 100644 index 0000000..173dc57 --- /dev/null +++ b/src/hooks/useUserEdit.ts @@ -0,0 +1,86 @@ +import { ChangeEvent, FormEvent, useState } from "react"; +import { useAuthContext } from "../contexts/authContext"; +import { putUserPhotoUpdate, putUserUpdate } from "../apis/user"; +import { defaultResponseHandler } from "../apis/custom"; + +export default function useUserEdit() { + const { myUserData, accessToken, setMyUserData } = useAuthContext(); + const [nickname, setNickname] = useState(myUserData?.nickname ?? ""); + const [bio, setBio] = useState(myUserData?.bio ?? ""); + const [backgroundPhotoFile, setBackgroundPhotoFile] = useState( + null + ); + const [profilePhotoFile, setProfilePhotoFile] = useState(null); + const [backgroundPhotoUrl, setBackgroundPhotoUrl] = useState( + myUserData?.background_photo ?? "" + ); + const [profilePhotoUrl, setProfilePhotoUrl] = useState( + myUserData?.profile_photo ?? "" + ); + + const handleBackgroundPhoto = (e: ChangeEvent) => { + const file = e.target.files?.[0]; + const reader = new FileReader(); + + reader.onload = (e) => { + const previewPhotoUrl = e.target?.result as string | null | undefined; + setBackgroundPhotoFile(file ?? null); + setBackgroundPhotoUrl(previewPhotoUrl ?? ""); + }; + file && reader.readAsDataURL(file); + }; + + const handleProfilePhoto = (e: ChangeEvent) => { + const file = e.target.files?.[0]; + const reader = new FileReader(); + + reader.onload = (e) => { + const previewPhotoUrl = e.target?.result as string | null | undefined; + setProfilePhotoFile(file ?? null); + setProfilePhotoUrl(previewPhotoUrl ?? ""); + }; + file && reader.readAsDataURL(file); + }; + + const handleNickname = (e: ChangeEvent) => { + setNickname(e.target.value); + }; + const handleBio = (e: ChangeEvent) => { + setBio(e.target.value); + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + + accessToken && + myUserData && + putUserUpdate(nickname, bio, accessToken) + .then(defaultResponseHandler) + .then((data) => { + setMyUserData(data); + }) + .catch((e) => alert(e)); + + accessToken && + myUserData && + (backgroundPhotoFile || profilePhotoFile) && + putUserPhotoUpdate(backgroundPhotoFile, profilePhotoFile, accessToken) + .then(defaultResponseHandler) + .then((data) => { + setMyUserData(data); + }) + .catch((e) => alert(e)); + }; + + return { + nickname, + bio, + profilePhotoUrl, + backgroundPhotoUrl, + handleBackgroundPhoto, + handleProfilePhoto, + handleNickname, + handleBio, + handleSubmit, + }; +} diff --git a/src/pages/Layout.tsx b/src/pages/Layout.tsx index b02e8da..511b436 100644 --- a/src/pages/Layout.tsx +++ b/src/pages/Layout.tsx @@ -9,8 +9,14 @@ import { getMyUserData, postNewToken } from "../apis/auth"; import { useAuthContext } from "../contexts/authContext"; import { defaultResponseHandler } from "../apis/custom"; import SettingModal from "../components/user/SettingModal"; +import UserEditModal from "../components/user/UserEditModal"; -export type CurrentModalType = null | "signup" | "login" | "setting"; +export type CurrentModalType = + | null + | "signup" + | "login" + | "setting" + | "userEdit"; export type OutletContextType = { setCurrentModal: (currentModal: CurrentModalType) => void; }; @@ -69,6 +75,9 @@ export default function Layout() { {currentModal === "setting" && ( )} + {currentModal === "userEdit" && ( + + )}
    diff --git a/src/pages/user/UserWrittenCommentListPage.tsx b/src/pages/user/UserWrittenCommentListPage.tsx index 7f1964f..07228f2 100644 --- a/src/pages/user/UserWrittenCommentListPage.tsx +++ b/src/pages/user/UserWrittenCommentListPage.tsx @@ -1,6 +1,5 @@ import CommentCard from "../../components/CommentCard"; import styles from "./UserWrittenCommentListPage.module.scss"; -// import profileDefault from "../../assets/user_default.jpg"; import { useNavigate, useParams } from "react-router-dom"; import { useEffect } from "react"; import { getUserWrittenComments } from "../../apis/user";