-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] 마이페이지 찜목록 및 프로필 편집 기능 구현 #79
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
Changes from all commits
ecaeb33
858dd80
edf09f5
a8ad229
7095425
213157a
0dbeac1
671c385
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import api from "@/apis/instance/api"; | ||
|
|
||
| export const postUpdateProfile = async ({ name, location, profileColor }) => { | ||
| const response = await api.post("/member/update", { | ||
| name, | ||
| location, | ||
| profileColor, | ||
| }); | ||
| return response.data; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { default as IcError } from "./errorIcon.svg?react"; | ||
| export { default as IcWarnning } from "./Ic_Warn.svg?react"; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,207 @@ | ||||||||||||||||||||||||||||||||||||||||
| import { useState, useEffect } from "react"; | ||||||||||||||||||||||||||||||||||||||||
| import { useNavigate } from "react-router-dom"; | ||||||||||||||||||||||||||||||||||||||||
| import { useMyProfile, useUpdateProfile } from "@/apis/member/queries"; | ||||||||||||||||||||||||||||||||||||||||
| import ToastModal from "@components/common/ToastModal"; | ||||||||||||||||||||||||||||||||||||||||
| import BackIcon from "/svgs/common/Ic_Arrow_Left.svg"; | ||||||||||||||||||||||||||||||||||||||||
| import { IcWarnning } from "@assets/svgs/modal"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
| IcCheck, | ||||||||||||||||||||||||||||||||||||||||
| IcNonCheck, | ||||||||||||||||||||||||||||||||||||||||
| ImgOrange, | ||||||||||||||||||||||||||||||||||||||||
| ImgGray, | ||||||||||||||||||||||||||||||||||||||||
| ImgPink, | ||||||||||||||||||||||||||||||||||||||||
| ImgBlue, | ||||||||||||||||||||||||||||||||||||||||
| } from "@assets/svgs/signup"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const profileSvgs = [ImgGray, ImgPink, ImgBlue, ImgOrange]; | ||||||||||||||||||||||||||||||||||||||||
| const profileColors = ["gray", "pink", "blue", "orange"]; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const MyPageEdit = () => { | ||||||||||||||||||||||||||||||||||||||||
| const navigate = useNavigate(); | ||||||||||||||||||||||||||||||||||||||||
| const { data } = useMyProfile(); | ||||||||||||||||||||||||||||||||||||||||
| const { mutate: updateProfile } = useUpdateProfile(); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const [name, setName] = useState(""); | ||||||||||||||||||||||||||||||||||||||||
| const [location, setAddress] = useState(""); | ||||||||||||||||||||||||||||||||||||||||
| const [profileColor, setProfileColor] = useState("gray"); | ||||||||||||||||||||||||||||||||||||||||
| const [toastVisible, setToastVisible] = useState(false); | ||||||||||||||||||||||||||||||||||||||||
| const [isNameFocused, setIsNameFocused] = useState(false); | ||||||||||||||||||||||||||||||||||||||||
| const [isLocationFocused, setIsLocationFocused] = useState(false); | ||||||||||||||||||||||||||||||||||||||||
| const [checked, setChecked] = useState(false); | ||||||||||||||||||||||||||||||||||||||||
| const [toastMessage, setToastMessage] = useState(""); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||
| if (data) { | ||||||||||||||||||||||||||||||||||||||||
| setName(data.name || ""); | ||||||||||||||||||||||||||||||||||||||||
| setAddress(data.address || ""); | ||||||||||||||||||||||||||||||||||||||||
| setProfileColor(data.profileColor || "gray"); | ||||||||||||||||||||||||||||||||||||||||
| setChecked(data.address === "서울 외 지역 거주"); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }, [data]); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const handleSubmit = () => { | ||||||||||||||||||||||||||||||||||||||||
| if (!name.trim()) { | ||||||||||||||||||||||||||||||||||||||||
| setToastVisible(true); | ||||||||||||||||||||||||||||||||||||||||
| setToastMessage("이름을 입력해주세요."); | ||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||
| setToastVisible(false); | ||||||||||||||||||||||||||||||||||||||||
| }, 1800); | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const fullLocation = checked | ||||||||||||||||||||||||||||||||||||||||
| ? "서울 외 지역 거주" | ||||||||||||||||||||||||||||||||||||||||
| : location.trim().startsWith("서울특별시") | ||||||||||||||||||||||||||||||||||||||||
| ? location.trim() | ||||||||||||||||||||||||||||||||||||||||
| : `서울특별시 ${location.trim()}`; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| updateProfile( | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| name, | ||||||||||||||||||||||||||||||||||||||||
| location: fullLocation, | ||||||||||||||||||||||||||||||||||||||||
| profileColor, | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| onSuccess: () => { | ||||||||||||||||||||||||||||||||||||||||
| setToastVisible(true); | ||||||||||||||||||||||||||||||||||||||||
| setToastMessage("프로필이 저장되었습니다!"); | ||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||
| setToastVisible(false); | ||||||||||||||||||||||||||||||||||||||||
| navigate("/mypage"); | ||||||||||||||||||||||||||||||||||||||||
| }, 1500); | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| onError: (err) => { | ||||||||||||||||||||||||||||||||||||||||
| console.error("프로필 업데이트 실패:", err); | ||||||||||||||||||||||||||||||||||||||||
| alert("프로필 업데이트에 실패했습니다."); | ||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const toggleCheck = () => { | ||||||||||||||||||||||||||||||||||||||||
| setChecked((prev) => { | ||||||||||||||||||||||||||||||||||||||||
| const next = !prev; | ||||||||||||||||||||||||||||||||||||||||
| if (next) setAddress("서울 외 지역 거주"); | ||||||||||||||||||||||||||||||||||||||||
| else setAddress(""); | ||||||||||||||||||||||||||||||||||||||||
| return next; | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const trimmed = location.trim(); | ||||||||||||||||||||||||||||||||||||||||
| const showWarning = | ||||||||||||||||||||||||||||||||||||||||
| (!checked && !trimmed.startsWith("서울특별시")) || checked; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const selectedIndex = profileColors.indexOf(profileColor); | ||||||||||||||||||||||||||||||||||||||||
| const SelectedProfile = profileSvgs[selectedIndex] ?? ImgGray; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <div className="min-h-screen bg-gray-2 pb-32"> | ||||||||||||||||||||||||||||||||||||||||
| <div className="fixed top-0 left-1/2 -translate-x-1/2 w-full max-w-[760px] h-24 pb-4 bg-white z-50 flex items-end justify-between px-4 sm:px-6 border-b"> | ||||||||||||||||||||||||||||||||||||||||
| <button onClick={() => navigate(-1)} className="w-8 h-8"> | ||||||||||||||||||||||||||||||||||||||||
| <img src={BackIcon} alt="뒤로가기" className="w-8 h-8" /> | ||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+101
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 접근성 개선 필요 뒤로가기 버튼에 적절한 접근성 속성이 부족합니다. 스크린 리더 사용자를 위한 명확한 정보를 제공해야 합니다. - <button onClick={() => navigate(-1)} className="w-8 h-8">
+ <button
+ onClick={() => navigate(-1)}
+ className="w-8 h-8"
+ aria-label="뒤로가기">
<img src={BackIcon} alt="뒤로가기" className="w-8 h-8" />
</button>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| <h1 className="h3 text-gray-12">프로필 편집</h1> | ||||||||||||||||||||||||||||||||||||||||
| <div className="w-8 h-8" /> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| <div className="pt-28 max-w-[760px] overflow-x-scroll"> | ||||||||||||||||||||||||||||||||||||||||
| <div className="flex justify-center mb-6 px-5"> | ||||||||||||||||||||||||||||||||||||||||
| <div className="w-32 h-32 sm:w-36 sm:h-36"> | ||||||||||||||||||||||||||||||||||||||||
| <SelectedProfile className="w-full h-full" /> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <div className="flex w-full gap-3 sm:gap-4 overflow-x-auto justify-center no-scrollbar mb-10"> | ||||||||||||||||||||||||||||||||||||||||
| {profileSvgs.map((SvgComponent, index) => ( | ||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||
| key={index} | ||||||||||||||||||||||||||||||||||||||||
| className="relative rounded-full cursor-pointer shrink-0" | ||||||||||||||||||||||||||||||||||||||||
| onClick={() => setProfileColor(profileColors[index])} | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| <SvgComponent className="w-20 h-20 sm:w-24 sm:h-24 rounded-full" /> | ||||||||||||||||||||||||||||||||||||||||
| {profileColor === profileColors[index] && ( | ||||||||||||||||||||||||||||||||||||||||
| <div className="absolute top-0 right-0 w-6 h-6 sm:w-8 sm:h-8"> | ||||||||||||||||||||||||||||||||||||||||
| <IcCheck className="w-full h-full text-primary-8" /> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <div className="mb-6 px-5"> | ||||||||||||||||||||||||||||||||||||||||
| <label className="b4 text-gray-8">이름</label> | ||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||
| className={`w-full b2 mt-3 rounded-lg p-3 bg-gray-3 text-gray-11 outline-none border-2 transition-colors ${ | ||||||||||||||||||||||||||||||||||||||||
| isNameFocused || name ? "border-primary-8" : "border-gray-4" | ||||||||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||||||||
| value={name} | ||||||||||||||||||||||||||||||||||||||||
| onFocus={() => setIsNameFocused(true)} | ||||||||||||||||||||||||||||||||||||||||
| onBlur={() => setIsNameFocused(false)} | ||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setName(e.target.value)} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <div className="mb-6 px-5"> | ||||||||||||||||||||||||||||||||||||||||
| <label className="b4 text-gray-8">주소</label> | ||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||
| className={`relative w-full mt-3 flex items-center rounded-lg px-3 py-3 border-2 transition-colors bg-gray-3 text-gray-11 ${ | ||||||||||||||||||||||||||||||||||||||||
| isLocationFocused || trimmed | ||||||||||||||||||||||||||||||||||||||||
| ? "border-primary-8" | ||||||||||||||||||||||||||||||||||||||||
| : "border-gray-4" | ||||||||||||||||||||||||||||||||||||||||
| } ${checked ? "bg-gray-2 cursor-not-allowed" : ""}`} | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||||||||
| value={checked ? "서울 외 지역 거주" : location} | ||||||||||||||||||||||||||||||||||||||||
| onFocus={() => setIsLocationFocused(true)} | ||||||||||||||||||||||||||||||||||||||||
| onBlur={() => setIsLocationFocused(false)} | ||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setAddress(e.target.value)} | ||||||||||||||||||||||||||||||||||||||||
| readOnly={checked} | ||||||||||||||||||||||||||||||||||||||||
| className={`flex-1 bg-transparent outline-none b2 placeholder-gray-6 ${ | ||||||||||||||||||||||||||||||||||||||||
| checked ? "cursor-not-allowed" : "" | ||||||||||||||||||||||||||||||||||||||||
| }`} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| {showWarning && ( | ||||||||||||||||||||||||||||||||||||||||
| <p className="b5 text-primary-8 mt-2"> | ||||||||||||||||||||||||||||||||||||||||
| 현재는 서울에 한해 사회적 기업들을 소개하고 있습니다. | ||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||
| onClick={toggleCheck} | ||||||||||||||||||||||||||||||||||||||||
| className="flex items-center mt-4 active:opacity-80" | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| {checked ? ( | ||||||||||||||||||||||||||||||||||||||||
| <IcCheck className="w-5 h-5 mr-2 text-primary-8" /> | ||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||
| <IcNonCheck className="w-5 h-5 mr-2 text-gray-6" /> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
| <span className={`b5 ${checked ? "text-gray-12" : "text-gray-6"}`}> | ||||||||||||||||||||||||||||||||||||||||
| 현재 서울에 살고 있지 않습니다. | ||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| <div className="fixed bottom-0 left-1/2 -translate-x-1/2 w-full max-w-[760px] bg-white px-5 pb-6 pt-4 border-t"> | ||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||
| onClick={handleSubmit} | ||||||||||||||||||||||||||||||||||||||||
| className="w-full h-12 px-4 py-3 bg-primary-8 text-white b1 rounded-xl" | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| 저장 | ||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+189
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 저장 버튼 상태 처리 개선 현재 저장 버튼은 프로필 업데이트 중에도 클릭 가능한 상태로 유지됩니다. 여러 번 클릭하면 중복 요청이 발생할 수 있습니다. 업데이트 진행 중일 때 버튼 비활성화와 로딩 상태를 표시하는 것이 좋습니다. + const { mutate: updateProfile, isLoading } = useUpdateProfile();
// ...
<button
onClick={handleSubmit}
- className="w-full h-12 px-4 py-3 bg-primary-8 text-white b1 rounded-xl"
+ className={`w-full h-12 px-4 py-3 text-white b1 rounded-xl ${
+ isLoading ? "bg-primary-5" : "bg-primary-8"
+ }`}
+ disabled={isLoading}
>
- 저장
+ {isLoading ? "저장 중..." : "저장"}
</button>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| {toastVisible && ( | ||||||||||||||||||||||||||||||||||||||||
| <ToastModal | ||||||||||||||||||||||||||||||||||||||||
| message={toastMessage} | ||||||||||||||||||||||||||||||||||||||||
| icon={toastMessage === "이름을 입력해주세요." ? IcWarnning : null} | ||||||||||||||||||||||||||||||||||||||||
| duration={1800} | ||||||||||||||||||||||||||||||||||||||||
| onClose={() => setToastVisible(false)} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export default MyPageEdit; | ||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
에러 처리 개선 필요
현재 프로필 업데이트 실패 시 콘솔 에러와 alert를 사용하고 있습니다. 사용자 경험 향상을 위해 ToastModal 컴포넌트를 활용하여 일관된 에러 피드백을 제공하는 것이 좋겠습니다.
onError: (err) => { console.error("프로필 업데이트 실패:", err); - alert("프로필 업데이트에 실패했습니다."); + setToastVisible(true); + setTimeout(() => { + setToastVisible(false); + }, 1500); },추가로 아래와 같이 ToastModal 컴포넌트에 에러 메시지를 전달할 수 있도록 상태 변수를 추가하세요:
🤖 Prompt for AI Agents