Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b306eaa
feat: 스타일 디테일 (목록/상세) 목데이터
soyyyyy Apr 8, 2026
45c3868
feat: 스타일 목록 페이지
soyyyyy Apr 8, 2026
6074a58
chore: 목데이터 수정
soyyyyy Apr 8, 2026
dbbf6c0
chore: 주석 수정
soyyyyy Apr 8, 2026
f157e0e
feat: 스타일 디테일 페이지
soyyyyy Apr 8, 2026
0985b61
chore: 목데이터 추가
soyyyyy Apr 8, 2026
9c182cf
style: cta 버튼 고정
soyyyyy Apr 8, 2026
65f5794
feat: 스타일 디테일 (목록/상세) 목데이터
soyyyyy Apr 8, 2026
1d96472
feat: 스타일 목록 페이지
soyyyyy Apr 8, 2026
1fac859
chore: 목데이터 수정
soyyyyy Apr 8, 2026
609ea23
chore: 주석 수정
soyyyyy Apr 8, 2026
0d27ecc
feat: 스타일 디테일 페이지
soyyyyy Apr 8, 2026
e44f989
chore: 목데이터 추가
soyyyyy Apr 8, 2026
45c8648
style: cta 버튼 고정
soyyyyy Apr 8, 2026
87b6420
style: cta버튼 여백
soyyyyy Apr 9, 2026
77d4a9e
chore: 공컴 수정 반영
soyyyyy Apr 9, 2026
c1fd3b9
Merge remote-tracking branch 'origin/feat/styleDetailPage/#507' into …
soyyyyy Apr 9, 2026
96687b6
style: listcard 텍스트 컨테이너 최소 넓이 수정
soyyyyy Apr 9, 2026
a6506c4
style: product container style
soyyyyy Apr 9, 2026
5b15123
fix: normalizeColorHexes 함수 객체 형태도 받도록 수정
soyyyyy Apr 9, 2026
db20d0b
chore: 스타일 주석 해제
soyyyyy Apr 9, 2026
aa95e85
chore: cta버튼 onclick 연결
soyyyyy Apr 9, 2026
a233414
fix: 컬러칩 객체 분기 검증 추가
soyyyyy Apr 9, 2026
c107595
fix: 찜하기 스토어 구독 (근데 목데이터라 안됨)
soyyyyy Apr 9, 2026
5be76b6
chore: 색상 hex 업데이트
soyyyyy Apr 9, 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { FurnitureProductInfo } from '@pages/generate/types/furniture';

const COLOR_NAME_TO_HEX_MAP: Record<string, string> = {
화이트: '#FFFFFF',
브라운: '#A52A2A',
브라운: '#8B4513',
블루: '#0000FF',
블랙: '#000000',
베이지: '#F5F5DC',
Expand All @@ -14,7 +14,7 @@ const COLOR_NAME_TO_HEX_MAP: Record<string, string> = {
그레이: '#808080',
실버: '#C0C0C0',
오렌지: '#FFA500',
바이올렛: '#EE82EE',
바이올렛: '#8F00FF',
네이비: '#000080',
};

Expand Down Expand Up @@ -75,11 +75,24 @@ export const normalizeColorHexes = (value: unknown) => {
if (!Array.isArray(value)) return [];

return value
.filter((color): color is string => typeof color === 'string')
.map((color) => color.trim())
.map((color) => {
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) return color;
return COLOR_NAME_TO_HEX_MAP[color] ?? null;
// { name, value } 형태
if (typeof color === 'object' && color !== null && 'value' in color) {
const rawValue = (color as { value?: unknown }).value;
if (typeof rawValue !== 'string') return null;
const trimmed = rawValue.trim();
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed)) {
return trimmed;
}
return COLOR_NAME_TO_HEX_MAP[trimmed] ?? null;
}
// string 형태 → 한글 이름이면 매핑
if (typeof color === 'string') {
const trimmed = color.trim();
if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed)) return trimmed;
return COLOR_NAME_TO_HEX_MAP[trimmed] ?? null;
}
return null;
Comment on lines +79 to +95
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

색상 파싱 로직 중복을 헬퍼로 추출해 유지보수성을 높여주세요.

객체 분기와 문자열 분기에서 trim → hex 검사 → 이름 매핑 로직이
반복됩니다. 규칙 변경 시 한쪽만 수정되는 리스크가 있습니다.

