From 48391a6e7c8fa45e3504d3beb2ed9a0a46c7ebe9 Mon Sep 17 00:00:00 2001 From: Jongchan Date: Wed, 16 Jul 2025 17:46:04 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20PostHeader=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 포스트 제목, 날짜, 태그를 표시하는 PostHeader 컴포넌트 작성 - formatDate 유틸리티 함수 구현 (ISO 날짜를 한국어 형식으로 변환) - 반응형 레이아웃 적용 (모바일/데스크톱) - 다크 모드 지원 및 stone 색상 팔레트 사용 - 태그에 # 접두사 추가 및 Badge 컴포넌트 스타일링 - formatDate 유틸리티 함수 단위 테스트 작성 - PostHeader 사용 예시 파일 추가 --- src/app/_lib/formatDate.test.ts | 58 ++++++++++++++++++ src/app/_lib/formatDate.ts | 50 ++++++++++++++++ .../posts/_components/PostHeader.example.tsx | 38 ++++++++++++ src/app/posts/_components/PostHeader.tsx | 60 +++++++++++++++++++ src/app/posts/_components/index.ts | 3 + 5 files changed, 209 insertions(+) create mode 100644 src/app/_lib/formatDate.test.ts create mode 100644 src/app/_lib/formatDate.ts create mode 100644 src/app/posts/_components/PostHeader.example.tsx create mode 100644 src/app/posts/_components/PostHeader.tsx create mode 100644 src/app/posts/_components/index.ts diff --git a/src/app/_lib/formatDate.test.ts b/src/app/_lib/formatDate.test.ts new file mode 100644 index 0000000..a3e3e5c --- /dev/null +++ b/src/app/_lib/formatDate.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest' + +import { formatDate, formatRelativeDate } from './formatDate' + +describe('formatDate', () => { + it('should format ISO date string to Korean format', () => { + const isoDate = '2025-01-15' + const result = formatDate(isoDate) + expect(result).toBe('2025년 1월 15일') + }) + + it('should format Date object to Korean format', () => { + const date = new Date('2025-12-25') + const result = formatDate(date) + expect(result).toBe('2025년 12월 25일') + }) + + it('should handle single digit months and days', () => { + const isoDate = '2025-03-05' + const result = formatDate(isoDate) + expect(result).toBe('2025년 3월 5일') + }) + + it('should return original string for invalid date', () => { + const invalidDate = 'invalid-date' + const result = formatDate(invalidDate) + expect(result).toBe('invalid-date') + }) +}) + +describe('formatRelativeDate', () => { + it('should return "오늘" for today', () => { + const today = new Date() + const result = formatRelativeDate(today) + expect(result).toBe('오늘') + }) + + it('should return "어제" for yesterday', () => { + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + const result = formatRelativeDate(yesterday) + expect(result).toBe('어제') + }) + + it('should return days for recent dates', () => { + const threeDaysAgo = new Date() + threeDaysAgo.setDate(threeDaysAgo.getDate() - 3) + const result = formatRelativeDate(threeDaysAgo) + expect(result).toBe('3일 전') + }) + + it('should return weeks for dates within a month', () => { + const twoWeeksAgo = new Date() + twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14) + const result = formatRelativeDate(twoWeeksAgo) + expect(result).toBe('2주일 전') + }) +}) diff --git a/src/app/_lib/formatDate.ts b/src/app/_lib/formatDate.ts new file mode 100644 index 0000000..59c2701 --- /dev/null +++ b/src/app/_lib/formatDate.ts @@ -0,0 +1,50 @@ +/** + * Formats a date string to Korean format + * @param dateString - ISO date string (YYYY-MM-DD) or Date object + * @returns Formatted date string in Korean format (YYYY년 M월 D일) + */ +export function formatDate(dateString: string | Date): string { + const date = + typeof dateString === 'string' ? new Date(dateString) : dateString + + // Check if date is valid + if (isNaN(date.getTime())) { + return dateString.toString() + } + + const year = date.getFullYear() + const month = date.getMonth() + 1 + const day = date.getDate() + + return `${year}년 ${month}월 ${day}일` +} + +/** + * Formats a date string to relative time (e.g., "2일 전", "1주일 전") + * @param dateString - ISO date string (YYYY-MM-DD) or Date object + * @returns Relative time string in Korean + */ +export function formatRelativeDate(dateString: string | Date): string { + const date = + typeof dateString === 'string' ? new Date(dateString) : dateString + const now = new Date() + const diffInMs = now.getTime() - date.getTime() + const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)) + + if (diffInDays === 0) { + return '오늘' + } else if (diffInDays === 1) { + return '어제' + } else if (diffInDays < 7) { + return `${diffInDays}일 전` + } else if (diffInDays < 30) { + const weeks = Math.floor(diffInDays / 7) + return `${weeks}주일 전` + } else if (diffInDays < 365) { + const months = Math.floor(diffInDays / 30) + return `${months}개월 전` + } else { + const years = Math.floor(diffInDays / 365) + return `${years}년 전` + } +} diff --git a/src/app/posts/_components/PostHeader.example.tsx b/src/app/posts/_components/PostHeader.example.tsx new file mode 100644 index 0000000..d3a0d13 --- /dev/null +++ b/src/app/posts/_components/PostHeader.example.tsx @@ -0,0 +1,38 @@ +// This is an example file showing how to use the PostHeader component +// This file is for demonstration purposes and can be removed after verification + +import { PostHeader } from './PostHeader' + +export function PostHeaderExample() { + return ( +
+ + +
+

+ PostHeader 컴포넌트 특징: +

+
    +
  • 한국어 날짜 포맷팅 (formatDate 유틸리티 사용)
  • +
  • + 반응형 레이아웃 (모바일에서 세로 배치, 데스크톱에서 가로 배치) +
  • +
  • 다크 모드 지원
  • +
  • 태그에 # 접두사 추가
  • +
  • 읽기 시간 및 작성자 정보 선택적 표시
  • +
  • 접근성 고려 (시맨틱 HTML, ARIA 속성)
  • +
