Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] - 프로필 이미지 수정 기능 및 여행기 등록 시 여행 장소마다 국가 코드 주도록 구현 #535

Merged
merged 24 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
004644c
refactor(Drawer): 기존 헤드와 높이 달라 선 위치가 다른 문제 개선
simorimi Oct 10, 2024
cef7429
feat: 장소 필터링을 위하여 장소 등록시 countryCode를 보내도록 기능 구현
simorimi Oct 13, 2024
7912136
refactor(AvatarCircle): props $네이밍 수정
simorimi Oct 14, 2024
0aa2cfc
Merge branch 'develop/fe' of https://github.com/woowacourse-teams/202…
simorimi Oct 14, 2024
dde854e
refactor(MyTravelogue):$ 제거에 따른 수정
simorimi Oct 15, 2024
932e318
refactor(usePostUploadImages): resize 와 convert 처리 내부에서 하도록 수정
simorimi Oct 15, 2024
aa16247
refactor(MainPage): div semantic 태그인 button으로 수정
simorimi Oct 15, 2024
2eb4464
feat(ProfileImageEditModalBottomSheet): 기능 구현
simorimi Oct 15, 2024
537c3e8
feat(usePutProfile): api 명세 변경에 따라 patch를 put으로, imageUrl body 값에 부여
simorimi Oct 15, 2024
98076f6
refactor(AvatarCircle): props 유연하게 수정
simorimi Oct 16, 2024
fbc78f4
feat(useMyPage): 훅 구현
simorimi Oct 16, 2024
9321263
feat(MyPage): 프로필 이미지 수정 기능 구현
simorimi Oct 16, 2024
3f702c4
refactor(MyPage): 기능 단위로 pr 분리하기 위한 수정
simorimi Oct 16, 2024
ae9be47
refactor(SearchPage): 기능 단위로 pr 분리하기 위한 수정
simorimi Oct 16, 2024
01b6786
refactor(useMyPage): useToggle 사용하도록 수정
simorimi Oct 16, 2024
c7561fd
refactor(common): 반복되는 타입PlaceInfo 타입으로 선언 및 수정
simorimi Oct 20, 2024
b889315
refactor(constants): 상수들 파일로 분리
simorimi Oct 20, 2024
cac5077
refactor(useMyPage): 책임에 따라 각각 커스텀 훅으로 분리
simorimi Oct 20, 2024
7e6ca5e
refactor(constants): 상수 파일로 분리
simorimi Oct 20, 2024
f318d32
refactor(usePostUploadImages): max width, height 값 받을 수 있도록 수정
simorimi Oct 20, 2024
c810327
refactor(usePostUploadImages): max width, height 값 받을 수 있도록 수정
simorimi Oct 20, 2024
0ec73d3
Merge branch 'feature/fe/#519' of https://github.com/woowacourse-team…
simorimi Oct 20, 2024
3f18f98
refactor(useProfileInitialization): 의존성 배열 추가
simorimi Oct 21, 2024
211c7d6
refactor: useCallback으로 update 함수 감싸주도록 수정
simorimi Oct 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 23 additions & 32 deletions frontend/src/components/pages/my/MyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,16 @@ import {
} from "@components/common";
import MyPageSkeleton from "@components/pages/my/MyPageSkeleton/MyPageSkeleton";

import { ERROR_MESSAGE_MAP } from "@constants/errorMessage";
import { FORM_VALIDATIONS_MAP } from "@constants/formValidation";
import { STORAGE_KEYS_MAP } from "@constants/storage";

import * as S from "./MyPage.styled";
import MyTravelPlans from "./MyTravelPlans/MyTravelPlans";
import MyTravelogues from "./MyTravelogues/MyTravelogues";
import ProfileImageEditModalBottomSheet from "./ProfileImageEditModalBottomSheet/ProfileImageEditModalBottomSheet";
import { IGNORED_ERROR_MESSAGES, TAB_CONTENT } from "./constants";
import useMyPage from "./hooks/useMyPage";

const TAB_CONTENT = [
{ label: "✈️ 내 여행 계획", component: MyTravelPlans },
{ label: "📝 내 여행기", component: MyTravelogues },
] as const;

const IGNORED_ERROR_MESSAGES = [ERROR_MESSAGE_MAP.api.login, "Network Error"];

