Skip to content
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
a9afa48
chore: 상품 탭 섹션 컴포넌트 순서 변경
earl9rey Mar 26, 2026
e10948d
feat: 상품 탭 검색 섹션 스켈레톤 UI 구현
earl9rey Mar 26, 2026
a74d4ff
style: 상품 탭 전체 css 적용
earl9rey Mar 26, 2026
dfef559
feat: 상품 배너 fallback 이미지 추가
earl9rey Mar 26, 2026
1801299
feat: 상품 인트로 배너 영역 UI 구현
earl9rey Mar 26, 2026
37c269b
chore: Chip 컴포넌트 SuffixIcon을 아이콘 공컴으로 변경
earl9rey Mar 26, 2026
b98aee9
chore: 상품 카드 공컴 변경 To-Do 주석 추가
earl9rey Mar 26, 2026
12427c0
feat: 상품 탭에서 메뉴 탭 Sticky 해제
earl9rey Mar 26, 2026
4c545dc
style: 검색창 및 필터 스티키 추가
earl9rey Mar 26, 2026
8901583
feat: 상품 카드 목업 데이터로 변경
earl9rey Apr 2, 2026
442d7bc
refactor: TitleNavBar에 아이콘 공컴 적용
earl9rey Apr 2, 2026
caf92b3
refactor: BottomSheetBase에 Icon 공컴 적용
earl9rey Apr 2, 2026
500f00b
feat: 상품 탭에 상품 선택 바텀시트 추가
earl9rey Apr 2, 2026
546d762
feat: DragHandleBottomSheet에 확장 여부 props 추가
earl9rey Apr 2, 2026
fc0ec84
feat: 선택한 가구 바텀시트 내부 콘텐츠 컴포넌트 구현
earl9rey Apr 2, 2026
dfd6566
style: 디자인 토큰 변경사항 적용
earl9rey Apr 5, 2026
ff1e400
Merge branch 'develop' of https://github.com/TEAM-HOUME/HOUME-CLIENT …
earl9rey Apr 5, 2026
ffd12a4
refactor: Chip 아이콘 reactNode로 변경
earl9rey Apr 5, 2026
e34d3d3
style: 색상 토큰 변경사항 적용
earl9rey Apr 5, 2026
c7d15db
feat: 상품 탭 바텀시트 기본 적용
earl9rey Apr 5, 2026
6e5d773
feat: 필터칩 선택 옵션 추가
earl9rey Apr 5, 2026
8475dd7
feat: 필터칩 선택시 closeBottomSheet 연결
earl9rey Apr 5, 2026
81c8ae0
refactor: 상품 탭 바텀시트 라이팅 변경
earl9rey Apr 7, 2026
530cb32
feat: 상품 탭 필터칩에 필터 바텀시트 연결
earl9rey Apr 7, 2026
ac48c27
style: 필터 바텀시트 타이틀 스타일 추가
earl9rey Apr 7, 2026
05888c4
feat: 상품 탭 필터 바텀시트 내부 컴포넌트 구현
earl9rey Apr 7, 2026
e9c934f
chore: 상품 탭 라이팅 변경
earl9rey Apr 9, 2026
a477462
Merge branch 'develop' of https://github.com/TEAM-HOUME/HOUME-CLIENT …
earl9rey Apr 9, 2026
b6d2000
Merge branch 'develop' of https://github.com/TEAM-HOUME/HOUME-CLIENT …
earl9rey Apr 9, 2026
edd8071
feat: ProductCard v2 컴포넌트 적용
earl9rey Apr 9, 2026
8702714
feat: 필터 적용하기 로직 구현
earl9rey Apr 9, 2026
4904c92
style: 하단바 높이만큼 padding 추가
earl9rey Apr 9, 2026
d63856a
feat: product Card에 이미지 목 추가
earl9rey Apr 9, 2026
2c448ba
refactor: 상품 바텀시트 내부 컴포넌트 네이밍 변경
earl9rey Apr 9, 2026
f83c236
feat: 상품 선택 로직 추가
earl9rey Apr 9, 2026
c9da247
feat: 상품 선택 해제 ui 추가
earl9rey Apr 9, 2026
1571fd5
feat: 상품 선택 해제 로직 추가
earl9rey Apr 9, 2026
566c90d
feat: 상품 선택 시 UI 적용
earl9rey Apr 9, 2026
81f93a9
style: 바텀시트 스크롤바 제거
earl9rey Apr 9, 2026
e817e43
feat: 상품 필터칩 대표값 선택 로직 변경
earl9rey Apr 11, 2026
e71a9c9
style: ProductCard 높이 minHeight 적용
earl9rey Apr 11, 2026
0d8fa58
style: 선택 상품 바텀시트 스타일 조정
earl9rey Apr 12, 2026
735ca6b
feat: 상품 선택 바텀시트 선택값 유지 로직 추가
earl9rey Apr 12, 2026
1297573
refactor: 인라인 함수를 별도 함수로 분리
earl9rey Apr 12, 2026
ca23546
style: 상품 선택 해제 관련 스타일에 recipe 적용
earl9rey Apr 12, 2026
8854984
feat: 상품 선택 개수에 따른 토스트 표시 로직 추가
earl9rey Apr 12, 2026
9e3f103
style: 상품 선택 해제 버튼 위치 조정
earl9rey Apr 12, 2026
2bbcb3d
feat: ActionButton에 visuallyDisabled 추가
earl9rey Apr 14, 2026
f11bc6f
feat: 팝업 모달 공컴 구현
earl9rey Apr 14, 2026
75a77dd
style: 팝업 모달 공컴 구현
earl9rey Apr 14, 2026
014c961
feat: 돋보기 아이콘에 onToggle 추가
earl9rey Apr 14, 2026
0688907
feat: ProductCard 상세보기 모달 적용
earl9rey Apr 16, 2026
b464aec
style: 상품 상세보기 모달 내부 컴포넌트 스타일 적용
earl9rey Apr 16, 2026
0ab10d4
style: 팝업 모달 zindex 조정
earl9rey Apr 16, 2026
c7fcf4d
style: ProductCard middleInfo 박스 minHeight 조정
earl9rey Apr 16, 2026
907560a
feat: 상품 상세 모달에 사이트 링크 버튼 추가
earl9rey Apr 16, 2026
17c6486
refactor: 상품명 및 스타일 조정
earl9rey Apr 16, 2026
c198ec0
refactor: 상품 탭 컴포넌트 내부에서 목데이터 제거
earl9rey Apr 21, 2026
d46b96f
feat: 상품 탭 메인 API 목데이터 분리 및 구조화
earl9rey Apr 21, 2026
841d754
refactor: 목데이터 파일 위치 변경
earl9rey Apr 21, 2026
3ae91c7
test: 상품 탭 메인 API 기반 테스트 추가
earl9rey Apr 21, 2026
e0609ed
feat: 상품 탭 메인 API 연동 기본 파일 생성
earl9rey Apr 21, 2026
72c0d7b
refactor: API 스펙과 목데이터 구조 통일
earl9rey Apr 21, 2026
a39d61c
refactor: 상품 선택 로직을 useProductTabState 훅으로 분리
earl9rey Apr 21, 2026
002dc20
fix: 필터 시트 오픈 시 상태 복원 로직 보완
earl9rey Apr 21, 2026
9b69f4b
chore: 불필요한 주석 삭제
earl9rey Apr 21, 2026
0869223
feat: 상품 필터 리스트 및 검색바 스크롤 여부에 따른 Sticky/노출 구현
earl9rey Apr 21, 2026
b11dcc7
refactor: 상품 검색/필터 헤더 Sticky 노출 로직 훅으로 분리
earl9rey Apr 21, 2026
882e9df
style: 검색바 노출 애니메이션 적용
earl9rey Apr 21, 2026
81068f3
chore: 불필요한 주석 제거
earl9rey Apr 21, 2026
43a5ff0
feat: forwardRef 컴포넌트에 displayName 추가
earl9rey Apr 21, 2026
0e1ec62
chore: aria 접근성 강화
earl9rey Apr 21, 2026
721ffca
fix: validateDOMNesting 해결
earl9rey Apr 21, 2026
69bced2
feat: 상품 모달에서 상품 선택 여부에 따른 버튼 비활성화 로직 추가
earl9rey Apr 21, 2026
76e62c7
style: 스티키 헤더 배경색 토큰으로 변경
earl9rey Apr 21, 2026
53ca8f5
refactor: 클래스네임 가독성 개선
earl9rey Apr 21, 2026
759c3b2
refactor: 상품 탭 스크롤 리스너 의존성 배열 최적화 및 초기 상태 동기화
earl9rey Apr 21, 2026
4e3e927
refactor: setValues 동기화 시점 간소화
earl9rey Apr 21, 2026
41e97e8
refactor: 불필요한 색상값 역조회 제거
earl9rey Apr 21, 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
1 change: 1 addition & 0 deletions src/pages/home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const HomePage = () => {
{ value: 'product', label: '상품' },
]}
activeTab={activeMenuTab}
sticky={activeMenuTab === 'explore'}
onTabChange={setActiveMenuTab}
/>
{activeMenuTab === 'explore' && <ExploreTab />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import { style } from '@vanilla-extract/css';

import { unitVars } from '@styles/tokensV2/unit.css';

/** 비율 박스 — introBanner가 inset으로 프레임에 맞게 채움 */
export const section = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
boxSizing: 'border-box',
aspectRatio: '37.5 / 22',
position: 'relative',
width: '100%',
minWidth: unitVars.unit.dimension.wMin,
maxWidth: unitVars.unit.dimension.wMax,
overflow: 'hidden',
});