리팩터링 예시
+const resolveColorToHex = (raw: string) => {
+  const trimmed = raw.trim();
+  if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed)) {
+    return trimmed;
+  }
+  return COLOR_NAME_TO_HEX_MAP[trimmed] ?? null;
+};
+
 export const normalizeColorHexes = (value: unknown) => {
   if (!Array.isArray(value)) return [];

   return value
     .map((color) => {
-      // { name, value } 형태
       if (typeof color === 'object' && color !== null && 'value' in color) {
         const rawValue = (color as { value?: unknown }).value;
         if (typeof rawValue !== 'string') return null;
-        const trimmed = rawValue.trim();
-        if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed)) {
-          return trimmed;
-        }
-        return COLOR_NAME_TO_HEX_MAP[trimmed] ?? null;
+        return resolveColorToHex(rawValue);
       }
-      // string 형태 → 한글 이름이면 매핑
       if (typeof color === 'string') {
-        const trimmed = color.trim();
-        if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed)) return trimmed;
-        return COLOR_NAME_TO_HEX_MAP[trimmed] ?? null;
+        return resolveColorToHex(color);
       }
       return null;
     })
     .filter((color): color is string => Boolean(color));
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/generate/pages/result/curationSection/curationProducts.ts` around
lines 79 - 95, The color parsing logic is duplicated across the object and
string branches; extract a helper (e.g., parseColorToHex(value: unknown): string
| null) that performs "trim → hex regex test → lookup in COLOR_NAME_TO_HEX_MAP"
and use it for both the object branch (pass (color as {value?: unknown}).value)
and the string branch (pass color). Ensure the helper returns null for
non-strings and preserves the current hex regex and mapping behavior so both
branches delegate to this single function.

})
.filter((color): color is string => Boolean(color));
Comment thread
soyyyyy marked this conversation as resolved.
};
Expand Down
48 changes: 48 additions & 0 deletions src/pages/style/StyleDetailPage.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { style } from '@vanilla-extract/css';

import { zIndex } from '@/shared/styles/tokens/zIndex';
import { fontVars } from '@/shared/styles/tokensV2/font.css';
import { unitVars } from '@/shared/styles/tokensV2/unit.css';

export const wrapper = style({
display: 'flex',
flexDirection: 'column',
paddingBottom: '9.6rem',
});

export const container = style({
display: 'flex',
flexDirection: 'column',
gap: unitVars.unit.gapPadding['700'],
padding: `${unitVars.unit.gapPadding['300']} ${unitVars.unit.gapPadding['500']}`,
});
Comment thread
soyyyyy marked this conversation as resolved.

export const styleCardInfo = style({});

export const productList = style({
display: 'flex',
flexDirection: 'column',
});

export const sectionTitle = style({
...fontVars.font.title_sb_16,
marginBottom: unitVars.unit.gapPadding['400'],
});

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

export const ctaBtn = style({
position: 'fixed',
zIndex: zIndex.button,
right: 0,
bottom: unitVars.unit.gapPadding['500'],
left: 0,
margin: '0 auto',
width: `calc(100% - ${unitVars.unit.gapPadding['500']} * 2)`,
maxWidth: `calc(${unitVars.unit.dimension.wMax} - ${unitVars.unit.gapPadding['500']} * 2)`,
});
76 changes: 73 additions & 3 deletions src/pages/style/StyleDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,37 @@ import { useNavigate, useLocation, useParams } from 'react-router-dom';
import { ROUTES } from '@routes/paths';

import { ENTRY_ROUTE, useImageFlowStore } from '@store/useImageFlowStore';
import { useSavedItemsStore } from '@store/useSavedItemsStore';
import { useUserStore } from '@store/useUserStore';

import { useJjymMutation } from '@apis/mutations/useJjymMutation';

import ActionButton from '@components/v2/button/actionButton/ActionButton';
import TitleNavBar from '@components/v2/navBar/TitleNavBar';
import ListCardProduct from '@components/v2/productCard/ListProductCard';
import StyleCard from '@components/v2/styleCard/StyleCard';

import { setLoginRedirect } from '@utils/loginRedirect';

import { STYLE_DETAIL_MOCK } from './mocks/styleDetail';
import * as styles from './StyleDetailPage.css';
import { normalizeColorHexes } from '../generate/pages/result/curationSection/curationProducts';
Comment on lines +18 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

현재 상세 화면은 styleId와 무관하게 항상 같은 데이터를 보여줍니다.

리스트에서는 styleId를 바꿔서 이동시키는데, 이 페이지 본문은
STYLE_DETAIL_MOCK.data를 고정 참조하고 있어서 어떤 카드를 눌러도 같은
상세가 렌더링됩니다. styleId로 mock을 선택하거나, 실제 API 연동 전이라도
id별 분기 정도는 넣어 두셔야 합니다.

Also applies to: 54-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/style/StyleDetailPage.tsx` around lines 18 - 20, The page always
renders the same data because StyleDetailPage imports and uses STYLE_DETAIL_MOCK
directly; change the rendering to derive the displayed mock from the incoming
styleId (route param or prop) instead of fixed STYLE_DETAIL_MOCK: read the
styleId (e.g., from useParams or props) in StyleDetailPage, pick the appropriate
mock entry (map/filter STYLE_DETAIL_MOCK.data by id) or fall back to a sensible
default, and pass that selected object through the same normalization pipeline
(normalizeColorHexes) and to the components that currently consume
STYLE_DETAIL_MOCK; update any logic around the current constant usage
(references between lines ~54-77) to use the selectedMock variable so different
cards render distinct details.


