Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9a4dfa1
fix(globals.css): body 스타일 수정 및 overflow-x-hidden 추가
Dobbymin Oct 25, 2025
a5e284e
refactor(page.tsx): PostCard 및 Tabs 제거, PostList로 대체
Dobbymin Oct 25, 2025
3ea7888
feat(post): PostList 컴포넌트 추가 및 index 업데이트
Dobbymin Oct 25, 2025
f6a894c
refactor(Header): Header 컴포넌트 구조 개선 및 스타일 수정
Dobbymin Oct 25, 2025
6c8aeb1
feat(tabs): 게시글 카테고리 기반 탭 생성 및 필터링 기능 추가
Dobbymin Oct 25, 2025
055dda9
fix(Footer): Footer 컴포넌트 스타일 개선 및 반응형 텍스트 추가
Dobbymin Oct 25, 2025
dc9fda3
refactor(main): PostList 경로 수정
Dobbymin Oct 25, 2025
d21900e
refactor(mdx): 지원하는 확장자 상수화 및 파일 검색 로직 개선
Dobbymin Oct 25, 2025
a445499
fix(PostDetailPage): params 타입 수정 및 비동기 처리 제거
Dobbymin Oct 25, 2025
d4c72c9
refactor(PostHeaderSection, PostCard): 카테고리 색상 매핑 함수 외부화 및 중복 코드 제거
Dobbymin Oct 25, 2025
93964f0
refactor(Tabs): 카테고리 카운트 로직 메모이제이션 및 중복 코드 제거
Dobbymin Oct 25, 2025
a5cbaa8
feat(post-utils): 카테고리 색상 매핑 및 배경 색상 생성 유틸리티 추가
Dobbymin Oct 25, 2025
cab6290
refactor: 컴포넌트 기능에 맞게 위치 변경
Dobbymin Oct 25, 2025
425f9bd
refactor: 변경된 컴포넌트 네이밍 적용
Dobbymin Oct 25, 2025
ced4849
fix(PostDetailPage): params 타입을 Promise로 변경하여 비동기 처리 보장
Dobbymin Oct 25, 2025
1431df5
refactor: ui와 로직을 분리하여 custom hook 구현
Dobbymin Oct 25, 2025
9f411b2
refactor: 단일 요소 선택 섹션의 코드 블록 정렬 개선
Dobbymin Oct 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,12 @@
* {
@apply border-border break-keep outline-ring/50;
}
html,
body {
@apply flex items-center justify-center bg-blog-gray-100 font-pretendard;
@apply overflow-x-hidden;
}
body {
@apply bg-blog-gray-100 font-pretendard;
}
}

Expand Down
20 changes: 2 additions & 18 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
import { getAllPosts } from '@/shared/lib';

import { PostCard, Tabs } from '@/shared';
import { Grid } from '@/widgets';
import { PostListSection } from '@/features';

export default function Home() {
const allPosts = getAllPosts();

return (
<div className='flex flex-col items-center justify-center gap-10'>
<Tabs />
<Grid
className='w-full px-4 md:px-8 lg:px-0'
cols='grid-cols-1 md:grid-cols-2 lg:grid-cols-[270px_270px_270px]'
gap={5}
justifyContent='center'
maxWidth='7xl'
>
{allPosts.map((post) => (
<PostCard key={post.slug} post={post} />
))}
</Grid>
</div>
);
return <PostListSection posts={allPosts} />;
}
1 change: 1 addition & 0 deletions src/features/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './post';
export * from './main';
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,14 @@ import Image from 'next/image';
import Link from 'next/link';

import { MDXPostMeta } from '@/shared/types';
import { getCategoryColor, getRandomBgColor } from '@/shared/utils';
import { CalendarDays } from 'lucide-react';

type PostCardProps = {
post: MDXPostMeta;
};

