diff --git a/src/pages/generate/pages/result/curationSection/curationProducts.ts b/src/pages/generate/pages/result/curationSection/curationProducts.ts index 60e5b7b34..8ef2d8c41 100644 --- a/src/pages/generate/pages/result/curationSection/curationProducts.ts +++ b/src/pages/generate/pages/result/curationSection/curationProducts.ts @@ -2,7 +2,7 @@ import type { FurnitureProductInfo } from '@pages/generate/types/furniture'; const COLOR_NAME_TO_HEX_MAP: Record = { 화이트: '#FFFFFF', - 브라운: '#A52A2A', + 브라운: '#8B4513', 블루: '#0000FF', 블랙: '#000000', 베이지: '#F5F5DC', @@ -14,7 +14,7 @@ const COLOR_NAME_TO_HEX_MAP: Record = { 그레이: '#808080', 실버: '#C0C0C0', 오렌지: '#FFA500', - 바이올렛: '#EE82EE', + 바이올렛: '#8F00FF', 네이비: '#000080', }; @@ -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; }) .filter((color): color is string => Boolean(color)); }; diff --git a/src/pages/style/StyleDetailPage.css.ts b/src/pages/style/StyleDetailPage.css.ts new file mode 100644 index 000000000..ee8bfdc26 --- /dev/null +++ b/src/pages/style/StyleDetailPage.css.ts @@ -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']}`, +}); + +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)`, +}); diff --git a/src/pages/style/StyleDetailPage.tsx b/src/pages/style/StyleDetailPage.tsx index cd187df32..8c6562531 100644 --- a/src/pages/style/StyleDetailPage.tsx +++ b/src/pages/style/StyleDetailPage.tsx @@ -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'; + 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); + }; + // 스타일 상세 CTA: setFlow(STYLE_RESTYLE) → 로그인 체크 → 스타일 상세로 복귀 const handleCta = () => { const parsedId = Number(styleId); @@ -33,9 +54,58 @@ const StyleDetailPage = () => { }; return ( -
-
스타일 상세 페이지 / styleId: {styleId}
- +
+ navigate(-1)} + /> +
+
+ +
+
+ 사용된 가구 +
+ {STYLE_DETAIL_MOCK.data.products.map((item) => ( + handleToggleSave(item.id), + }} + link={{ + href: item.linkUrl, + // onClick: logMyPageClickBtnFurnitureCard, + }} + enableWholeCardLink={true} + /> + ))} +
+
+
+ + 이 스타일로 우리 집 꾸미기 +
); }; diff --git a/src/pages/style/StyleListPage.css.ts b/src/pages/style/StyleListPage.css.ts new file mode 100644 index 000000000..bb8fff759 --- /dev/null +++ b/src/pages/style/StyleListPage.css.ts @@ -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']}`, +}); diff --git a/src/pages/style/StyleListPage.tsx b/src/pages/style/StyleListPage.tsx index 42f369d71..ef613b1e9 100644 --- a/src/pages/style/StyleListPage.tsx +++ b/src/pages/style/StyleListPage.tsx @@ -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
스타일 목록 페이지
; + const navigate = useNavigate(); + const handleStyleClick = (styleId: number) => { + navigate(generatePath(ROUTES.STYLE_DETAIL, { styleId: String(styleId) })); + }; + + return ( +
+ navigate(-1)} + /> +
+ {STYLELIST_MOCK.data.otherStyles.map((style) => ( + handleStyleClick(style.id)} + imageLoading="eager" + /> + ))} +
+
+ ); }; export default StyleListPage; diff --git a/src/pages/style/mocks/styleDetail.ts b/src/pages/style/mocks/styleDetail.ts new file mode 100644 index 000000000..e9dad5e5e --- /dev/null +++ b/src/pages/style/mocks/styleDetail.ts @@ -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', + }, + { + 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: + '블랙을 중심으로 모노톤 인테리어에 핑크 포인트로 감각을 더한 개발자의 집이다. 층고가 높은 복층으로 업무 책상을 복층에 배치해 공간 활용성을 높였다. 높였더높였다높였다높여다높여', + 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', + }, + ], + }, + ], + }, +}; diff --git a/src/shared/components/v2/productCard/ListProductCard.css.ts b/src/shared/components/v2/productCard/ListProductCard.css.ts index 2e49e11de..254ea912d 100644 --- a/src/shared/components/v2/productCard/ListProductCard.css.ts +++ b/src/shared/components/v2/productCard/ListProductCard.css.ts @@ -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', + }, }, }, });