Skip to content

Commit 1d11a2b

Browse files
authored
Merge pull request #101 from Leets-Official/86-feat/마이페이지-API-연결
[Feat] 마이페이지 API 연동
2 parents 5117b6f + 316a3c8 commit 1d11a2b

25 files changed

Lines changed: 482 additions & 53 deletions

src/pages/buy/ui/BuyFilter.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const FilterSection = ({ title, children }: { title: string; children: ReactNode
1515
type FilterItem = {
1616
id: string;
1717
label: string;
18+
[key: string]: unknown;
1819
};
1920

2021
type ModelItem = FilterItem & {
@@ -36,6 +37,8 @@ export type BuyFilterProps = {
3637
onSetShowAllModels: (value: boolean) => void;
3738
onReset: () => void;
3839
defaultModelCount?: number;
40+
availableOnly?: boolean;
41+
onSetAvailableOnly?: (value: boolean) => void;
3942
};
4043

4144
export const BuyFilter = ({

src/pages/mypage/model/filterTradeItems.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const filterTradeItems = (items: TradeListItem[], status: StatusFilter):
66
return items;
77
}
88
if (status === 'buying') {
9-
return items.filter((item) => item.status === 'buying' || item.status === 'reserved');
9+
return items.filter((item) => item.status === 'buying');
1010
}
1111
return items.filter((item) => item.status === status);
1212
};

src/pages/mypage/model/useMyPageState.ts

Lines changed: 168 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,180 @@
1-
import { BUY_ITEMS, FAVORITE_PRODUCT_ITEMS, FAVORITE_REPAIR_ITEMS, SELL_ITEMS } from '@shared/mocks/data/mypage';
2-
import { useState } from 'react';
1+
import {
2+
useMyPageBuyHistoryQuery,
3+
useMyPageProfileQuery,
4+
useMyPageSellHistoryQuery,
5+
type TradeHistoryItem,
6+
type TradeHistoryQueryStatus,
7+
type TradeHistoryStatus,
8+
} from '@shared/apis/mypage';
9+
import {
10+
useWishlistPostListQuery,
11+
useWishlistShopListQuery,
12+
type WishlistPostItem,
13+
type WishlistShopItem,
14+
} from '@shared/apis/wishlist';
15+
import { useCallback, useMemo, useState } from 'react';
316
import { createStatusTabs } from './createStatusTabs';
417
import { filterTradeItems } from './filterTradeItems';
518
import type { FavoriteCategory, FavoriteTabs, MainTabId, StatusFilter } from './types';
19+
import type { TradeListItem } from '@pages/mypage/ui/TradeItemList';
20+
import type { RepairListItem } from '@shared/ui/RepairList';
21+
22+
const formatPrice = (value: number) => `${new Intl.NumberFormat('ko-KR').format(value)}원`;
23+
const formatDate = (value: string) => {
24+
const date = new Date(value);
25+
if (Number.isNaN(date.getTime())) {
26+
return value;
27+
}
28+
const year = date.getFullYear();
29+
const month = String(date.getMonth() + 1).padStart(2, '0');
30+
const day = String(date.getDate()).padStart(2, '0');
31+
return `${year}.${month}.${day}`;
32+
};
33+
34+
const mapHistoryItem = (item: TradeHistoryItem, options?: { statusLabel?: string }): TradeListItem => ({
35+
id: String(item.postId),
36+
modelName: item.title,
37+
price: formatPrice(item.price),
38+
date: formatDate(item.createdAt),
39+
status: item.status === 'COMPLETED' ? 'completed' : 'buying',
40+
statusLabel: options?.statusLabel,
41+
imageUrl: item.thumbnailUrl ?? undefined,
42+
});
43+
44+
const mapWishlistPostItem = (item: WishlistPostItem, index: number): TradeListItem => {
45+
const resolvedId = item.postId ?? item.id ?? `wishlist-post-${index}`;
46+
return {
47+
id: String(resolvedId),
48+
modelName: item.title ?? '상품',
49+
price: item.price !== undefined ? formatPrice(item.price) : '가격 정보 없음',
50+
date: item.createdAt ? formatDate(item.createdAt) : '',
51+
status: 'favorite',
52+
favoriteActive: true,
53+
imageUrl: item.thumbnailUrl ?? item.thumbnail ?? undefined,
54+
};
55+
};
56+
57+
const mapWishlistShopItem = (item: WishlistShopItem, index: number): RepairListItem => ({
58+
id: `${item.shopName ?? 'shop'}-${index}`,
59+
name: item.shopName ?? '수리점',
60+
address: item.location ?? '',
61+
favoriteActive: true,
62+
});
663

764
export const useMyPageState = () => {
65+
const { data: profileData } = useMyPageProfileQuery();
66+
const { data: wishlistPosts = [] } = useWishlistPostListQuery();
67+
const { data: wishlistShops = [] } = useWishlistShopListQuery();
868
const [activeTab, setActiveTab] = useState<MainTabId>('buy');
969
const [buyStatus, setBuyStatus] = useState<StatusFilter>('all');
1070
const [sellStatus, setSellStatus] = useState<StatusFilter>('all');
1171
const [favoriteCategory, setFavoriteCategory] = useState<FavoriteCategory>('product');
72+
const [sellStatusOverrides, setSellStatusOverrides] = useState<Record<number, TradeHistoryStatus>>({});
73+
74+
const mapStatusToQuery = (status: StatusFilter): TradeHistoryQueryStatus => {
75+
if (status === 'completed') {
76+
return 'COMPLETED';
77+
}
78+
if (status === 'buying') {
79+
return 'RESERVED';
80+
}
81+
return 'ALL';
82+
};
83+
84+
const buyStatusQuery = mapStatusToQuery(buyStatus);
85+
const sellStatusQuery = mapStatusToQuery(sellStatus);
86+
87+
const { data: buyAllHistory } = useMyPageBuyHistoryQuery('ALL');
88+
const { data: sellAllHistory } = useMyPageSellHistoryQuery('ALL');
89+
const { data: buyHistory } = useMyPageBuyHistoryQuery(buyStatusQuery);
90+
const { data: sellHistory } = useMyPageSellHistoryQuery(sellStatusQuery);
91+
92+
const buyItemsForCount = useMemo(
93+
() => (buyAllHistory ?? profileData?.buyList ?? []).map((item) => mapHistoryItem(item)),
94+
[buyAllHistory, profileData?.buyList]
95+
);
96+
97+
const resolveSellStatus = useCallback(
98+
(item: TradeHistoryItem): TradeHistoryStatus => {
99+
return sellStatusOverrides[item.postId] ?? item.status;
100+
},
101+
[sellStatusOverrides]
102+
);
103+
104+
const mapSellHistoryItem = useCallback(
105+
(item: TradeHistoryItem): TradeListItem => {
106+
const resolvedStatus = resolveSellStatus(item);
107+
const mapped = mapHistoryItem(
108+
{
109+
...item,
110+
status: resolvedStatus,
111+
},
112+
{ statusLabel: resolvedStatus === 'COMPLETED' ? '판매완료' : '판매중' }
113+
);
114+
const uiStatus = resolvedStatus === 'COMPLETED' ? 'completed' : 'buying';
115+
return {
116+
...mapped,
117+
status: uiStatus,
118+
statusEditable: true,
119+
statusOptions: [
120+
{ value: 'buying', label: '판매중' },
121+
{ value: 'completed', label: '판매완료' },
122+
],
123+
onStatusChange: (nextStatus) => {
124+
const nextHistoryStatus: TradeHistoryStatus = nextStatus === 'completed' ? 'COMPLETED' : 'RESERVED';
125+
setSellStatusOverrides((prev) => ({
126+
...prev,
127+
[item.postId]: nextHistoryStatus,
128+
}));
129+
},
130+
};
131+
},
132+
[resolveSellStatus]
133+
);
134+
135+
const sellItemsForCount = useMemo(
136+
() => (sellAllHistory ?? []).map((item) => mapSellHistoryItem(item)),
137+
[sellAllHistory, mapSellHistoryItem]
138+
);
139+
140+
const buyItems = useMemo(() => {
141+
const serverItems = buyHistory ?? (buyStatusQuery === 'ALL' ? buyAllHistory : undefined);
142+
if (buyStatusQuery !== 'ALL' && serverItems && serverItems.length === 0 && buyItemsForCount.length) {
143+
return filterTradeItems(buyItemsForCount, buyStatus);
144+
}
145+
const baseItems = serverItems ?? buyAllHistory ?? profileData?.buyList ?? [];
146+
return baseItems.map((item) =>
147+
mapHistoryItem(item, { statusLabel: item.status === 'COMPLETED' ? '구매완료' : '구매중' })
148+
);
149+
}, [buyHistory, buyStatusQuery, buyAllHistory, buyItemsForCount, buyStatus, profileData?.buyList]);
150+
const sellItems = useMemo(() => {
151+
const serverItems = sellHistory ?? (sellStatusQuery === 'ALL' ? sellAllHistory : undefined);
152+
if (sellStatusQuery !== 'ALL' && serverItems && serverItems.length === 0 && sellItemsForCount.length) {
153+
return filterTradeItems(sellItemsForCount, sellStatus);
154+
}
155+
const baseItems = serverItems ?? sellAllHistory ?? [];
156+
return baseItems.map((item) => mapSellHistoryItem(item));
157+
}, [sellHistory, sellStatusQuery, sellAllHistory, sellItemsForCount, sellStatus, mapSellHistoryItem]);
158+
159+
const buyStatusTabs = createStatusTabs(buyItemsForCount, { buying: '구매중', completed: '구매완료' });
160+
const sellStatusTabs = createStatusTabs(sellItemsForCount, { buying: '판매중', completed: '판매완료' });
12161

13-
const buyStatusTabs = createStatusTabs(BUY_ITEMS, { buying: '구매중', completed: '구매완료' });
14-
const sellStatusTabs = createStatusTabs(SELL_ITEMS, { buying: '판매중', completed: '판매완료' });
162+
const favoriteProductItems = useMemo(
163+
() => wishlistPosts.map((item, index) => mapWishlistPostItem(item, index)),
164+
[wishlistPosts]
165+
);
166+
const favoriteRepairItems = useMemo(
167+
() => wishlistShops.map((item, index) => mapWishlistShopItem(item, index)),
168+
[wishlistShops]
169+
);
15170

16171
const favoriteTabs: FavoriteTabs = [
17-
{ id: 'product', label: '상품', count: FAVORITE_PRODUCT_ITEMS.length },
18-
{ id: 'repair', label: '수리점', count: FAVORITE_REPAIR_ITEMS.length },
172+
{ id: 'product', label: '상품', count: favoriteProductItems.length },
173+
{ id: 'repair', label: '수리점', count: favoriteRepairItems.length },
19174
];
20175

21-
const filteredBuyItems = filterTradeItems(BUY_ITEMS, buyStatus);
22-
const filteredSellItems = filterTradeItems(SELL_ITEMS, sellStatus);
176+
const filteredBuyItems = filterTradeItems(buyItems, buyStatus);
177+
const filteredSellItems = filterTradeItems(sellItems, sellStatus);
23178

24179
const currentStatusTabs = activeTab === 'buy' ? buyStatusTabs : sellStatusTabs;
25180
const currentStatus = activeTab === 'buy' ? buyStatus : sellStatus;
@@ -60,7 +215,10 @@ export const useMyPageState = () => {
60215
currentFilteredItems,
61216

62217
// Raw data
63-
favoriteProductItems: FAVORITE_PRODUCT_ITEMS,
64-
favoriteRepairItems: FAVORITE_REPAIR_ITEMS,
218+
profileData,
219+
buyItems,
220+
sellItems,
221+
favoriteProductItems,
222+
favoriteRepairItems,
65223
};
66224
};

src/pages/mypage/ui/AccountSettingsPage.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
1+
import { useMyPageProfileQuery } from '@shared/apis/mypage';
12
import { ChevronRightIcon, PictureIcon } from '@shared/assets/icons';
23
import { ROUTES } from '@shared/constants';
34
import { MY_PAGE_PROFILE } from '@shared/mocks/data/mypage';
45
import { Profile } from '@shared/ui/Profile';
5-
import { useRef, useState } from 'react';
6+
import { useEffect, useRef, useState } from 'react';
67
import { useNavigate } from 'react-router';
78
import { PageContainer } from './PageContainer';
89
import { getProfileSummary, saveProfile } from '../model/profileStorage';
910

1011
const AccountSettingsPage = () => {
1112
const navigate = useNavigate();
12-
const profileSummary = getProfileSummary();
13-
const [profileImage, setProfileImage] = useState(profileSummary.profileImage ?? MY_PAGE_PROFILE.profileImage);
13+
const { data: profileData } = useMyPageProfileQuery();
14+
const storedProfileSummary = getProfileSummary();
15+
const profileSummary = {
16+
nickname: profileData?.nickname ?? storedProfileSummary.nickname,
17+
email: profileData?.email ?? storedProfileSummary.email,
18+
profileImage: profileData?.profileImageUrl ?? storedProfileSummary.profileImage ?? MY_PAGE_PROFILE.profileImage,
19+
};
20+
const [profileImage, setProfileImage] = useState(profileSummary.profileImage);
1421
const fileInputRef = useRef<HTMLInputElement | null>(null);
1522

23+
useEffect(() => {
24+
if (profileData?.profileImageUrl) {
25+
setProfileImage(profileData.profileImageUrl);
26+
}
27+
}, [profileData?.profileImageUrl]);
28+
1629
const handleProfileClick = () => {
1730
fileInputRef.current?.click();
1831
};

src/pages/mypage/ui/CommonTabs.tsx

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type CommonTabsProps<TId extends string = string> = {
1616
countClassName: string;
1717
countActiveClassName: string;
1818
countInactiveClassName: string;
19+
countColorMode?: 'active' | 'positive';
1920
itemClassName?: string;
2021
};
2122

@@ -29,32 +30,38 @@ export const CommonTabs = <TId extends string>({
2930
countClassName,
3031
countActiveClassName,
3132
countInactiveClassName,
33+
countColorMode = 'active',
3234
itemClassName,
3335
}: CommonTabsProps<TId>) => {
3436
return (
3537
<section className="mt-10">
3638
<h2 className="typo-title-3 text-gray-900">{title}</h2>
3739
<div className="mt-6 border-b border-gray-200">
3840
<div className={cn('items-center self-stretch', gridClassName)}>
39-
{tabs.map((tab) => (
40-
<button
41-
key={tab.id}
42-
type="button"
43-
onClick={() => onChange(tab.id)}
44-
className={cn(
45-
'relative flex cursor-pointer flex-col items-center gap-1 pb-4 text-center transition-colors',
46-
itemClassName,
47-
activeId === tab.id
48-
? 'text-gray-900 after:absolute after:-bottom-px after:left-1/2 after:h-0.5 after:w-29.5 after:-translate-x-1/2 after:bg-black'
49-
: 'text-gray-500'
50-
)}
51-
>
52-
<span className={labelClassName}>{tab.label}</span>
53-
<span className={cn(countClassName, activeId === tab.id ? countActiveClassName : countInactiveClassName)}>
54-
{tab.count}
55-
</span>
56-
</button>
57-
))}
41+
{tabs.map((tab) => {
42+
const isCountHighlighted = countColorMode === 'positive' ? tab.count > 0 : activeId === tab.id;
43+
return (
44+
<button
45+
key={tab.id}
46+
type="button"
47+
onClick={() => onChange(tab.id)}
48+
className={cn(
49+
'relative flex cursor-pointer flex-col items-center gap-1 pb-4 text-center transition-colors',
50+
itemClassName,
51+
activeId === tab.id
52+
? 'text-gray-900 after:absolute after:-bottom-px after:left-1/2 after:h-0.5 after:w-29.5 after:-translate-x-1/2 after:bg-black'
53+
: 'text-gray-500'
54+
)}
55+
>
56+
<span className={labelClassName}>{tab.label}</span>
57+
<span
58+
className={cn(countClassName, isCountHighlighted ? countActiveClassName : countInactiveClassName)}
59+
>
60+
{tab.count}
61+
</span>
62+
</button>
63+
);
64+
})}
5865
</div>
5966
</div>
6067
</section>

src/pages/mypage/ui/MyPage.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ const MAIN_TABS: Array<MyPageTab<MainTabId>> = [
1818

1919
const MyPage = () => {
2020
const navigate = useNavigate();
21-
const profileSummary = getProfileSummary();
22-
2321
const {
2422
activeTab,
2523
setActiveTab,
@@ -32,7 +30,14 @@ const MyPage = () => {
3230
currentFilteredItems,
3331
favoriteProductItems,
3432
favoriteRepairItems,
33+
profileData,
3534
} = useMyPageState();
35+
const fallbackProfile = getProfileSummary();
36+
const profileSummary = {
37+
nickname: profileData?.nickname ?? fallbackProfile.nickname,
38+
email: profileData?.email ?? fallbackProfile.email,
39+
profileImage: profileData?.profileImageUrl ?? fallbackProfile.profileImage,
40+
};
3641

3742
const handleRepairContact = (_item: RepairListItem) => {
3843
// NOTE: 수리점 연락하기 기능 연동 후 처리 예정
@@ -72,14 +77,15 @@ const MyPage = () => {
7277
countClassName="typo-title-3"
7378
countActiveClassName="text-green-700"
7479
countInactiveClassName="text-gray-900"
80+
countColorMode="positive"
7581
itemClassName="lg:first:justify-self-start lg:last:justify-self-end"
7682
/>
7783
{favoriteCategory === 'product' ? (
7884
<TradeItemList items={favoriteProductItems} emptyMessage="찜한 목록이 아직 없어요." />
7985
) : (
8086
<RepairList
8187
items={favoriteRepairItems}
82-
emptyMessage="찜한 수리점이 아직 없어요."
88+
emptyMessage="찜한 목록이 아직 없어요."
8389
onContact={handleRepairContact}
8490
onFindRoute={handleRepairFindRoute}
8591
/>
@@ -97,6 +103,7 @@ const MyPage = () => {
97103
countClassName="typo-title-3"
98104
countActiveClassName="text-green-700"
99105
countInactiveClassName="text-gray-900"
106+
countColorMode="positive"
100107
itemClassName="lg:first:justify-self-start lg:last:justify-self-end"
101108
/>
102109
<TradeItemList items={currentFilteredItems} emptyMessage="선택한 조건에 해당하는 상품은 없어요." />

src/pages/mypage/ui/ProfileInfoPage.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useMyPageProfileQuery } from '@shared/apis/mypage';
12
import { ROUTES } from '@shared/constants';
23
import { useNavigate } from 'react-router';
34
import { PageContainer } from './PageContainer';
@@ -6,7 +7,13 @@ import { getPersonalInfoDefaults, saveProfile } from '../model/profileStorage';
67

78
const ProfileInfoPage = () => {
89
const navigate = useNavigate();
9-
const defaultValues = getPersonalInfoDefaults();
10+
const { data: profileData } = useMyPageProfileQuery();
11+
const storedDefaults = getPersonalInfoDefaults();
12+
const defaultValues = {
13+
...storedDefaults,
14+
nickname: profileData?.nickname ?? storedDefaults.nickname,
15+
email: profileData?.email ?? storedDefaults.email,
16+
};
1017

1118
return (
1219
<main className="min-h-screen bg-white">

0 commit comments

Comments
 (0)