export const PostCard = ({ post }: PostCardProps) => {
// 카테고리별 색상 매핑
const getCategoryColor = (category: string) => {
switch (category.toLowerCase()) {
case 'develop':
return 'text-blog-blue';
case 'daily':
return 'text-blog-pink';
case 'review':
return 'text-blog-purple';
default:
return 'text-blog-green';
}
};

// slug를 기반으로 랜덤 색상 생성 (같은 slug는 항상 같은 색상)
const getRandomBgColor = (slug: string) => {
const colors = [
'bg-blog-blue',
'bg-blog-pink',
'bg-blog-purple',
'bg-blog-green',
'bg-blog-yellow',
];

// slug를 숫자로 변환하여 일관된 색상 선택
let hash = 0;
for (let i = 0; i < slug.length; i++) {
hash = slug.charCodeAt(i) + ((hash << 5) - hash);
}
const index = Math.abs(hash) % colors.length;
return colors[index];
};

return (
<Link href={`/post/${post.slug}`}>
<div className='flex h-full min-h-[300px] w-full flex-col overflow-hidden bg-white shadow-lg transition-transform duration-200 transform-content hover:scale-105'>
Expand Down
41 changes: 41 additions & 0 deletions src/features/main/components/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { MDXPostMeta } from '@/shared/types';

import { useTabSelection, useTabs } from '../hooks';

type TabsProps = {
posts: MDXPostMeta[];
onFilterChange: (category: string | null) => void;
};

export const Tabs = ({ posts, onFilterChange }: TabsProps) => {
const { tabs } = useTabs(posts);
const { selectedIndex, selectTab } = useTabSelection();

const onTabClick = (index: number) => {
selectTab(index);
onFilterChange(tabs[index].category);
};

return (
<div className='w-full max-w-[850px] px-3 md:px-8 lg:px-0'>
<div className='flex h-auto justify-start overflow-x-auto py-2 font-fira-code'>
{tabs.map((tab, idx) => (
<div
key={tab.name}
className={`flex shrink-0 cursor-pointer items-center justify-center gap-2 border-r px-4 py-2 text-sm font-semibold transition-colors last:border-r-0 md:px-5 ${
selectedIndex === idx
? 'bg-blog-gray-100 text-blog-black'
: 'bg-blog-gray-200 text-blog-gray-600 hover:font-bold'
}`}
onClick={() => onTabClick(idx)}
>
<p className='whitespace-nowrap'>{tab.name}</p>
<p className='whitespace-nowrap text-blog-green'>({tab.count})</p>
</div>
))}
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './PostCard';
export * from './Tabs';
3 changes: 3 additions & 0 deletions src/features/main/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { useCategoryFilter } from './useCategoryFilter';
export { useTabs } from './useTabs';
export { useTabSelection } from './useTabSelection';
19 changes: 19 additions & 0 deletions src/features/main/hooks/useCategoryFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useMemo, useState } from 'react';

import { MDXPostMeta } from '@/shared/types';

export const useCategoryFilter = (posts: MDXPostMeta[]) => {
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);

const filteredPosts = useMemo(() => {
return selectedCategory
? posts.filter((post) => post.category === selectedCategory)
: posts;
}, [posts, selectedCategory]);

return {
selectedCategory,
filteredPosts,
setSelectedCategory,
};
};
14 changes: 14 additions & 0 deletions src/features/main/hooks/useTabSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useState } from 'react';

export const useTabSelection = () => {
const [selectedIndex, setSelectedIndex] = useState(0);

const selectTab = (index: number) => {
setSelectedIndex(index);
};

return {
selectedIndex,
selectTab,
};
};
26 changes: 26 additions & 0 deletions src/features/main/hooks/useTabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useMemo } from 'react';

import { MDXPostMeta } from '@/shared/types';

export const useTabs = (posts: MDXPostMeta[]) => {
const tabs = useMemo(() => {
const categoryCounts = posts.reduce<Record<string, number>>(
(acc, { category }) => {
acc[category] = (acc[category] || 0) + 1;
return acc;
},
{},
);

return [
{ name: 'All', count: posts.length, category: null },
...Object.entries(categoryCounts).map(([category, count]) => ({
name: category,
count,
category,
})),
];
}, [posts]);

return { tabs };
};
1 change: 1 addition & 0 deletions src/features/main/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ui';
32 changes: 32 additions & 0 deletions src/features/main/ui/PostListSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import { MDXPostMeta } from '@/shared/types';

