Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions src/apis/member/postUpdateProfile.js
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;
};
9 changes: 8 additions & 1 deletion src/apis/member/queries.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { getMyProfile } from "@/apis/member/auth";
import { getReviewCountOfMember } from "@/apis/member/getReviewCountOfMember";
import { getCheerCountOfMember } from "@/apis/member/getCheerCountOfMember";
import { getLikeCountOfMember } from "@/apis/member/getLikeCountOfMember";
import { postUpdateProfile } from "./postUpdateProfile";

export const useMyProfile = () => {
return useQuery({
Expand Down Expand Up @@ -34,3 +35,9 @@ export const useGetLikeCountOfMember = () => {
queryFn: () => getLikeCountOfMember(),
});
};

export const useUpdateProfile = () => {
return useMutation({
mutationFn: postUpdateProfile,
});
};
5 changes: 5 additions & 0 deletions src/apis/myPage/getDetail.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import api from "@/apis/instance/api";

export const getHearts = async () => {
const res = await api.get("/company/member-saves");
return res.data;
};

export const getReviews = async () => {
const res = await api.get("/reviews/get-all-member-reviews");
return res.data;
Expand Down
11 changes: 9 additions & 2 deletions src/apis/myPage/queries.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { getReviews } from "@/apis/myPage/getDetail";
import { getCheers } from "@/apis/myPage/getDetail";
import { getHearts, getReviews, getCheers } from "@/apis/myPage/getDetail";

export const useGetHearts = ({ enabled }) => {
return useQuery({
queryKey: ["userHearts"],
queryFn: getHearts,
enabled,
});
};

export const useGetReviews = () => {
return useQuery({ queryKey: ["userReview"], queryFn: () => getReviews() });
Expand Down
9 changes: 9 additions & 0 deletions src/assets/svgs/modal/Ic_Warn.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/assets/svgs/modal/index.js
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";
8 changes: 4 additions & 4 deletions src/pages/myPage/MyPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,15 @@ const MyPage = () => {
{showLogoutModal && <LogoutSuccessModal onClose={() => navigate("/")} />}

<div className="w-full bg-white rounded-2xl shadow-sm pt-20 pb-6 px-5 flex flex-col items-center relative z-10">
<button className="absolute top-4 right-4 opacity-50">
<button
className="absolute top-4 right-4 opacity-50"
onClick={() => navigate("/mypage/edit")}
>
<img src="/svgs/myPage/edit.svg" alt="edit" className="w-6 h-6" />
</button>

<p className="h2">{nickname}</p>
<p className="b4 text-gray-8 mt-1">{location}</p>

<div className="w-full bg-gray-3 h-[0.1px] my-4" />

<div className="w-full flex justify-around text-center">
{Object.entries(counts).map(([label, count]) => (
<div
Expand Down
Empty file.
207 changes: 207 additions & 0 deletions src/pages/myPage/components/MyPageEdit.jsx
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("프로필 업데이트에 실패했습니다.");
},
Comment on lines +75 to +77
Copy link

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 컴포넌트에 에러 메시지를 전달할 수 있도록 상태 변수를 추가하세요:

const [toastVisible, setToastVisible] = useState(false);
+ const [toastMessage, setToastMessage] = useState("");

// onSuccess에서:
setToastVisible(true);
+ setToastMessage("프로필이 저장되었습니다.");

// onError에서:
setToastVisible(true);
+ setToastMessage("프로필 업데이트에 실패했습니다.");

// 그리고 ToastModal 컴포넌트에서:
{toastVisible && (
  <ToastModal
-    message="프로필이 저장되었습니다."
+    message={toastMessage}
    duration={1800}
    onClose={() => setToastVisible(false)}
  />
)}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/pages/myPage/components/MyPageEdit.jsx around lines 62 to 64, replace the
current error handling that uses console.error and alert with the ToastModal
component for consistent user feedback. Add a state variable to hold the error
message and update it when the profile update fails, then render the ToastModal
with this error message to display the error in a user-friendly modal instead of
an 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
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button onClick={() => navigate(-1)} className="w-8 h-8">
<img src={BackIcon} alt="뒤로가기" className="w-8 h-8" />
</button>
<button
onClick={() => navigate(-1)}
className="w-8 h-8"
aria-label="뒤로가기"
>
<img src={BackIcon} alt="뒤로가기" className="w-8 h-8" />
</button>
🤖 Prompt for AI Agents
In src/pages/myPage/components/MyPageEdit.jsx around lines 88 to 90, the back
button lacks accessibility attributes for screen reader users. Add an aria-label
attribute to the button element with a descriptive label such as "Go back" to
provide clear context for assistive technologies.

<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
Copy link

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onClick={handleSubmit}
className="w-full h-12 px-4 py-3 bg-primary-8 text-white b1 rounded-xl"
>
저장
</button>
// Add at the top of your component (where hooks are initialized)
const { mutate: updateProfile, isLoading } = useUpdateProfile();
// …
<button
onClick={handleSubmit}
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>
🤖 Prompt for AI Agents
In src/pages/myPage/components/MyPageEdit.jsx around lines 180 to 184, the save
button remains clickable during profile updates, allowing multiple submissions.
Modify the button to be disabled while the update is in progress by linking its
disabled attribute to the loading state. Additionally, add a visual loading
indicator or change the button text to reflect the ongoing operation, preventing
duplicate requests and improving user feedback.

</div>
{toastVisible && (
<ToastModal
message={toastMessage}
icon={toastMessage === "이름을 입력해주세요." ? IcWarnning : null}
duration={1800}
onClose={() => setToastVisible(false)}
/>
)}
</div>
);
};

export default MyPageEdit;
59 changes: 54 additions & 5 deletions src/pages/myPageDetail/MyPageDetailPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,55 @@ import { useNavigate, useLocation } from "react-router-dom";
import heart from "/svgs/myPage/heart.svg";
import cheer from "/svgs/myPage/cheer.svg";
import review from "/svgs/myPage/review.svg";
import { useGetReviews, useGetCheers } from "@/apis/myPage/queries";
import {
useGetHearts,
useGetReviews,
useGetCheers,
} from "@/apis/myPage/queries";
import ReviewItem from "@/pages/myPageDetail/components/ReviewItem";
import StoryItem from "@/pages/myPageDetail/components/StoryItem";
import noResult from "/svgs/myPage/noResult.svg";
import Spinner from "@/components/common/Spinner";
import HeartItem from "./components/HeartItem";
import { useEffect, useMemo, useState } from "react";
import { getAllCompanies } from "@/apis/company/getAllCompanies";

const MyPageDetailPage = () => {
const navigate = useNavigate();
const location = useLocation();
const kind = location.state.kind;

const [allCompanies, setAllCompanies] = useState([]);

const { data: heartsData, isLoading: isLoadingHearts } = useGetHearts({
enabled: kind === "찜",
});

useEffect(() => {
if (kind === "찜") {
getAllCompanies().then(setAllCompanies);
}
}, [kind]);

const companyMap = useMemo(() => {
return new Map(allCompanies.map((c) => [c.companyId, c]));
}, [allCompanies]);

const enrichedHeartsData = heartsData?.map((heart) => {
const matched = companyMap.get(heart.companyId);
return {
...heart,
latitude: matched?.latitude,
longitude: matched?.longitude,
};
});

const { data: reviewsData, isLoading: isLoadingReviews } = useGetReviews({
enabled: kind === "리뷰",
});
const { data: cheersData, isLoading: isLoadingCheers } = useGetCheers({
enabled: kind === "응원",
});
// const { data: heartsData, isLoading: isLoadingHearts } = useGetHearts({ enabled: kind === "찜" });

if (isLoadingReviews || isLoadingCheers) {
return (
Expand Down Expand Up @@ -47,11 +78,11 @@ const MyPageDetailPage = () => {

{kind === "찜" && (
<div className="flex justify-between items-center px-5">
<div className="flex gap-2 items-center my-2">
<div className="flex gap-4 items-center my-2">
<img src={heart} className="w-8 h-8" />
<p className="h3">저장한 장소</p>
</div>
<p className="b5 text-gray-9">총 0개</p>
<p className="b5 text-gray-9">총 {heartsData?.length}개</p>
</div>
)}

Expand All @@ -65,7 +96,25 @@ const MyPageDetailPage = () => {
</div>
)}

<div className="flex flex-col gap-4 w-full bg-gray-2 p-5 min-h-screen">
<div className="flex flex-col gap-4 w-full bg-gray-2 p-5">
{kind === "찜" && (
<>
{!isLoadingHearts && enrichedHeartsData?.length > 0 ? (
enrichedHeartsData.map((item, idx) => (
<HeartItem data={item} key={idx} />
))
) : !isLoadingHearts && enrichedHeartsData?.length === 0 ? (
<div className="flex flex-col justify-center items-center mt-36">
<img src={noResult} />
<p className="h4 text-gray-9 text-center py-8">
아직 저장한 장소가
<br /> 없어요
</p>
</div>
) : null}
</>
)}

{kind === "리뷰" && (
<>
{!isLoadingReviews && reviewsData?.length > 0 ? (
Expand Down
Loading