Skip to content

Commit d10d2b1

Browse files
authored
feat(blog): 블로그 앱 셸과 탐색 경험 개선 (#92)
1 parent b4ae7c2 commit d10d2b1

27 files changed

Lines changed: 1062 additions & 1147 deletions

File tree

posts/2025년-회고/index.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
시간과 기회비용을 함께 가져간다는 걸 느꼈어요.
6262

6363
<img src="/images/posts/2025년-회고/a0d6d289b098.png" />
64-
- 시간 뻇는 범인 수배 -
64+
(시간 뺏는 범인 수배)
6565

6666
그 시간에 가족과 더 시간을 보내거나
6767
책을 읽거나 기술 트렌드를 공부하는 편이
@@ -92,7 +92,7 @@
9292
마지막 접수날까지 공고가 계속 눈에 밟혔어요. 지금 쓰지 않으면 후회할 것 같아서 와이프와 논의한 뒤 지원하기로 했죠.
9393

9494
"기술 학습"이 아니라
95-
**'전문가로 성장하기 위한 커리큘럼'**이라는 점이 매력적이었어요.
95+
<strong>'전문가로 성장하기 위한 커리큘럼'</strong>이라는 점이 매력적이었어요.
9696

9797
질문에 답하면서
9898
그동안의 경험과 고민을 정리할 수 있었고,
@@ -131,7 +131,7 @@
131131
### 📚 독서
132132

133133
올해는 기술 트렌드보다
134-
**'일하는 사람으로서의 나'**를 돌아보는 책을 많이 읽었어요.
134+
<strong>'일하는 사람으로서의 나'</strong>를 돌아보는 책을 많이 읽었어요.
135135

136136
인상 깊었던 책들
137137

posts/베트남-여행-후기/index.mdx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,11 @@
8383
![](/assets/posts/2026-02-05-travel-vietnam-review/food3.jpeg)
8484
</ImageGrid>
8585

86-
유튜브로 미리 찾아본 식당들 전부 만족했어요.
87-
그중에서도 가장 추천하는 식당 소개해요.
88-
⭐⭐⭐⭐⭐
89-
**[Beni 2](https://share.google/1wtyxMhTXyT2QvGCO)**
86+
유튜브로 미리 찾아본 식당들 전부 만족했어요. 그중에서도 가장 추천하는 식당은 [Beni 2](https://share.google/1wtyxMhTXyT2QvGCO)예요. ⭐⭐⭐⭐⭐
9087

9188
- 해산물 맛집이예요. 랍스터랑 세트메뉴로 주문해서 먹었는데 하나같이 다 맛있었어요.
9289
- 찐로컬 분위기로 현지인들도 많았어요.
93-
- 베트남의 시그니처 중 하나인 타이거 맥주 한잔 하기 좋아요.
94-
**[Van may](https://share.google/vdprnrkE1M71IsDqZ)**
90+
- 베트남의 시그니처 중 하나인 타이거 맥주 한잔 하기 좋아요. [Van may](https://share.google/vdprnrkE1M71IsDqZ)도 추천해요.
9591
- 여긴 메뉴부터 특별했어요.
9692
- 곱창 쌀국수 🫢 근데 진짜 맛있어요.
9793
- 그리고 분짜, 파인애플 볶음밥도 먹었는데 맛있었습니다.

posts/운영-자동화-회고/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
- 장애 대응 시간 단축 및 운영 만족도 향상
128128

129129
무엇보다 **운영팀과의 커뮤니케이션 방식 자체가 바뀌었어요.**
130-
"이거 수동으로 해야 하나요?" 대신 **"이건 자동화할 수 있을까요?"**라는 질문이 나오기 시작했죠.
130+
"이거 수동으로 해야 하나요?" 대신 <strong>"이건 자동화할 수 있을까요?"</strong>라는 질문이 나오기 시작했죠.
131131

132132
---
133133

src/features/blog/ui/components/GiscusComments.test.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { describe, expect, it, vi } from 'vitest';
33
import { useTheme } from 'next-themes';
44
import { GiscusComments } from './GiscusComments';
55

6+
const mockGiscus = vi.fn(() => <div data-testid="giscus-embed" />);
7+
68
vi.mock('next-themes', () => ({
79
useTheme: vi.fn(),
810
}));
911

1012
vi.mock('@giscus/react', () => ({
11-
default: () => <div data-testid="giscus-embed" />,
13+
default: (props: unknown) => mockGiscus(props),
1214
}));
1315

1416
describe('GiscusComments', () => {
@@ -38,6 +40,13 @@ describe('GiscusComments', () => {
3840

3941
expect(screen.getByRole('heading', { level: 2, name: '댓글' })).toBeInTheDocument();
4042
expect(screen.getByTestId('giscus-embed')).toBeInTheDocument();
43+
expect(mockGiscus).toHaveBeenCalledWith(
44+
expect.objectContaining({
45+
mapping: 'specific',
46+
term: 'redis',
47+
theme: 'dark',
48+
})
49+
);
4150
expect(postMessage).toHaveBeenCalledWith(
4251
{
4352
giscus: {
@@ -52,4 +61,3 @@ describe('GiscusComments', () => {
5261
document.body.removeChild(iframe);
5362
});
5463
});
55-

src/features/blog/ui/components/GiscusComments.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface GiscusCommentsProps {
1515
* @note repo, repoId, category, categoryId 값을 실제 값으로 교체해야 합니다.
1616
* https://giscus.app 에서 설정 후 값을 확인하세요.
1717
*/
18-
export function GiscusComments({ slug: _slug }: GiscusCommentsProps) {
18+
export function GiscusComments({ slug }: GiscusCommentsProps) {
1919
const { resolvedTheme } = useTheme();
2020
const giscusTheme = resolvedTheme === 'dark' ? 'dark' : 'light';
2121

@@ -40,21 +40,25 @@ export function GiscusComments({ slug: _slug }: GiscusCommentsProps) {
4040
<h2 className="text-xl font-bold text-[var(--color-grey-900)] mb-6">
4141
댓글
4242
</h2>
43-
<Giscus
44-
id="comments"
45-
repo="dev-wooyeon/eunu-log"
46-
repoId="R_kgDOQ9dweg"
47-
category="Announcements"
48-
categoryId="DIC_kwDOQ9dwes4C19H1"
49-
mapping="title"
50-
strict="0"
51-
reactionsEnabled="1"
52-
emitMetadata="0"
53-
inputPosition="top"
54-
theme={giscusTheme}
55-
lang="ko"
56-
loading="lazy"
57-
/>
43+
<div className="min-h-40">
44+
<Giscus
45+
key={`${slug}-${giscusTheme}`}
46+
id="comments"
47+
repo="dev-wooyeon/eunu-log"
48+
repoId="R_kgDOQ9dweg"
49+
category="Announcements"
50+
categoryId="DIC_kwDOQ9dwes4C19H1"
51+
mapping="specific"
52+
term={slug}
53+
strict="1"
54+
reactionsEnabled="1"
55+
emitMetadata="0"
56+
inputPosition="top"
57+
theme={giscusTheme}
58+
lang="ko"
59+
loading="lazy"
60+
/>
61+
</div>
5862
</section>
5963
);
6064
}

src/features/blog/ui/components/PostCard.test.tsx

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
import { render, screen } from '@testing-library/react';
2-
import { createElement, type ImgHTMLAttributes, type ReactNode } from 'react';
2+
import type { ReactNode } from 'react';
33
import { describe, expect, it } from 'vitest';
44
import PostCard from './PostCard/PostCard';
55
import type { FeedData } from '@/domains/post/model/types';
66

7-
vi.mock('next/image', () => ({
8-
default: ({
9-
alt,
10-
fill: _fill,
11-
priority: _priority,
12-
...props
13-
}: ImgHTMLAttributes<HTMLImageElement> & {
14-
fill?: boolean;
15-
priority?: boolean;
16-
}) => createElement('img', { ...props, alt: alt ?? '' }),
17-
}));
18-
197
vi.mock('next/link', () => ({
208
default: ({
219
href,
@@ -76,17 +64,14 @@ describe('PostCard', () => {
7664
expect(screen.getByText('약 12분')).toBeInTheDocument();
7765
});
7866

79-
it('renders thumbnail in list variant when image exists', () => {
67+
it('keeps list variant text-only even when image exists', () => {
8068
const withImage: FeedData = {
8169
...basePost,
8270
image: '/images/test.png',
8371
};
8472

8573
render(<PostCard post={withImage} variant="list" />);
86-
expect(screen.getByRole('presentation')).toHaveAttribute(
87-
'src',
88-
'/images/test.png'
89-
);
74+
expect(screen.queryByRole('presentation')).not.toBeInTheDocument();
9075
});
9176

9277
it('shows category, series title, and tags in list variant', () => {

src/features/blog/ui/components/PostCard/PostCard.tsx

Lines changed: 13 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,13 @@
1-
import Image from 'next/image';
21
import Link from 'next/link';
32
import { clsx } from 'clsx';
43
import { FeedData } from '@/domains/post/model/types';
4+
import { CategoryIcon } from '@/shared/ui/icons/AppSectionIcon';
55

66
interface PostCardProps {
77
post: FeedData;
88
variant?: 'default' | 'featured' | 'list';
99
}
1010

11-
function CategoryIcon({ category }: { category: string }) {
12-
switch (category) {
13-
case 'Tech':
14-
return (
15-
<svg
16-
viewBox="0 0 24 24"
17-
width="12"
18-
height="12"
19-
fill="none"
20-
stroke="currentColor"
21-
strokeWidth="2"
22-
strokeLinecap="round"
23-
strokeLinejoin="round"
24-
aria-hidden="true"
25-
>
26-
<rect x="3" y="4" width="18" height="14" rx="2" />
27-
<path d="M8 20h8" />
28-
<path d="M10 16v4" />
29-
<path d="M14 16v4" />
30-
</svg>
31-
);
32-
case 'Life':
33-
return (
34-
<svg
35-
viewBox="0 0 24 24"
36-
width="12"
37-
height="12"
38-
fill="none"
39-
stroke="currentColor"
40-
strokeWidth="2"
41-
strokeLinecap="round"
42-
strokeLinejoin="round"
43-
aria-hidden="true"
44-
>
45-
<path d="M4 5a2 2 0 0 1 2-2h12v18H6a2 2 0 0 1-2-2z" />
46-
<path d="M8 7h7" />
47-
<path d="M8 11h7" />
48-
</svg>
49-
);
50-
default:
51-
return (
52-
<svg
53-
viewBox="0 0 24 24"
54-
width="12"
55-
height="12"
56-
fill="none"
57-
stroke="currentColor"
58-
strokeWidth="2"
59-
strokeLinecap="round"
60-
strokeLinejoin="round"
61-
aria-hidden="true"
62-
>
63-
<path d="m20 12-8 8-8-8 8-8 8 8z" />
64-
</svg>
65-
);
66-
}
67-
}
68-
6911
export default function PostCard({ post, variant = 'default' }: PostCardProps) {
7012
const formattedDate = new Date(post.date).toLocaleDateString('ko-KR', {
7113
year: 'numeric',
@@ -108,15 +50,15 @@ export default function PostCard({ post, variant = 'default' }: PostCardProps) {
10850
<Link
10951
href={`/blog/${post.slug}`}
11052
className={clsx(
111-
'group block rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-bg-primary)] px-4 py-4',
53+
'group block overflow-hidden rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-bg-primary)] px-5 py-5 sm:px-6',
11254
'transition-all duration-[var(--duration-200)] ease-[var(--ease-default)]',
113-
'hover:border-[var(--color-border-hover)] hover:bg-[var(--color-grey-50)]',
55+
'hover:-translate-y-0.5 hover:border-[var(--color-border-hover)] hover:bg-[var(--color-grey-50)] hover:shadow-[var(--shadow-sm)]',
11456
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-toss-blue)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-bg-primary)]'
11557
)}
11658
>
117-
<div className="flex items-center justify-between gap-4">
118-
<div className="min-w-0 flex-1">
119-
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--color-text-tertiary)]">
59+
<div className="min-w-0">
60+
<div className="min-w-0">
61+
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--color-text-tertiary)] transition-transform duration-300 ease-[var(--ease-default)] group-hover:translate-x-0.5">
12062
<span className="inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border)] bg-[var(--color-grey-50)] px-2 py-1 text-xs font-medium text-[var(--color-text-secondary)]">
12163
<CategoryIcon category={post.category} />
12264
{post.category}
@@ -135,14 +77,14 @@ export default function PostCard({ post, variant = 'default' }: PostCardProps) {
13577
</>
13678
)}
13779
</div>
138-
<h3 className="mt-2 text-base font-semibold leading-snug text-[var(--color-text-primary)] transition-colors group-hover:text-[var(--color-toss-blue)] group-focus-visible:text-[var(--color-toss-blue)]">
80+
<h3 className="mt-3 text-lg font-semibold leading-snug text-[var(--color-text-primary)] transition-[color,transform] duration-300 ease-[var(--ease-default)] group-hover:translate-x-1 group-hover:text-[var(--color-toss-blue)] group-focus-visible:text-[var(--color-toss-blue)]">
13981
{post.title}
14082
</h3>
141-
<p className="mt-1 text-sm text-[var(--color-text-secondary)] line-clamp-2">
83+
<p className="mt-2 max-w-3xl text-sm leading-6 text-[var(--color-text-secondary)] line-clamp-2 transition-transform duration-300 ease-[var(--ease-default)] group-hover:translate-x-1">
14284
{post.description}
14385
</p>
14486
{visibleTags.length > 0 ? (
145-
<div className="mt-3 flex flex-wrap gap-2">
87+
<div className="mt-3 flex flex-wrap gap-2 transition-transform duration-300 ease-[var(--ease-default)] group-hover:translate-x-1">
14688
{visibleTags.map((tag) => (
14789
<span
14890
key={tag}
@@ -154,18 +96,6 @@ export default function PostCard({ post, variant = 'default' }: PostCardProps) {
15496
</div>
15597
) : null}
15698
</div>
157-
{post.image ? (
158-
<div className="hidden h-20 w-32 shrink-0 overflow-hidden rounded-xl border border-[var(--color-border)] sm:block">
159-
<Image
160-
src={post.image}
161-
alt=""
162-
width={256}
163-
height={160}
164-
sizes="128px"
165-
className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-105"
166-
/>
167-
</div>
168-
) : null}
16999
</div>
170100
</Link>
171101
);
@@ -175,14 +105,14 @@ export default function PostCard({ post, variant = 'default' }: PostCardProps) {
175105
<Link
176106
href={`/blog/${post.slug}`}
177107
className={clsx(
178-
'group flex h-full cursor-pointer flex-col rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-bg-primary)] p-6',
108+
'group flex h-full cursor-pointer flex-col overflow-hidden rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-bg-primary)] p-6',
179109
'transition-all duration-[var(--duration-200)] ease-[var(--ease-default)]',
180110
'hover:-translate-y-0.5 hover:border-[var(--color-border-hover)] hover:shadow-[var(--shadow-md)]',
181111
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-toss-blue)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-bg-primary)]',
182112
'active:translate-y-0 active:shadow-sm'
183113
)}
184114
>
185-
<div className="mb-4 flex items-center gap-2 text-xs text-[var(--color-text-tertiary)]">
115+
<div className="mb-4 flex items-center gap-2 text-xs text-[var(--color-text-tertiary)] transition-transform duration-300 ease-[var(--ease-default)] group-hover:translate-x-0.5">
186116
<span className="inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border)] bg-[var(--color-grey-50)] px-2.5 py-1 text-xs font-medium text-[var(--color-text-secondary)]">
187117
<CategoryIcon category={post.category} />
188118
{post.category}
@@ -196,10 +126,10 @@ export default function PostCard({ post, variant = 'default' }: PostCardProps) {
196126
</>
197127
)}
198128
</div>
199-
<h3 className="line-clamp-2 text-lg font-semibold leading-snug text-[var(--color-text-primary)] transition-colors group-hover:text-[var(--color-toss-blue)] group-focus-visible:text-[var(--color-toss-blue)]">
129+
<h3 className="line-clamp-2 text-lg font-semibold leading-snug text-[var(--color-text-primary)] transition-[color,transform] duration-300 ease-[var(--ease-default)] group-hover:translate-x-1 group-hover:text-[var(--color-toss-blue)] group-focus-visible:text-[var(--color-toss-blue)]">
200130
{post.title}
201131
</h3>
202-
<p className="mt-3 flex-grow line-clamp-2 text-sm text-[var(--color-text-secondary)]">
132+
<p className="mt-3 flex-grow line-clamp-2 text-sm text-[var(--color-text-secondary)] transition-transform duration-300 ease-[var(--ease-default)] group-hover:translate-x-1">
203133
{post.description}
204134
</p>
205135
</Link>

src/features/blog/ui/components/PostList/PostList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default function PostList({ posts, layout = 'grid' }: PostListProps) {
5050
variants={containerVariants}
5151
initial="hidden"
5252
animate="visible"
53-
className="space-y-3"
53+
className="space-y-4"
5454
>
5555
{posts.map((post) => (
5656
<motion.div key={post.slug} variants={itemVariants}>

src/features/blog/ui/components/TableOfContents/TableOfContents.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ export default function TableOfContents({ items }: TableOfContentsProps) {
5555

5656
return (
5757
<nav
58-
className="hidden xl:block fixed right-8 top-1/2 -translate-y-1/2 w-64 max-h-[80vh]"
58+
className="fixed right-8 top-20 bottom-8 hidden w-64 xl:block"
5959
aria-label="이 글의 목차"
6060
>
61-
<div className="p-4 bg-[var(--color-grey-50)] rounded-[var(--radius-md)] max-h-[80vh] overflow-y-auto">
61+
<div className="h-full overflow-y-auto rounded-[var(--radius-md)] bg-[var(--color-grey-50)] p-4">
6262
<h2 className="text-sm font-semibold text-[var(--color-grey-900)] mb-4 sticky top-0 bg-[var(--color-grey-50)] pb-2">
6363
이 글의 목차
6464
</h2>

src/features/blog/ui/pages/EngineeringPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import EngineeringPageClient from './EngineeringPageClient';
66

77
export const metadata: Metadata = {
88
title: 'Engineering',
9-
description: '기술 글과 시리즈를 같은 흐름에서 살펴볼 수 있어요',
9+
description: '기술 글을 한 흐름에서 탐색할 수 있어요',
1010
};
1111

1212
export default function EngineeringPage() {
@@ -22,7 +22,7 @@ export default function EngineeringPage() {
2222
Engineering
2323
</h1>
2424
<p className="mt-4 text-lg text-[var(--color-grey-600)]">
25-
기술 글과 시리즈를 같은 흐름에서 살펴볼 수 있어요
25+
기술 글을 한 흐름에서 살펴볼 수 있어요
2626
</p>
2727
</header>
2828

0 commit comments

Comments
 (0)