import { Grid } from '@/widgets';

import { PostCard, Tabs } from '../components';
import { useCategoryFilter } from '../hooks';

type PostListSectionProps = {
posts: MDXPostMeta[];
};

export const PostListSection = ({ posts }: PostListSectionProps) => {
const { filteredPosts, setSelectedCategory } = useCategoryFilter(posts);

return (
<div className='flex flex-col items-center justify-center gap-10'>
<Tabs posts={posts} onFilterChange={setSelectedCategory} />
<Grid
className='w-full gap-5 px-4 md:px-8 lg:px-0'
cols='grid-cols-1 md:grid-cols-2 lg:grid-cols-[270px_270px_270px]'
justifyContent='center'
maxWidth='7xl'
>
{filteredPosts.map((post) => (
<PostCard key={post.slug} post={post} />
))}
</Grid>
</div>
);
};
1 change: 1 addition & 0 deletions src/features/main/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PostListSection';
43 changes: 23 additions & 20 deletions src/features/post/contents/dom-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,29 @@ DOM API를 사용해 요소를 찾을 때는, 항상 모든 것의 시작점인

### 단일 요소 선택 (하나만 찾기)

- **`document.getElementById('id이름')`**
- 가장 빠르고 고전적인 방법. 고유한 `id` 속성 값으로 요소를 찾는다.
- `id`는 문서에서 유일해야 하므로, 항상 하나의 요소만 반환한다.
```tsx
// id가 "color"인 요소를 찾아서 $color 변수에 담는다.
const $color = document.getElementById('color');
console.log($color); // <div id="color">...</div>
```
- **`document.querySelector('CSS 선택자')`**

- `id`, `class`, `태그 이름` 등 **CSS 선택자 문법**을 그대로 사용해서 요소를 찾는다.
- 조건을 만족하는 요소가 여러 개라도, **가장 첫 번째 요소 하나만** 반환한다.

```jsx
// class가 "animal-info"인 div 요소를 찾는다.
const $animalInfo = document.querySelector('div.animal-info');

// id가 "age"인 div 요소를 찾는다.
const $age = document.querySelector('div#age');
```
**`document.getElementById('id이름')`**

- 가장 빠르고 고전적인 방법. 고유한 `id` 속성 값으로 요소를 찾는다.
- `id`는 문서에서 유일해야 하므로, 항상 하나의 요소만 반환한다.

```tsx
// id가 "color"인 요소를 찾아서 $color 변수에 담는다.
const $color = document.getElementById('color');
console.log($color); // <div id="color">...</div>
```

**`document.querySelector('CSS 선택자')`**

- `id`, `class`, `태그 이름` 등 **CSS 선택자 문법**을 그대로 사용해서 요소를 찾는다.
- 조건을 만족하는 요소가 여러 개라도, **가장 첫 번째 요소 하나만** 반환한다.

```jsx
// class가 "animal-info"인 div 요소를 찾는다.
const $animalInfo = document.querySelector('div.animal-info');

// id가 "age"인 div 요소를 찾는다.
const $age = document.querySelector('div#age');
```

### 여러 요소 선택 (조건에 맞는 것 모두 찾기)

Expand Down
15 changes: 1 addition & 14 deletions src/features/post/ui/PostHeaderSection.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getCategoryColor } from '@/shared/utils';
import { CalendarDays } from 'lucide-react';

import { PostHeader } from '@/entities';
Expand All @@ -7,20 +8,6 @@ type PostHeaderSectionProps = {
};