export const introBanner = style({
position: 'absolute',
inset: 0,
display: 'block',
objectFit: 'cover',
objectPosition: 'center',
width: '100%',
height: '100%',
});
39 changes: 37 additions & 2 deletions src/pages/home/components/product/IntroSection/IntroSection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,42 @@
import { useEffect, useState } from 'react';

import bannerFallback from '@assets/v2/images/bannerFallback.svg';

import * as styles from './IntroSection.css';

const IntroSection = () => {
return <section className={styles.section}>IntroSection</section>;
export type IntroSectionProps = {
/** API 등에서 받은 인트로 이미지·GIF URL. 없으면 bannerFallback부터 표시 */
bannerUrl?: string | null;
alt?: string;
};

const resolveInitialSrc = (url: string | null | undefined) =>
url ? url : bannerFallback;

const IntroSection = ({ bannerUrl, alt = '상품 배너' }: IntroSectionProps) => {
const initialImageSrc = resolveInitialSrc(bannerUrl);

// 이미지 로드 실패 시 fallback src를 유지하고,
// 부모가 새 mediaUrl을 내려주면 그 값으로 다시 동기화 (RoomTypeCard와 동일 패턴)
Comment thread
earl9rey marked this conversation as resolved.
Outdated
const [imageSrc, setImageSrc] = useState(initialImageSrc);

useEffect(() => {
setImageSrc(resolveInitialSrc(bannerUrl));
}, [bannerUrl]);

return (
<section className={styles.section} aria-label={alt}>
<img
className={styles.introBanner}
src={imageSrc}
alt={alt}
draggable={false}
loading="eager"
decoding="async"
onError={() => setImageSrc(bannerFallback)}
Comment thread
earl9rey marked this conversation as resolved.
Outdated
/>
</section>
);
};

export default IntroSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { style } from '@vanilla-extract/css';

import { colorVars } from '@styles/tokensV2/color.css';
import { fontVars } from '@styles/tokensV2/font.css';
import { unitVars } from '@styles/tokensV2/unit.css';

export const root = style({
display: 'flex',
flexDirection: 'column',
gap: unitVars.unit.gapPadding['200'],
});

export const section = style({
display: 'flex',
flexDirection: 'column',
gap: unitVars.unit.gapPadding['300'],
});

export const sectionTitle = style({
textAlign: 'left',
color: colorVars.color.text.secondary,
...fontVars.font.title_m_15,
});

export const chipGroup = style({
display: 'flex',
flexWrap: 'wrap',
gap: unitVars.unit.gapPadding['100'],
});

export const colorChipInner = style({
display: 'inline-flex',
alignItems: 'center',
gap: unitVars.unit.gapPadding['100'],
});

export const colorDot = style({
flexShrink: 0,
borderWidth: '0.1rem',
borderStyle: 'solid',
borderRadius: unitVars.unit.radius.full,
borderColor: colorVars.color.border.secondary,
width: '1.2rem',
height: '1.2rem',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { forwardRef, useCallback, useImperativeHandle, useState } from 'react';

import Chip from '@components/v2/chip/Chip';

import * as styles from './ProductFilterSheet.css';

const FURNITURE_OPTIONS: { id: string; label: string }[] = [
{ id: 'ALL', label: '전체' },
{ id: 'bed', label: '침대/프레임' },
{ id: 'desk', label: '업무용 책상' },
{ id: 'dining', label: '식탁' },
{ id: 'floorTable', label: '좌식 테이블' },
{ id: 'wardrobe', label: '옷장' },
{ id: 'storage', label: '수납/장식장' },
{ id: 'sofa', label: '소파' },
{ id: 'chair', label: '의자/스툴' },
{ id: 'vanity', label: '화장대/협탁' },
{ id: 'light', label: '조명' },
{ id: 'other', label: '그 외' },
];

const PRICE_OPTIONS: { id: string; label: string }[] = [
{ id: 'ALL', label: '전체' },
{ id: 'under50k', label: '5만 원 이하' },
{ id: '50to100k', label: '5-10만 원' },
{ id: '10man', label: '10만 원대' },
{ id: '20man', label: '20만 원대' },
{ id: '30man', label: '30만 원대' },
{ id: '40man', label: '40만 원대' },
{ id: 'over50man', label: '50만 원 이상' },
];

const COLOR_OPTIONS: { id: string; label: string; dot?: string }[] = [
{ id: 'ALL', label: '전체' },
{ id: 'white', label: '화이트', dot: '#FFFFFF' },
{ id: 'gray', label: '그레이', dot: '#8E959E' },
{ id: 'black', label: '블랙', dot: '#1B1E22' },
{ id: 'silver', label: '실버', dot: '#C8CDD2' },
{ id: 'gold', label: '골드', dot: '#D4AF37' },
{ id: 'beige', label: '베이지', dot: '#D4C4B0' },
{ id: 'brown', label: '브라운', dot: '#5C4033' },
{ id: 'red', label: '레드', dot: '#E53935' },
{ id: 'orange', label: '오렌지', dot: '#FB8C00' },
{ id: 'yellow', label: '옐로우', dot: '#FDD835' },
{ id: 'green', label: '그린', dot: '#43A047' },
{ id: 'blue', label: '블루', dot: '#1E88E5' },
{ id: 'navy', label: '네이비', dot: '#1A237E' },
{ id: 'violet', label: '바이올렛', dot: '#7E57C2' },
{ id: 'pink', label: '핑크', dot: '#EC407A' },
];
Comment thread
earl9rey marked this conversation as resolved.

const ALL = 'ALL';

const INITIAL_SELECTION: string[] = [ALL];

/** 섹션 내 다중 선택: ALL만 있으면 ‘전체’, 그 외는 선택된 id 목록 */
function toggleSectionSelection(current: string[], id: string): string[] {
if (id === ALL) {
return [ALL];
}
const withoutAll = current.filter((x) => x !== ALL);
const has = withoutAll.includes(id);
const next = has ? withoutAll.filter((x) => x !== id) : [...withoutAll, id];
return next.length === 0 ? [ALL] : next;
}

export interface ProductFilterValues {
furnitureTypeIds: string[];
priceRangeIds: string[];
colorIds: string[];
}

export interface ProductFilterSheetRef {
reset: () => void;
getValues: () => ProductFilterValues;
setValues: (values: ProductFilterValues) => void;
}

const ProductFilterSheet = forwardRef<ProductFilterSheetRef>(
function ProductFilterSheet(_props, ref) {
Comment thread
earl9rey marked this conversation as resolved.
const [furnitureTypeIds, setFurnitureTypeIds] =
useState<string[]>(INITIAL_SELECTION);
const [priceRangeIds, setPriceRangeIds] =
useState<string[]>(INITIAL_SELECTION);
const [colorIds, setColorIds] = useState<string[]>(INITIAL_SELECTION);

const reset = useCallback(() => {
setFurnitureTypeIds([...INITIAL_SELECTION]);
setPriceRangeIds([...INITIAL_SELECTION]);
setColorIds([...INITIAL_SELECTION]);
}, []);

const getValues = useCallback((): ProductFilterValues => {
return {
furnitureTypeIds: [...furnitureTypeIds],
priceRangeIds: [...priceRangeIds],
colorIds: [...colorIds],
};
}, [furnitureTypeIds, priceRangeIds, colorIds]);

const setValues = useCallback((values: ProductFilterValues) => {
setFurnitureTypeIds(
values.furnitureTypeIds.length > 0
? [...values.furnitureTypeIds]
: [...INITIAL_SELECTION]
);
setPriceRangeIds(
values.priceRangeIds.length > 0
? [...values.priceRangeIds]
: [...INITIAL_SELECTION]
);
setColorIds(
values.colorIds.length > 0
? [...values.colorIds]
: [...INITIAL_SELECTION]
);
}, []);

useImperativeHandle(
ref,
() => ({
reset,
getValues,
setValues,
}),
[reset, getValues, setValues]
);

return (
<div className={styles.root}>
<section
className={styles.section}
aria-labelledby="filter-furniture-heading"
>
<h2 id="filter-furniture-heading" className={styles.sectionTitle}>
카테고리
</h2>
<div className={styles.chipGroup} role="group" aria-label="카테고리">
{FURNITURE_OPTIONS.map(({ id, label }) => (
<Chip
key={id}
selected={furnitureTypeIds.includes(id)}
onClick={() =>
setFurnitureTypeIds((prev) =>
toggleSectionSelection(prev, id)
)
}
>
{label}
</Chip>
))}
</div>
</section>

<section
className={styles.section}
aria-labelledby="filter-price-heading"
>
<h2 id="filter-price-heading" className={styles.sectionTitle}>
가격대
</h2>
<div className={styles.chipGroup} role="group" aria-label="가격대">
{PRICE_OPTIONS.map(({ id, label }) => (
<Chip
key={id}
selected={priceRangeIds.includes(id)}
onClick={() =>
setPriceRangeIds((prev) => toggleSectionSelection(prev, id))
}
>
{label}
</Chip>
))}
</div>
</section>

<section
className={styles.section}
aria-labelledby="filter-color-heading"
>
<h2 id="filter-color-heading" className={styles.sectionTitle}>
색상
</h2>
<div className={styles.chipGroup} role="group" aria-label="색상">
{COLOR_OPTIONS.map(({ id, label, dot }) => (
<Chip
key={id}
selected={colorIds.includes(id)}
onClick={() =>
setColorIds((prev) => toggleSectionSelection(prev, id))
}
>
{dot ? (
<span className={styles.colorChipInner}>
<span
className={styles.colorDot}
style={{
backgroundColor: dot,
}}
/>
{label}
</span>
) : (
label
)}
</Chip>
))}
</div>
</section>
</div>
);
}
);

export default ProductFilterSheet;
15 changes: 14 additions & 1 deletion src/pages/home/components/product/ProductTab.css.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { style } from '@vanilla-extract/css';

import { colorVars } from '@styles/tokensV2/color.css';
import { fontVars } from '@styles/tokensV2/font.css';
import { unitVars } from '@styles/tokensV2/unit.css';

export const container = style({
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
alignItems: 'stretch',
alignSelf: 'stretch',
width: '100%',
minWidth: unitVars.unit.dimension.wMin,
maxWidth: unitVars.unit.dimension.wMax,
});

export const filterSheetTitle = style({
color: colorVars.color.text.primary,
...fontVars.font.title_m_16,
});
Loading
Loading