diff --git a/src/pages/generate/pages/result/ResultPage.css.ts b/src/pages/generate/pages/result/ResultPage.css.ts index 8ab3b3ad1..52a559507 100644 --- a/src/pages/generate/pages/result/ResultPage.css.ts +++ b/src/pages/generate/pages/result/ResultPage.css.ts @@ -11,17 +11,13 @@ export const wrapper = style({ display: 'flex', flexDirection: 'column', width: '100%', - height: `calc(100dvh - ${layoutVars.titleNavBarHeight})`, // TitleNavBar height - overflow: 'hidden', + minHeight: `calc(100dvh - ${layoutVars.titleNavBarHeight})`, // TitleNavBar height }); export const resultSection = style({ display: 'flex', flexDirection: 'column', width: '100%', - height: '100%', - minHeight: 0, - overflow: 'hidden', }); export const imgArea = recipe({ diff --git a/src/pages/generate/pages/result/components/GeneratedImg.css.ts b/src/pages/generate/pages/result/components/GeneratedImg.css.ts index c269b0c9b..0747c4b21 100644 --- a/src/pages/generate/pages/result/components/GeneratedImg.css.ts +++ b/src/pages/generate/pages/result/components/GeneratedImg.css.ts @@ -1,4 +1,4 @@ -import { style } from '@vanilla-extract/css'; +import { globalStyle, style } from '@vanilla-extract/css'; import { recipe } from '@vanilla-extract/recipes'; // import { zIndex } from '@/shared/styles/tokens/zIndex'; @@ -75,25 +75,55 @@ export const slideNumSkeleton = style({ animation: `${animationTokens.skeletonWave} 1.6s ease-in-out infinite`, }); +export const slideBtnCircle = style({ + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: '2.4rem', + height: '2.4rem', + borderRadius: '1.2rem', + backgroundColor: colorVars.color.gray999, + pointerEvents: 'none', + transition: 'opacity 0.2s ease-in-out', +}); + +export const slideBtnIcon = style({ + position: 'relative', + zIndex: 1, + width: '1.2rem', + height: '1.2rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); + +globalStyle(`${slideBtnIcon} > svg`, { + width: '1.2rem', + height: '1.2rem', +}); + export const slidePrevBtn = style({ position: 'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center', - left: '1.2rem', - bottom: '50%', + left: '0.6rem', + top: 'calc(50% + 0.8rem)', + transform: 'translateY(-50%)', width: '3.6rem', height: '3.6rem', - backgroundColor: colorVars.color.gray999_30, borderRadius: '99.9rem', + padding: 0, + border: 'none', + background: 'transparent', + appearance: 'none', + WebkitTapHighlightColor: 'transparent', zIndex: 1, - - ':active': { - backgroundColor: colorVars.color.gray999_50, - }, - - ':disabled': { - backgroundColor: colorVars.color.gray999_04, + selectors: { + '&:disabled': { + cursor: 'default', + }, }, }); @@ -102,21 +132,47 @@ export const slideNextBtn = style({ display: 'flex', alignItems: 'center', justifyContent: 'center', - right: '1.2rem', - bottom: '50%', + right: '0.6rem', + top: 'calc(50% + 0.8rem)', + transform: 'translateY(-50%)', width: '3.6rem', height: '3.6rem', - backgroundColor: colorVars.color.gray999_30, borderRadius: '99.9rem', + padding: 0, + border: 'none', + background: 'transparent', + appearance: 'none', + WebkitTapHighlightColor: 'transparent', zIndex: 1, - - ':active': { - backgroundColor: colorVars.color.gray999_50, + selectors: { + '&:disabled': { + cursor: 'default', + }, }, +}); - ':disabled': { - backgroundColor: colorVars.color.gray999_04, - }, +globalStyle(`${slidePrevBtn} ${slideBtnCircle}`, { + opacity: 0.3, +}); + +globalStyle(`${slidePrevBtn}:active:not(:disabled) ${slideBtnCircle}`, { + opacity: 0.5, +}); + +globalStyle(`${slidePrevBtn}:disabled ${slideBtnCircle}`, { + opacity: 0.04, +}); + +globalStyle(`${slideNextBtn} ${slideBtnCircle}`, { + opacity: 0.3, +}); + +globalStyle(`${slideNextBtn}:active:not(:disabled) ${slideBtnCircle}`, { + opacity: 0.5, +}); + +globalStyle(`${slideNextBtn}:disabled ${slideBtnCircle}`, { + opacity: 0.04, }); export const imgAreaBlurred = recipe({ diff --git a/src/pages/generate/pages/result/components/GeneratedImgA.tsx b/src/pages/generate/pages/result/components/GeneratedImgA.tsx index 6dc650990..24317835a 100644 --- a/src/pages/generate/pages/result/components/GeneratedImgA.tsx +++ b/src/pages/generate/pages/result/components/GeneratedImgA.tsx @@ -201,7 +201,10 @@ const GeneratedImgA = ({ className={styles.slidePrevBtn} disabled={!swiper || currentSlideIndex === 0} > - {currentSlideIndex === 0 ? : } + + + {currentSlideIndex === 0 ? : } + {images.map((image, index) => { const cachedDetection = @@ -252,11 +255,14 @@ const GeneratedImgA = ({ className={styles.slideNextBtn} disabled={!swiper || currentSlideIndex === totalSlideCount - 1} > - {currentSlideIndex === totalSlideCount - 1 ? ( - - ) : ( - - )} + + + {currentSlideIndex === totalSlideCount - 1 ? ( + + ) : ( + + )} + diff --git a/src/pages/generate/pages/result/curationSheet/CurationSheet.css.ts b/src/pages/generate/pages/result/curationSheet/CurationSheet.css.ts index 488f5c8a0..65a25883d 100644 --- a/src/pages/generate/pages/result/curationSheet/CurationSheet.css.ts +++ b/src/pages/generate/pages/result/curationSheet/CurationSheet.css.ts @@ -7,13 +7,10 @@ import { colorVars } from '@styles/tokens/color.css'; export const container = style({ width: '100%', - flex: '1 1 auto', - minHeight: 0, display: 'flex', flexDirection: 'column', padding: '2rem 2rem 0', backgroundColor: colorVars.color.gray000, - overflow: 'hidden', }); export const title = style({ @@ -63,13 +60,11 @@ export const filterSkeletonChipWidth = styleVariants({ }); export const content = style({ - flex: 1, - minHeight: 0, + width: '100%', display: 'flex', flexDirection: 'column', - overflowY: 'auto', marginTop: '0.8rem', - overscrollBehavior: 'contain', + paddingBottom: '2.4rem', selectors: { '&::-webkit-scrollbar': { @@ -84,16 +79,14 @@ export const gridbox = style({ width: '100%', height: 'fit-content', display: 'grid', - gridTemplateColumns: 'repeat(2, 16.4rem)', + gridTemplateColumns: 'repeat(2, minmax(16.4rem, 1fr))', columnGap: '0.7rem', rowGap: 0, - justifyContent: 'space-between', - justifyItems: 'start', }); export const statusContainer = style({ width: '100%', - height: '100%', + minHeight: '22rem', display: 'flex', flexDirection: 'column', alignItems: 'center', diff --git a/src/pages/generate/pages/result/curationSheet/CurationSheet.tsx b/src/pages/generate/pages/result/curationSheet/CurationSheet.tsx index 5af0a4c46..fced36342 100644 --- a/src/pages/generate/pages/result/curationSheet/CurationSheet.tsx +++ b/src/pages/generate/pages/result/curationSheet/CurationSheet.tsx @@ -38,6 +38,40 @@ type ProductPrefetchQueryKey = [ }, ]; +const RESULT_CARD_UI_FALLBACK = { + productName: '상품명 준비중', + mallName: '브랜드 준비중', + originalPrice: 0, + discountPrice: 0, + discountRate: 0, + colorHexes: ['#E7EBF0', '#D7DFE8', '#C3CFDD', '#AEBED0'], + saveCount: 0, +} as const; + +const normalizeText = (value: unknown, fallback: string) => { + if (typeof value !== 'string') return fallback; + const normalized = value.trim(); + return normalized.length > 0 ? normalized : fallback; +}; + +const toFiniteNumber = (value: unknown) => { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : null; +}; + +const normalizeColorHexes = (value: unknown) => { + if (!Array.isArray(value)) return [...RESULT_CARD_UI_FALLBACK.colorHexes]; + + const normalized = value + .filter((hex): hex is string => typeof hex === 'string') + .map((hex) => hex.trim()) + .filter((hex) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)); + + return normalized.length > 0 + ? normalized + : [...RESULT_CARD_UI_FALLBACK.colorHexes]; +}; + interface CurationSheetProps { groupId?: number | null; } @@ -96,37 +130,37 @@ export const CurationSheet = ({ groupId = null }: CurationSheetProps) => { ? byProductId : index + 1; - const originalPrice = Number(product.furnitureProductOriginalPrice); - const discountPrice = Number(product.furnitureProductDiscountPrice); - const discountRate = Number(product.furnitureProductDiscountRate); - const saveCount = Number(product.furnitureProductSaveCount); + const originalPrice = toFiniteNumber(product.furnitureProductOriginalPrice); + const discountPrice = toFiniteNumber(product.furnitureProductDiscountPrice); + const discountRate = toFiniteNumber(product.furnitureProductDiscountRate); + const saveCount = toFiniteNumber(product.furnitureProductSaveCount); return { id: recommendId, isRecommendId: Boolean(recommendId), furnitureProductId: safeProductId, - furnitureProductName: product.furnitureProductName, - furnitureProductMallName: product.furnitureProductMallName, + furnitureProductName: normalizeText( + product.furnitureProductName, + RESULT_CARD_UI_FALLBACK.productName + ), + furnitureProductMallName: normalizeText( + product.furnitureProductMallName, + RESULT_CARD_UI_FALLBACK.mallName + ), furnitureProductImageUrl: product.furnitureProductImageUrl || product.baseFurnitureImageUrl, furnitureProductSiteUrl: product.furnitureProductSiteUrl, - furnitureProductOriginalPrice: Number.isFinite(originalPrice) - ? originalPrice - : undefined, - furnitureProductDiscountPrice: Number.isFinite(discountPrice) - ? discountPrice - : undefined, - furnitureProductDiscountRate: Number.isFinite(discountRate) - ? discountRate - : undefined, - furnitureProductColorHexes: Array.isArray( + furnitureProductOriginalPrice: + originalPrice ?? RESULT_CARD_UI_FALLBACK.originalPrice, + furnitureProductDiscountPrice: + discountPrice ?? RESULT_CARD_UI_FALLBACK.discountPrice, + furnitureProductDiscountRate: + discountRate ?? RESULT_CARD_UI_FALLBACK.discountRate, + furnitureProductColorHexes: normalizeColorHexes( product.furnitureProductColorHexes - ) - ? product.furnitureProductColorHexes - : undefined, - furnitureProductSaveCount: Number.isFinite(saveCount) - ? saveCount - : undefined, + ), + furnitureProductSaveCount: + saveCount ?? RESULT_CARD_UI_FALLBACK.saveCount, }; }); }, [productsData]); diff --git a/src/shared/assets/icons/nextAbled.svg b/src/shared/assets/icons/nextAbled.svg index ef58a66e1..1dda8421b 100644 --- a/src/shared/assets/icons/nextAbled.svg +++ b/src/shared/assets/icons/nextAbled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/shared/components/button/linkButton/LinkButton.css.ts b/src/shared/components/button/linkButton/LinkButton.css.ts index 1a719ac08..4c1cb7ab5 100644 --- a/src/shared/components/button/linkButton/LinkButton.css.ts +++ b/src/shared/components/button/linkButton/LinkButton.css.ts @@ -1,3 +1,4 @@ +import { globalStyle, style } from '@vanilla-extract/css'; import { recipe } from '@vanilla-extract/recipes'; import { fontStyle } from '@/shared/styles/fontStyle'; @@ -8,27 +9,39 @@ export const linkButton = recipe({ base: { height: '3rem', padding: '0.6rem', - display: 'flex', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + flexWrap: 'nowrap', + textDecoration: 'none', + whiteSpace: 'nowrap', borderRadius: '999px', transition: 'all 0.2s ease-in-out', border: `1px solid ${colorVars.color.gray300}`, backgroundColor: colorVars.color.gray000, + ':hover': { + backgroundColor: colorVars.color.gray000, + }, ':active': { - backgroundColor: colorVars.color.gray300, + backgroundColor: colorVars.color.gray000, + }, + ':visited': { + color: colorVars.color.gray700, }, }, variants: { type: { withText: { - width: 'fit-content', - minWidth: '6.5rem', - gap: '0.3rem', - whiteSpace: 'nowrap', - ...fontStyle('caption_r_12'), + width: '6.1rem', + minWidth: '6.1rem', + height: '2.6rem', + padding: '0.4rem 0.6rem', + gap: 0, + flex: '0 0 6.1rem', + ...fontStyle('caption_r_11'), + lineHeight: '1.1rem', color: colorVars.color.gray700, }, onlyIcon: { @@ -40,3 +53,41 @@ export const linkButton = recipe({ type: 'withText', }, }); + +export const linkContent = style({ + width: '4.9rem', + height: '1.8rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: 0, + flex: '0 0 4.9rem', +}); + +export const linkLabel = style({ + width: '3.3rem', + height: '1.8rem', + lineHeight: '1.1rem', + textAlign: 'center', + flex: '0 0 3.3rem', + whiteSpace: 'nowrap', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', +}); + +export const linkIconWrapper = style({ + width: '1.6rem', + height: '1.6rem', + flex: '0 0 1.6rem', + padding: '0.1rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); + +globalStyle(`${linkIconWrapper} svg`, { + width: '1.4rem', + height: '1.4rem', +}); diff --git a/src/shared/components/button/linkButton/LinkButton.tsx b/src/shared/components/button/linkButton/LinkButton.tsx index fdcde324d..75da08812 100644 --- a/src/shared/components/button/linkButton/LinkButton.tsx +++ b/src/shared/components/button/linkButton/LinkButton.tsx @@ -11,6 +11,8 @@ const LinkButton = ({ typeVariant = 'withText', ...props }: LinkButtonProps) => { + const isWithText = typeVariant === 'withText'; + return ( - - {children} + {isWithText ? ( + + + + + {children ? ( + {children} + ) : null} + + ) : ( + + + + )} ); }; diff --git a/src/shared/components/button/saveButton/SaveButton.css.ts b/src/shared/components/button/saveButton/SaveButton.css.ts index 364b42afe..bc660972f 100644 --- a/src/shared/components/button/saveButton/SaveButton.css.ts +++ b/src/shared/components/button/saveButton/SaveButton.css.ts @@ -1,9 +1,35 @@ -import { style } from '@vanilla-extract/css'; +import { globalStyle, style } from '@vanilla-extract/css'; + +import { colorVars } from '@/shared/styles/tokens/color.css'; export const buttonWrapper = style({ - width: '100%', + width: '2.8rem', + height: '2.8rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', background: 'none', border: 'none', padding: 0, margin: 0, }); + +export const selected = style({}); + +export const unselected = style({}); + +globalStyle(`${buttonWrapper} svg`, { + width: '2.4rem', + height: '2.4rem', +}); + +globalStyle(`${selected} svg path`, { + fill: colorVars.color.primary, + stroke: 'none', +}); + +globalStyle(`${unselected} svg path`, { + fill: 'rgba(0, 0, 0, 0.2)', + stroke: colorVars.color.gray000, + strokeWidth: 1, +}); diff --git a/src/shared/components/button/saveButton/SaveButton.tsx b/src/shared/components/button/saveButton/SaveButton.tsx index ae1cd283e..08d72d19e 100644 --- a/src/shared/components/button/saveButton/SaveButton.tsx +++ b/src/shared/components/button/saveButton/SaveButton.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import SaveOnIcon from '@assets/icons/icnHeartColor.svg?react'; -import SaveOffIcon from '@assets/icons/icnHeartGray.svg?react'; +import SaveIcon from '@assets/icons/icnHeartGray.svg?react'; import * as styles from './SaveButton.css'; @@ -16,10 +15,12 @@ const SaveButton = ({ isSelected, onClick, ...props }: SaveButtonProps) => { type="button" onClick={onClick} aria-pressed={isSelected} - className={styles.buttonWrapper} + className={`${styles.buttonWrapper} ${ + isSelected ? styles.selected : styles.unselected + }`} {...props} > - {isSelected ? : } + ); }; diff --git a/src/shared/components/card/cardProduct/CardProduct.css.ts b/src/shared/components/card/cardProduct/CardProduct.css.ts index 4c256ba75..bde634820 100644 --- a/src/shared/components/card/cardProduct/CardProduct.css.ts +++ b/src/shared/components/card/cardProduct/CardProduct.css.ts @@ -72,7 +72,12 @@ export const cardImage = recipe({ export const skeleton = style({ position: 'absolute', inset: 0, - background: 'linear-gradient(90deg, #ececec 8%, #f0f0f0 18%, #ececec 33%)', + background: `linear-gradient( + 90deg, + ${colorVars.color.gray200} 8%, + ${colorVars.color.gray100} 18%, + ${colorVars.color.gray200} 33% + )`, backgroundSize: '200% 100%', animation: `${animationTokens.skeletonWave} 2s linear infinite`, }); @@ -139,6 +144,7 @@ export const infoSection = style({ display: 'flex', flexDirection: 'column', paddingTop: '1.2rem', + paddingBottom: '2rem', gap: '0.8rem', }); @@ -151,15 +157,23 @@ export const colorRow = style({ export const colorChip = style({ width: '1.4rem', height: '1.4rem', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flex: '0 0 1.4rem', +}); + +export const colorChipInner = style({ + width: '1.2rem', + height: '1.2rem', borderRadius: '50%', - border: `1px solid ${colorVars.color.gray999_30}`, + border: `0.5px solid ${colorVars.color.gray999_30}`, boxSizing: 'border-box', }); export const colorChipCount = style({ ...fontStyle('caption_r_11'), color: colorVars.color.gray500, - marginLeft: '0.2rem', }); export const productInfo = style({ diff --git a/src/shared/components/card/cardProduct/CardProduct.tsx b/src/shared/components/card/cardProduct/CardProduct.tsx index 5b6e17718..773f5577d 100644 --- a/src/shared/components/card/cardProduct/CardProduct.tsx +++ b/src/shared/components/card/cardProduct/CardProduct.tsx @@ -166,9 +166,13 @@ const CardProduct = ({ + > + + ))} {extraColorCount > 0 && (