Skip to content

Commit b80a55a

Browse files
Merge pull request #75 from dib3474/refactor/design-feedback
디자인 피드백 반영
2 parents 5fb4acf + 72ce18a commit b80a55a

21 files changed

Lines changed: 248 additions & 206 deletions

File tree

src/__tests__/components/common/__snapshots__/Header.test.tsx.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ exports[`Header 컴포넌트 > 스냅샷 테스트 - 핵심 요소별 > all vari
3131
class="flex-1 flex justify-end"
3232
>
3333
<div
34-
class="flex-1 flex justify-end items-center "
34+
class="flex-1 flex justify-end items-center gap-3"
3535
>
3636
<button
3737
class="flex items-center justify-center hover:opacity-90 transition-opacity duration-100 w-fit cursor-pointer bg-transparent text-gray-900 text-md rounded-xl px-5 py-3 h-6 w-6 !p-0 rounded-lg flex items-center justify-center "
@@ -81,7 +81,7 @@ exports[`Header 컴포넌트 > 스냅샷 테스트 - 핵심 요소별 > all vari
8181

8282
exports[`Header 컴포넌트 > 스냅샷 테스트 - 핵심 요소별 > logo variant - 로고 영역 스냅샷 > logo-svg 1`] = `
8383
<svg
84-
class="size-10"
84+
class="size-9"
8585
viewBox="0 0 206.68 235.04"
8686
xmlns="http://www.w3.org/2000/svg"
8787
>

src/__tests__/pages/FestivalInfoPage.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ describe('리뷰 섹션 컴포넌트', () => {
526526
const reviewCard = reviewer.closest('div')?.parentElement; // 카드 컨테이너
527527

528528
// 리뷰 미디어 슬라이더 내의 이미지를 찾기
529-
const mediaSlider = reviewCard?.querySelector('[class*="overflow-x-scroll"]');
529+
const mediaSlider = reviewCard?.querySelector('[class*="overflow-x-auto"]');
530530
const firstImage = mediaSlider?.querySelector('img');
531531

532532
expect(firstImage).toBeTruthy();

src/__tests__/pages/FestivalsPage.test.tsx

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -71,27 +71,25 @@ describe('FestivalsPage 테스트', () => {
7171
// Given: AI 추천 데이터가 있을 때
7272
mockLocationState = { requestData: mockRequestData };
7373

74-
const { container } = render(
74+
render(
7575
<TestWrapper initialEntries={initialEntriesWithAI}>
7676
<FestivalsPage />
7777
</TestWrapper>,
7878
);
7979

80-
// Suspense가 끝나고 섹션이 나타날 때까지 기다림
81-
await waitFor(() => {
82-
const sections = container.querySelectorAll('section');
83-
expect(sections.length).toBeGreaterThanOrEqual(2);
84-
});
80+
// Suspense fallbacks가 아닌 실제 섹션 제목이 나타날 때까지 대기
81+
await screen.findByRole('heading', { name: '맞춤 AI Pick 축제' });
82+
await screen.findByRole('heading', { name: '지역의 축제' });
8583

86-
const sections = container.querySelectorAll('section');
87-
const [aiPickSection, festivalsSection] = sections;
84+
const aiPickHeading = screen.getByRole('heading', { name: '맞춤 AI Pick 축제' });
85+
const aiPickSection = aiPickHeading.closest('section');
86+
const festivalsHeading = screen.getByRole('heading', { name: '지역의 축제' });
87+
const festivalsSection = festivalsHeading.closest('section');
8888

89-
// AI Pick 첫 카드 스냅샷
90-
const aiGrid = aiPickSection.querySelector('.grid');
89+
const aiGrid = aiPickSection?.querySelector('.grid');
9190
expect(aiGrid?.children[0]).toMatchSnapshot('ai-pick-first-card');
9291

93-
// Festivals 첫 카드 스냅샷
94-
const festivalsGrid = festivalsSection.querySelector('.grid');
92+
const festivalsGrid = festivalsSection?.querySelector('.grid');
9593
expect(festivalsGrid?.children[0]).toMatchSnapshot('festivals-first-card');
9694
});
9795

@@ -105,16 +103,12 @@ describe('FestivalsPage 테스트', () => {
105103
</TestWrapper>,
106104
);
107105

108-
// Suspense가 끝나고 섹션이 나타날 때까지 기다림
109-
await waitFor(() => {
110-
const sections = container.querySelectorAll('section');
111-
expect(sections.length).toBeGreaterThanOrEqual(1);
112-
});
106+
// 실제 섹션 제목 로드까지 대기
107+
await screen.findByRole('heading', { name: '지역의 축제' });
113108

114109
const sections = container.querySelectorAll('section');
115110
const [festivalsSection] = sections;
116111

117-
// Festivals 첫 카드 스냅샷
118112
const festivalsGrid = festivalsSection.querySelector('.grid');
119113
expect(festivalsGrid?.children[0]).toMatchSnapshot('festivals-only-first-card');
120114
});
@@ -175,7 +169,7 @@ describe('FestivalsPage 테스트', () => {
175169

176170
// Then: 일반 Festivals 섹션이 로드될 때까지 기다림
177171
await waitFor(() => {
178-
expect(screen.getByRole('heading', { name: 'Festivals' })).toBeInTheDocument();
172+
expect(screen.getByRole('heading', { name: '지역의 축제' })).toBeInTheDocument();
179173
});
180174

181175
// AI Pick 제목이 없어야 함
@@ -201,11 +195,12 @@ describe('FestivalsPage 테스트', () => {
201195

202196
// Then: 에러가 발생하지 않고 일반 Festivals 섹션은 표시되어야 한다
203197
await waitFor(() => {
204-
expect(screen.getByRole('heading', { name: 'Festivals' })).toBeInTheDocument();
198+
expect(screen.getByRole('heading', { name: '지역의 축제' })).toBeInTheDocument();
199+
// AI Pick 섹션은 표시되지 않아야 함
200+
expect(
201+
screen.queryByRole('heading', { name: '맞춤 AI Pick 축제' }),
202+
).not.toBeInTheDocument();
205203
});
206-
207-
// AI Pick 섹션은 표시되지 않아야 함
208-
expect(screen.queryByRole('heading', { name: '맞춤 AI Pick 축제' })).not.toBeInTheDocument();
209204
});
210205
});
211206
});
@@ -228,7 +223,7 @@ describe('축제 목록 섹션', () => {
228223
});
229224

230225
// "Festivals" 섹션을 기준으로 범위를 한정해 중복 매치를 방지한다
231-
const festivalsHeading = await screen.findByRole('heading', { name: 'Festivals' });
226+
const festivalsHeading = await screen.findByRole('heading', { name: '지역의 축제' });
232227
const festivalsSection = festivalsHeading.closest('section');
233228
expect(festivalsSection).toBeTruthy();
234229

@@ -291,7 +286,7 @@ test('로딩 상태가 올바르게 표시된다', async () => {
291286
});
292287
});
293288

294-
test('API 에러 상태가 올바르게 처리된다 (ErrorBoundary가 null을 렌더링)', async () => {
289+
test('API 에러 상태가 올바르게 처리된다 (ErrorBoundary가 렌더링되므로 해당 섹션 텍스트가 나타나지 않는다)', async () => {
295290
// Given: API 호출이 실패할 때
296291
const getFestivalsMock = await import('@/apis/festivals/getFestivals');
297292
vi.mocked(getFestivalsMock.default).mockRejectedValueOnce(new Error('API Error'));
@@ -302,8 +297,8 @@ test('API 에러 상태가 올바르게 처리된다 (ErrorBoundary가 null을
302297
</TestWrapper>,
303298
);
304299

305-
// Then: ErrorBoundary가 null을 렌더링하므로 해당 섹션 텍스트가 나타나지 않는다
300+
// Then: ErrorBoundary가 렌더링되므로 해당 섹션 텍스트가 나타나지 않는다
306301
await waitFor(() => {
307-
expect(screen.queryByText('Festivals')).not.toBeInTheDocument();
302+
expect(screen.queryByText('지역의 축제')).not.toBeInTheDocument();
308303
});
309304
});

src/__tests__/pages/__snapshots__/FestivalsPage.test.tsx.snap

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
exports[`FestivalsPage 테스트 > 스냅샷 테스트 > AI 추천 섹션과 일반 섹션 스냅샷 > ai-pick-first-card 1`] = `
44
<a
5-
class="bg-white rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-100 hover:scale-[1.02] cursor-pointer"
5+
class="bg-white rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-100 hover:scale-[1.02] cursor-pointer ring-3 ring-primary-300"
66
data-discover="true"
77
href="/festival/143"
88
>
@@ -19,7 +19,7 @@ exports[`FestivalsPage 테스트 > 스냅샷 테스트 > AI 추천 섹션과 일
1919
class="p-4"
2020
>
2121
<h3
22-
class="text-lg font-bold text-gray-900 mb-3 line-clamp-2 min-h-[3.5rem]"
22+
class="text-lg font-bold text-gray-900 line-clamp-2 leading-tight min-h-[3rem]"
2323
>
2424
안동국제탈춤페스티벌
2525
</h3>
@@ -45,77 +45,89 @@ exports[`FestivalsPage 테스트 > 스냅샷 테스트 > AI 추천 섹션과 일
4545
`;
4646

4747
exports[`FestivalsPage 테스트 > 스냅샷 테스트 > AI 추천 섹션과 일반 섹션 스냅샷 > festivals-first-card 1`] = `
48-
<div
49-
class="bg-white rounded-xl overflow-hidden shadow-lg"
48+
<a
49+
class="bg-white rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-100 hover:scale-[1.02] cursor-pointer "
50+
data-discover="true"
51+
href="/festival/2583"
5052
>
5153
<div
5254
class="aspect-[3/2] w-full"
5355
>
54-
<div
55-
class="bg-gray-200 animate-pulse rounded w-full h-full"
56+
<img
57+
alt="한옥마을전통연희 퍼레이드-노상놀이야 축제 이미지"
58+
class="w-full h-full object-cover"
59+
src="http://tong.visitkorea.or.kr/cms/resource/68/3485868_image2_1.jpg"
5660
/>
5761
</div>
5862
<div
59-
class="p-4 space-y-3"
63+
class="p-4"
6064
>
61-
<div
62-
class="space-y-2"
65+
<h3
66+
class="text-lg font-bold text-gray-900 line-clamp-2 leading-tight min-h-[3rem]"
6367
>
64-
<div
65-
class="bg-gray-200 animate-pulse rounded h-5 w-full"
66-
/>
67-
<div
68-
class="bg-gray-200 animate-pulse rounded h-5 w-3/4"
69-
/>
70-
</div>
68+
한옥마을전통연희 퍼레이드-노상놀이야
69+
</h3>
7170
<div
7271
class="space-y-1"
7372
>
74-
<div
75-
class="bg-gray-200 animate-pulse rounded h-4 w-2/3"
76-
/>
77-
<div
78-
class="bg-gray-200 animate-pulse rounded h-4 w-1/2"
79-
/>
73+
<p
74+
class="text-sm sm:text-xs text-gray-600 font-medium whitespace-nowrap overflow-hidden"
75+
>
76+
2025-04-12
77+
~
78+
2025-10-25
79+
</p>
80+
<p
81+
class="text-sm sm:text-xs text-gray-500"
82+
title="전북특별자치도 전주시 완산구 태조로 44 (풍남동3가)"
83+
>
84+
전북특별자치도 전주시
85+
</p>
8086
</div>
8187
</div>
82-
</div>
88+
</a>
8389
`;
8490

8591
exports[`FestivalsPage 테스트 > 스냅샷 테스트 > AI 추천이 없을 때 일반 섹션만 스냅샷 > festivals-only-first-card 1`] = `
86-
<div
87-
class="bg-white rounded-xl overflow-hidden shadow-lg"
92+
<a
93+
class="bg-white rounded-xl overflow-hidden shadow-lg hover:shadow-xl transition-all duration-100 hover:scale-[1.02] cursor-pointer "
94+
data-discover="true"
95+
href="/festival/2583"
8896
>
8997
<div
9098
class="aspect-[3/2] w-full"
9199
>
92-
<div
93-
class="bg-gray-200 animate-pulse rounded w-full h-full"
100+
<img
101+
alt="한옥마을전통연희 퍼레이드-노상놀이야 축제 이미지"
102+
class="w-full h-full object-cover"
103+
src="http://tong.visitkorea.or.kr/cms/resource/68/3485868_image2_1.jpg"
94104
/>
95105
</div>
96106
<div
97-
class="p-4 space-y-3"
107+
class="p-4"
98108
>
99-
<div
100-
class="space-y-2"
109+
<h3
110+
class="text-lg font-bold text-gray-900 line-clamp-2 leading-tight min-h-[3rem]"
101111
>
102-
<div
103-
class="bg-gray-200 animate-pulse rounded h-5 w-full"
104-
/>
105-
<div
106-
class="bg-gray-200 animate-pulse rounded h-5 w-3/4"
107-
/>
108-
</div>
112+
한옥마을전통연희 퍼레이드-노상놀이야
113+
</h3>
109114
<div
110115
class="space-y-1"
111116
>
112-
<div
113-
class="bg-gray-200 animate-pulse rounded h-4 w-2/3"
114-
/>
115-
<div
116-
class="bg-gray-200 animate-pulse rounded h-4 w-1/2"
117-
/>
117+
<p
118+
class="text-sm sm:text-xs text-gray-600 font-medium whitespace-nowrap overflow-hidden"
119+
>
120+
2025-04-12
121+
~
122+
2025-10-25
123+
</p>
124+
<p
125+
class="text-sm sm:text-xs text-gray-500"
126+
title="전북특별자치도 전주시 완산구 태조로 44 (풍남동3가)"
127+
>
128+
전북특별자치도 전주시
129+
</p>
118130
</div>
119131
</div>
120-
</div>
132+
</a>
121133
`;

src/apis/festivals/getFestivals.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ import type { ApiErrorResponse } from '@/apis/apiInstance';
66
import type { AxiosResponse } from 'axios';
77
import { generatePath } from 'react-router-dom';
88

9-
export const getFestivals = async (params: {
9+
export const getFestivals = async ({
10+
areaId,
11+
size = 6,
12+
page = 0,
13+
}: {
1014
areaId: string;
11-
size: number;
12-
page: number;
15+
size?: number;
16+
page?: number;
1317
}): Promise<AxiosResponse<ApiResponseList<Festival>, ApiErrorResponse>> => {
1418
return await apiInstance.get<ApiResponseList<Festival>>(
15-
generatePath(API_ENDPOINTS.FESTIVALS, { areaId: params.areaId }),
16-
{ params: { size: params.size ?? 5, page: params.page ?? 0 } as const },
19+
generatePath(API_ENDPOINTS.FESTIVALS, { areaId }),
20+
{ params: { size, page } },
1721
);
1822
};
1923

src/components/common/Header.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ const Header = ({ variant = 'logo', title = '' }: HeaderProps) => {
3333
if (variant === 'logo') {
3434
return (
3535
<div className={containerClasses}>
36-
<div className={`${baseClasses} gap-2`}>
37-
<Logo className="size-10" />
36+
<div className={`${baseClasses} gap-2 pl-3`}>
37+
<Logo className="size-9" />
3838
<h1 className="text-xl font-bold">FestaPick</h1>
3939
</div>
4040
</div>
@@ -58,7 +58,7 @@ const Header = ({ variant = 'logo', title = '' }: HeaderProps) => {
5858
variant="icon"
5959
className="h-6 w-6 !p-0 rounded-lg flex items-center justify-center mr-5"
6060
>
61-
<Settings className="size-6" />
61+
<Settings className="size-7" strokeWidth={2} />
6262
</Button>
6363
</Link>
6464
</div>
@@ -79,7 +79,7 @@ const Header = ({ variant = 'logo', title = '' }: HeaderProps) => {
7979
</div>
8080
<div className="flex-1 flex justify-end">
8181
{variant === 'all' ? (
82-
<div className="flex-1 flex justify-end items-center ">
82+
<div className="flex-1 flex justify-end items-center gap-3">
8383
<Button
8484
variant="icon"
8585
className="h-6 w-6 !p-0 rounded-lg flex items-center justify-center"

src/components/icon/CloseIcon.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const Close = ({ className, strokeWidth = 2 }: { className?: string; strokeWidth?: number }) => {
2+
return (
3+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className={className}>
4+
<path
5+
d="M18 6L6 18M6 6L18 18"
6+
stroke="currentColor"
7+
strokeWidth={strokeWidth}
8+
strokeLinecap="round"
9+
strokeLinejoin="round"
10+
/>
11+
</svg>
12+
);
13+
};
14+
15+
export default Close;

src/components/icon/MapIcon.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const Map = ({ className }: { className?: string }) => {
2+
return (
3+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
4+
<path
5+
d="M12 6H12.01M9 20L3 17V4L5 5M9 20L15 17M9 20V14M15 17L21 20V7L19 6M15 17V14M15 6.2C15 7.96731 13.5 9.4 12 11C10.5 9.4 9 7.96731 9 6.2C9 4.43269 10.3431 3 12 3C13.6569 3 15 4.43269 15 6.2Z"
6+
stroke="currentColor"
7+
strokeWidth="2"
8+
strokeLinecap="round"
9+
strokeLinejoin="round"
10+
/>
11+
</svg>
12+
);
13+
};
14+
15+
export default Map;

0 commit comments

Comments
 (0)