+
+
+ ) +} diff --git a/src/app/posts/_components/PostHeader.tsx b/src/app/posts/_components/PostHeader.tsx new file mode 100644 index 0000000..c6c6b59 --- /dev/null +++ b/src/app/posts/_components/PostHeader.tsx @@ -0,0 +1,60 @@ +import { Badge } from '@/app/_components/ui/badge' +import { cn } from '@/app/_lib/cn' +import { formatDate } from '@/app/_lib/formatDate' + +interface PostHeaderProps { + title: string + date: string + tags: string[] + readingTime?: number + author?: string + className?: string +} + +export function PostHeader({ + title, + date, + tags, + readingTime, + author, + className, +}: PostHeaderProps) { + return ( +
+

+ {title} +

+
+ + {readingTime && ( + 약 {readingTime}분 읽기 + )} + {author && 작성자: {author}} + {tags.length > 0 && ( +
+ {tags.map((tag) => ( + + #{tag} + + ))} +
+ )} +
+
+ ) +} diff --git a/src/app/posts/_components/index.ts b/src/app/posts/_components/index.ts new file mode 100644 index 0000000..37a1a4c --- /dev/null +++ b/src/app/posts/_components/index.ts @@ -0,0 +1,3 @@ +export { PostContent } from './PostContent' +export { PostFooter } from './PostFooter' +export { PostHeader } from './PostHeader' From b7caadc4b1727bcd96affaa2ff56c755f90e299c Mon Sep 17 00:00:00 2001 From: Jongchan Date: Wed, 16 Jul 2025 17:47:49 +0900 Subject: [PATCH 02/12] =?UTF-8?q?fix:=20formatDate=EC=97=90=EC=84=9C=20Num?= =?UTF-8?q?ber.isNaN=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Biome 린트 경고 해결: isNaN 대신 Number.isNaN 사용 --- src/app/_lib/formatDate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/_lib/formatDate.ts b/src/app/_lib/formatDate.ts index 59c2701..5d140fe 100644 --- a/src/app/_lib/formatDate.ts +++ b/src/app/_lib/formatDate.ts @@ -8,7 +8,7 @@ export function formatDate(dateString: string | Date): string { typeof dateString === 'string' ? new Date(dateString) : dateString // Check if date is valid - if (isNaN(date.getTime())) { + if (Number.isNaN(date.getTime())) { return dateString.toString() } From ced55c2ecba728a47d933c203f2cefb2fc322882 Mon Sep 17 00:00:00 2001 From: Jongchan Date: Wed, 16 Jul 2025 17:56:26 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20Post=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PostFrontMatter에 description, author, readingTime, featured 필드 추가 - PostWithNavigation 타입 추가 (이전/다음 포스트, 관련 포스트) - Heading 타입 추가 (목차 생성용) - PostMetadata 타입 추가 (SEO 메타데이터용) --- src/entities/posts/types.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/entities/posts/types.ts b/src/entities/posts/types.ts index e20deb8..bb79cd9 100644 --- a/src/entities/posts/types.ts +++ b/src/entities/posts/types.ts @@ -4,6 +4,10 @@ export type PostFrontMatter = { title: string date: string tags: Array + description?: string + author?: string + readingTime?: number + featured?: boolean } export type Post = { @@ -15,3 +19,33 @@ export type Post = { export type PostGrayMatter = GrayMatterFile & { data: PostFrontMatter } + +export type PostWithNavigation = Post & { + previousPost?: Pick + nextPost?: Pick + relatedPosts?: Array> +} + +export type Heading = { + id: string + text: string + level: number +} + +export type PostMetadata = { + title: string + description: string + openGraph: { + title: string + description: string + type: 'article' + publishedTime: string + authors: string[] + tags: string[] + } + twitter: { + card: 'summary_large_image' + title: string + description: string + } +} From 12befc9355d300eee4900579f60f70f56ce7209e Mon Sep 17 00:00:00 2001 From: Jongchan Date: Wed, 16 Jul 2025 17:56:57 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20shadcn/ui=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Badge 컴포넌트 추가 (태그 표시용) - Card 컴포넌트 추가 (콘텐츠 카드용) - class-variance-authority 기반 variant 시스템 적용 --- src/app/_components/ui/badge.tsx | 53 ++++++++++++++++++ src/app/_components/ui/card.tsx | 92 ++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 src/app/_components/ui/badge.tsx create mode 100644 src/app/_components/ui/card.tsx diff --git a/src/app/_components/ui/badge.tsx b/src/app/_components/ui/badge.tsx new file mode 100644 index 0000000..24b0444 --- /dev/null +++ b/src/app/_components/ui/badge.tsx @@ -0,0 +1,53 @@ +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import type * as React from 'react' + +import { cn } from '@/app/_lib/cn' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : 'span' + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/src/app/_components/ui/card.tsx b/src/app/_components/ui/card.tsx new file mode 100644 index 0000000..a8b9fbf --- /dev/null +++ b/src/app/_components/ui/card.tsx @@ -0,0 +1,92 @@ +import type * as React from 'react' + +import { cn } from '@/app/_lib/cn' + +function Card({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} From fbf6ce5c0addeca9f2111ce78d8541cc1065bbc3 Mon Sep 17 00:00:00 2001 From: Jongchan Date: Wed, 16 Jul 2025 17:58:39 +0900 Subject: [PATCH 05/12] =?UTF-8?q?refactor:=20=EB=B8=94=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /blog/[slug] → /posts/[slug]로 라우트 경로 변경 - 더 명확한 URL 구조로 개선 --- package.json | 3 ++- src/app/{blog => posts}/[slug]/page.tsx | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename src/app/{blog => posts}/[slug]/page.tsx (100%) diff --git a/package.json b/package.json index 1a63d2d..212d42c 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "test:coverage": "vitest run --coverage", "biome:check": "biome check --verbose", "biome:fix": "biome check --write --verbose", - "biome:staged": "biome check --write --staged" + "biome:staged": "biome check --write --staged", + "type": "tsc --noEmit" }, "dependencies": { "@mdx-js/loader": "^3.1.0", diff --git a/src/app/blog/[slug]/page.tsx b/src/app/posts/[slug]/page.tsx similarity index 100% rename from src/app/blog/[slug]/page.tsx rename to src/app/posts/[slug]/page.tsx From 5fbb169fcbd701901a8389e82e35efc162935a75 Mon Sep 17 00:00:00 2001 From: Jongchan Date: Wed, 16 Jul 2025 17:59:20 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20PostContent,=20PostFooter=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PostContent: MDX 콘텐츠 렌더링 컴포넌트 - PostFooter: 포스트 하단 정보 표시 컴포넌트 - 포스트 페이지 레이아웃 구성 요소 완성 --- src/app/posts/_components/PostContent.tsx | 35 ++++++++++++++++ src/app/posts/_components/PostFooter.tsx | 50 +++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/app/posts/_components/PostContent.tsx create mode 100644 src/app/posts/_components/PostFooter.tsx diff --git a/src/app/posts/_components/PostContent.tsx b/src/app/posts/_components/PostContent.tsx new file mode 100644 index 0000000..2cc7d83 --- /dev/null +++ b/src/app/posts/_components/PostContent.tsx @@ -0,0 +1,35 @@ +import { cn } from '@/app/_lib/cn' +import type { Heading } from '@/entities/posts/types' + +interface PostContentProps { + children: React.ReactNode + showTOC?: boolean + headings?: Heading[] + className?: string +} + +export function PostContent({ + children, + showTOC = false, + headings, + className, +}: PostContentProps) { + return ( +
+ {showTOC && headings && ( + + )} +
+ {children} +
+
+ ) +} diff --git a/src/app/posts/_components/PostFooter.tsx b/src/app/posts/_components/PostFooter.tsx new file mode 100644 index 0000000..c7036fc --- /dev/null +++ b/src/app/posts/_components/PostFooter.tsx @@ -0,0 +1,50 @@ +import { cn } from '@/app/_lib/cn' +import type { Post } from '@/entities/posts/types' + +interface PostFooterProps { + previousPost?: Pick + nextPost?: Pick + relatedPosts?: Array> + author?: string + className?: string +} + +export function PostFooter({ + previousPost, + nextPost, + relatedPosts, + author, + className, +}: PostFooterProps) { + return ( +
+ {/* TODO: Implement PostNavigation component */} + {(previousPost || nextPost) && ( +
+
Navigation (TODO)
+
+ )} + + {/* TODO: Implement RelatedPosts component */} + {relatedPosts && relatedPosts.length > 0 && ( +
+
Related Posts (TODO)
+
+ )} + + {/* TODO: Implement SocialShare component */} +
+
Social Share (TODO)
+
+ + {/* TODO: Implement AuthorInfo component */} + {author && ( +
+
+ Author Info: {author} (TODO) +
+
+ )} +
+ ) +} From 227c5f716dc8a3ad9c50f2868bf1d4ab5fe4ed10 Mon Sep 17 00:00:00 2001 From: Jongchan Date: Wed, 16 Jul 2025 18:00:38 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20Kiro=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=8A=A4?= =?UTF-8?q?=ED=8E=99=20=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .kiro/steering/: 개발 표준, 프로젝트 구조, UI 디자인 시스템 가이드라인 - .kiro/specs/post-page-layout/: 포스트 페이지 레이아웃 스펙 문서 - requirements.md: 요구사항 정의 - design.md: 설계 문서 - tasks.md: 구현 작업 목록 --- .kiro/specs/post-page-layout/design.md | 400 +++++++++++++++++++ .kiro/specs/post-page-layout/requirements.md | 62 +++ .kiro/specs/post-page-layout/tasks.md | 90 +++++ .kiro/steering/development-standards.md | 171 ++++++++ .kiro/steering/project-structure.md | 142 +++++++ .kiro/steering/ui-design-system.md | 280 +++++++++++++ 6 files changed, 1145 insertions(+) create mode 100644 .kiro/specs/post-page-layout/design.md create mode 100644 .kiro/specs/post-page-layout/requirements.md create mode 100644 .kiro/specs/post-page-layout/tasks.md create mode 100644 .kiro/steering/development-standards.md create mode 100644 .kiro/steering/project-structure.md create mode 100644 .kiro/steering/ui-design-system.md diff --git a/.kiro/specs/post-page-layout/design.md b/.kiro/specs/post-page-layout/design.md new file mode 100644 index 0000000..dbd4735 --- /dev/null +++ b/.kiro/specs/post-page-layout/design.md @@ -0,0 +1,400 @@ +# Design Document + +## Overview + +포스트 페이지 레이아웃 개선은 현재의 기본적인 MDX 렌더링에서 체계적이고 사용자 친화적인 블로그 포스트 경험으로 발전시키는 것을 목표로 합니다. 이 설계는 Next.js 15 App Router, MDX, Tailwind CSS, shadcn/ui를 활용하여 성능과 사용자 경험을 모두 고려한 솔루션을 제공합니다. + +## Architecture + +### 컴포넌트 계층 구조 + +``` +PostPage (src/app/posts/[slug]/page.tsx) +├── PostHeader +│ ├── PostTitle +│ ├── PostMeta (date, tags) +│ └── PostActions (share, bookmark) +├── PostContent +│ ├── TableOfContents (optional) +│ └── MDXContent (prose styling) +├── PostFooter +│ ├── PostNavigation (prev/next) +│ ├── RelatedPosts +│ ├── AuthorInfo +│ └── SocialShare +└── PostSEO (metadata, JSON-LD) +``` + +### 데이터 흐름 + +1. **Static Generation**: `generateStaticParams()`로 모든 포스트 경로 생성 +2. **Post Data Fetching**: 기존 `src/entities/posts` 활용하여 포스트 데이터 가져오기 +3. **MDX Rendering**: 동적 import로 MDX 컴포넌트 로드 +4. **Metadata Generation**: `generateMetadata()`로 SEO 메타데이터 생성 + +## Components and Interfaces + +### PostHeader 컴포넌트 + +```typescript +interface PostHeaderProps { + title: string + date: string + tags: string[] + readingTime?: number +} + +export function PostHeader({ title, date, tags, readingTime }: PostHeaderProps) { + return ( +
+

+ {title} +

+
+ + {readingTime && ( + 약 {readingTime}분 읽기 + )} +
+ {tags.map(tag => ( + + {tag} + + ))} +
+
+
+ ) +} +``` + +### PostContent 컴포넌트 + +```typescript +interface PostContentProps { + children: React.ReactNode + showTOC?: boolean + headings?: Heading[] +} + +export function PostContent({ children, showTOC, headings }: PostContentProps) { + return ( +
+ {showTOC && headings && ( + + )} +
+ {children} +
+
+ ) +} +``` + +### PostNavigation 컴포넌트 + +```typescript +interface PostNavigationProps { + previousPost?: { + slug: string + title: string + } + nextPost?: { + slug: string + title: string + } +} + +export function PostNavigation({ previousPost, nextPost }: PostNavigationProps) { + return ( + + ) +} +``` + +### RelatedPosts 컴포넌트 + +```typescript +interface RelatedPostsProps { + posts: Array<{ + slug: string + title: string + date: string + tags: string[] + }> +} + +export function RelatedPosts({ posts }: RelatedPostsProps) { + if (posts.length === 0) return null + + return ( +
+

+ 관련 글 +

+
+ {posts.map(post => ( + + + +

+ {post.title} +

+ + +
+
+ ))} +
+
+ ) +} +``` + +## Data Models + +### Post 타입 확장 + +```typescript +// src/entities/posts/types.ts 확장 +export type PostFrontMatter = { + title: string + date: string + tags: Array + description?: string + author?: string + readingTime?: number + featured?: boolean +} + +export type PostWithNavigation = Post & { + previousPost?: Pick + nextPost?: Pick + relatedPosts?: Array> +} + +export type Heading = { + id: string + text: string + level: number +} +``` + +### 메타데이터 타입 + +```typescript +export type PostMetadata = { + title: string + description: string + openGraph: { + title: string + description: string + type: 'article' + publishedTime: string + authors: string[] + tags: string[] + } + twitter: { + card: 'summary_large_image' + title: string + description: string + } +} +``` + +## Error Handling + +### 에러 시나리오 및 처리 + +1. **포스트 파일 없음** + - `notFound()` 함수 호출하여 404 페이지 표시 + - 사용자에게 명확한 에러 메시지 제공 + +2. **MDX 파싱 에러** + - try-catch로 에러 캐치 + - 개발 환경에서는 상세 에러 로그 + - 프로덕션에서는 일반적인 에러 메시지 + +3. **관련 포스트 로딩 실패** + - 관련 포스트 섹션 숨김 + - 메인 콘텐츠는 정상 표시 + +```typescript +// 에러 처리 예시 +export default async function PostPage({ params }: { params: { slug: string } }) { + try { + const post = await getPostBySlug(params.slug) + if (!post) { + notFound() + } + + const { default: MDXContent } = await import(`@/contents/${post.slug}.mdx`) + + return ( + + + + ) + } catch (error) { + console.error('Post loading error:', error) + notFound() + } +} +``` + +## Testing Strategy + +### 단위 테스트 + +1. **유틸리티 함수 테스트** + - `formatDate()` 함수 + - `calculateReadingTime()` 함수 + - `extractHeadings()` 함수 + +2. **컴포넌트 테스트** + - PostHeader 렌더링 테스트 + - PostNavigation 링크 테스트 + - RelatedPosts 표시 테스트 + +### 통합 테스트 + +1. **페이지 렌더링 테스트** + - 포스트 페이지 전체 렌더링 + - 메타데이터 생성 확인 + - SEO 태그 검증 + +2. **반응형 테스트** + - 모바일/데스크톱 레이아웃 확인 + - 터치 인터랙션 테스트 + +### 성능 테스트 + +1. **로딩 성능** + - 페이지 로드 시간 측정 + - 이미지 최적화 확인 + - 코드 분할 효과 검증 + +2. **SEO 검증** + - Lighthouse SEO 점수 + - 구조화된 데이터 검증 + - 메타 태그 완성도 확인 + +## Performance Considerations + +### 최적화 전략 + +1. **정적 생성 최적화** + - 모든 포스트 페이지 빌드 타임 생성 + - 메타데이터 사전 생성 + - 관련 포스트 계산 최적화 + +2. **이미지 최적화** + - Next.js Image 컴포넌트 사용 + - 적절한 크기와 포맷 제공 + - 지연 로딩 적용 + +3. **코드 분할** + - MDX 컴포넌트 동적 로딩 + - 필요한 컴포넌트만 로드 + - 번들 크기 최소화 + +### 접근성 고려사항 + +1. **시맨틱 HTML** + - 적절한 heading 계층구조 + - article, section, nav 태그 활용 + - 스크린 리더 친화적 구조 + +2. **키보드 네비게이션** + - 모든 인터랙티브 요소 접근 가능 + - 포커스 표시 명확히 + - 논리적인 탭 순서 + +3. **색상 대비** + - WCAG 가이드라인 준수 + - 다크 모드 지원 + - 색상에 의존하지 않는 정보 전달 + +## SEO Optimization + +### 메타데이터 최적화 + +```typescript +export async function generateMetadata({ params }: { params: { slug: string } }): Promise { + const post = await getPostBySlug(params.slug) + + if (!post) { + return { + title: 'Post Not Found' + } + } + + return { + title: post.data.title, + description: post.data.description || extractExcerpt(post.content), + openGraph: { + title: post.data.title, + description: post.data.description || extractExcerpt(post.content), + type: 'article', + publishedTime: post.data.date, + authors: [post.data.author || 'Blog Author'], + tags: post.data.tags, + }, + twitter: { + card: 'summary_large_image', + title: post.data.title, + description: post.data.description || extractExcerpt(post.content), + } + } +} +``` + +### 구조화된 데이터 + +```typescript +function generateJSONLD(post: Post) { + return { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + headline: post.data.title, + description: post.data.description, + author: { + '@type': 'Person', + name: post.data.author || 'Blog Author' + }, + datePublished: post.data.date, + keywords: post.data.tags.join(', '), + mainEntityOfPage: { + '@type': 'WebPage', + '@id': `https://yourdomain.com/posts/${post.slug}` + } + } +} +``` \ No newline at end of file diff --git a/.kiro/specs/post-page-layout/requirements.md b/.kiro/specs/post-page-layout/requirements.md new file mode 100644 index 0000000..5d86d0d --- /dev/null +++ b/.kiro/specs/post-page-layout/requirements.md @@ -0,0 +1,62 @@ +# Requirements Document + +## Introduction + +현재 블로그의 포스트 페이지는 매우 기본적인 구조로 되어 있어, 사용자 경험과 콘텐츠 가독성을 개선할 필요가 있습니다. 이 기능은 포스트 페이지의 레이아웃을 체계적으로 구성하여 더 나은 읽기 경험을 제공하고, SEO 최적화 및 사용자 참여도를 높이는 것을 목표로 합니다. + +## Requirements + +### Requirement 1 + +**User Story:** As a 블로그 독자, I want 포스트 메타데이터(제목, 날짜, 태그)를 명확하게 볼 수 있기를, so that 포스트에 대한 기본 정보를 빠르게 파악할 수 있다 + +#### Acceptance Criteria + +1. WHEN 포스트 페이지에 접근하면 THEN 시스템 SHALL 포스트 제목을 페이지 상단에 표시한다 +2. WHEN 포스트 페이지에 접근하면 THEN 시스템 SHALL 작성 날짜를 제목 하단에 표시한다 +3. WHEN 포스트에 태그가 있으면 THEN 시스템 SHALL 태그들을 시각적으로 구분되는 형태로 표시한다 +4. WHEN 포스트 메타데이터를 표시할 때 THEN 시스템 SHALL 일관된 스타일링을 적용한다 + +### Requirement 2 + +**User Story:** As a 블로그 독자, I want 포스트 콘텐츠가 읽기 좋은 형태로 구성되기를, so that 편안하게 글을 읽을 수 있다 + +#### Acceptance Criteria + +1. WHEN 포스트 콘텐츠를 표시할 때 THEN 시스템 SHALL 적절한 여백과 줄 간격을 적용한다 +2. WHEN 코드 블록이 있으면 THEN 시스템 SHALL 구문 강조와 함께 표시한다 +3. WHEN 이미지가 있으면 THEN 시스템 SHALL 반응형으로 크기를 조정하여 표시한다 +4. WHEN 긴 포스트를 읽을 때 THEN 시스템 SHALL 목차(Table of Contents)를 제공한다 + +### Requirement 3 + +**User Story:** As a 블로그 독자, I want 포스트 하단에 관련 정보와 네비게이션을 볼 수 있기를, so that 더 많은 콘텐츠를 탐색할 수 있다 + +#### Acceptance Criteria + +1. WHEN 포스트를 다 읽었을 때 THEN 시스템 SHALL 이전/다음 포스트로의 네비게이션을 제공한다 +2. WHEN 포스트 하단에 도달하면 THEN 시스템 SHALL 관련 포스트 추천을 표시한다 +3. WHEN 포스트 하단에 도달하면 THEN 시스템 SHALL 소셜 공유 버튼을 제공한다 +4. WHEN 포스트 하단에 도달하면 THEN 시스템 SHALL 작성자 정보를 표시한다 + +### Requirement 4 + +**User Story:** As a 블로그 운영자, I want 포스트 페이지가 SEO에 최적화되기를, so that 검색 엔진에서 더 잘 노출될 수 있다 + +#### Acceptance Criteria + +1. WHEN 포스트 페이지가 로드되면 THEN 시스템 SHALL 적절한 메타 태그를 설정한다 +2. WHEN 포스트 페이지가 로드되면 THEN 시스템 SHALL Open Graph 태그를 설정한다 +3. WHEN 포스트 페이지가 로드되면 THEN 시스템 SHALL 구조화된 데이터(JSON-LD)를 포함한다 +4. WHEN 포스트 페이지가 로드되면 THEN 시스템 SHALL 적절한 제목 태그 계층구조를 유지한다 + +### Requirement 5 + +**User Story:** As a 모바일 사용자, I want 포스트 페이지가 모바일에서도 잘 보이기를, so that 어떤 기기에서든 편안하게 읽을 수 있다 + +#### Acceptance Criteria + +1. WHEN 모바일 기기에서 접근하면 THEN 시스템 SHALL 반응형 레이아웃을 적용한다 +2. WHEN 모바일에서 스크롤할 때 THEN 시스템 SHALL 적절한 터치 인터랙션을 제공한다 +3. WHEN 모바일에서 코드 블록을 볼 때 THEN 시스템 SHALL 가로 스크롤을 지원한다 +4. WHEN 모바일에서 이미지를 볼 때 THEN 시스템 SHALL 화면 크기에 맞게 조정한다 \ No newline at end of file diff --git a/.kiro/specs/post-page-layout/tasks.md b/.kiro/specs/post-page-layout/tasks.md new file mode 100644 index 0000000..a8c1b9a --- /dev/null +++ b/.kiro/specs/post-page-layout/tasks.md @@ -0,0 +1,90 @@ +# Implementation Plan + +- [x] 1. 기본 컴포넌트 구조 설정 및 타입 정의 + - PostHeader, PostContent, PostFooter 컴포넌트의 기본 인터페이스 생성 + - 확장된 Post 타입과 메타데이터 타입 정의 + - shadcn/ui Badge, Card 컴포넌트 설치 및 설정 + - _Requirements: 1.1, 1.4_ + +- [x] 2. PostHeader 컴포넌트 구현 + - 포스트 제목, 날짜, 태그를 표시하는 헤더 컴포넌트 작성 + - 날짜 포맷팅 유틸리티 함수 구현 + - 태그 표시를 위한 Badge 컴포넌트 스타일링 + - 반응형 레이아웃 적용 (모바일/데스크톱) + - _Requirements: 1.1, 1.2, 1.3, 1.4, 5.1_ + +- [ ] 3. 향상된 MDX 콘텐츠 스타일링 구현 + - prose 스타일링 커스터마이징으로 가독성 향상 + - 코드 블록 스타일링 개선 (이미 rehype-pretty-code 설정됨) + - 이미지 반응형 처리를 위한 스타일 추가 + - 모바일에서 코드 블록 가로 스크롤 지원 + - _Requirements: 2.1, 2.2, 2.3, 5.3, 5.4_ + +- [ ] 4. 목차(Table of Contents) 기능 구현 + - MDX 콘텐츠에서 헤딩 추출하는 유틸리티 함수 작성 + - TableOfContents 컴포넌트 구현 + - 스크롤 위치에 따른 활성 헤딩 하이라이트 기능 + - 데스크톱에서만 표시되는 반응형 처리 + - _Requirements: 2.4, 5.1_ + +- [ ] 5. 포스트 네비게이션 시스템 구현 + - 이전/다음 포스트 데이터를 가져오는 로직 작성 + - PostNavigation 컴포넌트 구현 + - 포스트 순서 기반 네비게이션 링크 생성 + - 호버 효과 및 접근성 고려한 스타일링 + - _Requirements: 3.1_ + +- [ ] 6. 관련 포스트 추천 시스템 구현 + - 태그 기반 관련 포스트 찾기 알고리즘 작성 + - RelatedPosts 컴포넌트 구현 + - 카드 형태의 포스트 미리보기 레이아웃 + - 최대 4개 관련 포스트 표시 로직 + - _Requirements: 3.2_ + +- [ ] 7. 소셜 공유 기능 구현 + - SocialShare 컴포넌트 작성 + - Twitter, Facebook, LinkedIn 공유 링크 생성 + - 클립보드 복사 기능 추가 + - 공유 버튼 아이콘 및 스타일링 + - _Requirements: 3.3_ + +- [ ] 8. 작성자 정보 섹션 구현 + - AuthorInfo 컴포넌트 작성 + - 작성자 프로필 정보 표시 + - 소셜 링크 및 연락처 정보 포함 + - 카드 형태의 깔끔한 레이아웃 + - _Requirements: 3.4_ + +- [ ] 9. SEO 메타데이터 최적화 구현 + - generateMetadata 함수 작성하여 동적 메타데이터 생성 + - Open Graph 태그 설정 + - Twitter Card 메타데이터 추가 + - 포스트별 맞춤 title, description 생성 + - _Requirements: 4.1, 4.2_ + +- [ ] 10. 구조화된 데이터(JSON-LD) 구현 + - BlogPosting 스키마 JSON-LD 생성 함수 작성 + - 포스트 메타데이터를 구조화된 데이터로 변환 + - 검색 엔진 최적화를 위한 스키마 마크업 추가 + - _Requirements: 4.3_ + +- [ ] 11. 포스트 페이지 레이아웃 통합 및 리팩토링 + - 기존 PostPage 컴포넌트를 새로운 레이아웃으로 교체 + - 모든 하위 컴포넌트들을 통합하여 완전한 포스트 페이지 구성 + - 에러 처리 및 로딩 상태 개선 + - 성능 최적화 (이미지 최적화, 코드 분할) + - _Requirements: 4.4, 5.2_ + +- [ ] 12. 반응형 디자인 및 접근성 최종 검증 + - 모바일, 태블릿, 데스크톱에서 레이아웃 테스트 + - 키보드 네비게이션 지원 확인 + - 색상 대비 및 WCAG 가이드라인 준수 검증 + - 스크린 리더 호환성 테스트 + - _Requirements: 5.1, 5.2, 5.3, 5.4_ + +- [ ] 13. 단위 테스트 작성 + - 유틸리티 함수들(날짜 포맷팅, 헤딩 추출 등) 테스트 + - 주요 컴포넌트들의 렌더링 테스트 + - 관련 포스트 알고리즘 테스트 + - SEO 메타데이터 생성 테스트 + - _Requirements: 전체 요구사항 검증_ \ No newline at end of file diff --git a/.kiro/steering/development-standards.md b/.kiro/steering/development-standards.md new file mode 100644 index 0000000..41a810e --- /dev/null +++ b/.kiro/steering/development-standards.md @@ -0,0 +1,171 @@ +# 개발 표준 및 베스트 프랙티스 + +## 코드 작성 원칙 + +### TypeScript 사용 +- 모든 파일에서 엄격한 타입 체크 적용 +- `any` 타입 사용 금지, 적절한 타입 정의 필수 +- 인터페이스와 타입 별칭 적절히 활용 +- 제네릭 타입 활용으로 재사용성 향상 + +### 컴포넌트 설계 +- **단일 책임 원칙**: 하나의 컴포넌트는 하나의 역할만 +- **Props 인터페이스**: 모든 props에 대한 명시적 타입 정의 +- **기본값 설정**: 선택적 props에 대한 적절한 기본값 +- **컴포넌트 분리**: 50줄 이상의 컴포넌트는 분리 고려 + +```typescript +// 좋은 예 +interface ButtonProps { + variant?: 'primary' | 'secondary' + size?: 'sm' | 'md' | 'lg' + disabled?: boolean + onClick?: () => void + children: React.ReactNode +} + +export function Button({ + variant = 'primary', + size = 'md', + disabled = false, + onClick, + children +}: ButtonProps) { + // 구현 +} +``` + +### 상태 관리 +- **로컬 상태**: `useState` 사용, 필요시에만 상태 끌어올리기 +- **서버 상태**: Next.js의 정적 생성 활용 +- **전역 상태**: 복잡한 상태는 Context API 또는 상태 관리 라이브러리 고려 + +## 파일 구조 및 네이밍 + +### 파일 네이밍 +- **컴포넌트**: PascalCase (`Header.tsx`, `PostCard.tsx`) +- **유틸리티**: camelCase (`formatDate.ts`, `cn.ts`) +- **페이지**: Next.js 컨벤션 따름 (`page.tsx`, `layout.tsx`) +- **타입**: PascalCase with suffix (`PostType.ts`, `UserInterface.ts`) + +### Import 순서 (Biome 설정 준수) +1. Node.js 내장 모듈 +2. 외부 패키지 +3. 내부 모듈 (`@/` 경로) +4. 상대 경로 (`../`, `./`) + +```typescript +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' + +import matter from 'gray-matter' +import { clsx } from 'clsx' + +import { cn } from '@/app/_lib/cn' + +import { parsePostData } from './logic' +``` + +## 스타일링 표준 + +### Tailwind CSS 사용법 +- **유틸리티 클래스**: 인라인 스타일 대신 Tailwind 클래스 사용 +- **컴포넌트 추출**: 반복되는 스타일은 컴포넌트로 추출 +- **반응형**: 모바일 퍼스트, `sm:`, `md:`, `lg:` 브레이크포인트 활용 +- **다크모드**: `dark:` 접두사로 다크모드 스타일 정의 + +### 색상 팔레트 +- **주 색상**: `stone` 계열 (`stone-50` ~ `stone-950`) +- **강조 색상**: 필요시 `blue`, `green`, `red` 등 사용 +- **일관성**: 전체 애플리케이션에서 동일한 색상 체계 유지 + +```typescript +// 좋은 예 +
+``` + +## 성능 최적화 + +### Next.js 최적화 +- **이미지**: `next/image` 컴포넌트 사용 +- **폰트**: `next/font` 사용으로 폰트 최적화 +- **동적 import**: 필요시에만 컴포넌트 로드 +- **메타데이터**: 적절한 SEO 메타데이터 설정 + +### 번들 크기 최적화 +- **Tree shaking**: 사용하지 않는 코드 제거 +- **코드 분할**: 페이지별 코드 분할 +- **외부 라이브러리**: 필요한 기능만 import + +## 테스트 전략 + +### 단위 테스트 +- **비즈니스 로직**: `src/entities/` 내 로직 함수들 +- **유틸리티 함수**: `src/app/_lib/` 내 헬퍼 함수들 +- **테스트 파일**: `*.test.ts` 또는 `*.spec.ts` 확장자 + +### 테스트 작성 원칙 +- **AAA 패턴**: Arrange, Act, Assert +- **의미있는 테스트명**: 테스트 의도가 명확히 드러나는 이름 +- **독립성**: 각 테스트는 독립적으로 실행 가능 + +```typescript +// 좋은 예 +describe('formatDate', () => { + it('should format ISO date string to Korean format', () => { + // Arrange + const isoDate = '2025-01-15' + + // Act + const result = formatDate(isoDate) + + // Assert + expect(result).toBe('2025년 1월 15일') + }) +}) +``` + +## 에러 처리 + +### 클라이언트 사이드 +- **에러 바운더리**: React 에러 바운더리 활용 +- **사용자 친화적 메시지**: 기술적 에러를 사용자가 이해할 수 있는 메시지로 변환 +- **로깅**: 개발 환경에서 적절한 에러 로깅 + +### 서버 사이드 +- **404 처리**: `notFound()` 함수 활용 +- **에러 페이지**: 커스텀 에러 페이지 제공 +- **Graceful degradation**: 일부 기능 실패 시에도 기본 기능은 동작 + +## 접근성 (a11y) + +### 기본 원칙 +- **시맨틱 HTML**: 적절한 HTML 태그 사용 +- **ARIA 레이블**: 스크린 리더를 위한 적절한 레이블 +- **키보드 네비게이션**: 모든 인터랙티브 요소 키보드 접근 가능 +- **색상 대비**: WCAG 가이드라인 준수 + +### 구현 예시 +```typescript +// 좋은 예 + +``` + +## 보안 고려사항 + +### 콘텐츠 보안 +- **XSS 방지**: 사용자 입력 적절히 이스케이프 +- **MDX 보안**: 신뢰할 수 있는 MDX 콘텐츠만 렌더링 +- **이미지 최적화**: 외부 이미지 소스 검증 + +### 정적 사이트 보안 +- **환경 변수**: 민감한 정보는 빌드 타임에만 사용 +- **의존성 관리**: 정기적인 보안 업데이트 +- **CSP 헤더**: 적절한 Content Security Policy 설정 \ No newline at end of file diff --git a/.kiro/steering/project-structure.md b/.kiro/steering/project-structure.md new file mode 100644 index 0000000..fcbe195 --- /dev/null +++ b/.kiro/steering/project-structure.md @@ -0,0 +1,142 @@ +# 프로젝트 구조 및 개발 가이드라인 + +## 프로젝트 개요 + +Next.js 15 기반의 정적 블로그 애플리케이션으로, MDX를 사용한 콘텐츠 관리와 도메인 주도 설계를 따릅니다. + +## 기술 스택 + +- **프레임워크**: Next.js 15 (App Router) +- **콘텐츠**: MDX with rehype-pretty-code +- **스타일링**: Tailwind CSS + shadcn/ui +- **언어**: TypeScript +- **코드 품질**: Biome (formatting/linting) +- **테스팅**: Vitest +- **패키지 매니저**: pnpm + +## 디렉토리 구조 + +``` +src/ +├── app/ # Next.js App Router 루트 +│ ├── _components/ # 전역 컴포넌트 +│ ├── _fonts/ # 폰트 설정 +│ ├── _lib/ # 유틸리티 함수 +│ ├── posts/[slug]/ # 동적 포스트 페이지 +│ ├── about/ # About 페이지 +│ ├── layout.tsx # 루트 레이아웃 +│ └── globals.css # 전역 스타일 +├── entities/ # 도메인 엔티티 +│ ├── posts/ # 포스트 도메인 로직 +│ └── tags/ # 태그 도메인 로직 +└── contents/ # MDX 블로그 포스트 +``` + +## 네이밍 컨벤션 + +- **라우트가 아닌 폴더**: 언더스코어 접두사 사용 (`_components`, `_hooks`) +- **컴포넌트 스코프**: + - 전역: `src/app/_components/` + - 페이지별: `src/app/[route]/_components/` +- **파일명**: kebab-case 또는 PascalCase (컴포넌트) + +## 도메인 아키텍처 + +### 엔티티 구조 +- `src/entities/posts/`: 포스트 관련 비즈니스 로직 + - `index.ts`: 공개 API + - `types.ts`: 타입 정의 + - `logic.ts`: 비즈니스 로직 + - `*.test.ts`: 테스트 파일 + +### 데이터 흐름 +1. MDX 파일 (`src/contents/`) → gray-matter 파싱 +2. 엔티티 레이어에서 비즈니스 로직 처리 +3. App Router 페이지에서 렌더링 + +## 스타일링 가이드라인 + +### Tailwind CSS +- **컬러 팔레트**: `stone` 계열 사용 (`text-stone-900`, `border-stone-200`) +- **반응형**: 모바일 퍼스트 접근 +- **다크모드**: CSS 변수 기반 테마 지원 + +### shadcn/ui 컴포넌트 +- **설치**: `npx shadcn@latest add ` +- **위치**: `src/app/_components/ui/` +- **스타일**: "new-york" 스타일, stone 베이스 컬러 +- **아이콘**: Lucide React 사용 + +## 개발 워크플로우 + +### 브랜치 전략 +- `main`: 프로덕션 준비 코드 +- `feat/`: 새 기능 (`feat/search-functionality`) +- `fix/`: 버그 수정 (`fix/mobile-nav-issue`) +- `docs/`: 문서 업데이트 + +### 커밋 컨벤션 +``` +: + +[optional body] +``` + +#### 커밋 전략 + +논리적 단위로 나누어서 커밋 + +**타입:** +- `feat:` - 새 기능 +- `fix:` - 버그 수정 +- `docs:` - 문서 업데이트 +- `config:` - 설정 변경 +- `refactor:` - 리팩토링 +- `chore:` - 유지보수 + +### 코드 품질 +- **Biome**: `pnpm biome:fix` 실행 후 커밋 +- **테스트**: `pnpm test` 실행 +- **빌드 확인**: 필요시에만 `pnpm build` 실행 + +## 콘텐츠 관리 + +### MDX 구조 +```yaml +--- +title: '포스트 제목' +slug: 'post-slug' +date: 2025-01-01 +tags: ['tag1', 'tag2'] +--- + +# 포스트 내용 +``` + +### 정적 생성 +- `generateStaticParams()`로 빌드 타임 페이지 생성 +- `output: 'export'` 설정으로 정적 파일 생성 + +## 개발 시 주의사항 + +### UI 구현 +- **플레이스홀더 사용**: 실제 콘텐츠 대신 `[Page Title]`, `[Description]` 등 사용 +- **사용자 승인**: UI 구조 구현 전 명시적 요구사항 확인 +- **점진적 구현**: 한 번에 모든 기능 구현하지 않기 + +### 성능 최적화 +- 이미지 최적화: Next.js Image 컴포넌트 사용 +- 코드 분할: 동적 import 활용 +- 정적 생성: 가능한 모든 페이지 사전 생성 + +### 접근성 +- 시맨틱 HTML 사용 +- ARIA 레이블 적절히 활용 +- 키보드 네비게이션 지원 +- 색상 대비 준수 + +## 배포 설정 + +- **basePath**: `/blog` (GitHub Pages 등을 위한 설정) +- **출력**: 정적 파일 (`out/` 디렉토리) +- **환경**: Node.js 환경에서 빌드, 정적 호스팅 가능 \ No newline at end of file diff --git a/.kiro/steering/ui-design-system.md b/.kiro/steering/ui-design-system.md new file mode 100644 index 0000000..c991333 --- /dev/null +++ b/.kiro/steering/ui-design-system.md @@ -0,0 +1,280 @@ +# UI 디자인 시스템 + +## 디자인 원칙 + +### 일관성 (Consistency) +- 전체 애플리케이션에서 동일한 컴포넌트와 패턴 사용 +- 색상, 타이포그래피, 간격의 일관된 적용 +- 인터랙션 패턴의 예측 가능성 + +### 단순성 (Simplicity) +- 불필요한 시각적 요소 제거 +- 명확하고 직관적인 인터페이스 +- 콘텐츠 중심의 디자인 + +### 접근성 (Accessibility) +- 모든 사용자가 접근 가능한 디자인 +- 충분한 색상 대비와 터치 타겟 크기 +- 키보드 네비게이션 지원 + +## 색상 시스템 + +### 주 색상 팔레트 (Stone) +```css +/* Light Mode */ +--stone-50: #fafaf9 +--stone-100: #f5f5f4 +--stone-200: #e7e5e4 +--stone-300: #d6d3d1 +--stone-400: #a8a29e +--stone-500: #78716c +--stone-600: #57534e +--stone-700: #44403c +--stone-800: #292524 +--stone-900: #1c1917 +--stone-950: #0c0a09 + +/* Dark Mode */ +--stone-50: #0c0a09 +--stone-100: #1c1917 +--stone-200: #292524 +--stone-300: #44403c +--stone-400: #57534e +--stone-500: #78716c +--stone-600: #a8a29e +--stone-700: #d6d3d1 +--stone-800: #e7e5e4 +--stone-900: #f5f5f4 +--stone-950: #fafaf9 +``` + +### 사용 가이드라인 +- **배경**: `stone-50` (light) / `stone-950` (dark) +- **카드/컨테이너**: `stone-100` (light) / `stone-900` (dark) +- **테두리**: `stone-200` (light) / `stone-800` (dark) +- **텍스트 주색**: `stone-900` (light) / `stone-100` (dark) +- **텍스트 보조색**: `stone-600` (light) / `stone-400` (dark) + +## 타이포그래피 + +### 폰트 패밀리 +- **주 폰트**: Noto Sans KR (한글 지원) +- **코드 폰트**: 시스템 monospace 폰트 + +### 텍스트 스케일 +```css +/* Tailwind Typography Scale */ +text-xs: 12px / 16px +text-sm: 14px / 20px +text-base: 16px / 24px +text-lg: 18px / 28px +text-xl: 20px / 28px +text-2xl: 24px / 32px +text-3xl: 30px / 36px +text-4xl: 36px / 40px +``` + +### 사용 예시 +- **페이지 제목**: `text-3xl font-bold text-stone-900` +- **섹션 제목**: `text-2xl font-semibold text-stone-800` +- **본문**: `text-base text-stone-700` +- **캡션**: `text-sm text-stone-500` + +## 간격 시스템 + +### Tailwind Spacing Scale +```css +/* 주요 간격 값 */ +1: 4px +2: 8px +3: 12px +4: 16px +5: 20px +6: 24px +8: 32px +10: 40px +12: 48px +16: 64px +20: 80px +24: 96px +``` + +### 사용 가이드라인 +- **컴포넌트 내부 패딩**: `p-4` ~ `p-6` +- **컴포넌트 간 마진**: `mb-6` ~ `mb-8` +- **섹션 간 간격**: `mb-12` ~ `mb-16` +- **페이지 패딩**: `px-5` (모바일), `px-8` (데스크톱) + +## 컴포넌트 라이브러리 + +### shadcn/ui 컴포넌트 사용 +- **설치**: `npx shadcn@latest add ` +- **커스터마이징**: 필요시 컴포넌트 코드 직접 수정 +- **일관성**: 기본 스타일 유지하되 프로젝트에 맞게 조정 + +### 주요 컴포넌트 +```typescript +// Button 컴포넌트 예시 + + + + +// Card 컴포넌트 예시 + + + 카드 제목 + + + 카드 내용 + + +``` + +## 레이아웃 패턴 + +### 컨테이너 +```typescript +// 기본 컨테이너 +
+ {/* 콘텐츠 */} +
+ +// 최대 너비 제한 +
+ {/* 콘텐츠 */} +
+``` + +### 그리드 시스템 +```typescript +// 반응형 그리드 +
+ {/* 그리드 아이템들 */} +
+ +// 플렉스 레이아웃 +
+ {/* 플렉스 아이템들 */} +
+``` + +## 반응형 디자인 + +### 브레이크포인트 +```css +sm: 640px /* 태블릿 */ +md: 768px /* 작은 데스크톱 */ +lg: 1024px /* 데스크톱 */ +xl: 1280px /* 큰 데스크톱 */ +2xl: 1536px /* 매우 큰 화면 */ +``` + +### 모바일 퍼스트 접근 +```typescript +// 모바일 기본, 태블릿 이상에서 변경 +
+ 반응형 텍스트 +
+ +// 모바일에서 숨김, 데스크톱에서 표시 +
+ 데스크톱 전용 콘텐츠 +
+``` + +## 상태 및 인터랙션 + +### 호버 상태 +```typescript + +``` + +### 포커스 상태 +```typescript + +``` + +### 로딩 상태 +```typescript + +``` + +## 다크 모드 + +### 구현 방식 +- CSS 변수 기반 테마 시스템 +- `dark:` 접두사로 다크 모드 스타일 정의 +- 시스템 설정 자동 감지 + +### 사용 예시 +```typescript +
+ 다크 모드 지원 콘텐츠 +
+``` + +## 애니메이션 + +### 기본 트랜지션 +```typescript +// 색상 변화 +
+ +// 크기 변화 +
+ +// 투명도 변화 +
+``` + +### 페이지 전환 +- 부드러운 페이지 전환 효과 +- 로딩 상태 표시 +- 스켈레톤 UI 활용 + +## 아이콘 시스템 + +### Lucide React 사용 +```typescript +import { Search, Menu, X, ChevronRight } from 'lucide-react' + +// 일관된 크기 사용 + + +``` + +### 아이콘 가이드라인 +- **크기**: `w-4 h-4` (16px), `w-5 h-5` (20px), `w-6 h-6` (24px) +- **색상**: 텍스트 색상과 동일하게 유지 +- **의미**: 직관적이고 일반적으로 인식되는 아이콘 사용 + +## 에러 및 피드백 + +### 에러 메시지 +```typescript +
+ 에러 메시지 +
+``` + +### 성공 메시지 +```typescript +
+ 성공 메시지 +
+``` + +### 정보 메시지 +```typescript +
+ 정보 메시지 +
+``` \ No newline at end of file From 0c5bfad86581c4e23e2e775731c3714641d837fb Mon Sep 17 00:00:00 2001 From: Jongchan Date: Wed, 16 Jul 2025 18:14:55 +0900 Subject: [PATCH 08/12] feat: enhance MDX content styling with improved prose and code blocks - Add comprehensive prose styling customization for better readability - Implement enhanced code block styling with mobile scroll support - Add responsive image handling with proper shadows and alignment - Improve mobile experience with touch-friendly scrollbars - Add full dark mode support for all content elements - Update PostContent component with detailed typography classes - Add custom CSS for code blocks, tables, blockquotes, and images - Integrate PostContent component in post page layout Requirements: 2.1, 2.2, 2.3, 5.3, 5.4 --- .kiro/specs/post-page-layout/tasks.md | 2 +- .kiro/steering/project-structure.md | 3 + src/app/globals.css | 211 ++++++++++++++++++++++ src/app/posts/[slug]/page.tsx | 7 +- src/app/posts/_components/PostContent.tsx | 59 ++++++ 5 files changed, 279 insertions(+), 3 deletions(-) diff --git a/.kiro/specs/post-page-layout/tasks.md b/.kiro/specs/post-page-layout/tasks.md index a8c1b9a..23c3b31 100644 --- a/.kiro/specs/post-page-layout/tasks.md +++ b/.kiro/specs/post-page-layout/tasks.md @@ -13,7 +13,7 @@ - 반응형 레이아웃 적용 (모바일/데스크톱) - _Requirements: 1.1, 1.2, 1.3, 1.4, 5.1_ -- [ ] 3. 향상된 MDX 콘텐츠 스타일링 구현 +- [x] 3. 향상된 MDX 콘텐츠 스타일링 구현 - prose 스타일링 커스터마이징으로 가독성 향상 - 코드 블록 스타일링 개선 (이미 rehype-pretty-code 설정됨) - 이미지 반응형 처리를 위한 스타일 추가 diff --git a/.kiro/steering/project-structure.md b/.kiro/steering/project-structure.md index fcbe195..12b04e2 100644 --- a/.kiro/steering/project-structure.md +++ b/.kiro/steering/project-structure.md @@ -101,6 +101,9 @@ src/ ## 콘텐츠 관리 +### 콘텐츠 위치 +`src/contents/*.mdx` + ### MDX 구조 ```yaml --- diff --git a/src/app/globals.css b/src/app/globals.css index e9f7055..f3556ba 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -119,3 +119,214 @@ @apply bg-background text-foreground; } } + +@layer components { + /* Enhanced code block styling for better mobile experience */ + .prose pre { + /* Ensure proper scrolling on mobile */ + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + scrollbar-color: theme(colors.stone.400) theme(colors.stone.100); + } + + .dark .prose pre { + scrollbar-color: theme(colors.stone.600) theme(colors.stone.800); + } + + /* Custom scrollbar for webkit browsers */ + .prose pre::-webkit-scrollbar { + height: 8px; + width: 8px; + } + + .prose pre::-webkit-scrollbar-track { + background: theme(colors.stone.100); + border-radius: 4px; + } + + .prose pre::-webkit-scrollbar-thumb { + background: theme(colors.stone.400); + border-radius: 4px; + } + + .prose pre::-webkit-scrollbar-thumb:hover { + background: theme(colors.stone.500); + } + + .dark .prose pre::-webkit-scrollbar-track { + background: theme(colors.stone.800); + } + + .dark .prose pre::-webkit-scrollbar-thumb { + background: theme(colors.stone.600); + } + + .dark .prose pre::-webkit-scrollbar-thumb:hover { + background: theme(colors.stone.500); + } + + /* Enhanced code block styling with rehype-pretty-code */ + .prose pre[data-theme] { + background-color: theme(colors.stone.900) !important; + border: 1px solid theme(colors.stone.200); + border-radius: 0.5rem; + padding: 1.5rem; + margin: 2rem 0; + overflow-x: auto; + font-size: 0.875rem; + line-height: 1.5; + } + + .dark .prose pre[data-theme] { + background-color: theme(colors.stone.950) !important; + border-color: theme(colors.stone.700); + } + + /* Code block title styling */ + .prose pre[data-title]::before { + content: attr(data-title); + display: block; + background: theme(colors.stone.800); + color: theme(colors.stone.200); + padding: 0.5rem 1rem; + margin: -1.5rem -1.5rem 1rem -1.5rem; + font-size: 0.75rem; + font-weight: 500; + border-bottom: 1px solid theme(colors.stone.700); + } + + .dark .prose pre[data-title]::before { + background: theme(colors.stone.900); + border-bottom-color: theme(colors.stone.600); + } + + /* Line highlighting */ + .prose pre [data-highlighted-line] { + background: rgba(255, 255, 255, 0.1); + border-left: 3px solid theme(colors.blue.400); + padding-left: 1rem; + margin-left: -1.5rem; + margin-right: -1.5rem; + padding-right: 1.5rem; + } + + /* Line numbers */ + .prose pre [data-line-numbers] { + counter-reset: line; + } + + .prose pre [data-line-numbers] > [data-line]::before { + counter-increment: line; + content: counter(line); + display: inline-block; + width: 2rem; + margin-right: 1rem; + text-align: right; + color: theme(colors.stone.500); + font-size: 0.75rem; + } + + /* Enhanced image styling for better responsive behavior */ + .prose img { + border-radius: 0.5rem; + box-shadow: + 0 10px 25px -5px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); + margin: 2rem auto; + max-width: 100%; + height: auto; + display: block; + } + + .dark .prose img { + box-shadow: + 0 10px 25px -5px rgba(0, 0, 0, 0.3), + 0 4px 6px -2px rgba(0, 0, 0, 0.2); + } + + /* Enhanced blockquote styling */ + .prose blockquote { + position: relative; + background: theme(colors.stone.50); + border-left: 4px solid theme(colors.stone.300); + padding: 1.5rem; + margin: 2rem 0; + font-style: italic; + font-weight: 500; + color: theme(colors.stone.700); + } + + .dark .prose blockquote { + background: rgba(120, 113, 108, 0.1); + border-left-color: theme(colors.stone.600); + color: theme(colors.stone.300); + } + + .prose blockquote::before { + content: '"'; + position: absolute; + top: 0.5rem; + left: 1rem; + font-size: 2rem; + color: theme(colors.stone.400); + font-family: serif; + } + + .dark .prose blockquote::before { + color: theme(colors.stone.600); + } + + /* Enhanced table styling */ + .prose table { + border-collapse: collapse; + margin: 2rem 0; + width: 100%; + overflow-x: auto; + display: block; + white-space: nowrap; + } + + @media (min-width: 768px) { + .prose table { + display: table; + white-space: normal; + } + } + + .prose thead { + background: theme(colors.stone.50); + } + + .dark .prose thead { + background: theme(colors.stone.800); + } + + .prose th, + .prose td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid theme(colors.stone.200); + } + + .dark .prose th, + .dark .prose td { + border-bottom-color: theme(colors.stone.700); + } + + .prose th { + font-weight: 600; + color: theme(colors.stone.900); + } + + .dark .prose th { + color: theme(colors.stone.100); + } + + .prose td { + color: theme(colors.stone.700); + } + + .dark .prose td { + color: theme(colors.stone.300); + } +} diff --git a/src/app/posts/[slug]/page.tsx b/src/app/posts/[slug]/page.tsx index 96c4d0a..3d6d0f1 100644 --- a/src/app/posts/[slug]/page.tsx +++ b/src/app/posts/[slug]/page.tsx @@ -1,5 +1,6 @@ import { notFound } from 'next/navigation' +import { PostContent } from '@/app/posts/_components' import { getAllPosts } from '@/entities/posts' export async function generateStaticParams() { @@ -27,8 +28,10 @@ export default async function PostPage({ ) return ( -
- +
+ + +
) } catch (_error) { diff --git a/src/app/posts/_components/PostContent.tsx b/src/app/posts/_components/PostContent.tsx index 2cc7d83..b256d56 100644 --- a/src/app/posts/_components/PostContent.tsx +++ b/src/app/posts/_components/PostContent.tsx @@ -24,7 +24,66 @@ export function PostContent({ )}
From 69bad45813c4c26cdb80c3ce9aaee838fc8db3c0 Mon Sep 17 00:00:00 2001 From: Jongchan Date: Wed, 16 Jul 2025 19:09:23 +0900 Subject: [PATCH 09/12] =?UTF-8?q?kiro:=20spec,=20steering=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kiro/specs/post-page-layout/design.md | 400 ------------------- .kiro/specs/post-page-layout/requirements.md | 62 --- .kiro/specs/post-page-layout/tasks.md | 90 ----- .kiro/steering/development-standards.md | 171 -------- .kiro/steering/project-structure.md | 54 --- .kiro/steering/task-execution-process.md | 170 ++++++++ 6 files changed, 170 insertions(+), 777 deletions(-) delete mode 100644 .kiro/specs/post-page-layout/design.md delete mode 100644 .kiro/specs/post-page-layout/requirements.md delete mode 100644 .kiro/specs/post-page-layout/tasks.md delete mode 100644 .kiro/steering/development-standards.md create mode 100644 .kiro/steering/task-execution-process.md diff --git a/.kiro/specs/post-page-layout/design.md b/.kiro/specs/post-page-layout/design.md deleted file mode 100644 index dbd4735..0000000 --- a/.kiro/specs/post-page-layout/design.md +++ /dev/null @@ -1,400 +0,0 @@ -# Design Document - -## Overview - -포스트 페이지 레이아웃 개선은 현재의 기본적인 MDX 렌더링에서 체계적이고 사용자 친화적인 블로그 포스트 경험으로 발전시키는 것을 목표로 합니다. 이 설계는 Next.js 15 App Router, MDX, Tailwind CSS, shadcn/ui를 활용하여 성능과 사용자 경험을 모두 고려한 솔루션을 제공합니다. - -## Architecture - -### 컴포넌트 계층 구조 - -``` -PostPage (src/app/posts/[slug]/page.tsx) -├── PostHeader -│ ├── PostTitle -│ ├── PostMeta (date, tags) -│ └── PostActions (share, bookmark) -├── PostContent -│ ├── TableOfContents (optional) -│ └── MDXContent (prose styling) -├── PostFooter -│ ├── PostNavigation (prev/next) -│ ├── RelatedPosts -│ ├── AuthorInfo -│ └── SocialShare -└── PostSEO (metadata, JSON-LD) -``` - -### 데이터 흐름 - -1. **Static Generation**: `generateStaticParams()`로 모든 포스트 경로 생성 -2. **Post Data Fetching**: 기존 `src/entities/posts` 활용하여 포스트 데이터 가져오기 -3. **MDX Rendering**: 동적 import로 MDX 컴포넌트 로드 -4. **Metadata Generation**: `generateMetadata()`로 SEO 메타데이터 생성 - -## Components and Interfaces - -### PostHeader 컴포넌트 - -```typescript -interface PostHeaderProps { - title: string - date: string - tags: string[] - readingTime?: number -} - -export function PostHeader({ title, date, tags, readingTime }: PostHeaderProps) { - return ( -
-

- {title} -

-
- - {readingTime && ( - 약 {readingTime}분 읽기 - )} -
- {tags.map(tag => ( - - {tag} - - ))} -
-
-
- ) -} -``` - -### PostContent 컴포넌트 - -```typescript -interface PostContentProps { - children: React.ReactNode - showTOC?: boolean - headings?: Heading[] -} - -export function PostContent({ children, showTOC, headings }: PostContentProps) { - return ( -
- {showTOC && headings && ( - - )} -
- {children} -
-
- ) -} -``` - -### PostNavigation 컴포넌트 - -```typescript -interface PostNavigationProps { - previousPost?: { - slug: string - title: string - } - nextPost?: { - slug: string - title: string - } -} - -export function PostNavigation({ previousPost, nextPost }: PostNavigationProps) { - return ( - - ) -} -``` - -### RelatedPosts 컴포넌트 - -```typescript -interface RelatedPostsProps { - posts: Array<{ - slug: string - title: string - date: string - tags: string[] - }> -} - -export function RelatedPosts({ posts }: RelatedPostsProps) { - if (posts.length === 0) return null - - return ( -
-

- 관련 글 -

-
- {posts.map(post => ( - - - -

- {post.title} -

- - -
-
- ))} -
-
- ) -} -``` - -## Data Models - -### Post 타입 확장 - -```typescript -// src/entities/posts/types.ts 확장 -export type PostFrontMatter = { - title: string - date: string - tags: Array - description?: string - author?: string - readingTime?: number - featured?: boolean -} - -export type PostWithNavigation = Post & { - previousPost?: Pick - nextPost?: Pick - relatedPosts?: Array> -} - -export type Heading = { - id: string - text: string - level: number -} -``` - -### 메타데이터 타입 - -```typescript -export type PostMetadata = { - title: string - description: string - openGraph: { - title: string - description: string - type: 'article' - publishedTime: string - authors: string[] - tags: string[] - } - twitter: { - card: 'summary_large_image' - title: string - description: string - } -} -``` - -## Error Handling - -### 에러 시나리오 및 처리 - -1. **포스트 파일 없음** - - `notFound()` 함수 호출하여 404 페이지 표시 - - 사용자에게 명확한 에러 메시지 제공 - -2. **MDX 파싱 에러** - - try-catch로 에러 캐치 - - 개발 환경에서는 상세 에러 로그 - - 프로덕션에서는 일반적인 에러 메시지 - -3. **관련 포스트 로딩 실패** - - 관련 포스트 섹션 숨김 - - 메인 콘텐츠는 정상 표시 - -```typescript -// 에러 처리 예시 -export default async function PostPage({ params }: { params: { slug: string } }) { - try { - const post = await getPostBySlug(params.slug) - if (!post) { - notFound() - } - - const { default: MDXContent } = await import(`@/contents/${post.slug}.mdx`) - - return ( - - - - ) - } catch (error) { - console.error('Post loading error:', error) - notFound() - } -} -``` - -## Testing Strategy - -### 단위 테스트 - -1. **유틸리티 함수 테스트** - - `formatDate()` 함수 - - `calculateReadingTime()` 함수 - - `extractHeadings()` 함수 - -2. **컴포넌트 테스트** - - PostHeader 렌더링 테스트 - - PostNavigation 링크 테스트 - - RelatedPosts 표시 테스트 - -### 통합 테스트 - -1. **페이지 렌더링 테스트** - - 포스트 페이지 전체 렌더링 - - 메타데이터 생성 확인 - - SEO 태그 검증 - -2. **반응형 테스트** - - 모바일/데스크톱 레이아웃 확인 - - 터치 인터랙션 테스트 - -### 성능 테스트 - -1. **로딩 성능** - - 페이지 로드 시간 측정 - - 이미지 최적화 확인 - - 코드 분할 효과 검증 - -2. **SEO 검증** - - Lighthouse SEO 점수 - - 구조화된 데이터 검증 - - 메타 태그 완성도 확인 - -## Performance Considerations - -### 최적화 전략 - -1. **정적 생성 최적화** - - 모든 포스트 페이지 빌드 타임 생성 - - 메타데이터 사전 생성 - - 관련 포스트 계산 최적화 - -2. **이미지 최적화** - - Next.js Image 컴포넌트 사용 - - 적절한 크기와 포맷 제공 - - 지연 로딩 적용 - -3. **코드 분할** - - MDX 컴포넌트 동적 로딩 - - 필요한 컴포넌트만 로드 - - 번들 크기 최소화 - -### 접근성 고려사항 - -1. **시맨틱 HTML** - - 적절한 heading 계층구조 - - article, section, nav 태그 활용 - - 스크린 리더 친화적 구조 - -2. **키보드 네비게이션** - - 모든 인터랙티브 요소 접근 가능 - - 포커스 표시 명확히 - - 논리적인 탭 순서 - -3. **색상 대비** - - WCAG 가이드라인 준수 - - 다크 모드 지원 - - 색상에 의존하지 않는 정보 전달 - -## SEO Optimization - -### 메타데이터 최적화 - -```typescript -export async function generateMetadata({ params }: { params: { slug: string } }): Promise { - const post = await getPostBySlug(params.slug) - - if (!post) { - return { - title: 'Post Not Found' - } - } - - return { - title: post.data.title, - description: post.data.description || extractExcerpt(post.content), - openGraph: { - title: post.data.title, - description: post.data.description || extractExcerpt(post.content), - type: 'article', - publishedTime: post.data.date, - authors: [post.data.author || 'Blog Author'], - tags: post.data.tags, - }, - twitter: { - card: 'summary_large_image', - title: post.data.title, - description: post.data.description || extractExcerpt(post.content), - } - } -} -``` - -### 구조화된 데이터 - -```typescript -function generateJSONLD(post: Post) { - return { - '@context': 'https://schema.org', - '@type': 'BlogPosting', - headline: post.data.title, - description: post.data.description, - author: { - '@type': 'Person', - name: post.data.author || 'Blog Author' - }, - datePublished: post.data.date, - keywords: post.data.tags.join(', '), - mainEntityOfPage: { - '@type': 'WebPage', - '@id': `https://yourdomain.com/posts/${post.slug}` - } - } -} -``` \ No newline at end of file diff --git a/.kiro/specs/post-page-layout/requirements.md b/.kiro/specs/post-page-layout/requirements.md deleted file mode 100644 index 5d86d0d..0000000 --- a/.kiro/specs/post-page-layout/requirements.md +++ /dev/null @@ -1,62 +0,0 @@ -# Requirements Document - -## Introduction - -현재 블로그의 포스트 페이지는 매우 기본적인 구조로 되어 있어, 사용자 경험과 콘텐츠 가독성을 개선할 필요가 있습니다. 이 기능은 포스트 페이지의 레이아웃을 체계적으로 구성하여 더 나은 읽기 경험을 제공하고, SEO 최적화 및 사용자 참여도를 높이는 것을 목표로 합니다. - -## Requirements - -### Requirement 1 - -**User Story:** As a 블로그 독자, I want 포스트 메타데이터(제목, 날짜, 태그)를 명확하게 볼 수 있기를, so that 포스트에 대한 기본 정보를 빠르게 파악할 수 있다 - -#### Acceptance Criteria - -1. WHEN 포스트 페이지에 접근하면 THEN 시스템 SHALL 포스트 제목을 페이지 상단에 표시한다 -2. WHEN 포스트 페이지에 접근하면 THEN 시스템 SHALL 작성 날짜를 제목 하단에 표시한다 -3. WHEN 포스트에 태그가 있으면 THEN 시스템 SHALL 태그들을 시각적으로 구분되는 형태로 표시한다 -4. WHEN 포스트 메타데이터를 표시할 때 THEN 시스템 SHALL 일관된 스타일링을 적용한다 - -### Requirement 2 - -**User Story:** As a 블로그 독자, I want 포스트 콘텐츠가 읽기 좋은 형태로 구성되기를, so that 편안하게 글을 읽을 수 있다 - -#### Acceptance Criteria - -1. WHEN 포스트 콘텐츠를 표시할 때 THEN 시스템 SHALL 적절한 여백과 줄 간격을 적용한다 -2. WHEN 코드 블록이 있으면 THEN 시스템 SHALL 구문 강조와 함께 표시한다 -3. WHEN 이미지가 있으면 THEN 시스템 SHALL 반응형으로 크기를 조정하여 표시한다 -4. WHEN 긴 포스트를 읽을 때 THEN 시스템 SHALL 목차(Table of Contents)를 제공한다 - -### Requirement 3 - -**User Story:** As a 블로그 독자, I want 포스트 하단에 관련 정보와 네비게이션을 볼 수 있기를, so that 더 많은 콘텐츠를 탐색할 수 있다 - -#### Acceptance Criteria - -1. WHEN 포스트를 다 읽었을 때 THEN 시스템 SHALL 이전/다음 포스트로의 네비게이션을 제공한다 -2. WHEN 포스트 하단에 도달하면 THEN 시스템 SHALL 관련 포스트 추천을 표시한다 -3. WHEN 포스트 하단에 도달하면 THEN 시스템 SHALL 소셜 공유 버튼을 제공한다 -4. WHEN 포스트 하단에 도달하면 THEN 시스템 SHALL 작성자 정보를 표시한다 - -### Requirement 4 - -**User Story:** As a 블로그 운영자, I want 포스트 페이지가 SEO에 최적화되기를, so that 검색 엔진에서 더 잘 노출될 수 있다 - -#### Acceptance Criteria - -1. WHEN 포스트 페이지가 로드되면 THEN 시스템 SHALL 적절한 메타 태그를 설정한다 -2. WHEN 포스트 페이지가 로드되면 THEN 시스템 SHALL Open Graph 태그를 설정한다 -3. WHEN 포스트 페이지가 로드되면 THEN 시스템 SHALL 구조화된 데이터(JSON-LD)를 포함한다 -4. WHEN 포스트 페이지가 로드되면 THEN 시스템 SHALL 적절한 제목 태그 계층구조를 유지한다 - -### Requirement 5 - -**User Story:** As a 모바일 사용자, I want 포스트 페이지가 모바일에서도 잘 보이기를, so that 어떤 기기에서든 편안하게 읽을 수 있다 - -#### Acceptance Criteria - -1. WHEN 모바일 기기에서 접근하면 THEN 시스템 SHALL 반응형 레이아웃을 적용한다 -2. WHEN 모바일에서 스크롤할 때 THEN 시스템 SHALL 적절한 터치 인터랙션을 제공한다 -3. WHEN 모바일에서 코드 블록을 볼 때 THEN 시스템 SHALL 가로 스크롤을 지원한다 -4. WHEN 모바일에서 이미지를 볼 때 THEN 시스템 SHALL 화면 크기에 맞게 조정한다 \ No newline at end of file diff --git a/.kiro/specs/post-page-layout/tasks.md b/.kiro/specs/post-page-layout/tasks.md deleted file mode 100644 index 23c3b31..0000000 --- a/.kiro/specs/post-page-layout/tasks.md +++ /dev/null @@ -1,90 +0,0 @@ -# Implementation Plan - -- [x] 1. 기본 컴포넌트 구조 설정 및 타입 정의 - - PostHeader, PostContent, PostFooter 컴포넌트의 기본 인터페이스 생성 - - 확장된 Post 타입과 메타데이터 타입 정의 - - shadcn/ui Badge, Card 컴포넌트 설치 및 설정 - - _Requirements: 1.1, 1.4_ - -- [x] 2. PostHeader 컴포넌트 구현 - - 포스트 제목, 날짜, 태그를 표시하는 헤더 컴포넌트 작성 - - 날짜 포맷팅 유틸리티 함수 구현 - - 태그 표시를 위한 Badge 컴포넌트 스타일링 - - 반응형 레이아웃 적용 (모바일/데스크톱) - - _Requirements: 1.1, 1.2, 1.3, 1.4, 5.1_ - -- [x] 3. 향상된 MDX 콘텐츠 스타일링 구현 - - prose 스타일링 커스터마이징으로 가독성 향상 - - 코드 블록 스타일링 개선 (이미 rehype-pretty-code 설정됨) - - 이미지 반응형 처리를 위한 스타일 추가 - - 모바일에서 코드 블록 가로 스크롤 지원 - - _Requirements: 2.1, 2.2, 2.3, 5.3, 5.4_ - -- [ ] 4. 목차(Table of Contents) 기능 구현 - - MDX 콘텐츠에서 헤딩 추출하는 유틸리티 함수 작성 - - TableOfContents 컴포넌트 구현 - - 스크롤 위치에 따른 활성 헤딩 하이라이트 기능 - - 데스크톱에서만 표시되는 반응형 처리 - - _Requirements: 2.4, 5.1_ - -- [ ] 5. 포스트 네비게이션 시스템 구현 - - 이전/다음 포스트 데이터를 가져오는 로직 작성 - - PostNavigation 컴포넌트 구현 - - 포스트 순서 기반 네비게이션 링크 생성 - - 호버 효과 및 접근성 고려한 스타일링 - - _Requirements: 3.1_ - -- [ ] 6. 관련 포스트 추천 시스템 구현 - - 태그 기반 관련 포스트 찾기 알고리즘 작성 - - RelatedPosts 컴포넌트 구현 - - 카드 형태의 포스트 미리보기 레이아웃 - - 최대 4개 관련 포스트 표시 로직 - - _Requirements: 3.2_ - -- [ ] 7. 소셜 공유 기능 구현 - - SocialShare 컴포넌트 작성 - - Twitter, Facebook, LinkedIn 공유 링크 생성 - - 클립보드 복사 기능 추가 - - 공유 버튼 아이콘 및 스타일링 - - _Requirements: 3.3_ - -- [ ] 8. 작성자 정보 섹션 구현 - - AuthorInfo 컴포넌트 작성 - - 작성자 프로필 정보 표시 - - 소셜 링크 및 연락처 정보 포함 - - 카드 형태의 깔끔한 레이아웃 - - _Requirements: 3.4_ - -- [ ] 9. SEO 메타데이터 최적화 구현 - - generateMetadata 함수 작성하여 동적 메타데이터 생성 - - Open Graph 태그 설정 - - Twitter Card 메타데이터 추가 - - 포스트별 맞춤 title, description 생성 - - _Requirements: 4.1, 4.2_ - -- [ ] 10. 구조화된 데이터(JSON-LD) 구현 - - BlogPosting 스키마 JSON-LD 생성 함수 작성 - - 포스트 메타데이터를 구조화된 데이터로 변환 - - 검색 엔진 최적화를 위한 스키마 마크업 추가 - - _Requirements: 4.3_ - -- [ ] 11. 포스트 페이지 레이아웃 통합 및 리팩토링 - - 기존 PostPage 컴포넌트를 새로운 레이아웃으로 교체 - - 모든 하위 컴포넌트들을 통합하여 완전한 포스트 페이지 구성 - - 에러 처리 및 로딩 상태 개선 - - 성능 최적화 (이미지 최적화, 코드 분할) - - _Requirements: 4.4, 5.2_ - -- [ ] 12. 반응형 디자인 및 접근성 최종 검증 - - 모바일, 태블릿, 데스크톱에서 레이아웃 테스트 - - 키보드 네비게이션 지원 확인 - - 색상 대비 및 WCAG 가이드라인 준수 검증 - - 스크린 리더 호환성 테스트 - - _Requirements: 5.1, 5.2, 5.3, 5.4_ - -- [ ] 13. 단위 테스트 작성 - - 유틸리티 함수들(날짜 포맷팅, 헤딩 추출 등) 테스트 - - 주요 컴포넌트들의 렌더링 테스트 - - 관련 포스트 알고리즘 테스트 - - SEO 메타데이터 생성 테스트 - - _Requirements: 전체 요구사항 검증_ \ No newline at end of file diff --git a/.kiro/steering/development-standards.md b/.kiro/steering/development-standards.md deleted file mode 100644 index 41a810e..0000000 --- a/.kiro/steering/development-standards.md +++ /dev/null @@ -1,171 +0,0 @@ -# 개발 표준 및 베스트 프랙티스 - -## 코드 작성 원칙 - -### TypeScript 사용 -- 모든 파일에서 엄격한 타입 체크 적용 -- `any` 타입 사용 금지, 적절한 타입 정의 필수 -- 인터페이스와 타입 별칭 적절히 활용 -- 제네릭 타입 활용으로 재사용성 향상 - -### 컴포넌트 설계 -- **단일 책임 원칙**: 하나의 컴포넌트는 하나의 역할만 -- **Props 인터페이스**: 모든 props에 대한 명시적 타입 정의 -- **기본값 설정**: 선택적 props에 대한 적절한 기본값 -- **컴포넌트 분리**: 50줄 이상의 컴포넌트는 분리 고려 - -```typescript -// 좋은 예 -interface ButtonProps { - variant?: 'primary' | 'secondary' - size?: 'sm' | 'md' | 'lg' - disabled?: boolean - onClick?: () => void - children: React.ReactNode -} - -export function Button({ - variant = 'primary', - size = 'md', - disabled = false, - onClick, - children -}: ButtonProps) { - // 구현 -} -``` - -### 상태 관리 -- **로컬 상태**: `useState` 사용, 필요시에만 상태 끌어올리기 -- **서버 상태**: Next.js의 정적 생성 활용 -- **전역 상태**: 복잡한 상태는 Context API 또는 상태 관리 라이브러리 고려 - -## 파일 구조 및 네이밍 - -### 파일 네이밍 -- **컴포넌트**: PascalCase (`Header.tsx`, `PostCard.tsx`) -- **유틸리티**: camelCase (`formatDate.ts`, `cn.ts`) -- **페이지**: Next.js 컨벤션 따름 (`page.tsx`, `layout.tsx`) -- **타입**: PascalCase with suffix (`PostType.ts`, `UserInterface.ts`) - -### Import 순서 (Biome 설정 준수) -1. Node.js 내장 모듈 -2. 외부 패키지 -3. 내부 모듈 (`@/` 경로) -4. 상대 경로 (`../`, `./`) - -```typescript -import { readFile } from 'node:fs/promises' -import { join } from 'node:path' - -import matter from 'gray-matter' -import { clsx } from 'clsx' - -import { cn } from '@/app/_lib/cn' - -import { parsePostData } from './logic' -``` - -## 스타일링 표준 - -### Tailwind CSS 사용법 -- **유틸리티 클래스**: 인라인 스타일 대신 Tailwind 클래스 사용 -- **컴포넌트 추출**: 반복되는 스타일은 컴포넌트로 추출 -- **반응형**: 모바일 퍼스트, `sm:`, `md:`, `lg:` 브레이크포인트 활용 -- **다크모드**: `dark:` 접두사로 다크모드 스타일 정의 - -### 색상 팔레트 -- **주 색상**: `stone` 계열 (`stone-50` ~ `stone-950`) -- **강조 색상**: 필요시 `blue`, `green`, `red` 등 사용 -- **일관성**: 전체 애플리케이션에서 동일한 색상 체계 유지 - -```typescript -// 좋은 예 -
-``` - -## 성능 최적화 - -### Next.js 최적화 -- **이미지**: `next/image` 컴포넌트 사용 -- **폰트**: `next/font` 사용으로 폰트 최적화 -- **동적 import**: 필요시에만 컴포넌트 로드 -- **메타데이터**: 적절한 SEO 메타데이터 설정 - -### 번들 크기 최적화 -- **Tree shaking**: 사용하지 않는 코드 제거 -- **코드 분할**: 페이지별 코드 분할 -- **외부 라이브러리**: 필요한 기능만 import - -## 테스트 전략 - -### 단위 테스트 -- **비즈니스 로직**: `src/entities/` 내 로직 함수들 -- **유틸리티 함수**: `src/app/_lib/` 내 헬퍼 함수들 -- **테스트 파일**: `*.test.ts` 또는 `*.spec.ts` 확장자 - -### 테스트 작성 원칙 -- **AAA 패턴**: Arrange, Act, Assert -- **의미있는 테스트명**: 테스트 의도가 명확히 드러나는 이름 -- **독립성**: 각 테스트는 독립적으로 실행 가능 - -```typescript -// 좋은 예 -describe('formatDate', () => { - it('should format ISO date string to Korean format', () => { - // Arrange - const isoDate = '2025-01-15' - - // Act - const result = formatDate(isoDate) - - // Assert - expect(result).toBe('2025년 1월 15일') - }) -}) -``` - -## 에러 처리 - -### 클라이언트 사이드 -- **에러 바운더리**: React 에러 바운더리 활용 -- **사용자 친화적 메시지**: 기술적 에러를 사용자가 이해할 수 있는 메시지로 변환 -- **로깅**: 개발 환경에서 적절한 에러 로깅 - -### 서버 사이드 -- **404 처리**: `notFound()` 함수 활용 -- **에러 페이지**: 커스텀 에러 페이지 제공 -- **Graceful degradation**: 일부 기능 실패 시에도 기본 기능은 동작 - -## 접근성 (a11y) - -### 기본 원칙 -- **시맨틱 HTML**: 적절한 HTML 태그 사용 -- **ARIA 레이블**: 스크린 리더를 위한 적절한 레이블 -- **키보드 네비게이션**: 모든 인터랙티브 요소 키보드 접근 가능 -- **색상 대비**: WCAG 가이드라인 준수 - -### 구현 예시 -```typescript -// 좋은 예 - -``` - -## 보안 고려사항 - -### 콘텐츠 보안 -- **XSS 방지**: 사용자 입력 적절히 이스케이프 -- **MDX 보안**: 신뢰할 수 있는 MDX 콘텐츠만 렌더링 -- **이미지 최적화**: 외부 이미지 소스 검증 - -### 정적 사이트 보안 -- **환경 변수**: 민감한 정보는 빌드 타임에만 사용 -- **의존성 관리**: 정기적인 보안 업데이트 -- **CSP 헤더**: 적절한 Content Security Policy 설정 \ No newline at end of file diff --git a/.kiro/steering/project-structure.md b/.kiro/steering/project-structure.md index 12b04e2..6012217 100644 --- a/.kiro/steering/project-structure.md +++ b/.kiro/steering/project-structure.md @@ -67,38 +67,6 @@ src/ - **스타일**: "new-york" 스타일, stone 베이스 컬러 - **아이콘**: Lucide React 사용 -## 개발 워크플로우 - -### 브랜치 전략 -- `main`: 프로덕션 준비 코드 -- `feat/`: 새 기능 (`feat/search-functionality`) -- `fix/`: 버그 수정 (`fix/mobile-nav-issue`) -- `docs/`: 문서 업데이트 - -### 커밋 컨벤션 -``` -: - -[optional body] -``` - -#### 커밋 전략 - -논리적 단위로 나누어서 커밋 - -**타입:** -- `feat:` - 새 기능 -- `fix:` - 버그 수정 -- `docs:` - 문서 업데이트 -- `config:` - 설정 변경 -- `refactor:` - 리팩토링 -- `chore:` - 유지보수 - -### 코드 품질 -- **Biome**: `pnpm biome:fix` 실행 후 커밋 -- **테스트**: `pnpm test` 실행 -- **빌드 확인**: 필요시에만 `pnpm build` 실행 - ## 콘텐츠 관리 ### 콘텐츠 위치 @@ -116,28 +84,6 @@ tags: ['tag1', 'tag2'] # 포스트 내용 ``` -### 정적 생성 -- `generateStaticParams()`로 빌드 타임 페이지 생성 -- `output: 'export'` 설정으로 정적 파일 생성 - -## 개발 시 주의사항 - -### UI 구현 -- **플레이스홀더 사용**: 실제 콘텐츠 대신 `[Page Title]`, `[Description]` 등 사용 -- **사용자 승인**: UI 구조 구현 전 명시적 요구사항 확인 -- **점진적 구현**: 한 번에 모든 기능 구현하지 않기 - -### 성능 최적화 -- 이미지 최적화: Next.js Image 컴포넌트 사용 -- 코드 분할: 동적 import 활용 -- 정적 생성: 가능한 모든 페이지 사전 생성 - -### 접근성 -- 시맨틱 HTML 사용 -- ARIA 레이블 적절히 활용 -- 키보드 네비게이션 지원 -- 색상 대비 준수 - ## 배포 설정 - **basePath**: `/blog` (GitHub Pages 등을 위한 설정) diff --git a/.kiro/steering/task-execution-process.md b/.kiro/steering/task-execution-process.md new file mode 100644 index 0000000..5784d53 --- /dev/null +++ b/.kiro/steering/task-execution-process.md @@ -0,0 +1,170 @@ +# 작업 실행 프로세스 + +## 작업 진행 원칙 + +- 작업의 단위를 가능하면 작게 설정 +- 지시가 모호하다고 느껴지면 질문 후 진행 + +## 구현 원칙 + +### 점진적 개발 +- 한 번에 하나의 태스크만 집중하여 구현 +- 태스크 완료 후 사용자 검토 대기, 자동으로 다음 태스크 진행하지 않음 +- 각 단계에서 이전 단계의 결과물을 기반으로 구축 + +### 코드 품질 +- TypeScript 엄격 모드 준수, any 타입 사용 금지 +- 컴포넌트는 단일 책임 원칙 적용 +- Props 인터페이스 명시적 정의 +- 적절한 기본값 설정 + +### 테스트 우선 +- 비즈니스 로직 구현 시 단위 테스트 함께 작성 +- AAA 패턴 (Arrange, Act, Assert) 준수 +- 의미있는 테스트명 사용 + +## 구현 패턴 + +### 컴포넌트 설계 +- **단일 책임 원칙**: 하나의 컴포넌트는 하나의 역할만 +- **Props 인터페이스**: 모든 props에 대한 명시적 타입 정의 +- **기본값 설정**: 선택적 props에 대한 적절한 기본값 +- **커스텀훅**: 로직은 커스텀훅으로 분리함 +- **컴포넌트 분리**: 50줄 이상의 컴포넌트는 분리 고려 + +### 컴포넌트 구현 +```typescript +// 1. 인터페이스 정의 +interface ComponentProps { + required: string + optional?: boolean + children?: React.ReactNode +} + +// 2. 컴포넌트 구현 +export function Component({ + required, + optional = false, + children +}: ComponentProps) { + // 구현 +} +``` + +### 비즈니스 로직 구현 +```typescript +// 1. 타입 정의 +export type DataType = { + id: string + value: string +} + +// 2. 로직 함수 구현 +export function processData(data: DataType[]): DataType[] { + // 구현 +} + +// 3. 테스트 작성 +describe('processData', () => { + it('should process data correctly', () => { + // 테스트 구현 + }) +}) +``` + +### 기능 구조 우선 +- 스타일링보다 기능적 구조와 로직에 집중 +- 컴포넌트의 역할과 책임을 명확히 정의 +- 데이터 흐름과 상태 관리 구조 우선 설계 +- UI는 기본적인 레이아웃만 구현하고 세부 스타일링은 후순위 + +### 아키텍처 중심 접근 +- 도메인 로직과 UI 로직의 명확한 분리 +- 컴포넌트 간의 의존성과 데이터 전달 구조 설계 +- 재사용 가능한 로직의 추상화 +- 확장 가능한 구조로 설계 + +### 최소 스타일링 +- 구조화에 필요한 최소한의 스타일링 가능 +- 필요시 shadcn/ui의 컴포넌트를 이용 + +```typescript +// 구조에 집중한 컴포넌트 예시 +
{/* 기본 레이아웃만 */} +
+ {/* 기능적 구조 우선 */} +
+
+
+ {/* ... */} +
+
+
+``` + +### UI 구현 +- **플레이스홀더 사용**: 실제 콘텐츠 대신 `[Page Title]`, `[Description]` 등 사용 +- **사용자 승인**: UI 구조 구현 전 명시적 요구사항 확인 +- **점진적 구현**: 한 번에 모든 기능 구현하지 않기 + +## 에러 처리 + +### 클라이언트 사이드 +- 사용자 친화적 에러 메시지 +- 적절한 폴백 UI 제공 +- 개발 환경에서 상세 로깅 + +### 서버 사이드 +- notFound() 함수 활용 +- 적절한 에러 바운더리 설정 +- Graceful degradation 적용 + +## 접근성 고려사항 + +### 필수 요소 +- 시맨틱 HTML 태그 사용 +- 적절한 ARIA 레이블 +- 키보드 네비게이션 지원 +- 충분한 색상 대비 + +### 구현 예시 +```typescript +