const MyPage = () => {
const { states, handlers, userProfile, profileImageFileInputRef } = useMyPage();
const { editModal, profileImage, profileNickname, profileEdit, userProfile } = useMyPage();

const showErrorAlert = (error: Error | null) => {
if (error && !IGNORED_ERROR_MESSAGES.includes(error.message)) alert(error.message);
Expand All @@ -43,45 +34,45 @@ const MyPage = () => {

return (
<S.Layout>
{states.isModifying ? (
{profileEdit.isModifying ? (
<S.ProfileContainer>
<S.EditButtonContainer>
<S.ProfileEditButtonContainer>
<S.EditButton type="button" onClick={handlers.handleClickProfileEditCancelButton}>
<S.EditButton type="button" onClick={profileEdit.handleClickProfileEditCancelButton}>
취소
</S.EditButton>
<S.EditButton type="button" onClick={handlers.handleClickProfileEditConfirmButton}>
<S.EditButton type="button" onClick={profileEdit.handleClickProfileEditConfirmButton}>
확인
</S.EditButton>
</S.ProfileEditButtonContainer>
</S.EditButtonContainer>

<S.ProfileImageContainer>
<S.ProfileImageWrapper $isProfileImageLoading={states.isProfileImageLoading}>
<S.ProfileImageWrapper $isProfileImageLoading={profileImage.isProfileImageLoading}>
<AvatarCircle
size="large"
profileImageUrl={states.profileImageUrl}
onLoad={handlers.handleLoadProfileImage}
profileImageUrl={profileImage.profileImageUrl}
onLoad={profileImage.handleLoadProfileImage}
/>
</S.ProfileImageWrapper>

{states.isProfileImageLoading ? (
{profileImage.isProfileImageLoading ? (
<S.ProfileImageLoadingWrapper>
<Spinner variants="circle" size={40} />
</S.ProfileImageLoadingWrapper>
) : (
<>
<S.ProfileImageHiddenInput
ref={profileImageFileInputRef}
ref={profileImage.profileImageFileInputRef}
type="file"
accept="image/*"
onChange={handlers.handleChangeProfileImage}
onChange={profileImage.handleChangeProfileImage}
aria-label="썸네일 이미지 선택"
title="이미지 파일을 선택하세요"
/>
<IconButton
iconType="camera-icon"
onClick={handlers.handleClickEditModalOpenButton}
onClick={editModal.handleOpenEditModal}
css={S.profileImageEditButtonStyle}
/>
</>
Expand All @@ -91,34 +82,34 @@ const MyPage = () => {
<S.NickNameEditContainer>
<Input
placeholder={userProfile.data?.nickname}
value={states.nickname}
value={profileNickname.nickname}
autoFocus
maxLength={FORM_VALIDATIONS_MAP.title.maxLength}
spellCheck={false}
css={S.inputStyle}
onChange={handlers.handleChangeNickname}
onChange={profileNickname.handleChangeNickname}
/>
<CharacterCount
count={states.nickname?.length}
count={profileNickname.nickname?.length}
maxCount={FORM_VALIDATIONS_MAP.title.maxLength}
/>
</S.NickNameEditContainer>
</S.ProfileContainer>
) : (
<S.ProfileContainer>
<S.EditButtonContainer>
<S.EditButton type="button" onClick={handlers.handleClickProfileEditButton}>
<S.EditButton type="button" onClick={profileEdit.handleClickProfileEditButton}>
프로필 수정
</S.EditButton>
</S.EditButtonContainer>

<S.ProfileImageContainer>
<AvatarCircle size="large" profileImageUrl={states.profileImageUrl} />
<AvatarCircle size="large" profileImageUrl={profileImage.profileImageUrl} />
</S.ProfileImageContainer>

<S.NicknameWrapper>
<Text textType="bodyBold" css={S.nicknameStyle}>
{states.nickname}
{profileNickname.nickname}
</Text>
</S.NicknameWrapper>
</S.ProfileContainer>
Expand All @@ -136,14 +127,14 @@ const MyPage = () => {
/>

<ProfileImageEditModalBottomSheet
isOpen={states.isModalOpen}
onClose={handlers.handleClickEditModalCloseButton}
isOpen={editModal.isEditModalOpen}
onClose={editModal.handleCloseEditModal}
>
<S.Button onClick={handlers.handleClickProfileImageEditButton}>
<S.Button onClick={profileImage.handleClickProfileImageEditButton}>
<Text textType="detail">앨범에서 선택</Text>
</S.Button>
{states.profileImageUrl && (
<S.Button onClick={handlers.handleClickProfileImageDeleteButton}>
{profileImage.profileImageUrl && (
<S.Button onClick={profileImage.handleClickProfileImageDeleteButton}>
<Text textType="detailBold" css={S.deleteTextColor}>
프로필 사진 삭제
</Text>
Expand Down
144 changes: 57 additions & 87 deletions frontend/src/components/pages/my/hooks/useMyPage.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,78 @@
import React, { useEffect, useRef, useState } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확실히 이전보다 코드가 더 깔끔해졌네요 :) 감사합니다!


import { usePostUploadImages } from "@queries/usePostUploadImages";
import usePutProfile from "@queries/usePutProfile";
import { useUserProfile } from "@queries/useUserProfile";

import useToggle from "@hooks/useToggle";

import { FORM_VALIDATIONS_MAP } from "@constants/formValidation";
import useProfileEdit from "./useProfileEdit";
import useProfileImage from "./useProfileImage";
import useProfileInitialization from "./useProfileInitialization";
import useProfileNickname from "./useProfileNickname";

const useMyPage = () => {
const { data, status, error } = useUserProfile();

const onError = (error: Error) => {
alert(error.message);
setNickname(data?.nickname ?? "");
};

const { mutate: mutateModifyProfile } = usePutProfile(onError);

const profileImageFileInputRef = useRef<HTMLInputElement>(null);

const [profileImageUrl, setProfileImageUrl] = useState(data?.profileImageUrl ?? "");
const [nickname, setNickname] = useState(data?.nickname ?? "");

const [isModifying, setIsModifying] = useState(false);
const [isProfileImageLoading, setIsProfileImageLoading] = useState(false);

const [isModalOpen, handleOpenModal, handleCloseModal] = useToggle();

const handleClickEditModalOpenButton = () => handleOpenModal();
const handleClickEditModalCloseButton = () => handleCloseModal();
const [isEditModalOpen, handleOpenEditModal, handleCloseEditModal] = useToggle();

const handleClickProfileEditButton = () => setIsModifying(true);
const handleClickProfileImageEditButton = () => profileImageFileInputRef.current?.click();

const { mutateAsync: mutateAddImage } = usePostUploadImages();

const handleChangeProfileImage = async (e: React.ChangeEvent<HTMLInputElement>) => {
setIsProfileImageLoading(true);
handleCloseModal();

const files = Array.from(e.target.files as FileList);
const profileImage = await mutateAddImage(files);

setProfileImageUrl(profileImage[0]);
};

const handleLoadProfileImage = () => {
setIsProfileImageLoading(false);
};

const handleClickProfileImageDeleteButton = () => {
setProfileImageUrl("");

handleCloseModal();
};

const handleClickProfileEditConfirmButton = () => {
const trimmedNickname = nickname.trim();
const newNickname = trimmedNickname || data?.nickname || "";

setNickname(newNickname);
mutateModifyProfile({ nickname: newNickname, profileImageUrl: profileImageUrl });

setIsModifying(false);
};

const handleClickProfileEditCancelButton = () => {
setNickname(data?.nickname ?? "");
setProfileImageUrl(data?.profileImageUrl ?? "");

setIsModifying(false);
};

const handleChangeNickname = (e: React.ChangeEvent<HTMLInputElement>) => {
setNickname(
e.target.value.slice(
FORM_VALIDATIONS_MAP.title.minLength,
FORM_VALIDATIONS_MAP.title.maxLength,
),
);
};
const { data, status, error } = useUserProfile();
const { nickname: userNickname, profileImageUrl: userProfileImageUrl } = data ?? {};

useEffect(() => {
if (data?.nickname) setNickname(data.nickname);
if (data?.profileImageUrl) setProfileImageUrl(data.profileImageUrl);
}, [data?.nickname, data?.profileImageUrl]);
const {
profileImageFileInputRef,
profileImageUrl,
isProfileImageLoading,
handleClickProfileImageEditButton,
handleChangeProfileImage,
handleLoadProfileImage,
handleClickProfileImageDeleteButton,
updateProfileImageUrl,
} = useProfileImage({ userProfileImageUrl, handleCloseEditModal });

const { nickname, handleChangeNickname, updateNickname } = useProfileNickname(userNickname);

const {
isModifying,
handleClickProfileEditButton,
handleClickProfileEditConfirmButton,
handleClickProfileEditCancelButton,
} = useProfileEdit({
userNickname,
nickname,
updateNickname,
userProfileImageUrl,
profileImageUrl,
updateProfileImageUrl,
});

useProfileInitialization({
userNickname,
updateNickname,
userProfileImageUrl,
updateProfileImageUrl,
});

return {
states: { profileImageUrl, nickname, isModifying, isProfileImageLoading, isModalOpen },
handlers: {
handleClickEditModalOpenButton,
handleClickEditModalCloseButton,
handleClickProfileEditButton,
editModal: {
isEditModalOpen,
handleOpenEditModal,
handleCloseEditModal,
},
profileImage: {
profileImageFileInputRef,
profileImageUrl,
isProfileImageLoading,
handleClickProfileImageEditButton,
handleChangeProfileImage,
handleLoadProfileImage,
handleClickProfileImageDeleteButton,
},
profileNickname: {
nickname,
handleChangeNickname,
},
profileEdit: {
isModifying,
handleClickProfileEditButton,
handleClickProfileEditConfirmButton,
handleClickProfileEditCancelButton,
handleChangeNickname,
},
userProfile: { data, status, error },
profileImageFileInputRef,
};
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시간이 충분하다면 이 훅을 조금 더 리팩터링 해보는 것도 좋을거 같단 생각이 들었어요,,, (너무 많은 책임들이 쏠려 있어서 그런지 코드 해석에 시간이 걸렸어요)

제 생각에는

  1. 프로필을 초기화 하는 책임(useEffect)
  2. 프로필 수정 모달에 대한 책임(handleClickEditModalCloseButton, handleClickProfileImageEditButton, handleClickProfileImageDeleteButton, handleClickEditModalOpenButton)
  3. 프로필 수정에 대한 책임(handleClickProfileEditButton, handleChangeProfileImage, handleLoadProfileImage, handleClickProfileEditConfirmButton, handleClickProfileEditCancelButton, handleChangeNickname)

정도로만 나눠도 괜찮을거 같다는 생각이 들었어요~!

Copy link
Author

@simorimi simorimi Oct 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 states, handler 이런식으로 반환하던 것을 editModal, profileImage, profileNickname, ProfileEdit, userProfile 로 반환하고 해당 순서로 정리해 뒀습니다. 또한 해당 부분을 각각 훅으로 분리해서 훨씬 가독성이 좋아졌네요 좋은 피드백 감사합니다 :)


Expand Down
58 changes: 58 additions & 0 deletions frontend/src/components/pages/my/hooks/useProfileEdit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from "react";

import usePutProfile from "@queries/usePutProfile";

interface UseProfileEdit {
userNickname: string | undefined;
nickname: string;
updateNickname: (newNickname: string) => void;
userProfileImageUrl: string | undefined;
profileImageUrl: string;
updateProfileImageUrl: (newProfileImageUrl: string) => void;
}

const useProfileEdit = ({
userNickname,
nickname,
updateNickname,
userProfileImageUrl,
profileImageUrl,
updateProfileImageUrl,
}: UseProfileEdit) => {
const onError = (error: Error) => {
alert(error.message);
updateNickname(userNickname ?? "");
updateProfileImageUrl(userProfileImageUrl ?? "");
};

const { mutate: mutateModifyProfile } = usePutProfile(onError);

const [isModifying, setIsModifying] = useState(false);

const handleClickProfileEditButton = () => setIsModifying(true);

const handleClickProfileEditConfirmButton = () => {
const trimmedNickname = nickname.trim();
const newNickname = trimmedNickname || userNickname || "";

updateNickname(newNickname);
mutateModifyProfile({ nickname: newNickname, profileImageUrl });

setIsModifying(false);
};

const handleClickProfileEditCancelButton = () => {
updateNickname(userNickname ?? "");
updateProfileImageUrl(userProfileImageUrl ?? "");

setIsModifying(false);
};
return {
isModifying,
handleClickProfileEditButton,
handleClickProfileEditConfirmButton,
handleClickProfileEditCancelButton,
};
};

export default useProfileEdit;
Loading