
diff --git a/src/shared/assets/icons/common/chatbot.svg b/src/shared/assets/icons/common/chatbot.svg
new file mode 100644
index 00000000..8fda76ea
--- /dev/null
+++ b/src/shared/assets/icons/common/chatbot.svg
@@ -0,0 +1,59 @@
+
diff --git a/src/shared/assets/icons/common/chatbot.webp b/src/shared/assets/icons/common/chatbot.webp
index 2c1bdbc6..322ce848 100644
Binary files a/src/shared/assets/icons/common/chatbot.webp and b/src/shared/assets/icons/common/chatbot.webp differ
diff --git a/src/shared/assets/icons/common/index.ts b/src/shared/assets/icons/common/index.ts
index d8d43f1a..d0dcdaa6 100644
--- a/src/shared/assets/icons/common/index.ts
+++ b/src/shared/assets/icons/common/index.ts
@@ -11,5 +11,5 @@ export { default as CloseIcon } from './close.svg?react';
export { default as HamburgerIcon } from './hamburger-md.svg?react';
export { default as PictureIcon } from './picture.svg?react';
export { default as kakaoLoginImg } from './kakao.png';
-export { default as chatbotImg } from './chatbot.webp';
+export { default as ChatbotIcon } from './chatbot.svg?react';
export { default as ChevronRightIcon } from './chevron-right.svg?react';
diff --git a/src/shared/assets/icons/index.ts b/src/shared/assets/icons/index.ts
index 5a835022..c5a3be31 100644
--- a/src/shared/assets/icons/index.ts
+++ b/src/shared/assets/icons/index.ts
@@ -12,7 +12,7 @@ export {
PictureIcon,
SearchMagnifyingGlassIcon,
SendIcon,
- chatbotImg,
+ ChatbotIcon,
ChevronRightIcon,
} from './common';
export { GearIcon, ShoppingIcon, SellIcon } from './banner';
diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts
index 2a59fc13..1999058e 100644
--- a/src/shared/hooks/index.ts
+++ b/src/shared/hooks/index.ts
@@ -1,6 +1,8 @@
+export { useBodyScrollLock } from './useBodyScrollLock';
export { useClickOutside } from './useClickOutside';
export { useFocusTrap } from './useFocusTrap';
export { useImageUpload } from './useImageUpload';
export { useModal } from './useModal';
+export { useScrolled } from './useScrolled';
export { useToast } from './useToast';
export { useToastProvider } from './useToastProvider';
diff --git a/src/shared/hooks/useBodyScrollLock.ts b/src/shared/hooks/useBodyScrollLock.ts
new file mode 100644
index 00000000..8a5bc980
--- /dev/null
+++ b/src/shared/hooks/useBodyScrollLock.ts
@@ -0,0 +1,16 @@
+import { useEffect } from 'react';
+
+export const useBodyScrollLock = (isLocked: boolean) => {
+ useEffect(() => {
+ if (!isLocked) {
+ return;
+ }
+
+ const previousOverflow = document.body.style.overflow;
+ document.body.style.overflow = 'hidden';
+
+ return () => {
+ document.body.style.overflow = previousOverflow;
+ };
+ }, [isLocked]);
+};
diff --git a/src/shared/hooks/useScrolled.ts b/src/shared/hooks/useScrolled.ts
new file mode 100644
index 00000000..d92d440d
--- /dev/null
+++ b/src/shared/hooks/useScrolled.ts
@@ -0,0 +1,18 @@
+import { useEffect, useState } from 'react';
+
+export const useScrolled = (threshold = 0) => {
+ const [isScrolled, setIsScrolled] = useState(false);
+
+ useEffect(() => {
+ const handleScroll = () => {
+ setIsScrolled(window.scrollY > threshold);
+ };
+
+ handleScroll();
+ window.addEventListener('scroll', handleScroll, { passive: true });
+
+ return () => window.removeEventListener('scroll', handleScroll);
+ }, [threshold]);
+
+ return isScrolled;
+};
diff --git a/src/shared/ui/Card/Card.variants.ts b/src/shared/ui/Card/Card.variants.ts
index 489d3897..dc2aee3a 100644
--- a/src/shared/ui/Card/Card.variants.ts
+++ b/src/shared/ui/Card/Card.variants.ts
@@ -32,8 +32,8 @@ export const cardVariants = tv({
variants: {
variant: {
default: {
- root: ['w-[180px]', 'h-[299px]'],
- imageWrapper: ['py-[10px]', 'px-[var(--padding-s)]', 'flex-[1_0_0]'],
+ root: ['w-full', 'h-auto', 'lg:w-[180px]', 'lg:h-[299px]'],
+ imageWrapper: ['py-[10px]', 'px-[var(--padding-s)]', 'aspect-square', 'lg:aspect-auto', 'lg:flex-[1_0_0]'],
image: ['absolute', 'inset-0'],
title: ['line-clamp-2', 'text-gray-900'],
price: ['text-gray-900'],
diff --git a/src/shared/ui/Carousel3D/CarouselImageSlide.tsx b/src/shared/ui/Carousel3D/CarouselImageSlide.tsx
index 69823b04..c2219128 100644
--- a/src/shared/ui/Carousel3D/CarouselImageSlide.tsx
+++ b/src/shared/ui/Carousel3D/CarouselImageSlide.tsx
@@ -1,3 +1,4 @@
+import { Link } from 'react-router';
import { carousel3DVariants } from './Carousel3D.variants';
import type { ComponentPropsWithoutRef } from 'react';
@@ -7,23 +8,39 @@ export interface CarouselImageSlideProps extends ComponentPropsWithoutRef<'div'>
imageSrc: string;
title: string;
subtitle?: string;
+ href?: string;
}
export const CarouselImageSlide = ({
imageSrc,
title: titleText,
subtitle,
+ href,
className,
...props
}: CarouselImageSlideProps) => {
- return (
-
+ const content = (
+ <>
{titleText}
{subtitle &&
{subtitle}
}
+ >
+ );
+
+ if (href) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+
+ {content}
);
};
diff --git a/src/shared/ui/ChatBubble/ChatBubble.variants.ts b/src/shared/ui/ChatBubble/ChatBubble.variants.ts
index c1edf249..e3f5a687 100644
--- a/src/shared/ui/ChatBubble/ChatBubble.variants.ts
+++ b/src/shared/ui/ChatBubble/ChatBubble.variants.ts
@@ -11,7 +11,8 @@ export const chatBubbleVariants = tv({
'px-[31px]',
'py-[18px]',
'rounded-[var(--radius-l)]',
- 'max-w-[559px]',
+ 'max-w-full',
+ 'xl:max-w-[559px]',
],
text: ['typo-body-1', 'whitespace-pre-wrap', 'break-words'],
meta: ['typo-caption-2', 'text-gray-400'],
@@ -33,7 +34,7 @@ export const chatBubbleVariants = tv({
},
chatbotNotice: {
root: ['justify-start'],
- bubble: ['bg-gray-100', 'max-w-[720px]'],
+ bubble: ['bg-gray-100', 'max-w-full', 'xl:max-w-[720px]'],
text: ['text-gray-900', 'whitespace-pre-line', 'break-keep'],
meta: ['order-last'],
},
diff --git a/src/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.tsx b/src/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.tsx
index 710b6786..a00a28d0 100644
--- a/src/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.tsx
+++ b/src/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.tsx
@@ -1,4 +1,4 @@
-import { chatbotImg } from '@shared/assets/icons';
+import { ChatbotIcon } from '@shared/assets/icons';
import { chatbotFloatingButtonVariants } from './ChatbotFloatingButton.variants';
import type { ComponentPropsWithoutRef } from 'react';
@@ -17,7 +17,7 @@ export const ChatbotFloatingButton = ({
return (
diff --git a/src/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.variants.ts b/src/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.variants.ts
index 0a276a1b..93b6e76a 100644
--- a/src/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.variants.ts
+++ b/src/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.variants.ts
@@ -3,12 +3,12 @@ import { tv } from 'tailwind-variants';
export const chatbotFloatingButtonVariants = tv({
slots: {
root: [
- 'group flex items-center',
+ 'group flex flex-col justify-center items-center',
'w-[80px] h-[80px]',
'rounded-full',
'bg-gray-50',
'shadow-[0_4px_4px_0_var(--color-gray-100)]',
- 'p-0',
+ 'py-[9px] px-[8px]',
'overflow-hidden',
'transition-[width,background-color,box-shadow] duration-200 ease-out',
'hover:py-[9px] hover:pl-[8px] hover:pr-[19px]',
@@ -19,12 +19,11 @@ export const chatbotFloatingButtonVariants = tv({
content: [
'flex items-center justify-center w-full h-full',
'gap-0 transition-[gap] duration-200 ease-out',
- 'group-hover:justify-start group-focus-visible:justify-start',
- 'group-hover:gap-0 group-focus-visible:gap-0',
- 'group-hover:w-[144px] group-hover:h-[67px] group-focus-visible:w-[144px] group-focus-visible:h-[67px]',
- 'group-hover:mx-auto group-focus-visible:mx-auto',
+ 'group-hover:justify-center group-focus-visible:justify-center',
+ 'group-hover:gap-[8px] group-focus-visible:gap-[8px]',
+ 'group-hover:w-[144px] group-focus-visible:w-[144px]',
],
- icon: ['w-[74px] h-[67px] shrink-0'],
+ icon: ['w-[47px] h-[38px] shrink-0'],
label: [
'typo-title-3 text-black whitespace-nowrap',
'opacity-0 max-w-0 translate-x-2',
diff --git a/src/shared/ui/FavoriteButton/FavoriteButton.variants.ts b/src/shared/ui/FavoriteButton/FavoriteButton.variants.ts
index c7217e79..1e1f3b9b 100644
--- a/src/shared/ui/FavoriteButton/FavoriteButton.variants.ts
+++ b/src/shared/ui/FavoriteButton/FavoriteButton.variants.ts
@@ -2,7 +2,7 @@ import { tv } from 'tailwind-variants';
export const favoriteButtonVariants = tv({
slots: {
- root: ['group inline-flex items-center justify-center', 'w-[44px] h-[44px]', 'transition-colors'],
+ root: ['group inline-flex items-center justify-center', 'w-[44px] h-[44px]', 'cursor-pointer', 'transition-colors'],
icon: ['w-[33px] h-[29.26px]', 'transition-colors', '[--icon-fill-active:var(--color-brand-primary)]'],
},
diff --git a/src/shared/ui/Header/Header.variants.ts b/src/shared/ui/Header/Header.variants.ts
index 4c140b0a..83749727 100644
--- a/src/shared/ui/Header/Header.variants.ts
+++ b/src/shared/ui/Header/Header.variants.ts
@@ -6,12 +6,16 @@ export const headerVariants = tv({
slots: {
// Layout
root: [
+ 'fixed',
+ 'top-0',
+ 'left-0',
+ 'right-0',
+ 'z-50',
'flex',
'w-full',
'max-w-[1440px]',
'mx-auto',
- 'h-[92px]',
- 'mb-(--spacing-header-content)',
+ 'h-(--header-height)',
'justify-between',
'items-center',
'bg-white',
diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx
index 0782d1f0..02269f16 100644
--- a/src/shared/ui/Modal/Modal.tsx
+++ b/src/shared/ui/Modal/Modal.tsx
@@ -1,4 +1,4 @@
-import { useFocusTrap } from '@shared/hooks';
+import { useBodyScrollLock, useFocusTrap } from '@shared/hooks';
import { Button } from '@shared/ui/Button/Button';
import { useCallback, useEffect, useRef, useState } from 'react';
import { modalStyles } from './Modal.styles';
@@ -24,6 +24,7 @@ export const Modal = ({
const modalRef = useRef
(null);
useFocusTrap(modalRef, !closing);
+ useBodyScrollLock(true);
const handleClose = useCallback(
(callback: () => void) => {
diff --git a/src/shared/ui/TradeItem/TradeItem.tsx b/src/shared/ui/TradeItem/TradeItem.tsx
index 6adcae7d..5caa7a1b 100644
--- a/src/shared/ui/TradeItem/TradeItem.tsx
+++ b/src/shared/ui/TradeItem/TradeItem.tsx
@@ -43,20 +43,24 @@ export const TradeItem = ({
const resolvedStatusLabel = statusLabel ?? statusConfig?.text;
return (
-
+
-
+
{imageUrl ? (
-

+

) : (
)}
-
+
{modelName}
{price}
{date}
@@ -66,7 +70,7 @@ export const TradeItem = ({
{status === 'favorite' ? (
) : (
- {resolvedStatusLabel}
+ {resolvedStatusLabel}
)}
diff --git a/tests/unit/shared/ui/Card/Card.test.tsx b/tests/unit/shared/ui/Card/Card.test.tsx
index ee418aaf..3e23a911 100644
--- a/tests/unit/shared/ui/Card/Card.test.tsx
+++ b/tests/unit/shared/ui/Card/Card.test.tsx
@@ -48,7 +48,7 @@ describe('Card', () => {
const { container } = render(
);
const card = container.firstChild;
- expect(card).toHaveClass('w-[180px]', 'h-[299px]');
+ expect(card).toHaveClass('w-full', 'h-auto', 'lg:w-[180px]', 'lg:h-[299px]');
});
it('seller variant 적용', () => {
diff --git a/tests/unit/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.test.tsx b/tests/unit/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.test.tsx
index 050c4193..c1689db0 100644
--- a/tests/unit/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.test.tsx
+++ b/tests/unit/shared/ui/ChatbotFloatingButton/ChatbotFloatingButton.test.tsx
@@ -19,10 +19,10 @@ describe('ChatbotFloatingButton', () => {
expect(screen.getByRole('button')).toBeInTheDocument();
});
- it('아이콘 이미지 렌더링', () => {
+ it('아이콘 렌더링', () => {
const { container } = render(
);
- const img = container.querySelector('img');
- expect(img).toBeInTheDocument();
+ const icon = container.querySelector('svg');
+ expect(icon).toBeInTheDocument();
});
});
@@ -46,8 +46,8 @@ describe('ChatbotFloatingButton', () => {
it('아이콘에 aria-hidden 적용', () => {
const { container } = render(
);
- const img = container.querySelector('img');
- expect(img).toHaveAttribute('aria-hidden', 'true');
+ const icon = container.querySelector('svg');
+ expect(icon).toHaveAttribute('aria-hidden', 'true');
});
it('키보드로 클릭 가능', async () => {