const StyleDetailPage = () => {
const { styleId } = useParams();
const navigate = useNavigate();
const location = useLocation();
const isLoggedIn = !!useUserStore((state) => state.accessToken);

const savedProductIds = useSavedItemsStore((state) => state.savedProductIds);

// 찜 해제 토글
const { mutate: toggleJjym } = useJjymMutation();

const handleToggleSave = (id: number) => {
toggleJjym(id);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 스타일 상세 CTA: setFlow(STYLE_RESTYLE) → 로그인 체크 → 스타일 상세로 복귀
const handleCta = () => {
const parsedId = Number(styleId);
Expand All @@ -33,9 +54,58 @@ const StyleDetailPage = () => {
};

return (
<div>
<div>스타일 상세 페이지 / styleId: {styleId}</div>
<button onClick={handleCta}>이 스타일로 우리 집 꾸미기</button>
<div className={styles.wrapper}>
<TitleNavBar
title="스타일 상세 보기"
backLabel="이전"
onBackClick={() => navigate(-1)}
/>
<div className={styles.container}>
<section className={styles.styleCardInfo}>
<StyleCard
size="L"
title={STYLE_DETAIL_MOCK.data.styleName}
largeContents={{
title: STYLE_DETAIL_MOCK.data.styleName,
description: STYLE_DETAIL_MOCK.data.styleDescription,
}}
imageSrc={STYLE_DETAIL_MOCK.data.styleImageUrl}
imageLoading="eager"
/>
</section>
<section className={styles.productList}>
<span className={styles.sectionTitle}>사용된 가구</span>
<div className={styles.products}>
{STYLE_DETAIL_MOCK.data.products.map((item) => (
<ListCardProduct
key={item.id}
product={{
title: item.name,
imageUrl: item.imageUrl,
colorHexes: normalizeColorHexes(item.colors),
}}
price={{
original: item.originalPrice,
discount: item.finalPrice,
discountRate: item.discountRate,
}}
save={{
isSaved: savedProductIds.has(item.id),
onToggle: () => handleToggleSave(item.id),
}}
link={{
href: item.linkUrl,
// onClick: logMyPageClickBtnFurnitureCard,
}}
enableWholeCardLink={true}
/>
))}
</div>
</section>
</div>
<ActionButton onClick={handleCta} className={styles.ctaBtn}>
이 스타일로 우리 집 꾸미기
</ActionButton>
</div>
);
};
Expand Down
15 changes: 15 additions & 0 deletions src/pages/style/StyleListPage.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { style } from '@vanilla-extract/css';

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

export const wrapper = style({
display: 'flex',
flexDirection: 'column',
});

export const cardList = style({
display: 'flex',
flexDirection: 'column',
gap: unitVars.unit.gapPadding['400'],
padding: `${unitVars.unit.gapPadding['300']} ${unitVars.unit.gapPadding['500']}`,
});
36 changes: 35 additions & 1 deletion src/pages/style/StyleListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
import { generatePath, useNavigate } from 'react-router-dom';

import { ROUTES } from '@/routes/paths';
import TitleNavBar from '@/shared/components/v2/navBar/TitleNavBar';
import StyleCard from '@/shared/components/v2/styleCard/StyleCard';

import { STYLELIST_MOCK } from './mocks/styleDetail';
import * as styles from './StyleListPage.css';

const StyleListPage = () => {
return <div>스타일 목록 페이지</div>;
const navigate = useNavigate();
const handleStyleClick = (styleId: number) => {
navigate(generatePath(ROUTES.STYLE_DETAIL, { styleId: String(styleId) }));
};

return (
<section className={styles.wrapper}>
<TitleNavBar
title="스타일 전체 보기"
backLabel="이전"
onBackClick={() => navigate(-1)}
/>
<div className={styles.cardList}>
{STYLELIST_MOCK.data.otherStyles.map((style) => (
<StyleCard
size="L"
key={style.id}
imageSrc={style.imageUrl}
title={style.name}
onClick={() => handleStyleClick(style.id)}
imageLoading="eager"
/>
))}
</div>
</section>
);
};

export default StyleListPage;
116 changes: 116 additions & 0 deletions src/pages/style/mocks/styleDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
export const STYLELIST_MOCK = {
data: {
otherStyles: [
{
id: 1,
name: '미니멀한 개발자의 집',
imageUrl: '@assets/v2/images/Imgbanner_01.png',
},
{
id: 2,
name: '미니멀한 개발자의 집',
imageUrl: '@assets/v2/images/Imgbanner_02.png',
},
{
id: 3,
name: '빈티지 카페 감성 공간',
imageUrl: '@assets/v2/images/Imgbanner_03.png',
Comment thread
soyyyyy marked this conversation as resolved.
},
{
id: 4,
name: '미니멀한 개발자의 집',
imageUrl: '@assets/v2/images/Imgbanner_04.png',
},
{
id: 5,
name: '미니멀한 개발자의 집',
imageUrl: '@assets/v2/images/Imgbanner_01.png',
},
{
id: 6,
name: '미니멀한 개발자의 집',
imageUrl: '@assets/v2/images/Imgbanner_02.png',
},
{
id: 7,
name: '미니멀한 개발자의 집',
imageUrl: '@assets/v2/images/Imgbanner_03.png',
},
],
},
};

export const STYLE_DETAIL_MOCK = {
data: {
styleName: '미니멀한 개발자의 집',
styleImageUrl: '@assets/v2/images/Imgbanner_01.png',
styleDescription:
'블랙을 중심으로 모노톤 인테리어에 핑크 포인트로 감각을 더한 개발자의 집이다. 층고가 높은 복층으로 업무 책상을 복층에 배치해 공간 활용성을 높였다. 높였더높였다높였다높여다높여',
Comment thread
soyyyyy marked this conversation as resolved.
products: [
{
id: 1,
name: '리샘 코지 저상형 평상형 무헤드 침대(SS/Q) 매트리스 선택',
imageUrl: '@assets/v2/images/Product_01.png',
originalPrice: 39900,
discountRate: 30,
finalPrice: 27990,
linkUrl: '@assets/v2/images/Imgbanner_01.png',
isLiked: false,
colors: [
{
name: '블루',
value: '#0000FF',
},
],
},
{
id: 2,
name: '리샘 코지 저상형 평상형 무헤드 침대(SS/Q) 매트리스 선택',
imageUrl: '@assets/v2/images/Product_01.png',
originalPrice: 39900,
discountRate: 30,
finalPrice: 27990,
linkUrl: '@assets/v2/images/Imgbanner_01.png',
isLiked: true,
colors: [
{
name: '브라운',
value: '#8B4513',
},
],
},
{
id: 3,
name: '리샘 코지 저상형 평상형 무헤드 침대(SS/Q) 매트리스 선택',
imageUrl: '@assets/v2/images/Product_01.png',
originalPrice: 39900,
discountRate: 30,
finalPrice: 27990,
linkUrl: '@assets/v2/images/Imgbanner_01.png',
isLiked: true,
colors: [
{
name: '브라운',
value: '#8B4513',
},
],
},
{
id: 4,
name: '리샘 코지 저상형 평상형 무헤드 침대(SS/Q) 매트리스 선택',
imageUrl: '@assets/v2/images/Product_01.png',
originalPrice: 39900,
discountRate: 30,
finalPrice: 27990,
linkUrl: '@assets/v2/images/Imgbanner_01.png',
isLiked: true,
colors: [
{
name: '브라운',
value: '#8B4513',
},
],
},
],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ export const infoSection = recipe({
variants: {
size: {
s: { gap: unitVars.unit.gapPadding['050'], minWidth: '22rem' },
m: { gap: unitVars.unit.gapPadding['100'], minWidth: '26.1rem' },
m: {
gap: unitVars.unit.gapPadding['100'],
minWidth: '24.7rem',
},
},
},
});
Expand Down
Loading