diff --git a/package.json b/package.json index 5785878..0f628a1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev --turbopack", - "prebuild": "tsx src/scripts/buildPostMeta.ts && tsx src/scripts/buildPost.ts && tsx src/scripts/buildSeries.ts && tsx src/scripts/buildCategory.ts", + "prebuild": "tsx src/scripts/buildAll.ts", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/public/img/thumbnail/blog-thumbnail.png b/public/img/thumbnail/blog-thumbnail.png new file mode 100644 index 0000000..e81f6b0 Binary files /dev/null and b/public/img/thumbnail/blog-thumbnail.png differ diff --git a/public/img/thumbnail/chim-planet-thumbnail.png b/public/img/thumbnail/chim-planet-thumbnail.png new file mode 100644 index 0000000..3816ab4 Binary files /dev/null and b/public/img/thumbnail/chim-planet-thumbnail.png differ diff --git a/public/img/thumbnail/default-thumbnail.png b/public/img/thumbnail/default-thumbnail.png new file mode 100644 index 0000000..3de3a56 Binary files /dev/null and b/public/img/thumbnail/default-thumbnail.png differ diff --git a/public/img/thumbnail/default.png b/public/img/thumbnail/default.png deleted file mode 100644 index 966009b..0000000 Binary files a/public/img/thumbnail/default.png and /dev/null differ diff --git a/public/img/thumbnail/discord-bot-thumbnail.png b/public/img/thumbnail/discord-bot-thumbnail.png new file mode 100644 index 0000000..ca940b8 Binary files /dev/null and b/public/img/thumbnail/discord-bot-thumbnail.png differ diff --git a/public/img/thumbnail/fandom-k-thumbnail.png b/public/img/thumbnail/fandom-k-thumbnail.png new file mode 100644 index 0000000..83f3dc4 Binary files /dev/null and b/public/img/thumbnail/fandom-k-thumbnail.png differ diff --git a/public/img/thumbnail/global-nomad-thumbnail.png b/public/img/thumbnail/global-nomad-thumbnail.png new file mode 100644 index 0000000..1a2cf4d Binary files /dev/null and b/public/img/thumbnail/global-nomad-thumbnail.png differ diff --git a/public/img/thumbnail/maple-helper-thumbnail.png b/public/img/thumbnail/maple-helper-thumbnail.png new file mode 100644 index 0000000..fffb665 Binary files /dev/null and b/public/img/thumbnail/maple-helper-thumbnail.png differ diff --git a/public/img/thumbnail/maplehelper.png b/public/img/thumbnail/maplehelper.png deleted file mode 100644 index 6534427..0000000 Binary files a/public/img/thumbnail/maplehelper.png and /dev/null differ diff --git a/public/img/thumbnail/sprint-bootcamp-thumbnail.png b/public/img/thumbnail/sprint-bootcamp-thumbnail.png new file mode 100644 index 0000000..c2cea01 Binary files /dev/null and b/public/img/thumbnail/sprint-bootcamp-thumbnail.png differ diff --git a/public/img/thumbnail/the-julge-thumbnail.png b/public/img/thumbnail/the-julge-thumbnail.png new file mode 100644 index 0000000..437eac8 Binary files /dev/null and b/public/img/thumbnail/the-julge-thumbnail.png differ diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index a49cc89..3b5d6f1 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -1,15 +1,19 @@ -import { getPostContent } from "@/lib/client/getBlogData"; +import { getPostContent } from "@/lib/postService"; import type { NextRequest } from "next/server"; export const GET = async (request: NextRequest) => { - const pathroot = request.nextUrl.searchParams.get("pathroot"); + const searchParams = request.nextUrl.searchParams; + const category = searchParams.get("category"); + const slug = searchParams.get("slug"); - if (!pathroot) { - return new Response("pathroot를 찾을 수 없어요", { status: 400 }); + if (!category || !slug) { + return new Response("카테고리 또는 슬러그를 찾을 수 없어요", { + status: 400, + }); } try { - const post = getPostContent(pathroot); + const post = getPostContent(category, slug); return new Response(post, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" }, diff --git a/src/app/blog/[category]/[post]/page.tsx b/src/app/blog/[category]/[post]/page.tsx index 60169b5..2989a9f 100644 --- a/src/app/blog/[category]/[post]/page.tsx +++ b/src/app/blog/[category]/[post]/page.tsx @@ -1,8 +1,4 @@ -import { - getPostContent, - getPostList, - getPostMeta, -} from "@/lib/client/getBlogData"; +import { getPostDetail, getPostList, getPostMeta } from "@/lib/postService"; import { Metadata } from "next"; import PostContainer from "./_components/PostContainer"; @@ -59,10 +55,9 @@ export const generateStaticParams = () => { const PostPage = async ({ params }: PageProps) => { const { category, post } = await params; - const { metadata, htmlFilePath } = getPostMeta(category, post); - const contentHtml = getPostContent(htmlFilePath); + const { post: meta, contentHtml } = getPostDetail(category, post); - return ; + return ; }; export default PostPage; diff --git a/src/app/blog/_components/PostSection/PostSectionHeader/CategoryModalButton.tsx b/src/app/blog/_components/PostSection/PostSectionHeader/CategoryModalButton.tsx index ad4b739..fb26566 100644 --- a/src/app/blog/_components/PostSection/PostSectionHeader/CategoryModalButton.tsx +++ b/src/app/blog/_components/PostSection/PostSectionHeader/CategoryModalButton.tsx @@ -1,12 +1,11 @@ import ModalTriggerButton from "@/components/ModalTriggerButton"; -import type { CategoryList } from "@/types/posts.types"; -import { memo } from "react"; +import type { Category } from "@/types/posts.types"; import { POSTSECTION_TEXT } from ".."; interface CategoryModalButtonProps { currentCategories: string[]; onChange: React.ChangeEventHandler; - categorys: CategoryList; + categorys: Category[]; } const CategoryModalButton = ({ @@ -21,24 +20,32 @@ const CategoryModalButton = ({ containerProps={{ onChange }} modalRootId="tags-modal" > - {categorys.map((el) => ( -
- {" "} - -
- ))} +
+ {categorys.map((el) => { + const isChecked = currentCategories.includes(el.name); + return ( + + ); + })} +
); }; -export default memo(CategoryModalButton); +export default CategoryModalButton; diff --git a/src/app/blog/_components/PostSection/PostSectionHeader/SortModalButton.tsx b/src/app/blog/_components/PostSection/PostSectionHeader/SortModalButton.tsx index 9f844e4..df555f8 100644 --- a/src/app/blog/_components/PostSection/PostSectionHeader/SortModalButton.tsx +++ b/src/app/blog/_components/PostSection/PostSectionHeader/SortModalButton.tsx @@ -1,40 +1,40 @@ -import ModalTriggerButton from "@/components/ModalTriggerButton"; -import { memo } from "react"; -import { POSTSECTION_TEXT, Sort } from ".."; - -interface SortModalButtonProps { - onClick: (sort: Sort) => void; - currentSort: string; -} - -const SortModalButton = ({ onClick, currentSort }: SortModalButtonProps) => { - return ( - - {POSTSECTION_TEXT.header.sortButton.sort.map((text) => ( - - ))} - - ); -}; - -export default memo(SortModalButton); +import ModalTriggerButton from "@/components/ModalTriggerButton"; +import { POSTSECTION_TEXT, Sort } from ".."; + +interface SortModalButtonProps { + onClick: (sort: Sort) => void; + currentSort: string; +} + +const SortModalButton = ({ onClick, currentSort }: SortModalButtonProps) => { + return ( + +
+ {POSTSECTION_TEXT.header.sortButton.sort.map((text) => { + const isSelected = text === currentSort; + return ( + + ); + })} +
+
+ ); +}; + +export default SortModalButton; diff --git a/src/app/blog/_components/PostSection/PostSectionHeader/index.tsx b/src/app/blog/_components/PostSection/PostSectionHeader/index.tsx index be36f71..19a45eb 100644 --- a/src/app/blog/_components/PostSection/PostSectionHeader/index.tsx +++ b/src/app/blog/_components/PostSection/PostSectionHeader/index.tsx @@ -1,4 +1,4 @@ -import type { CategoryList } from "@/types/posts.types"; +import type { Category } from "@/types/posts.types"; import { POSTSECTION_TEXT, Sort } from ".."; import CategoryModalButton from "./CategoryModalButton"; import SortModalButton from "./SortModalButton"; @@ -11,7 +11,7 @@ interface PostSectionHeaderProps { }; onClick: (sort: Sort) => void; currentSort: string; - categorys: CategoryList; + categorys: Category[]; } const PostSectionHeader = ({ diff --git a/src/app/blog/_components/PostSection/PostSectionMain/ArticleViewPost.tsx b/src/app/blog/_components/PostSection/PostSectionMain/ArticleViewPost.tsx index dbdbd23..e7ad0fb 100644 --- a/src/app/blog/_components/PostSection/PostSectionMain/ArticleViewPost.tsx +++ b/src/app/blog/_components/PostSection/PostSectionMain/ArticleViewPost.tsx @@ -20,8 +20,11 @@ const ArticleViewPost = ({ post, searchText }: ArticleViewPostProps) => { const handleGetPostContent = async () => { setLoading(true); - const encoded = encodeURIComponent(post.htmlFilePath); - const res = await fetch(`/api/posts?pathroot=${encoded}`, { + + const slug = post.href.split("/").pop() as string; + const category = post.category; + + const res = await fetch(`/api/posts?category=${category}&slug=${slug}`, { cache: "force-cache", }); @@ -33,63 +36,68 @@ const ArticleViewPost = ({ post, searchText }: ArticleViewPostProps) => { if (isArticleOpen) { return ( -
  • -
    +
  • +
    {loading ? ( - "로딩 중…" +
    + 게시글을 불러오는 중... +
    ) : ( )}
    -
    - -
    + +
  • ); } return ( -
  • -
    -
    +
  • { + handleArticleOpen(); + if (!postContent) handleGetPostContent(); + }} + className="group flex cursor-pointer flex-col overflow-hidden rounded-xl border border-neutral-200 bg-white hover:bg-zinc-50 dark:border-neutral-700 dark:bg-neutral-800/40 dark:hover:bg-zinc-900" + > +
    +
    -
    -

    +
    +

    -
    + +
    -
    -
    - {post.metadata.date} -
    -
    + +
    + {post.metadata.date} + {post.metadata.tags.length > 0 && ( + + )} +
    {post.metadata.tags.map((tag) => ( - - # {tag} + + #{tag} ))}
    -
    { - handleArticleOpen(); - handleGetPostContent(); - }} - className="w-full flex justify-center items-center hover:text-yellow-400 py-1 hover:bg-neutral-100 dark:hover:bg-neutral-700/50" - > - +
    +

  • ); diff --git a/src/app/blog/_components/PostSection/PostSectionMain/ListViewPost.tsx b/src/app/blog/_components/PostSection/PostSectionMain/ListViewPost.tsx index ea63569..9f98064 100644 --- a/src/app/blog/_components/PostSection/PostSectionMain/ListViewPost.tsx +++ b/src/app/blog/_components/PostSection/PostSectionMain/ListViewPost.tsx @@ -4,48 +4,50 @@ import type { Post } from "@/types/posts.types"; import Link from "next/link"; import { memo } from "react"; -// 본문 보기 컴포넌트 -interface ListViewPostPorps { +interface ListViewPostProps { post: Post; searchText: string; } -const ListViewPost = ({ post, searchText }: ListViewPostPorps) => { +const ListViewPost = ({ post, searchText }: ListViewPostProps) => { return (
  • -
    - -
    -
    -

    - -

    -
    - +
    +
    + {post.metadata.title}
    -
    -
    - {post.metadata.date} +
    +

    + +

    +
    +
    -
    - {post.metadata.tags.map((tag) => ( - - # {tag} - - ))} +
    + {post.metadata.date} + {post.metadata.tags.length > 0 && ( + + )} +
    + {post.metadata.tags.map((tag) => ( + + #{tag} + + ))} +
    -
    - +
    +
  • diff --git a/src/app/blog/_components/PostSection/PostSectionMain/ViewModalButton.tsx b/src/app/blog/_components/PostSection/PostSectionMain/ViewModalButton.tsx index ccba4e7..f741588 100644 --- a/src/app/blog/_components/PostSection/PostSectionMain/ViewModalButton.tsx +++ b/src/app/blog/_components/PostSection/PostSectionMain/ViewModalButton.tsx @@ -1,51 +1,61 @@ -import ModalTriggerButton from "@/components/ModalTriggerButton"; -import { memo } from "react"; -import { CurrentView } from "."; -import { POSTSECTION_TEXT } from ".."; - -// 목록 / 본문 보기 버튼 컴포넌트 -interface ViewModalButtonProps { - currentView: CurrentView; - onClick: (view: CurrentView) => void; -} - -const ViewModalButton = ({ onClick, currentView }: ViewModalButtonProps) => { - const viewButtonItems = POSTSECTION_TEXT.main.viewButtonItems; - - return ( - - {viewButtonItems.map((item) => ( - - ))} - - ); -}; - -export default memo(ViewModalButton); +import ModalTriggerButton from "@/components/ModalTriggerButton"; +import { CurrentView } from "."; +import { POSTSECTION_TEXT } from ".."; + +interface ViewModalButtonProps { + currentView: CurrentView; + onClick: (view: CurrentView) => void; +} + +const ViewModalButton = ({ onClick, currentView }: ViewModalButtonProps) => { + const viewButtonItems = POSTSECTION_TEXT.main.viewButtonItems; + + return ( + +
    + {viewButtonItems.map((item) => { + const isSelected = currentView.text === item.text; + + return ( + + ); + })} +
    +
    + ); +}; + +export default ViewModalButton; diff --git a/src/app/blog/_components/PostSection/PostSectionMain/index.tsx b/src/app/blog/_components/PostSection/PostSectionMain/index.tsx index 8086770..9b9e977 100644 --- a/src/app/blog/_components/PostSection/PostSectionMain/index.tsx +++ b/src/app/blog/_components/PostSection/PostSectionMain/index.tsx @@ -1,5 +1,5 @@ import type { Post } from "@/types/posts.types"; -import { JSX, memo, Suspense, useCallback, useState } from "react"; +import { JSX, Suspense, useCallback, useState } from "react"; import { POSTSECTION_TEXT } from ".."; import ArticleViewPost from "./ArticleViewPost"; import ListViewPost from "./ListViewPost"; @@ -61,4 +61,4 @@ const PostSectionMain = ({ posts, searchText }: PostSectionMainProps) => { ); }; -export default memo(PostSectionMain); +export default PostSectionMain; diff --git a/src/app/blog/_components/PostSection/index.tsx b/src/app/blog/_components/PostSection/index.tsx index c528ced..9e41f7a 100644 --- a/src/app/blog/_components/PostSection/index.tsx +++ b/src/app/blog/_components/PostSection/index.tsx @@ -5,7 +5,7 @@ import ArrowHeadSVG from "@/svg/ArrowHeadSVG"; import ArticleSVG from "@/svg/ArticleSVG"; import ListSVG from "@/svg/ListSVG"; import SearchSVG from "@/svg/SearchSVG"; -import type { CategoryList, Post } from "@/types/posts.types"; +import type { Category, Post } from "@/types/posts.types"; import { useCallback, useMemo, useState } from "react"; import PostSectionHeader from "./PostSectionHeader"; import PostSectionMain from "./PostSectionMain"; @@ -50,7 +50,7 @@ const DefaultSort = POSTSECTION_TEXT.header.sortButton.sort[0]; interface PostSectionProps { posts: Post[]; - categorys: CategoryList; + categorys: Category[]; } const PostSection = ({ posts, categorys }: PostSectionProps) => { @@ -106,7 +106,7 @@ const PostSection = ({ posts, categorys }: PostSectionProps) => { currentSort === DefaultSort ? filteredPosts : [...filteredPosts].reverse(); return ( -
    +
    { + return ( + +
    +

    + {title} +

    + +
    +
    + {postCount} posts + + {lastUpdated} 업데이트 +
    + + ); +}; + +export default SeriesCard; diff --git a/src/app/blog/_components/SeriesAside/index.tsx b/src/app/blog/_components/SeriesAside/index.tsx index 61b8cd0..a9eb479 100644 --- a/src/app/blog/_components/SeriesAside/index.tsx +++ b/src/app/blog/_components/SeriesAside/index.tsx @@ -1,68 +1,37 @@ "use client"; -import KeyboardArrowSVG from "@/svg/ArrowHeadSVG"; -import type { SeriesGroup } from "@/types/posts.types"; -import Link from "next/link"; -import { useState } from "react"; - -interface SeriesCardProps { - series: SeriesGroup; -} - -const SeriesCard = ({ series }: SeriesCardProps) => { - const [isOpen, setIsOpen] = useState(false); - - const toggleDropdown = () => setIsOpen((prev) => !prev); - - return ( -
  • -
    - {series.series} - -
    -
    -
    - {series.posts.map((el) => ( - -
    - {el.metadata.title} -
    - - ))} -
    -
    -
  • - ); -}; +import type { Series } from "@/types/posts.types"; +import SeriesCard from "./SeriesCard"; interface SeriesAsideProps { - series: SeriesGroup[]; + series: Series[]; } const SeriesAside = ({ series }: SeriesAsideProps) => { return ( -