export const PostHeaderSection = ({ post }: PostHeaderSectionProps) => {
// 카테고리별 색상 매핑
const getCategoryColor = (category: string) => {
switch (category.toLowerCase()) {
case 'develop':
return 'text-blog-blue';
case 'daily':
return 'text-blog-pink';
case 'review':
return 'text-blog-purple';
default:
return 'text-blog-green';
}
};

return (
<header className='w-full bg-white p-6 md:p-8'>
<div className='flex flex-col gap-4'>
Expand Down
2 changes: 0 additions & 2 deletions src/shared/components/features/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
export * from './card';
export * from './layout';
export * from './mdx';
export * from './tabs';
10 changes: 5 additions & 5 deletions src/shared/components/features/layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import Link from 'next/link';

export const Footer = () => {
return (
<footer className='flex h-footer w-full items-center justify-center bg-blog-gray-300'>
<footer className='flex h-footer w-full items-center justify-center bg-blog-gray-300 px-4'>
<div className='flex flex-col items-start justify-between gap-1'>
<p className='text-blog-gray-600'>
<p className='text-sm text-blog-gray-600 md:text-base'>
Copyright &copy; 2025 <b>Dobbymin</b> All rights reserved.
</p>
<div className='flex items-center justify-center gap-1 font-fira-code text-blog-gray-600'>
<div className='flex flex-wrap items-center justify-center gap-1 font-fira-code text-sm text-blog-gray-600 md:text-base'>
<span>gmin0701@knu.ac.kr</span>
<span className='font-bold'>.</span>
<span>+82-10-3095-7259</span>
<span className='font-bold'>.</span>
<span className='hidden md:inline'>+82-10-3095-7259</span>
<span className='hidden font-bold md:inline'>.</span>
<Link
className='hover:font-bold hover:underline'
href='https://github.com/Dobbymin'
Expand Down
44 changes: 29 additions & 15 deletions src/shared/components/features/layout/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Image from 'next/image';
import Link from 'next/link';

import { Search, Sun } from 'lucide-react';
Expand All @@ -7,22 +8,35 @@ import { Input } from '../../ui';

export const Header = () => {
return (
<header className='fixed top-0 right-0 left-0 z-100 flex h-header items-center justify-between bg-blog-gray-500 px-header transition-colors'>
<Link href={routerPath.main}>
<p className='font-lemon text-3xl'>Dobblog</p>
</Link>
<div className='absolute top-1/2 left-1/2 hidden w-[400px] max-w-[60vw] -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-3 rounded-sm bg-blog-gray-100 md:flex'>
<Input
className='border-none bg-transparent text-sm text-blog-gray-500 shadow-none outline-none placeholder:text-blog-gray-400 focus:border-none focus:ring-0 focus:outline-none'
type='text'
/>
<Search className='mr-3 text-blog-gray-500' size={20} />
</div>
<div className='flex items-center justify-between gap-5'>
<Link href={routerPath.about}>
<p className='font-lemon text-xl'>About</p>
<header className='fixed top-0 right-0 left-0 z-100 flex items-center justify-center bg-blog-gray-500 transition-colors'>
<div className='relative flex h-header w-full max-w-7xl items-center justify-between px-4 md:px-8'>
<Link href={routerPath.main}>
<div className='flex items-center gap-4'>
<Image
alt='Dobblog Logo'
className='rounded-full'
height={40}
src='/logo.png'
width={40}
/>
<p className='hidden font-lemon text-xl md:block md:text-3xl'>
Dobblog
</p>
</div>
</Link>
<Sun className='cursor-pointer text-blog-pink' />
<div className='absolute top-1/2 left-1/2 hidden w-[400px] max-w-[60vw] -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-3 rounded-sm bg-blog-gray-100 md:flex'>
<Input
className='border-none bg-transparent text-sm text-blog-gray-500 shadow-none outline-none placeholder:text-blog-gray-400 focus:border-none focus:ring-0 focus:outline-none'
type='text'
/>
<Search className='mr-3 text-blog-gray-500' size={20} />
</div>
<div className='flex items-center justify-between gap-3 md:gap-5'>
<Link href={routerPath.about}>
<p className='font-lemon text-base md:text-xl'>About</p>
</Link>
<Sun className='cursor-pointer text-blog-pink' size={20} />
</div>
</div>
</header>
);
Expand Down
Loading