Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
893096f
refactor: MyPage CommonTabs 반응형 레이아웃 적용
KyeongJooni Feb 2, 2026
cff9ebe
refactor: MyPageTabs, PageContainer UI 수정
KyeongJooni Feb 2, 2026
a4eaf59
refactor: Card 모바일 반응형 적용
KyeongJooni Feb 2, 2026
90889a0
refactor: TradeItem 모바일 반응형 적용
KyeongJooni Feb 2, 2026
34613ff
refactor: 구매 페이지 UI 수정
KyeongJooni Feb 2, 2026
8e4e554
refactor: 챗봇 페이지 UI 수정
KyeongJooni Feb 2, 2026
23c8aea
refactor: ChatBubble, ChatbotFloatingButton UI 수정
KyeongJooni Feb 2, 2026
d240b73
chore: chatbot 아이콘 수정
KyeongJooni Feb 2, 2026
dab54ec
refactor: 수리점 페이지 UI 수정
KyeongJooni Feb 2, 2026
ca6b25f
refactor: 수리점 훅 수정
KyeongJooni Feb 2, 2026
4b6323f
refactor: 메인 페이지 및 캐러셀 UI 수정
KyeongJooni Feb 2, 2026
9e6dfc5
refactor: 판매 페이지 UI 수정
KyeongJooni Feb 2, 2026
1dc04d7
refactor: 판매자 프로필 페이지 UI 수정
KyeongJooni Feb 2, 2026
3f2b683
refactor: 레이아웃 및 전역 스타일 수정
KyeongJooni Feb 2, 2026
431b86f
refactor: Modal, Header, FavoriteButton UI 수정
KyeongJooni Feb 2, 2026
fd28c3d
feat: useBodyScrollLock, useScrolled 훅 추가
KyeongJooni Feb 2, 2026
80fc9f5
refactor: icons export 수정
KyeongJooni Feb 2, 2026
651f2c1
refactor: 테일윈드css 속성 사용
KyeongJooni Feb 2, 2026
18875d2
refactor: BuyPage에서 BuyFilter 컴포넌트 분리
KyeongJooni Feb 2, 2026
e02b403
test: 테스트 케이스 수정
KyeongJooni Feb 2, 2026
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
4 changes: 3 additions & 1 deletion src/app/layout/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ const MainLayout = () => {
return (
<div className="min-h-screen bg-white">
<Header />
<Outlet />
<div className="mt-header-total">
<Outlet />
</div>
</div>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/app/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@
--radius-s: 8px;

/* Layout */
--header-height: 92px;
--spacing-header-content: 54px;
--spacing-header-total: calc(var(--header-height) + var(--spacing-header-content));
}

@layer base {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/buy-detail/ui/BuyDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const BuyDetailPage = () => {
}

return (
<main className="mx-auto flex w-[1200px] flex-col items-start gap-[157px] pt-10">
<main className="md:px-xxxl mx-auto flex w-full max-w-[1200px] flex-col items-start gap-10 px-(--margin-l) pt-10 lg:gap-[157px] lg:px-0">
<div className="flex flex-col items-start justify-center gap-[30px] self-stretch">
<div className="grid gap-6 lg:grid-cols-[590px_590px]">
<div className="h-[568px] w-[590px] overflow-hidden rounded-(--radius-l) bg-gray-50">
Expand Down
136 changes: 136 additions & 0 deletions src/pages/buy/ui/BuyFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Checkbox } from '@shared/ui/Checkbox';
import { cn } from '@shared/utils/cn';
import { useState, type ReactNode } from 'react';

const FilterSection = ({ title, children }: { title: string; children: ReactNode }) => {
return (
<section className="flex flex-col gap-3 pb-4">
<h3 className="typo-body-2 text-gray-900">{title}</h3>
<div className="flex flex-col gap-2">{children}</div>
<div className="h-px w-full bg-gray-100" />
</section>
);
};

type FilterItem = {
id: string;
label: string;
};

export type BuyFilterProps = {
manufacturers: FilterItem[];
models: FilterItem[];
priceRanges: FilterItem[];
selectedManufacturers: string[];
selectedModels: string[];
selectedPrices: string[];
availableOnly: boolean;
showAllModels: boolean;
onToggleManufacturer: (id: string) => void;
onToggleModel: (id: string) => void;
onTogglePrice: (id: string) => void;
onSetAvailableOnly: (value: boolean) => void;
onSetShowAllModels: (value: boolean) => void;
onReset: () => void;
defaultModelCount?: number;
};

export const BuyFilter = ({
manufacturers,
models,
priceRanges,
selectedManufacturers,
selectedModels,
selectedPrices,
availableOnly,
showAllModels,
onToggleManufacturer,
onToggleModel,
onTogglePrice,
onSetAvailableOnly,
onSetShowAllModels,
onReset,
defaultModelCount = 8,
}: BuyFilterProps) => {
const [isOpen, setIsOpen] = useState(false);

return (
<aside className="flex w-full shrink-0 flex-col items-start gap-[21px] lg:h-[702px] lg:w-[183px]">
{/* 모바일: 토글 버튼 */}
<button
type="button"
className="flex w-full cursor-pointer items-center justify-between self-stretch lg:hidden"
onClick={() => setIsOpen((prev) => !prev)}
>
<h2 className="typo-body-2 text-gray-900">필터</h2>
<span className={cn('typo-caption-2 transition-transform', isOpen && 'rotate-180')}>▼</span>
</button>

{/* 데스크탑: 기존 헤더 */}
<div className="hidden items-center justify-between self-stretch lg:flex">
<h2 className="typo-body-2 text-gray-900">필터</h2>
<button type="button" className="typo-caption-2 font-medium text-gray-400" onClick={onReset}>
초기화
</button>
</div>

{/* 모바일: 토글 콘텐츠 / 데스크탑: 항상 표시 */}
<div className={cn('w-full flex-col gap-[21px] lg:flex', isOpen ? 'flex' : 'hidden')}>
{/* 모바일용 초기화 버튼 */}
<div className="flex items-center justify-end self-stretch lg:hidden">
<button type="button" className="typo-caption-2 font-medium text-gray-400" onClick={onReset}>
초기화
</button>
</div>

<Checkbox
label="구매가능만 보기"
checked={availableOnly}
onChange={(e) => onSetAvailableOnly(e.target.checked)}
/>

<div className="flex flex-col gap-[21px] self-stretch">
<FilterSection title="제조사">
{manufacturers.map((item) => (
<Checkbox
key={item.id}
label={item.label}
checked={selectedManufacturers.includes(item.id)}
onChange={() => onToggleManufacturer(item.id)}
/>
))}
</FilterSection>

<FilterSection title="모델명">
{(showAllModels ? models : models.slice(0, defaultModelCount)).map((item) => (
<Checkbox
key={item.id}
label={item.label}
checked={selectedModels.includes(item.id)}
onChange={() => onToggleModel(item.id)}
/>
))}
<button
type="button"
className="typo-caption-1 mt-2 text-left text-green-700"
onClick={() => onSetShowAllModels(!showAllModels)}
>
{showAllModels ? '접기' : '더보기'}
</button>
</FilterSection>

<FilterSection title="가격">
{priceRanges.map((item) => (
<Checkbox
key={item.id}
label={item.label}
checked={selectedPrices.includes(item.id)}
onChange={() => onTogglePrice(item.id)}
/>
))}
</FilterSection>
</div>
</div>
</aside>
);
};
97 changes: 22 additions & 75 deletions src/pages/buy/ui/BuyPage.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import { useBuyFilter } from '@pages/buy/model/useBuyFilter';
import { BuyFilter } from '@pages/buy/ui/BuyFilter';
import { CloseIcon } from '@shared/assets/icons';
import { ROUTES } from '@shared/constants';
import { MANUFACTURERS, MODELS, PRICE_RANGES } from '@shared/mocks/data/buy';
import { Card } from '@shared/ui/Card';
import { Checkbox } from '@shared/ui/Checkbox';
import { SearchBar } from '@shared/ui/SearchBar';
import { cn } from '@shared/utils/cn';
import { Link } from 'react-router';
import type { ReactNode } from 'react';

const FilterSection = ({ title, children }: { title: string; children: ReactNode }) => {
return (
<section className="flex flex-col gap-3 pb-4">
<h3 className="typo-body-2 text-gray-900">{title}</h3>
<div className="flex flex-col gap-2">{children}</div>
<div className="h-px w-full bg-gray-100" />
</section>
);
};

const FilterChip = ({ label, onRemove }: { label: string; onRemove: () => void }) => {
return (
Expand All @@ -32,8 +21,6 @@ const FilterChip = ({ label, onRemove }: { label: string; onRemove: () => void }
);
};

const DEFAULT_MODEL_COUNT = 8;

const BuyPage = () => {
const {
query,
Expand All @@ -57,7 +44,7 @@ const BuyPage = () => {
const showEmpty = filteredItems.length === 0;

return (
<main className="mx-auto flex w-[1200px] flex-col items-center gap-[68px]">
<main className="md:px-xxxl mx-auto flex w-full max-w-[1200px] flex-col items-center gap-6 px-(--margin-l) md:gap-10 lg:gap-[68px] lg:px-0">
<SearchBar
placeholder="어떤 제품을 찾으시나요?"
onSearch={(value) => setQuery(value)}
Expand All @@ -66,76 +53,36 @@ const BuyPage = () => {
/>

<div className="flex w-full flex-col gap-8 lg:flex-row lg:gap-[22px]">
<aside className="flex w-full shrink-0 flex-col items-start gap-[21px] lg:h-[702px] lg:w-[183px]">
<div className="flex items-center justify-between self-stretch">
<h2 className="typo-body-2 text-gray-900">필터</h2>
<button type="button" className="typo-caption-2 font-medium text-gray-400" onClick={resetFilters}>
초기화
</button>
</div>

<Checkbox
label="구매가능만 보기"
checked={availableOnly}
onChange={(e) => setAvailableOnly(e.target.checked)}
/>

<div className="flex flex-col gap-[21px] self-stretch">
<FilterSection title="제조사">
{MANUFACTURERS.map((item) => (
<Checkbox
key={item.id}
label={item.label}
checked={selectedManufacturers.includes(item.id)}
onChange={() => toggleManufacturer(item.id)}
/>
))}
</FilterSection>

<FilterSection title="모델명">
{(showAllModels ? MODELS : MODELS.slice(0, DEFAULT_MODEL_COUNT)).map((item) => (
<Checkbox
key={item.id}
label={item.label}
checked={selectedModels.includes(item.id)}
onChange={() => toggleModel(item.id)}
/>
))}
<button
type="button"
className="typo-caption-1 mt-2 text-left text-green-700"
onClick={() => setShowAllModels((prev) => !prev)}
>
{showAllModels ? '접기' : '더보기'}
</button>
</FilterSection>

<FilterSection title="가격">
{PRICE_RANGES.map((item) => (
<Checkbox
key={item.id}
label={item.label}
checked={selectedPrices.includes(item.id)}
onChange={() => togglePrice(item.id)}
/>
))}
</FilterSection>
</div>
</aside>
<BuyFilter
manufacturers={MANUFACTURERS}
models={MODELS}
priceRanges={PRICE_RANGES}
selectedManufacturers={selectedManufacturers}
selectedModels={selectedModels}
selectedPrices={selectedPrices}
availableOnly={availableOnly}
showAllModels={showAllModels}
onToggleManufacturer={toggleManufacturer}
onToggleModel={toggleModel}
onTogglePrice={togglePrice}
onSetAvailableOnly={setAvailableOnly}
onSetShowAllModels={setShowAllModels}
onReset={resetFilters}
/>

<section className="flex w-[1008px] flex-col items-start gap-[17px]">
<div className="min-h-xxl flex items-start gap-[18px] self-stretch">
<section className="flex w-full flex-1 flex-col items-start gap-[17px]">
<div className="min-h-xxl flex flex-wrap items-start gap-2 self-stretch md:gap-[18px]">
{activeChips.map((chip) => (
<FilterChip key={`${chip.type}-${chip.id}`} label={chip.label} onRemove={() => removeChip(chip)} />
))}
</div>

{showEmpty ? (
<div className="rounded-m flex h-[992px] w-full items-center justify-center bg-gray-50 text-gray-500">
<div className="rounded-m flex min-h-[400px] w-full flex-1 items-center justify-center bg-gray-50 text-gray-500 lg:min-h-[992px]">
관련된 상품이 없어요.
</div>
) : (
<div className="lg:gap-xl grid w-full grid-cols-2 gap-6 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-[repeat(5,180px)]">
<div className="lg:gap-xl grid w-full grid-cols-2 gap-4 sm:grid-cols-3 sm:gap-5 md:grid-cols-4 md:gap-6 lg:grid-cols-5">
{filteredItems.map((item) => (
<Link key={item.id} to={`${ROUTES.BUY}/${item.id}`} className="block focus-visible:outline-none">
<Card
Expand Down
2 changes: 1 addition & 1 deletion src/pages/chatbot/ui/ChatMessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const ChatMessageList = ({ messages }: ChatMessageListProps) => {
return (
<div key={message.id} className="flex w-full justify-start">
{message.status === 'loading' ? (
<div className="inline-flex min-h-[60px] items-center justify-start gap-[var(--spacing-xxxs)] rounded-(--radius-l) bg-gray-100 px-[31px] py-[18px]">
<div className="gap-xxxs inline-flex min-h-[60px] items-center justify-start rounded-(--radius-l) bg-gray-100 px-[31px] py-[18px]">
<span className="h-[6px] w-[6px] animate-bounce rounded-full bg-gray-400 [animation-delay:-0.2s]" />
<span className="h-[6px] w-[6px] animate-bounce rounded-full bg-gray-400 [animation-delay:-0.1s]" />
<span className="h-[6px] w-[6px] animate-bounce rounded-full bg-gray-400" />
Expand Down
12 changes: 6 additions & 6 deletions src/pages/chatbot/ui/ChatbotPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const INITIAL_BOT_MESSAGE = `루핏이 예상 수리비를 빠르게 계산해

보험/케어 가입 여부: 예) 애플케어 O/X, 통신사 보험 O/X

가능하면\u00A0원하는\u00A0방향도\u00A0한\u00A0줄로\u00A0적어주세요:\u00A0“정품\u00A0우선”\u00A0/\u00A0“최대한\u00A0저렴하\u2060게”\u00A0/\u00A0“빨리”
가능하면 원하는 방향도 한 줄로 적어주세요: "정품 우선" / "최대한 저렴하게" / "빨리"

예시) “아이폰 15, 액정 깨짐, 애플케어 X, 최대한 저렴하게”

Expand Down Expand Up @@ -80,18 +80,18 @@ const ChatbotPage = () => {
return (
<div className="w-full bg-white">
<div className="mx-auto flex min-h-screen w-full max-w-[1440px] flex-col bg-white xl:min-h-[1024px]">
<main className="flex flex-1 flex-col px-[var(--margin-l)] md:px-[var(--spacing-xxxl)] xl:px-0">
<main className="md:px-xxxl flex flex-1 flex-col px-(--margin-l) xl:px-0">
<section className="mx-auto flex w-full max-w-full flex-col items-start gap-[19px] self-stretch pb-[140px] xl:max-w-[1200px]">
<div className="flex items-center gap-[var(--spacing-m)] md:gap-[var(--spacing-xl)] xl:gap-[36px]">
<div className="flex h-[64px] w-[64px] flex-shrink-0 flex-col items-center justify-center gap-[10px] rounded-full bg-black pt-[var(--padding-xl)] pr-[10px] pb-[var(--padding-xl)] pl-[14px] md:h-[72px] md:w-[72px] md:pt-[26px] md:pr-[11px] md:pb-[28px] md:pl-[15px] xl:h-[80px] xl:w-[80px] xl:pt-[31px] xl:pr-[13px] xl:pb-[33px] xl:pl-[17px]">
<Logo4 className="h-[80px] w-[80px] flex-shrink-0" aria-hidden="true" />
<div className="gap-m md:gap-xl flex items-center xl:gap-[36px]">
<div className="gap-xxs pt-xl pr-xxs pb-xl flex h-[64px] w-[64px] shrink-0 flex-col items-center justify-center rounded-full bg-black pl-[14px] md:h-[72px] md:w-[72px] md:pt-[26px] md:pr-[11px] md:pb-[28px] md:pl-[15px] xl:h-[80px] xl:w-[80px] xl:pt-[31px] xl:pr-[13px] xl:pb-[33px] xl:pl-[17px]">
<Logo4 className="h-[80px] w-[80px] shrink-0" aria-hidden="true" />
</div>
<h1 className="typo-title-2 text-black">루핏봇</h1>
</div>
<ChatMessageList messages={messages} />
<div ref={endOfMessagesRef} className="scroll-mb-[96px]" />
</section>
<div className="fixed inset-x-0 bottom-[var(--margin-s)] z-50 px-[var(--margin-l)] md:px-[var(--spacing-xxxl)] xl:px-[120px]">
<div className="md:px-xxxl fixed inset-x-0 bottom-0 z-50 bg-white px-(--margin-l) pt-4 pb-(--margin-s) xl:px-[120px]">
<div className="mx-auto w-full max-w-full xl:max-w-[1200px]">
<ChatInput
placeholder="무슨 견적을 원하시나요?"
Expand Down
17 changes: 15 additions & 2 deletions src/pages/main/ui/CarouselSlides.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ export const CAROUSEL_SLIDES = [
},
{
id: 2,
content: <CarouselImageSlide imageSrc={Carousel2Image} title="중고폰 구매" subtitle="합리적인 가격, 검증된 상품" />,
content: (
<CarouselImageSlide
imageSrc={Carousel2Image}
title="중고폰 구매"
subtitle="합리적인 가격, 검증된 상품"
href="/buy"
/>
),
},
{
id: 3,
Expand All @@ -19,13 +26,19 @@ export const CAROUSEL_SLIDES = [
imageSrc={Carousel3Image}
title="나의 폰 판매"
subtitle="나는 쓰지 않는 폰, 다른 사람은 쓰기 좋은 폰"
href="/sell"
/>
),
},
{
id: 4,
content: (
<CarouselImageSlide imageSrc={Carousel4Image} title="수리점 찾기" subtitle="내 집 근처 수리점 어디에 있을까?" />
<CarouselImageSlide
imageSrc={Carousel4Image}
title="수리점 찾기"
subtitle="내 집 근처 수리점 어디에 있을까?"
href="/repair"
/>
),
},
{
Expand Down
2 changes: 1 addition & 1 deletion src/pages/main/ui/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const MainPage = () => {
const navigate = useNavigate();

return (
<main className="flex w-full flex-col items-center gap-[68px] px-4 pb-24">
<main className="md:px-xxxl flex w-full flex-col items-center gap-6 px-(--margin-l) pb-24 md:gap-10 lg:gap-[68px] lg:px-0">
<section className="h-[317px] w-full max-w-[1200px]" aria-label="메인 슬로건 영역">
<ClientOnly>
<Carousel3D slides={CAROUSEL_SLIDES} />
Expand Down
Loading
Loading