diff --git a/src/app/globals.css b/src/app/globals.css index e2e1866..95694c6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -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; } } diff --git a/src/app/page.tsx b/src/app/page.tsx index 28d1fa3..7c5452f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 ( -
- - - {allPosts.map((post) => ( - - ))} - -
- ); + return ; } diff --git a/src/features/index.ts b/src/features/index.ts index 336abe1..c4793e1 100644 --- a/src/features/index.ts +++ b/src/features/index.ts @@ -1 +1,2 @@ export * from './post'; +export * from './main'; diff --git a/src/shared/components/features/card/PostCard.tsx b/src/features/main/components/PostCard.tsx similarity index 68% rename from src/shared/components/features/card/PostCard.tsx rename to src/features/main/components/PostCard.tsx index 1f3ebf5..28ed8a4 100644 --- a/src/shared/components/features/card/PostCard.tsx +++ b/src/features/main/components/PostCard.tsx @@ -2,6 +2,7 @@ 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 = { @@ -9,39 +10,6 @@ type PostCardProps = { }; 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 (
diff --git a/src/features/main/components/Tabs.tsx b/src/features/main/components/Tabs.tsx new file mode 100644 index 0000000..e85a300 --- /dev/null +++ b/src/features/main/components/Tabs.tsx @@ -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 ( +
+
+ {tabs.map((tab, idx) => ( +
onTabClick(idx)} + > +

{tab.name}

+

({tab.count})

+
+ ))} +
+
+ ); +}; diff --git a/src/shared/components/features/card/index.ts b/src/features/main/components/index.ts similarity index 53% rename from src/shared/components/features/card/index.ts rename to src/features/main/components/index.ts index 4add1a6..44e48c0 100644 --- a/src/shared/components/features/card/index.ts +++ b/src/features/main/components/index.ts @@ -1 +1,2 @@ export * from './PostCard'; +export * from './Tabs'; diff --git a/src/features/main/hooks/index.ts b/src/features/main/hooks/index.ts new file mode 100644 index 0000000..6d10aca --- /dev/null +++ b/src/features/main/hooks/index.ts @@ -0,0 +1,3 @@ +export { useCategoryFilter } from './useCategoryFilter'; +export { useTabs } from './useTabs'; +export { useTabSelection } from './useTabSelection'; diff --git a/src/features/main/hooks/useCategoryFilter.ts b/src/features/main/hooks/useCategoryFilter.ts new file mode 100644 index 0000000..eadcdf6 --- /dev/null +++ b/src/features/main/hooks/useCategoryFilter.ts @@ -0,0 +1,19 @@ +import { useMemo, useState } from 'react'; + +import { MDXPostMeta } from '@/shared/types'; + +export const useCategoryFilter = (posts: MDXPostMeta[]) => { + const [selectedCategory, setSelectedCategory] = useState(null); + + const filteredPosts = useMemo(() => { + return selectedCategory + ? posts.filter((post) => post.category === selectedCategory) + : posts; + }, [posts, selectedCategory]); + + return { + selectedCategory, + filteredPosts, + setSelectedCategory, + }; +}; diff --git a/src/features/main/hooks/useTabSelection.ts b/src/features/main/hooks/useTabSelection.ts new file mode 100644 index 0000000..9197e60 --- /dev/null +++ b/src/features/main/hooks/useTabSelection.ts @@ -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, + }; +}; diff --git a/src/features/main/hooks/useTabs.ts b/src/features/main/hooks/useTabs.ts new file mode 100644 index 0000000..d0406f4 --- /dev/null +++ b/src/features/main/hooks/useTabs.ts @@ -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>( + (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 }; +}; diff --git a/src/features/main/index.ts b/src/features/main/index.ts new file mode 100644 index 0000000..5ecdd1f --- /dev/null +++ b/src/features/main/index.ts @@ -0,0 +1 @@ +export * from './ui'; diff --git a/src/features/main/ui/PostListSection.tsx b/src/features/main/ui/PostListSection.tsx new file mode 100644 index 0000000..8b06b18 --- /dev/null +++ b/src/features/main/ui/PostListSection.tsx @@ -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 ( +
+ + + {filteredPosts.map((post) => ( + + ))} + +
+ ); +}; diff --git a/src/features/main/ui/index.ts b/src/features/main/ui/index.ts new file mode 100644 index 0000000..5e81e20 --- /dev/null +++ b/src/features/main/ui/index.ts @@ -0,0 +1 @@ +export * from './PostListSection'; diff --git a/src/features/post/contents/dom-api.mdx b/src/features/post/contents/dom-api.mdx index 5481a12..8394a2e 100644 --- a/src/features/post/contents/dom-api.mdx +++ b/src/features/post/contents/dom-api.mdx @@ -33,26 +33,29 @@ DOM API를 사용해 요소를 찾을 때는, 항상 모든 것의 시작점인 ### 단일 요소 선택 (하나만 찾기) -- **`document.getElementById('id이름')`** - - 가장 빠르고 고전적인 방법. 고유한 `id` 속성 값으로 요소를 찾는다. - - `id`는 문서에서 유일해야 하므로, 항상 하나의 요소만 반환한다. - ```tsx - // id가 "color"인 요소를 찾아서 $color 변수에 담는다. - const $color = document.getElementById('color'); - console.log($color); //
...
- ``` -- **`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); //
...
+``` + +**`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'); +``` ### 여러 요소 선택 (조건에 맞는 것 모두 찾기) diff --git a/src/features/post/ui/PostHeaderSection.tsx b/src/features/post/ui/PostHeaderSection.tsx index fa9609f..a1634ff 100644 --- a/src/features/post/ui/PostHeaderSection.tsx +++ b/src/features/post/ui/PostHeaderSection.tsx @@ -1,3 +1,4 @@ +import { getCategoryColor } from '@/shared/utils'; import { CalendarDays } from 'lucide-react'; import { PostHeader } from '@/entities'; @@ -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 (
diff --git a/src/shared/components/features/index.ts b/src/shared/components/features/index.ts index b9cb10f..98a95ae 100644 --- a/src/shared/components/features/index.ts +++ b/src/shared/components/features/index.ts @@ -1,4 +1,2 @@ -export * from './card'; export * from './layout'; export * from './mdx'; -export * from './tabs'; diff --git a/src/shared/components/features/layout/Footer.tsx b/src/shared/components/features/layout/Footer.tsx index 5483d9f..f0b787b 100644 --- a/src/shared/components/features/layout/Footer.tsx +++ b/src/shared/components/features/layout/Footer.tsx @@ -2,16 +2,16 @@ import Link from 'next/link'; export const Footer = () => { return ( -