diff --git a/src/app/routes.ts b/src/app/routes.ts index 43998e6c..3951f602 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -7,6 +7,9 @@ export default [ index('routes/(main)/_index.tsx'), route('sell', 'routes/(main)/sell.tsx'), route('sell/confirm', 'routes/(main)/sell.confirm.tsx'), + route('mypage', 'routes/mypage.tsx'), + route('mypage/settings', 'routes/mypage-settings.tsx'), + route('mypage/profile', 'routes/mypage-profile.tsx'), ]), route('playground', 'routes/playground.tsx'), ] satisfies RouteConfig; diff --git a/src/app/routes/mypage-profile.tsx b/src/app/routes/mypage-profile.tsx new file mode 100644 index 00000000..e688168d --- /dev/null +++ b/src/app/routes/mypage-profile.tsx @@ -0,0 +1,3 @@ +import { ProfileInfoPage } from '@pages/mypage'; + +export default ProfileInfoPage; diff --git a/src/app/routes/mypage-settings.tsx b/src/app/routes/mypage-settings.tsx new file mode 100644 index 00000000..7e054adb --- /dev/null +++ b/src/app/routes/mypage-settings.tsx @@ -0,0 +1,3 @@ +import { AccountSettingsPage } from '@pages/mypage'; + +export default AccountSettingsPage; diff --git a/src/app/routes/mypage.tsx b/src/app/routes/mypage.tsx new file mode 100644 index 00000000..eb34f2ce --- /dev/null +++ b/src/app/routes/mypage.tsx @@ -0,0 +1,3 @@ +import { MyPage } from '@pages/mypage'; + +export default MyPage; diff --git a/src/pages/main/ui/MainPage.tsx b/src/pages/main/ui/MainPage.tsx index dd28aeab..bbeb5fb7 100644 --- a/src/pages/main/ui/MainPage.tsx +++ b/src/pages/main/ui/MainPage.tsx @@ -1,38 +1,14 @@ -import { ChatCircleCloseIcon } from '@shared/assets/icons'; import { ROUTES } from '@shared/constants'; -import { useToast } from '@shared/hooks'; import { BannerCard } from '@shared/ui/BannerCard'; import { Carousel3D } from '@shared/ui/Carousel3D'; import { ChatbotFloatingButton } from '@shared/ui/ChatbotFloatingButton'; import { ClientOnly } from '@shared/ui/ClientOnly'; -import { useEffect, useRef } from 'react'; -import { useLocation, useNavigate } from 'react-router'; +import { useNavigate } from 'react-router'; import { BANNER_CARDS } from './BannerCards'; import { CAROUSEL_SLIDES } from './CarouselSlides'; -type MainLocationState = { - deleted?: boolean; -}; - export default function MainPage() { - const location = useLocation(); const navigate = useNavigate(); - const { showToast } = useToast(); - const didShowDeleteToast = useRef(false); - - useEffect(() => { - const state = location.state as MainLocationState | null; - if (state?.deleted && !didShowDeleteToast.current) { - didShowDeleteToast.current = true; - showToast('삭제되었습니다!', { - tone: 'info', - icon: ( - - ), - }); - navigate(location.pathname, { replace: true, state: null }); - } - }, [location.pathname, location.state, navigate, showToast]); return (
diff --git a/src/pages/main/ui/bannerCards.tsx b/src/pages/main/ui/bannerCards.tsx new file mode 100644 index 00000000..9b9ab4f0 --- /dev/null +++ b/src/pages/main/ui/bannerCards.tsx @@ -0,0 +1,44 @@ +import { GearIcon, SellIcon, ShoppingIcon } from '@shared/assets/icons'; +import { ROUTES } from '@shared/constants'; + +export const BANNER_CARDS = [ + { + id: 'buy', + title: ( + <> + 중고 전자기기 +
+ 구매하기 + + ), + description: '합리적인 매물을 찾아보세요.', + imageSrc: ShoppingIcon, + route: ROUTES.BUY, + }, + { + id: 'sell', + title: ( + <> + 중고 전자기기 +
+ 판매하기 + + ), + description: '사용하지 않는 기기를 판매해보세요.', + imageSrc: SellIcon, + route: ROUTES.SELL, + }, + { + id: 'repair', + title: ( + <> + 근처 +
+ 수리점 찾기 + + ), + description: '가까운 수리점을 찾아드려요.', + imageSrc: GearIcon, + route: ROUTES.REPAIR, + }, +]; diff --git a/src/pages/main/ui/carouselSlides.tsx b/src/pages/main/ui/carouselSlides.tsx new file mode 100644 index 00000000..f89c7009 --- /dev/null +++ b/src/pages/main/ui/carouselSlides.tsx @@ -0,0 +1,39 @@ +import { + PhoneRecycleImage, + PhoneRepairImage, + PhoneSellImage, + SloganImage, + SmartphoneImage, +} from '@shared/assets/images'; +import { CarouselImageSlide } from '@shared/ui/Carousel3D'; + +export const CAROUSEL_SLIDES = [ + { + id: 1, + content: ( + + ), + }, + { + id: 2, + content: ( + + ), + }, + { + id: 3, + content: ( + + ), + }, + { + id: 4, + content: ( + + ), + }, + { + id: 5, + content: , + }, +]; diff --git a/src/pages/mypage/index.ts b/src/pages/mypage/index.ts new file mode 100644 index 00000000..6fe7bd1a --- /dev/null +++ b/src/pages/mypage/index.ts @@ -0,0 +1,3 @@ +export { default as MyPage } from './ui/MyPage'; +export { default as AccountSettingsPage } from './ui/AccountSettingsPage'; +export { default as ProfileInfoPage } from './ui/ProfileInfoPage'; diff --git a/src/pages/mypage/mocks/index.ts b/src/pages/mypage/mocks/index.ts new file mode 100644 index 00000000..df176232 --- /dev/null +++ b/src/pages/mypage/mocks/index.ts @@ -0,0 +1,76 @@ +import type { TradeListItem } from '../ui/components/TradeItemList'; +import type { RepairListItem } from '@shared/ui/RepairList'; + +export const MY_PAGE_PROFILE = { + nickname: '폰박살', + email: 'loveloopit@email.com', + profileImage: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?q=80&w=320&auto=format&fit=crop', +}; + +export const PERSONAL_INFO_DEFAULTS = { + nickname: '폰박살', + name: '김민아', + birthDate: '2001-11-11', + email: 'loveloopit@email.com', +}; + +export const BUY_ITEMS: TradeListItem[] = [ + { + id: 'buy-1', + modelName: '아이폰 17 256GB 실버', + price: '1,450,000원', + date: '2026.02.09', + status: 'buying', + imageUrl: 'https://images.unsplash.com/photo-1512499617640-c2f999018b72?q=80&w=200&auto=format&fit=crop', + }, +]; + +export const SELL_ITEMS: TradeListItem[] = [ + { + id: 'sell-1', + modelName: '아이폰 16 프로 512GB', + price: '1,320,000원', + date: '2026.02.07', + status: 'reserved', + imageUrl: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?q=80&w=200&auto=format&fit=crop', + }, + { + id: 'sell-2', + modelName: '갤럭시 S26 울트라', + price: '1,550,000원', + date: '2026.01.30', + status: 'completed', + statusLabel: '판매완료', + imageUrl: 'https://images.unsplash.com/photo-1510557880182-3d4d3cba35a5?q=80&w=200&auto=format&fit=crop', + }, +]; + +export const FAVORITE_PRODUCT_ITEMS: TradeListItem[] = [ + { + id: 'fav-product-1', + modelName: '새제품 아이폰 17 256GB', + price: '1,500,000원', + date: '2026.02.08', + status: 'favorite', + favoriteActive: true, + imageUrl: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?q=80&w=200&auto=format&fit=crop', + }, + { + id: 'fav-product-2', + modelName: '아이폰 17 (새제품, 케이스 증정)', + price: '1,550,000원', + date: '2026.02.07', + status: 'favorite', + favoriteActive: true, + imageUrl: 'https://images.unsplash.com/photo-1512499617640-c2f999018b72?q=80&w=200&auto=format&fit=crop', + }, +]; + +export const FAVORITE_REPAIR_ITEMS: RepairListItem[] = [ + { + id: 'fav-repair-1', + name: '뚝딱 핸드폰수리점', + address: '서울시 강남구', + favoriteActive: true, + }, +]; diff --git a/src/pages/mypage/ui/AccountSettingsPage.tsx b/src/pages/mypage/ui/AccountSettingsPage.tsx new file mode 100644 index 00000000..001606ef --- /dev/null +++ b/src/pages/mypage/ui/AccountSettingsPage.tsx @@ -0,0 +1,78 @@ +import { ChevronRightIcon, PictureIcon } from '@shared/assets/icons'; +import { Profile } from '@shared/ui/Profile'; +import { useRef, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { MY_PAGE_PROFILE } from '../mocks'; +import { getProfileSummary, saveProfile } from '../utils/profileStorage'; +import { PageContainer } from './components/PageContainer'; + +export default function AccountSettingsPage() { + const navigate = useNavigate(); + const profileSummary = getProfileSummary(); + const [profileImage, setProfileImage] = useState(profileSummary.profileImage ?? MY_PAGE_PROFILE.profileImage); + const fileInputRef = useRef(null); + + const handleProfileClick = () => { + fileInputRef.current?.click(); + }; + + const handleProfileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) { + return; + } + const reader = new FileReader(); + reader.onload = () => { + if (typeof reader.result !== 'string') { + return; + } + setProfileImage(reader.result); + saveProfile({ profileImage: reader.result }); + }; + reader.readAsDataURL(file); + }; + + return ( +
+ +

계정 설정

+ +
+
+ + + +
+
+ +
+

내 정보

+
+ + +
+
+
+
+ ); +} diff --git a/src/pages/mypage/ui/MyPage.tsx b/src/pages/mypage/ui/MyPage.tsx new file mode 100644 index 00000000..4132577c --- /dev/null +++ b/src/pages/mypage/ui/MyPage.tsx @@ -0,0 +1,148 @@ +import { RepairList, type RepairListItem } from '@shared/ui/RepairList'; +import { useMemo, useState } from 'react'; +import { useNavigate } from 'react-router'; +import { BUY_ITEMS, FAVORITE_PRODUCT_ITEMS, FAVORITE_REPAIR_ITEMS, SELL_ITEMS } from '../mocks'; +import { getProfileSummary } from '../utils/profileStorage'; +import { FavoriteTabs } from './components/FavoriteTabs'; +import { MyPageTabs, type MyPageTab } from './components/MyPageTabs'; +import { PageContainer } from './components/PageContainer'; +import { ProfileSummaryCard } from './components/ProfileSummaryCard'; +import { StatusTabs } from './components/StatusTabs'; +import { TradeItemList, type TradeListItem } from './components/TradeItemList'; + +const MAIN_TABS: MyPageTab[] = [ + { id: 'buy', label: '구매 내역' }, + { id: 'sell', label: '판매 내역' }, + { id: 'favorite', label: '찜한 목록' }, +] as const; + +type MainTabId = MyPageTab['id']; + +type StatusFilter = 'all' | 'buying' | 'reserved' | 'completed'; + +const getStatusCount = (items: TradeListItem[], status: StatusFilter) => { + if (status === 'all') { + return items.length; + } + return items.filter((item) => item.status === status).length; +}; + +export default function MyPage() { + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState('buy'); + const [buyStatus, setBuyStatus] = useState('all'); + const [sellStatus, setSellStatus] = useState('all'); + const [favoriteCategory, setFavoriteCategory] = useState<'product' | 'repair'>('product'); + const profileSummary = getProfileSummary(); + + const buyStatusTabs = useMemo( + () => [ + { id: 'all', label: '전체', count: getStatusCount(BUY_ITEMS, 'all') }, + { id: 'buying', label: '구매중', count: getStatusCount(BUY_ITEMS, 'buying') }, + { id: 'reserved', label: '예약중', count: getStatusCount(BUY_ITEMS, 'reserved') }, + { id: 'completed', label: '구매완료', count: getStatusCount(BUY_ITEMS, 'completed') }, + ], + [] + ); + + const sellStatusTabs = useMemo( + () => [ + { id: 'all', label: '전체', count: getStatusCount(SELL_ITEMS, 'all') }, + { id: 'buying', label: '판매중', count: getStatusCount(SELL_ITEMS, 'buying') }, + { id: 'reserved', label: '예약중', count: getStatusCount(SELL_ITEMS, 'reserved') }, + { id: 'completed', label: '판매완료', count: getStatusCount(SELL_ITEMS, 'completed') }, + ], + [] + ); + + const favoriteTabs = useMemo( + () => [ + { id: 'product', label: '상품', count: FAVORITE_PRODUCT_ITEMS.length }, + { id: 'repair', label: '수리점', count: FAVORITE_REPAIR_ITEMS.length }, + ], + [] + ); + + const filteredBuyItems = useMemo(() => { + if (buyStatus === 'all') { + return BUY_ITEMS; + } + return BUY_ITEMS.filter((item) => item.status === buyStatus); + }, [buyStatus]); + + const filteredSellItems = useMemo(() => { + if (sellStatus === 'all') { + return SELL_ITEMS; + } + return SELL_ITEMS.filter((item) => item.status === sellStatus); + }, [sellStatus]); + + const handleRepairContact = (item: RepairListItem) => { + console.log('contact repair', item); + }; + + const handleRepairFindRoute = (item: RepairListItem) => { + console.log('find route', item); + }; + + return ( +
+ +

마이페이지

+ +
+ navigate('/mypage/settings', { viewTransition: true })} + /> +
+ +
+ +
+ + {activeTab === 'favorite' ? ( + <> + setFavoriteCategory(id as 'product' | 'repair')} + /> + {favoriteCategory === 'product' ? ( + + ) : ( + + )} + + ) : ( + <> + { + if (activeTab === 'buy') { + setBuyStatus(id as StatusFilter); + } else { + setSellStatus(id as StatusFilter); + } + }} + /> + + + )} +
+
+ ); +} diff --git a/src/pages/mypage/ui/ProfileInfoPage.tsx b/src/pages/mypage/ui/ProfileInfoPage.tsx new file mode 100644 index 00000000..e9e40779 --- /dev/null +++ b/src/pages/mypage/ui/ProfileInfoPage.tsx @@ -0,0 +1,25 @@ +import { useMemo } from 'react'; +import { useNavigate } from 'react-router'; +import { getPersonalInfoDefaults, saveProfile } from '../utils/profileStorage'; +import { PageContainer } from './components/PageContainer'; +import { PersonalInfoForm } from './components/PersonalInfoForm'; + +export default function ProfileInfoPage() { + const navigate = useNavigate(); + const defaultValues = useMemo(() => getPersonalInfoDefaults(), []); + + return ( +
+ +

개인정보 관리

+ { + saveProfile(values); + navigate('/mypage', { viewTransition: true }); + }} + /> +
+
+ ); +} diff --git a/src/pages/mypage/ui/components/FavoriteTabs.tsx b/src/pages/mypage/ui/components/FavoriteTabs.tsx new file mode 100644 index 00000000..19f3011b --- /dev/null +++ b/src/pages/mypage/ui/components/FavoriteTabs.tsx @@ -0,0 +1,44 @@ +import { cn } from '@shared/utils/cn'; + +export type FavoriteTabItem = { + id: string; + label: string; + count: number; +}; + +export type FavoriteTabsProps = { + title: string; + tabs: FavoriteTabItem[]; + activeId: string; + onChange: (id: string) => void; +}; + +export const FavoriteTabs = ({ title, tabs, activeId, onChange }: FavoriteTabsProps) => { + return ( +
+

{title}

+
+
+ {tabs.map((tab) => ( + + ))} +
+
+
+ ); +}; diff --git a/src/pages/mypage/ui/components/MyPageTabs.tsx b/src/pages/mypage/ui/components/MyPageTabs.tsx new file mode 100644 index 00000000..33ede67d --- /dev/null +++ b/src/pages/mypage/ui/components/MyPageTabs.tsx @@ -0,0 +1,35 @@ +import { cn } from '@shared/utils/cn'; + +export type MyPageTab = { + id: string; + label: string; +}; + +export type MyPageTabsProps = { + tabs: MyPageTab[]; + activeId: string; + onChange: (id: string) => void; +}; + +export const MyPageTabs = ({ tabs, activeId, onChange }: MyPageTabsProps) => { + return ( +
+
+ {tabs.map((tab) => ( + + ))} +
+
+ ); +}; diff --git a/src/pages/mypage/ui/components/PageContainer.tsx b/src/pages/mypage/ui/components/PageContainer.tsx new file mode 100644 index 00000000..c5fcfb14 --- /dev/null +++ b/src/pages/mypage/ui/components/PageContainer.tsx @@ -0,0 +1,6 @@ +import { cn } from '@shared/utils/cn'; +import type { ComponentPropsWithoutRef } from 'react'; + +export const PageContainer = ({ className, ...props }: ComponentPropsWithoutRef<'div'>) => { + return
; +}; diff --git a/src/pages/mypage/ui/components/PersonalInfoForm.tsx b/src/pages/mypage/ui/components/PersonalInfoForm.tsx new file mode 100644 index 00000000..77665ab3 --- /dev/null +++ b/src/pages/mypage/ui/components/PersonalInfoForm.tsx @@ -0,0 +1,60 @@ +import { Button } from '@shared/ui/Button/Button'; +import { TextField } from '@shared/ui/TextField/TextField'; +import { useEffect, useState } from 'react'; + +export type PersonalInfoValues = { + nickname: string; + name: string; + birthDate: string; + email: string; +}; + +export type PersonalInfoFormProps = { + defaultValues: PersonalInfoValues; + onSave: (values: PersonalInfoValues) => void; +}; + +export const PersonalInfoForm = ({ defaultValues, onSave }: PersonalInfoFormProps) => { + const [values, setValues] = useState(defaultValues); + + useEffect(() => { + setValues(defaultValues); + }, [defaultValues]); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + onSave(values); + }; + + return ( +
+ setValues((prev) => ({ ...prev, nickname: event.target.value }))} + /> + setValues((prev) => ({ ...prev, name: event.target.value }))} + /> + setValues((prev) => ({ ...prev, birthDate: event.target.value }))} + /> + setValues((prev) => ({ ...prev, email: event.target.value }))} + /> + +
+ +
+ + ); +}; diff --git a/src/pages/mypage/ui/components/ProfileSummaryCard.tsx b/src/pages/mypage/ui/components/ProfileSummaryCard.tsx new file mode 100644 index 00000000..c77a0109 --- /dev/null +++ b/src/pages/mypage/ui/components/ProfileSummaryCard.tsx @@ -0,0 +1,47 @@ +import { Button } from '@shared/ui/Button/Button'; +import { Profile } from '@shared/ui/Profile'; +import { cn } from '@shared/utils/cn'; +import type { ComponentPropsWithoutRef } from 'react'; + +export type ProfileSummaryCardProps = ComponentPropsWithoutRef<'section'> & { + profileImage?: string; + nickname: string; + email: string; + onSettingsClick?: () => void; +}; + +export const ProfileSummaryCard = ({ + profileImage, + nickname, + email, + onSettingsClick, + className, + ...props +}: ProfileSummaryCardProps) => { + return ( +
+
+ +
+ {nickname} + {email} +
+
+ + +
+ ); +}; diff --git a/src/pages/mypage/ui/components/StatusTabs.tsx b/src/pages/mypage/ui/components/StatusTabs.tsx new file mode 100644 index 00000000..237bf303 --- /dev/null +++ b/src/pages/mypage/ui/components/StatusTabs.tsx @@ -0,0 +1,44 @@ +import { cn } from '@shared/utils/cn'; + +export type StatusTabItem = { + id: string; + label: string; + count: number; +}; + +export type StatusTabsProps = { + title: string; + tabs: StatusTabItem[]; + activeId: string; + onChange: (id: string) => void; +}; + +export const StatusTabs = ({ title, tabs, activeId, onChange }: StatusTabsProps) => { + return ( +
+

{title}

+
+
+ {tabs.map((tab) => ( + + ))} +
+
+
+ ); +}; diff --git a/src/pages/mypage/ui/components/TradeItemList.tsx b/src/pages/mypage/ui/components/TradeItemList.tsx new file mode 100644 index 00000000..b049011c --- /dev/null +++ b/src/pages/mypage/ui/components/TradeItemList.tsx @@ -0,0 +1,22 @@ +import { TradeItem, type TradeItemProps } from '@shared/ui/TradeItem/TradeItem'; + +export type TradeListItem = TradeItemProps & { id: string }; + +export type TradeItemListProps = { + items: TradeListItem[]; + emptyMessage: string; +}; + +export const TradeItemList = ({ items, emptyMessage }: TradeItemListProps) => { + if (items.length === 0) { + return

{emptyMessage}

; + } + + return ( +
+ {items.map((item) => ( + + ))} +
+ ); +}; diff --git a/src/pages/mypage/utils/profileStorage.ts b/src/pages/mypage/utils/profileStorage.ts new file mode 100644 index 00000000..74aaa382 --- /dev/null +++ b/src/pages/mypage/utils/profileStorage.ts @@ -0,0 +1,57 @@ +import { MY_PAGE_PROFILE, PERSONAL_INFO_DEFAULTS } from '../mocks'; + +export type PersonalInfoValues = { + nickname: string; + name: string; + birthDate: string; + email: string; +}; + +export type StoredProfile = PersonalInfoValues & { + profileImage?: string; +}; + +const STORAGE_KEY = 'myPageProfile'; + +const readProfileStorage = (): Partial | null => { + if (typeof window === 'undefined') { + return null; + } + try { + const raw = window.localStorage.getItem(STORAGE_KEY); + if (!raw) { + return null; + } + return JSON.parse(raw) as Partial; + } catch { + return null; + } +}; + +export const getPersonalInfoDefaults = (): PersonalInfoValues => { + const stored = readProfileStorage(); + return { + ...PERSONAL_INFO_DEFAULTS, + ...stored, + }; +}; + +export const getProfileSummary = () => { + const stored = readProfileStorage(); + return { + ...MY_PAGE_PROFILE, + ...stored, + }; +}; + +export const saveProfile = (values: Partial) => { + if (typeof window === 'undefined') { + return; + } + const stored = readProfileStorage() ?? {}; + const next = { + ...stored, + ...values, + }; + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); +}; diff --git a/src/shared/assets/icons/common/chevron-right.svg b/src/shared/assets/icons/common/chevron-right.svg new file mode 100644 index 00000000..f62e10ba --- /dev/null +++ b/src/shared/assets/icons/common/chevron-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/shared/assets/icons/common/heart-outline.svg b/src/shared/assets/icons/common/heart-outline.svg index 514dd178..c922387d 100644 --- a/src/shared/assets/icons/common/heart-outline.svg +++ b/src/shared/assets/icons/common/heart-outline.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/shared/assets/icons/common/index.ts b/src/shared/assets/icons/common/index.ts index 07a2e42e..a6581a1f 100644 --- a/src/shared/assets/icons/common/index.ts +++ b/src/shared/assets/icons/common/index.ts @@ -10,4 +10,5 @@ export { default as CloseIcon } from './close.svg?react'; export { default as HamburgerIcon } from './hamburger-md.svg?react'; export { default as PictureIcon } from './picture.svg?react'; export { default as kakaoLoginImg } from './kakao.png'; +export { default as ChevronRightIcon } from './chevron-right.svg?react'; export { default as chatbotImg } from './chatbot.webp'; diff --git a/src/shared/assets/icons/common/picture.svg b/src/shared/assets/icons/common/picture.svg index f03f4943..2bebadd0 100644 --- a/src/shared/assets/icons/common/picture.svg +++ b/src/shared/assets/icons/common/picture.svg @@ -1,4 +1,4 @@ - + diff --git a/src/shared/assets/icons/index.ts b/src/shared/assets/icons/index.ts index 2a92a7ae..699b7e1c 100644 --- a/src/shared/assets/icons/index.ts +++ b/src/shared/assets/icons/index.ts @@ -3,15 +3,15 @@ export { ChatCircleCloseIcon, CloseIcon, HamburgerIcon, + kakaoLoginImg, CaretDownMdIcon, CheckIcon, DoneIcon, HeartIcon, - kakaoLoginImg, PictureIcon, - SearchMagnifyingGlassIcon, SendIcon, - ChatbotIcon, + SearchMagnifyingGlassIcon, chatbotImg, + ChevronRightIcon, } from './common'; export { GearIcon, ShoppingIcon, SellIcon } from './banner'; diff --git a/src/shared/ui/Carousel3D/Carousel3D.variants.ts b/src/shared/ui/Carousel3D/Carousel3D.variants.ts index f39d54e6..34b56de0 100644 --- a/src/shared/ui/Carousel3D/Carousel3D.variants.ts +++ b/src/shared/ui/Carousel3D/Carousel3D.variants.ts @@ -38,7 +38,7 @@ export const carousel3DVariants = tv({ 'backdrop-blur-xl', 'border', 'border-gray-700/30', - 'rounded-(--radius-l)', + 'rounded-l', 'shadow-2xl', 'shadow-black/30', 'transition-all', @@ -72,7 +72,7 @@ export const carousel3DVariants = tv({ 'w-full', 'h-full', 'min-h-[320px]', - 'rounded-(--radius-l)', + 'rounded-l', 'overflow-hidden', 'shadow-2xl', 'shadow-black/30', diff --git a/src/shared/ui/Modal/Modal.styles.ts b/src/shared/ui/Modal/Modal.styles.ts index 5d83a18d..ca7fe471 100644 --- a/src/shared/ui/Modal/Modal.styles.ts +++ b/src/shared/ui/Modal/Modal.styles.ts @@ -18,9 +18,7 @@ export const modalStyles = { 'flex', 'flex-col', 'justify-between', - 'rounded-[var(--radius-l)]', - 'bg-[var(--color-white)]', - 'rounded-(--radius-l)', + 'rounded-l', 'bg-green-500', 'px-[var(--padding-xxl)]', 'py-[var(--padding-xl)]', diff --git a/src/shared/ui/Profile/Profile.variants.ts b/src/shared/ui/Profile/Profile.variants.ts index 0c0e8205..7bb65d1c 100644 --- a/src/shared/ui/Profile/Profile.variants.ts +++ b/src/shared/ui/Profile/Profile.variants.ts @@ -20,9 +20,12 @@ export const profileVariants = tv({ sm: { root: ['w-[44px]', 'h-[44px]'], }, - lg: { + md: { root: ['w-[152px]', 'h-[152px]'], }, + lg: { + root: ['w-[180px]', 'h-[180px]'], + }, }, }, diff --git a/src/shared/ui/RepairList/RepairList.tsx b/src/shared/ui/RepairList/RepairList.tsx new file mode 100644 index 00000000..04911de6 --- /dev/null +++ b/src/shared/ui/RepairList/RepairList.tsx @@ -0,0 +1,64 @@ +import { Button } from '@shared/ui/Button/Button'; +import { FavoriteButton } from '@shared/ui/FavoriteButton/FavoriteButton'; +import { cn } from '@shared/utils/cn'; + +export type RepairListItem = { + id: string; + name: string; + address: string; + favoriteActive?: boolean; +}; + +export type RepairListProps = { + items: RepairListItem[]; + emptyMessage: string; + onContact?: (item: RepairListItem) => void; + onFindRoute?: (item: RepairListItem) => void; +}; + +export const RepairList = ({ items, emptyMessage, onContact, onFindRoute }: RepairListProps) => { + if (items.length === 0) { + return

{emptyMessage}

; + } + + return ( +
+ {items.map((item) => ( +
+
+ {item.name} + {item.address} +
+ +
+ +
+ + +
+
+
+ ))} +
+ ); +}; diff --git a/src/shared/ui/RepairList/index.ts b/src/shared/ui/RepairList/index.ts new file mode 100644 index 00000000..8883db1b --- /dev/null +++ b/src/shared/ui/RepairList/index.ts @@ -0,0 +1,2 @@ +export { RepairList } from './RepairList'; +export type { RepairListItem } from './RepairList'; diff --git a/src/shared/ui/TradeItem/TradeItem.tsx b/src/shared/ui/TradeItem/TradeItem.tsx index 825d48f9..6adcae7d 100644 --- a/src/shared/ui/TradeItem/TradeItem.tsx +++ b/src/shared/ui/TradeItem/TradeItem.tsx @@ -9,6 +9,7 @@ export interface TradeItemProps { price: string; date: string; status?: TradeStatus; + statusLabel?: string; favoriteActive?: boolean; onToggleFavorite?: (active: boolean) => void; } @@ -34,9 +35,13 @@ export const TradeItem = ({ price, date, status = 'buying', + statusLabel, favoriteActive = false, onToggleFavorite, }: TradeItemProps) => { + const statusConfig = status === 'favorite' ? null : statusConfigByStatus[status]; + const resolvedStatusLabel = statusLabel ?? statusConfig?.text; + return (
@@ -61,7 +66,7 @@ export const TradeItem = ({ {status === 'favorite' ? ( ) : ( - {statusConfigByStatus[status].text} + {resolvedStatusLabel} )}
diff --git a/tests/unit/shared/ui/Profile/Profile.test.tsx b/tests/unit/shared/ui/Profile/Profile.test.tsx index 217591f5..4f0826cf 100644 --- a/tests/unit/shared/ui/Profile/Profile.test.tsx +++ b/tests/unit/shared/ui/Profile/Profile.test.tsx @@ -39,7 +39,7 @@ describe('Profile', () => { it('size="lg" 적용', () => { const { container } = render(); const root = container.firstChild as HTMLElement; - expect(root).toHaveClass('w-[152px]', 'h-[152px]'); + expect(root).toHaveClass('w-[180px]', 'h-[180px]'); }); });