{
+ 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 (
+
+
+
+
프로필 편집
+
+
+
+
+
+
+ {profileSvgs.map((SvgComponent, index) => (
+
setProfileColor(profileColors[index])}
+ >
+
+ {profileColor === profileColors[index] && (
+
+
+
+ )}
+
+ ))}
+
+
+
+
+ setIsNameFocused(true)}
+ onBlur={() => setIsNameFocused(false)}
+ onChange={(e) => setName(e.target.value)}
+ />
+
+
+
+
+
+ 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" : ""
+ }`}
+ />
+
+
+ {showWarning && (
+
+ 현재는 서울에 한해 사회적 기업들을 소개하고 있습니다.
+
+ )}
+
+
+
+
+
+
+
+ {toastVisible && (
+
setToastVisible(false)}
+ />
+ )}
+
+ );
+};
+
+export default MyPageEdit;
diff --git a/src/pages/myPageDetail/MyPageDetailPage.jsx b/src/pages/myPageDetail/MyPageDetailPage.jsx
index b7acf29..c21a23b 100644
--- a/src/pages/myPageDetail/MyPageDetailPage.jsx
+++ b/src/pages/myPageDetail/MyPageDetailPage.jsx
@@ -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 (
@@ -47,11 +78,11 @@ const MyPageDetailPage = () => {
{kind === "찜" && (
-
+
저장한 장소
-
총 0개
+
총 {heartsData?.length}개
)}
@@ -65,7 +96,25 @@ const MyPageDetailPage = () => {
)}
-
+
+ {kind === "찜" && (
+ <>
+ {!isLoadingHearts && enrichedHeartsData?.length > 0 ? (
+ enrichedHeartsData.map((item, idx) => (
+
+ ))
+ ) : !isLoadingHearts && enrichedHeartsData?.length === 0 ? (
+
+

+
+ 아직 저장한 장소가
+
없어요
+
+
+ ) : null}
+ >
+ )}
+
{kind === "리뷰" && (
<>
{!isLoadingReviews && reviewsData?.length > 0 ? (
diff --git a/src/pages/myPageDetail/components/HeartItem.jsx b/src/pages/myPageDetail/components/HeartItem.jsx
new file mode 100644
index 0000000..d64506b
--- /dev/null
+++ b/src/pages/myPageDetail/components/HeartItem.jsx
@@ -0,0 +1,142 @@
+import {
+ companyTypeNameMap,
+ companyTypeIconMap,
+ businessTypeNameMap,
+} from "@constants/categoryMap";
+import HeartIcon from "/svgs/common/Ic_Heart_Fill.svg";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { unlikeCompany } from "@apis/company/getLikedCompanies";
+import ToastModal from "@components/common/ToastModal";
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { useUserCoords } from "@pages/search/hooks/useUserCoords";
+import { getDistanceFromLatLon } from "@pages/map/utils/getDistanceFromLatLon";
+
+const HeartItem = ({ data }) => {
+ const {
+ companyId,
+ companyName,
+ companyCategory,
+ companyType,
+ companyLocation,
+ business,
+ latitude,
+ longitude,
+ } = data;
+
+ const navigate = useNavigate();
+ const userCoords = useUserCoords();
+ console.log("userCoords", userCoords);
+
+ const handleClick = () => {
+ navigate(`/map/${companyId}`);
+ };
+
+ const [distance, setDistance] = useState(null);
+
+ useEffect(() => {
+ if (userCoords && latitude && longitude) {
+ const d = getDistanceFromLatLon(
+ userCoords.lat,
+ userCoords.lng,
+ latitude,
+ longitude
+ );
+ setDistance(d);
+ }
+ }, [userCoords, latitude, longitude]);
+
+ const formatDistance = (distanceInMeters) => {
+ if (distanceInMeters < 1000) {
+ return `${Math.round(distanceInMeters)}m`;
+ }
+ return `${(distanceInMeters / 1000).toFixed(1)}km`;
+ };
+
+ const queryClient = useQueryClient();
+ const [toastVisible, setToastVisible] = useState(false);
+
+ const { mutate: unlike, isLoading } = useMutation({
+ mutationFn: () => unlikeCompany(companyId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["userHearts"] });
+ setToastVisible(true);
+ setTimeout(() => setToastVisible(false), 2000);
+ },
+ });
+
+ const handleUnlike = (e) => {
+ e.stopPropagation();
+ if (!isLoading) unlike();
+ };
+
+ return (
+ <>
+
+
+
+
+ {companyName}
+
+ {businessTypeNameMap[companyCategory] ?? companyCategory}
+
+
+
+ {distance !== null && (
+
+
+ {formatDistance(distance)}
+
+
·
+
+ {companyLocation}
+
+
+ )}
+
+
+
+
+
+ {business && (
+
+
+
{business}
+
+ {companyType && (
+
+

+
+ {companyTypeNameMap[companyType]}
+
+
+ )}
+
+
+ )}
+
+
+ {toastVisible && (
+
setToastVisible(false)}
+ />
+ )}
+ >
+ );
+};
+
+export default HeartItem;
diff --git a/src/pages/myPageDetail/components/MapCompanyPage.jsx b/src/pages/myPageDetail/components/MapCompanyPage.jsx
new file mode 100644
index 0000000..63d2c66
--- /dev/null
+++ b/src/pages/myPageDetail/components/MapCompanyPage.jsx
@@ -0,0 +1,110 @@
+import { useParams, useNavigate } from "react-router-dom";
+import { useEffect, useState } from "react";
+import { getCompanyPreview } from "@apis/company/getCompanyPreview";
+import { getAllCompanies } from "@apis/company/getAllCompanies";
+import { getLikedCompanies } from "@apis/company/getLikedCompanies";
+import useAuthStore from "@/store/authStore";
+import { useToggleLike } from "@pages/map/hooks/useToggleLike";
+import PlaceBottomSheet from "@pages/map/components/PlaceBottomSheet";
+import MapViewer from "@/pages/search/components/MapViewer";
+
+const MapCompanyPage = () => {
+ const { companyId } = useParams();
+ const navigate = useNavigate();
+
+ const [place, setPlace] = useState(null);
+ const [isBottomSheetVisible, setIsBottomSheetVisible] = useState(true);
+ const [isBottomSheetExpanded, setIsBottomSheetExpanded] = useState(false);
+ const [bottomSheetHeight, setBottomSheetHeight] = useState(220);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const [preview, allCompanies] = await Promise.all([
+ getCompanyPreview(companyId),
+ getAllCompanies(),
+ ]);
+
+ const locationData = allCompanies.find(
+ (c) => String(c.companyId) === String(companyId)
+ );
+
+ if (!locationData?.latitude || !locationData?.longitude) {
+ console.warn("기업 좌표 정보가 누락되었습니다:", locationData);
+ return;
+ }
+
+ const isAuthenticated = await useAuthStore.getState().checkAuth();
+ const likedList = isAuthenticated ? await getLikedCompanies() : [];
+ const liked = likedList.some((c) => c.companyId === Number(companyId));
+
+ const enriched = {
+ ...preview,
+ coords: {
+ lat: locationData.latitude,
+ lng: locationData.longitude,
+ },
+ liked,
+ };
+
+ setPlace(enriched);
+ } catch (err) {
+ console.error("기업 정보 로딩 실패:", err);
+ }
+ };
+
+ fetchData();
+ }, [companyId]);
+
+ const { toggleLike } = useToggleLike({
+ placesWithDistance: place ? [place] : [],
+ setPlacesWithDistance: () => {},
+ setFilteredPlaces: () => {},
+ selectedPlace: place,
+ setSelectedPlace: setPlace,
+ showOnlyLiked: false,
+ onRequireLogin: () => {},
+ });
+
+ if (!place) return 로딩 중...
;
+
+ return (
+
+
navigate(-1)}
+ >
+

+
+
+
setIsBottomSheetVisible(true)}
+ moveToCurrentLocation={false}
+ onMoveComplete={() => {}}
+ userCoords={null}
+ disableAutoUserPan={true}
+ />
+ {place && isBottomSheetVisible && (
+ setIsBottomSheetVisible(false)}
+ onExpandChange={setIsBottomSheetExpanded}
+ onToggleLike={toggleLike}
+ onHeightChange={setBottomSheetHeight}
+ />
+ )}
+
+ );
+};
+
+export default MapCompanyPage;
diff --git a/src/pages/story/components/StoryDetail.jsx b/src/pages/story/components/StoryDetail.jsx
index 6955ba1..c01610d 100644
--- a/src/pages/story/components/StoryDetail.jsx
+++ b/src/pages/story/components/StoryDetail.jsx
@@ -5,12 +5,14 @@ import useUIStore from "@/store/uiStore";
import { useGetBestStoryDetail } from "@/apis/story/queries";
import { usePatchStoryLike } from "@/apis/story/queries";
import HaveToLoginModal from "@components/common/HaveToLoginModal";
+import ToastModal from "@/components/common/ToastModal";
const StoryDetail = () => {
const navigate = useNavigate();
const { storyId } = useParams();
const { setIsStoryDetail } = useUIStore();
const { data, isLoading } = useGetBestStoryDetail(storyId);
+ const [showToast, setShowToast] = useState(false);
useEffect(() => {
setIsStoryDetail(true);
@@ -22,6 +24,10 @@ const StoryDetail = () => {
const handleLike = () => {
likeStory(undefined, {
+ onSuccess: () => {
+ setShowToast(true);
+ setTimeout(() => setShowToast(false), 3000);
+ },
onError: (error) => {
const status = error?.response?.status;
@@ -118,6 +124,13 @@ const StoryDetail = () => {
+ {showToast && (
+
setShowToast(false)}
+ />
+ )}
+
{errorModal.open && (
,
},
+ {
+ path: "/map/:companyId",
+ element: ,
+ },
{
path: "/mypage",
element: ,
@@ -92,6 +98,10 @@ const router = createBrowserRouter([
path: "/signup", // 탭 없는 별도 페이지
element: ,
},
+ {
+ path: "/mypage/edit",
+ element: ,
+ },
]);
export default router;