Skip to content

Commit 9de3d29

Browse files
committed
test: 테스트 추가
1 parent 951c4b1 commit 9de3d29

25 files changed

+1123
-2181
lines changed

README.md

-3
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ SI 업체에서 근무하면서 언제나 기술적인 목마름을 느꼈습니
4343

4444
- next-themes: 0.4.4 : Next.js에서 다크모드 지원을 위한 라이브러리
4545

46-
- SVG 파일 처리
47-
48-
- @svgr/webpack: 8.1.0 : SVG 파일을 React 컴포넌트로 변환
4946

5047
- 웹 에니메이션
5148

package-lock.json

+316-2,044
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
"coverage:ui": "vitest run --coverage && npx vite preview coverage"
1515
},
1616
"dependencies": {
17-
"@svgr/webpack": "^8.1.0",
1817
"clsx": "^2.1.1",
1918
"motion": "^11.15.0",
2019
"next": "15.1.3",

src/app/_components/Header.tsx

+30-29
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,19 @@
22

33
import { useState, useEffect } from 'react'
44
import { motion, AnimatePresence } from 'framer-motion'
5+
import clsx from 'clsx'
56

6-
import AboutMe from '@/assets/aboutMe.svg'
7-
import Blog from '@/assets/blog.svg'
8-
// import Portfolio from '@/assets/portfolio.svg'
97
import DarkModeToggle from './sub/DarkModeToggle'
108
import { usePreventScroll } from '@/hooks/usePreventScroll'
11-
import Button from './sub/Button'
9+
import { useSmoothScroll } from '@/hooks/useSmoothScroll'
10+
import { navbarData } from '@/assets/index'
1211

13-
interface NavItem {
14-
icon: any;
15-
text: string;
16-
href: string;
17-
}
12+
import Button from './sub/Button'
1813

1914
export default function Header() {
2015
const [isOpen, setIsOpen] = useState(false);
2116
const [isMobile, setIsMobile] = useState(true);
22-
23-
const navItems: NavItem[] = [
24-
{ icon: AboutMe, text: 'About Me', href: '/' },
25-
{ icon: Blog, text: 'Blog', href: 'https://daunje0.tistory.com/' },
26-
// { icon: Portfolio, text: 'Portfolio', href: '/' },
27-
];
17+
const { scrollToElement } = useSmoothScroll();
2818

2919
useEffect(() => {
3020
const handleResize = () => {
@@ -41,11 +31,17 @@ export default function Header() {
4131
setIsOpen(!isOpen);
4232
};
4333

34+
// 스크롤 이동과 메뉴 닫기를 함께 처리하는 핸들러
35+
const handleScrollAndClose = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
36+
setIsOpen(false);
37+
scrollToElement(e, id);
38+
};
39+
4440
usePreventScroll(isOpen && isMobile);
4541

4642
return (
47-
<header className="relative z-50">
48-
<div className="flex justify-between items-center">
43+
<header className="fixed top-0 left-0 w-full pt-8 bg-white/80 dark:bg-black/80 backdrop-blur-sm z-50 px-4 py-2">
44+
<div className="flex justify-between items-center max-w-7xl mx-auto">
4945
<nav className='relative flex-grow' >
5046
<AnimatePresence>
5147
{(isOpen || !isMobile) && (
@@ -65,23 +61,28 @@ export default function Header() {
6561
bg-slate-100/95 dark:bg-black/95 dark:sm:bg-transparent sm:bg-transparent
6662
flex flex-col items-center justify-center gap-10 z-50
6763
`}
68-
6964
>
70-
{navItems.map(({ icon: Icon, text, href }) => (
65+
{navbarData.map((item, i) => (
7166
<motion.div
72-
key={text}
67+
key={item.id}
7368
whileHover={{ scale: 1.2 }}
7469
whileTap={{ scale: 0.9 }}
7570
transition={{ type: "spring", stiffness: 400, damping: 17 }}
7671
>
77-
<Icon className='size-6'/>
78-
<li>
79-
{/* <Link href={href}>{text}</Link> */}
80-
<a href={href}
81-
target="_blank"
82-
rel="noopener noreferrer"
83-
>{text}</a>
84-
</li>
72+
<a
73+
href={`/#${item.id}`}
74+
onClick={(e) => handleScrollAndClose(e, item.id)}
75+
className="flex flex-row items-center gap-x-2"
76+
>
77+
<i className={`${item.icon} text-3xl text-yellow-600 leading-none`}></i>
78+
<span className={clsx(
79+
'text-xl tracking-wide text-center dark:text-white leading-none',
80+
'transition-all duration-300',
81+
'sm:opacity-0 sm:group-hover:opacity-100 md:opacity-100'
82+
)}>
83+
{item.name}
84+
</span>
85+
</a>
8586
</motion.div>
8687
))}
8788
</motion.ul>
@@ -115,6 +116,6 @@ export default function Header() {
115116
<DarkModeToggle />
116117
</div>
117118
</div>
118-
</header>
119+
</header>
119120
)
120121
}

src/app/_components/sub/Modal.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,14 @@ const Modal = ({ onClose }: ModalProps) => {
4242
return (
4343
<dialog
4444
ref={dialogRef}
45+
role="dialog"
46+
aria-modal="true"
47+
aria-label="Image gallery modal"
48+
data-testid="modal-dialog"
4549
className="fixed inset-0 bg-transparent p-0 border-none outline-none"
4650
>
4751
<motion.div
52+
data-testid="modal-backdrop"
4853
initial={{ opacity: 0 }}
4954
animate={{ opacity: 1 }}
5055
exit={{ opacity: 0 }}

src/app/_components/sub/Swiper.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,18 @@ const Swiper = () => {
1414
[--swiper-navigation-color:theme(colors.black)] dark:[--swiper-navigation-color:theme(colors.white)]
1515
[--swiper-pagination-color:theme(colors.black)] dark:[--swiper-pagination-color:theme(colors.white)]
1616
dark:[--swiper-pagination-fraction-color:theme(colors.black)]
17+
[--swiper-navigation-size:1.25rem] sm:[--swiper-navigation-size:2rem]
18+
[--swiper-navigation-sides-offset:1px] sm:[--swiper-navigation-sides-offset:5rem]
1719
"
18-
spaceBetween={50}
20+
spaceBetween={20}
21+
breakpoints={{
22+
320: {
23+
spaceBetween: 20,
24+
},
25+
640: {
26+
spaceBetween: 50,
27+
}
28+
}}
1929
virtual={true}
2030
pagination={{
2131
type: 'fraction',
@@ -27,7 +37,7 @@ const Swiper = () => {
2737
>
2838
{swiperData.map((slide, index) => (
2939
<SwiperSlide key={index} className="flex items-center justify-center">
30-
<div className="relative w-full h-[37.5rem] sm:h-[43.75rem] flex items-center justify-center">
40+
<div className="relative w-full h-[30rem] md:h-[43.75rem] flex items-center justify-center">
3141
<Image
3242
src={slide.imgPath}
3343
alt={slide.title}

src/app/_test/DarkModeToggle.test.tsx

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { render, screen, fireEvent, act } from '@testing-library/react';
2+
import DarkModeToggle from '../_components/sub/DarkModeToggle';
3+
import { vi } from 'vitest';
4+
5+
// next-themes 모킹
6+
const mockSetTheme = vi.fn();
7+
vi.mock('next-themes', () => ({
8+
useTheme: () => ({
9+
theme: 'light',
10+
setTheme: mockSetTheme,
11+
}),
12+
}));
13+
14+
// Framer Motion 모킹
15+
vi.mock('framer-motion', () => ({
16+
motion: {
17+
button: ({ children, onClick, ...props }: any) => (
18+
<button onClick={onClick}>{children}</button>
19+
),
20+
i: ({ children, className }: any) => (
21+
<i className={className}>{children}</i>
22+
),
23+
},
24+
AnimatePresence: ({ children }: any) => <>{children}</>,
25+
}));
26+
27+
describe('DarkModeToggle 컴포넌트', () => {
28+
beforeEach(() => {
29+
vi.clearAllMocks();
30+
localStorage.clear();
31+
});
32+
33+
it('초기 마운트 시 로컬스토리지의 테마를 사용해야 함', () => {
34+
localStorage.setItem('theme', 'dark');
35+
render(<DarkModeToggle />);
36+
expect(mockSetTheme).toHaveBeenCalledWith('dark');
37+
});
38+
39+
it('로컬스토리지에 테마가 없을 경우 시스템 테마를 사용해야 함', () => {
40+
Object.defineProperty(window, 'matchMedia', {
41+
value: vi.fn().mockImplementation(query => ({
42+
matches: true,
43+
media: query,
44+
})),
45+
});
46+
47+
render(<DarkModeToggle />);
48+
expect(mockSetTheme).toHaveBeenCalledWith('dark');
49+
});
50+
51+
it('토글 버튼 클릭 시 테마가 변경되어야 함', () => {
52+
render(<DarkModeToggle />);
53+
const button = screen.getByRole('button');
54+
55+
fireEvent.click(button);
56+
expect(mockSetTheme).toHaveBeenCalledWith('dark');
57+
expect(localStorage.getItem('theme')).toBe('dark');
58+
});
59+
60+
it('matchMedia API를 사용할 수 없는 경우 기본값으로 light를 사용해야 함', () => {
61+
Object.defineProperty(window, 'matchMedia', {
62+
value: vi.fn().mockImplementation(() => {
63+
throw new Error('matchMedia not supported');
64+
}),
65+
});
66+
67+
render(<DarkModeToggle />);
68+
expect(mockSetTheme).toHaveBeenCalledWith('light');
69+
});
70+
});

src/app/_test/Experience.test.tsx

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { render, screen, fireEvent, within } from '@testing-library/react';
2+
import Experience from '../_components/Experience';
3+
import { experienceData } from '@/assets/index';
4+
5+
// Framer Motion 모킹
6+
vi.mock('framer-motion', () => ({
7+
motion: {
8+
div: ({ children, className, onClick, ...props }: any) => (
9+
<div className={className} onClick={onClick}>{children}</div>
10+
),
11+
},
12+
useScroll: () => ({ scrollYProgress: 0 }),
13+
useSpring: () => 0,
14+
AnimatePresence: ({ children }: any) => <>{children}</>,
15+
}));
16+
17+
// Heading 컴포넌트 모킹
18+
vi.mock('../_components/sub/Heading', () => ({
19+
default: ({ text }: { text: string }) => <h1>{text}</h1>
20+
}));
21+
22+
// Modal 컴포넌트 모킹
23+
vi.mock('../_components/sub/Modal', () => ({
24+
default: ({ onClose }: any) => (
25+
<>
26+
<div
27+
data-testid="modal-backdrop"
28+
className="fixed inset-0 bg-black/50 z-50"
29+
onClick={onClose}
30+
/>
31+
<div
32+
data-testid="modal-content"
33+
className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50"
34+
>
35+
<button
36+
onClick={onClose}
37+
data-testid="modal-close-button"
38+
>
39+
Close
40+
</button>
41+
</div>
42+
</>
43+
)
44+
}));
45+
46+
describe('Experience 컴포넌트', () => {
47+
beforeEach(() => {
48+
render(<Experience />);
49+
});
50+
51+
it('제목이 올바르게 렌더링되어야 함', () => {
52+
expect(screen.getByText('Experience & Education')).toBeInTheDocument();
53+
});
54+
55+
it('swaper가 true인 항목 클릭시 모달이 열려야 함', () => {
56+
const swaperItem = experienceData.find(data => data.swaper);
57+
if (swaperItem) {
58+
const cardElement = screen.getByText(swaperItem.title).closest('div');
59+
fireEvent.click(cardElement!);
60+
61+
expect(screen.getByTestId('modal-backdrop')).toBeInTheDocument();
62+
expect(screen.getByTestId('modal-content')).toBeInTheDocument();
63+
}
64+
});
65+
66+
it('모달이 열린 상태에서 닫기 버튼 클릭시 모달이 닫혀야 함', () => {
67+
const swaperItem = experienceData.find(data => data.swaper);
68+
if (swaperItem) {
69+
// 모달 열기
70+
const cardElement = screen.getByText(swaperItem.title).closest('div');
71+
fireEvent.click(cardElement!);
72+
73+
// 닫기 버튼 클릭
74+
const closeButton = screen.getByTestId('modal-close-button');
75+
fireEvent.click(closeButton);
76+
77+
// 모달이 닫혔는지 확인
78+
expect(screen.queryByTestId('modal-content')).not.toBeInTheDocument();
79+
}
80+
});
81+
82+
it('모달 backdrop 클릭시 모달이 닫혀야 함', () => {
83+
const swaperItem = experienceData.find(data => data.swaper);
84+
if (swaperItem) {
85+
// 모달 열기
86+
const cardElement = screen.getByText(swaperItem.title).closest('div');
87+
fireEvent.click(cardElement!);
88+
89+
// backdrop 클릭
90+
const backdrop = screen.getByTestId('modal-backdrop');
91+
fireEvent.click(backdrop);
92+
93+
// 모달이 닫혔는지 확인
94+
expect(screen.queryByTestId('modal-backdrop')).not.toBeInTheDocument();
95+
}
96+
});
97+
});

0 commit comments

Comments
 (0)