Skip to content

Commit 6f21137

Browse files
dev-wooyeon박은우
andcommitted
refactor(site): 탐색과 읽기 경험 단순화 (#115)
* feat(content): 에세이와 이력서 공개 개선 * fix(site): PR 리뷰 피드백 반영 * refactor(blog): 비공개 WebGL 시각화 제거 * docs(content): Three.js 구현 서사 정리 * refactor(site): 전역 내비게이션과 조작 요소 단순화 * refactor(home): 아카이브 탐색 흐름 단순화 * style(site): Graphite Ink 편집 표면 적용 * docs(adr): 사이트 단순화 결정 기록 * refactor(site): 모바일 읽기 흐름 개선 모바일 헤더와 메뉴를 텍스트 중심으로 단순화하고 홈 아카이브에서 날짜와 제목의 읽기 순서를 조정했습니다.\n\nADR 0035에 모바일 아카이브와 메뉴의 반복 규칙을 기록했습니다. * refactor(blog): 콘텐츠 공개 기준 정리 공개 후보 글을 현재 구조와 검증 기준에 맞춰 리라이팅했습니다. Redis·Flink 학습 시리즈는 llm-wiki raw·draft로 이관하고 Ark에서 제거했습니다. --------- Co-authored-by: 박은우 <noah@bag-eun-uui-MacBookAir.local>
1 parent bdcc085 commit 6f21137

129 files changed

Lines changed: 1313 additions & 10050 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ ADR 작성 기준:
4545
- `blog/`: 글 도메인. post schema, repository, publication policy, series,
4646
blog UI, RSS feed serialization, view-count use case
4747
- `resume/`: 이력서 도메인. resume data, ordering, resume UI
48-
- `search/`: 검색 도메인. command palette, search action, recommendation
4948
- `site/`: 도메인 조합 layer. home, AppShell, navigation, provider, site config
5049
- `infra/`: 외부/런타임 인프라. Supabase, Umami analytics, SEO helper,
5150
integration adapter

README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242

4343
**애니메이션:**
4444

45-
- **3D:** Three.js + @react-three/fiber + @react-three/drei
4645
- **모션:** Framer Motion
4746

4847
**콘텐츠 처리:**
@@ -73,7 +72,6 @@ ark/
7372
├── 📁 app/ # Next.js route adapter
7473
├── 📁 blog/ # 글 도메인
7574
├── 📁 resume/ # 이력서 도메인
76-
├── 📁 search/ # 검색 도메인
7775
├── 📁 site/ # 홈, AppShell, provider, site config
7876
├── 📁 infra/ # Supabase, Umami, SEO integration
7977
├── 📁 ui/ # 도메인 지식 없는 UI/레이아웃/모션

app/api/og/route.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export async function GET(req: NextRequest) {
4040
flexDirection: 'column',
4141
alignItems: 'flex-start',
4242
justifyContent: 'center',
43-
backgroundImage: 'linear-gradient(to bottom right, #E0F2FF, #fff)',
43+
backgroundImage: 'linear-gradient(to bottom right, #EAEBEA, #E0E1E3)',
4444
padding: '80px',
4545
}}
4646
>
@@ -56,7 +56,7 @@ export async function GET(req: NextRequest) {
5656
<div
5757
style={{
5858
fontSize: '30px',
59-
color: '#6b7684',
59+
color: '#52525B',
6060
}}
6161
>
6262
{date}
@@ -66,7 +66,7 @@ export async function GET(req: NextRequest) {
6666
style={{
6767
fontSize: '60px',
6868
fontWeight: 700,
69-
color: '#191f28',
69+
color: '#252525',
7070
lineHeight: 1.2,
7171
wordBreak: 'keep-all',
7272
}}
@@ -86,8 +86,8 @@ export async function GET(req: NextRequest) {
8686
key={tag}
8787
style={{
8888
display: 'flex',
89-
backgroundColor: 'rgba(49, 130, 246, 0.1)',
90-
color: '#3182f6',
89+
backgroundColor: '#E0E1E3',
90+
color: '#3F3F46',
9191
padding: '8px 24px',
9292
borderRadius: '50px',
9393
fontSize: '24px',
@@ -114,7 +114,7 @@ export async function GET(req: NextRequest) {
114114
style={{
115115
fontSize: '32px',
116116
fontWeight: 700,
117-
color: '#3182f6',
117+
color: '#3F3F46',
118118
fontFamily: 'Pretendard',
119119
}}
120120
>

app/layout.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ describe('root layout font loading', () => {
1313
expect(layoutContent).not.toContain('href="/fonts/TossFaceFontWeb.otf"');
1414
});
1515

16-
it('passes a public client DTO instead of full editorial metadata', () => {
17-
expect(layoutContent).toContain('selectClientPosts(getSortedFeedData())');
18-
expect(layoutContent).toContain('<AppProviders posts={posts}>');
16+
it('passes only navigation metadata to the app shell', () => {
17+
expect(layoutContent).toContain('getSortedFeedData().map');
18+
expect(layoutContent).toContain('<AppProviders>');
19+
expect(layoutContent).toContain('<AppShell posts={posts}>');
20+
expect(layoutContent).not.toContain('selectClientPosts');
1921
});
2022

2123
it('keeps Tossface demand-driven for matching emoji glyphs', () => {

app/layout.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import '@/styles/tossface.css';
55

66
import AppProviders from '@/site/providers/AppProviders';
77
import { getSortedFeedData } from '@/blog/services/post-repository';
8-
import { selectClientPosts } from '@/site/providers/client-posts';
98
import { AppShell } from '@/site/shell/AppShell';
109
import {
1110
SITE_AUTHOR,
@@ -55,16 +54,19 @@ export const metadata: Metadata = {
5554
};
5655

5756
export const viewport: Viewport = {
58-
themeColor: '#ffffff',
57+
themeColor: '#EAEBEA',
5958
width: 'device-width',
6059
initialScale: 1,
6160
};
6261

6362
export default function RootLayout({ children }: { children: ReactNode }) {
64-
const posts = selectClientPosts(getSortedFeedData());
63+
const posts = getSortedFeedData().map(({ slug, category }) => ({
64+
slug,
65+
category,
66+
}));
6567

6668
return (
67-
<html lang="ko" suppressHydrationWarning>
69+
<html lang="ko">
6870
<head>
6971
<link
7072
rel="preload"
@@ -75,11 +77,10 @@ export default function RootLayout({ children }: { children: ReactNode }) {
7577
/>
7678
</head>
7779
<body>
78-
<AppProviders posts={posts}>
80+
<AppProviders>
7981
<div id="app-root">
8082
<AppShell posts={posts}>{children}</AppShell>
8183
</div>
82-
<div id="overlay-root" />
8384
</AppProviders>
8485
</body>
8586
</html>

app/not-found.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function NotFound() {
1717
</p>
1818
<Link
1919
href="/"
20-
className="mt-8 inline-flex min-h-11 items-center justify-center rounded-[var(--radius-action)] bg-[var(--color-toss-blue)] px-5 text-sm font-semibold text-white transition-colors hover:bg-[var(--color-toss-blue-dark)] 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)]"
20+
className="mt-8 inline-flex min-h-11 items-center justify-center rounded-[var(--radius-action)] bg-[var(--color-toss-blue)] px-5 text-sm font-semibold text-[var(--color-accent-foreground)] transition-colors hover:bg-[var(--color-toss-blue-dark)] 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)]"
2121
>
2222
홈으로 돌아가기
2323
</Link>

blog/services/markdown-parser.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('parseHeadingsFromMdx', () => {
6868

6969
describe('getMdxSource', () => {
7070
it('returns markdown source for an existing slug', () => {
71-
const source = getMdxSource('redis-deep-dive-02-core-data-types');
71+
const source = getMdxSource('llm-wiki-build-retrospective');
7272

7373
expect(typeof source).toBe('string');
7474
expect(source).toContain('#');

blog/services/post-repository.test.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,13 @@ ${'x'.repeat(5000)}
6060
expect(hasExtractedImage).toBe(true);
6161
});
6262

63-
it('returns series posts sorted by order', () => {
64-
const redisPosts = getSeriesPosts('redis-deep-dive', {
65-
includePrivate: true,
66-
});
67-
68-
expect(redisPosts.length).toBeGreaterThan(0);
69-
const orders = redisPosts.map((post) => post.series?.order ?? 0);
70-
expect(orders).toEqual([...orders].sort((a, b) => a - b));
63+
it('does not retain learning series migrated to llm-wiki', () => {
64+
expect(getSeriesPosts('redis-deep-dive', { includePrivate: true })).toEqual(
65+
[]
66+
);
67+
expect(getSeriesPosts('flink-mastery', { includePrivate: true })).toEqual(
68+
[]
69+
);
7170
});
7271

7372
it('returns empty array for unknown series id', () => {
@@ -112,13 +111,6 @@ ${'x'.repeat(5000)}
112111
expect(await getFeedData(privateSlug)).toBeNull();
113112
});
114113

115-
it('filters private series posts from helper lookups', () => {
116-
expect(getSeriesPosts('redis-deep-dive')).toEqual([]);
117-
expect(
118-
getSeriesPosts('redis-deep-dive', { includePrivate: true }).length
119-
).toBeGreaterThan(0);
120-
});
121-
122114
it('returns null for non-existent posts folder slug', () => {
123115
expect(getFolderSlug('missing-folder-slug')).toBeNull();
124116
});

blog/ui/components/CategoryFilter.test.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,28 +79,30 @@ describe('CategoryFilter', () => {
7979
expect(onCategoryChange).toHaveBeenCalledWith('Life');
8080
});
8181

82-
it('renders active style classes', () => {
82+
it('renders the active category as an underlined text control', () => {
8383
render(
8484
<CategoryFilter
8585
{...baseProps}
8686
activeCategory="Life"
8787
onCategoryChange={vi.fn()}
88+
variant="links"
8889
/>
8990
);
9091

9192
const lifeButton = screen
9293
.getAllByRole('button')
93-
.find((button) => button.textContent === 'Life2');
94+
.find((button) => button.textContent === 'Life(2)');
9495

9596
expect(lifeButton).toBeDefined();
9697

9798
if (!lifeButton) {
9899
throw new Error('Life category button not found');
99100
}
100101

101-
expect(lifeButton).toHaveClass('bg-[var(--color-toss-blue)]');
102-
expect(lifeButton).toHaveClass('rounded-[var(--radius-selection)]');
103-
expect(lifeButton).not.toHaveClass('shadow-sm');
104-
expect(lifeButton).not.toHaveClass('active:translate-y-px');
102+
expect(lifeButton).toHaveClass('border-b-2');
103+
expect(lifeButton).toHaveClass('border-[var(--color-toss-blue)]');
104+
expect(lifeButton).toHaveClass('text-[var(--color-toss-blue)]');
105+
expect(lifeButton).not.toHaveClass('rounded-[var(--radius-selection)]');
106+
expect(lifeButton).not.toHaveClass('bg-[var(--color-toss-blue)]');
105107
});
106108
});

blog/ui/components/CategoryFilter/CategoryFilter.tsx

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { clsx } from 'clsx';
44

55
type Category = 'All' | 'Tech' | 'Life';
6+
type CategoryFilterVariant = 'links' | 'pills';
67

78
const CATEGORY_LABELS: Record<Category, string> = {
89
All: 'All',
@@ -15,44 +16,70 @@ interface CategoryFilterProps {
1516
activeCategory: Category;
1617
onCategoryChange: (category: Category) => void;
1718
categoryCounts: Record<Category, number>;
19+
variant?: CategoryFilterVariant;
1820
}
1921

2022
export default function CategoryFilter({
2123
categories,
2224
activeCategory,
2325
onCategoryChange,
2426
categoryCounts,
27+
variant = 'pills',
2528
}: CategoryFilterProps) {
29+
const isLinkVariant = variant === 'links';
30+
2631
return (
27-
<div className="flex gap-2 flex-wrap">
28-
{categories.map((category) => (
29-
<button
30-
key={category}
31-
onClick={() => onCategoryChange(category)}
32-
className={clsx(
33-
'inline-flex min-h-11 items-center gap-2 rounded-[var(--radius-selection)] border px-4 py-2 text-sm transition-colors duration-[var(--duration-200)] ease-[var(--ease-default)]',
34-
'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)]',
35-
activeCategory === category
36-
? 'border-[var(--color-toss-blue)] bg-[var(--color-toss-blue)] text-white'
37-
: 'border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-grey-600)] hover:border-[var(--color-border-hover)] hover:bg-[var(--color-grey-50)]'
38-
)}
39-
aria-pressed={activeCategory === category}
40-
>
41-
<span className="font-medium">{CATEGORY_LABELS[category]}</span>
42-
<span
32+
<nav aria-label="글 분류">
33+
<div
34+
className={clsx(
35+
'flex flex-wrap items-center',
36+
isLinkVariant ? 'gap-x-5 gap-y-1' : 'gap-2'
37+
)}
38+
>
39+
{categories.map((category) => (
40+
<button
41+
key={category}
42+
onClick={() => onCategoryChange(category)}
4343
className={clsx(
44-
'rounded-[var(--radius-selection)] px-1.5 py-0.5 text-[10px] font-semibold',
45-
activeCategory === category
46-
? 'bg-white/20 text-white'
47-
: 'bg-[var(--color-grey-100)] text-[var(--color-text-tertiary)]'
44+
'inline-flex min-h-11 items-center text-sm transition-colors duration-[var(--duration-200)] ease-[var(--ease-default)]',
45+
isLinkVariant
46+
? 'border-b-2 px-0 py-2 font-medium'
47+
: 'gap-2 rounded-[var(--radius-selection)] border px-4 py-2',
48+
'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)]',
49+
isLinkVariant
50+
? activeCategory === category
51+
? 'border-[var(--color-toss-blue)] text-[var(--color-toss-blue)]'
52+
: 'border-transparent text-[var(--color-text-secondary)] hover:border-[var(--color-grey-300)] hover:text-[var(--color-text-primary)]'
53+
: activeCategory === category
54+
? 'border-[var(--color-toss-blue)] bg-[var(--color-toss-blue)] text-[var(--color-accent-foreground)]'
55+
: 'border-[var(--color-border)] bg-[var(--color-bg-primary)] text-[var(--color-grey-600)] hover:border-[var(--color-border-hover)] hover:bg-[var(--color-grey-50)]'
4856
)}
57+
aria-pressed={activeCategory === category}
4958
>
50-
{categoryCounts[category]}
51-
</span>
52-
</button>
53-
))}
54-
</div>
59+
<span className={clsx(!isLinkVariant && 'font-medium')}>
60+
{CATEGORY_LABELS[category]}
61+
</span>
62+
{isLinkVariant ? (
63+
<span className="ml-1 text-meta font-normal text-[var(--color-text-tertiary)]">
64+
({categoryCounts[category]})
65+
</span>
66+
) : (
67+
<span
68+
className={clsx(
69+
'rounded-[var(--radius-selection)] px-1.5 py-0.5 text-[10px] font-semibold',
70+
activeCategory === category
71+
? 'bg-[var(--color-grey-700)] text-[var(--color-accent-foreground)]'
72+
: 'bg-[var(--color-grey-100)] text-[var(--color-text-tertiary)]'
73+
)}
74+
>
75+
{categoryCounts[category]}
76+
</span>
77+
)}
78+
</button>
79+
))}
80+
</div>
81+
</nav>
5582
);
5683
}
5784

58-
export type { CategoryFilterProps, Category };
85+
export type { CategoryFilterProps, Category, CategoryFilterVariant };

0 commit comments

Comments
 (0)