Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Binary file added public/img/thumbnail/blog-thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/thumbnail/chim-planet-thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/thumbnail/default-thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/img/thumbnail/default.png
Binary file not shown.
Binary file added public/img/thumbnail/discord-bot-thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/thumbnail/fandom-k-thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/thumbnail/global-nomad-thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/thumbnail/maple-helper-thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/img/thumbnail/maplehelper.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/img/thumbnail/the-julge-thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 9 additions & 5 deletions src/app/api/posts/route.ts
Original file line number Diff line number Diff line change
@@ -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" },
Expand Down
11 changes: 3 additions & 8 deletions src/app/blog/[category]/[post]/page.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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 <PostContainer metadata={metadata} contentHtml={contentHtml} />;
return <PostContainer metadata={meta.metadata} contentHtml={contentHtml} />;
};

export default PostPage;
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>;
categorys: CategoryList;
categorys: Category[];
}

const CategoryModalButton = ({
Expand All @@ -21,24 +20,32 @@ const CategoryModalButton = ({
containerProps={{ onChange }}
modalRootId="tags-modal"
>
{categorys.map((el) => (
<div
key={el.name}
className="whitespace-nowrap p-1 text-sm text-neutral-600 dark:text-neutral-400 flex items-center"
>
<input
id={el.name}
type="checkbox"
defaultChecked={currentCategories.includes(el.name)}
className="size-4 mr-2 appearance-none rounded fill-neutral-300 dark:fill-neutral-700 text-neutral-900 dark:text-neutral-100 bg-transparent border-neutral-300 dark:border-neutral-700 disabled:!bg-neutral-300 dark:disabled:!bg-neutral-700 disabled:cursor-not-allowed focus:ring-transparent focus:ring-offset-transparent focus:outline-neutral-700 dark:focus:outline-neutral-300 cursor-pointer"
/>{" "}
<label className=" cursor-pointer w-20" htmlFor={el.name}>
{el.name}
</label>
</div>
))}
<div className="flex flex-col gap-1">
{categorys.map((el) => {
const isChecked = currentCategories.includes(el.name);
return (
<label
key={el.name}
htmlFor={el.name}
className={`group flex w-full cursor-pointer items-center gap-3 whitespace-nowrap rounded-lg px-3 py-2 outline-none transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800/50 ${
isChecked
? "typo-14-b text-blue-600 dark:text-blue-400"
: "typo-14-m text-neutral-600 dark:text-neutral-400"
}`}
>
<input
id={el.name}
type="checkbox"
defaultChecked={isChecked}
className="size-4 shrink-0 cursor-pointer rounded border-neutral-300 bg-transparent text-blue-600 accent-blue-600 focus:ring-0 focus:ring-offset-0 dark:border-neutral-700 dark:bg-transparent dark:text-blue-400 dark:accent-blue-400"
/>
<span className="flex grow items-center">{el.name}</span>
</label>
);
})}
</div>
</ModalTriggerButton>
);
};

export default memo(CategoryModalButton);
export default CategoryModalButton;
Original file line number Diff line number Diff line change
@@ -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 (
<ModalTriggerButton
title={POSTSECTION_TEXT.header.sortButton.text}
icon={POSTSECTION_TEXT.header.sortButton.svg}
closeOnSelfClick={true}
modalRootId="sort-modal"
>
{POSTSECTION_TEXT.header.sortButton.sort.map((text) => (
<button
onClick={() => onClick(text)}
type="button"
className="flex grow items-center px-2 py-1.5 pr-10 w-full text-left rounded select-none outline-none hover:bg-neutral-100 dark:hover:bg-neutral-800/70 focus:bg-neutral-100 dark:focus:bg-neutral-800/70"
key={text}
>
<span
className={` text-sm whitespace-nowrap flex grow items-center gap-x-2 ${
text === currentSort
? "text-neutral-900 dark:text-[#eafe7c] font-semibold"
: "text-neutral-600 dark:text-neutral-400"
}`}
>
{text}
</span>
</button>
))}
</ModalTriggerButton>
);
};

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 (
<ModalTriggerButton
title={POSTSECTION_TEXT.header.sortButton.text}
icon={POSTSECTION_TEXT.header.sortButton.svg}
closeOnSelfClick={true}
modalRootId="sort-modal"
>
<div className="flex flex-col gap-1">
{POSTSECTION_TEXT.header.sortButton.sort.map((text) => {
const isSelected = text === currentSort;
return (
<button
onClick={() => onClick(text)}
type="button"
key={text}
className={`flex w-full items-center gap-3 whitespace-nowrap rounded-lg px-3 py-2 text-left outline-none transition-colors hover:bg-neutral-100 focus:bg-neutral-100 dark:hover:bg-neutral-800/50 dark:focus:bg-neutral-800/50 ${
isSelected
? "typo-14-b text-blue-600 dark:text-blue-400"
: "typo-14-m text-neutral-600 dark:text-neutral-400"
}`}
>
<span className="flex grow items-center">{text}</span>
</button>
);
})}
</div>
</ModalTriggerButton>
);
};
export default SortModalButton;
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,7 +11,7 @@ interface PostSectionHeaderProps {
};
onClick: (sort: Sort) => void;
currentSort: string;
categorys: CategoryList;
categorys: Category[];
}

const PostSectionHeader = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
});

Expand All @@ -33,63 +36,68 @@ const ArticleViewPost = ({ post, searchText }: ArticleViewPostProps) => {

if (isArticleOpen) {
return (
<li className="border group flex flex-col items-center gap-2 rounded-lg bg-white text-neutral-90 dark:bg-neutral-800/40 border-neutral-200 dark:border-neutral-700">
<article className="mx-auto max-w-[886px] p-4 markdown-body">
<li className="flex flex-col overflow-hidden rounded-xl border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-800/40">
<article className="markdown-body mx-auto w-full max-w-[886px] p-5 sm:p-8">
{loading ? (
"로딩 중…"
<div className="flex h-32 items-center justify-center typo-14-m text-neutral-500">
게시글을 불러오는 중...
</div>
) : (
<HighlightCode metadata={post.metadata} contentHtml={postContent} />
)}
</article>
<div
<button
onClick={handleArticleOpen}
className="w-full flex justify-center items-center hover:text-yellow-400 py-1 hover:bg-neutral-100 dark:hover:bg-neutral-700/50"
className="group flex w-full cursor-pointer items-center justify-center border-t border-neutral-200 bg-white py-3 hover:bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-800/40 dark:hover:bg-neutral-700/50"
>
<ArrowHeadSVG className="size-6 rotate-180" />
</div>
<ArrowHeadSVG className="size-5 shrink-0 rotate-180 text-neutral-400 transition-transform group-hover:-translate-y-1 dark:text-neutral-500" />
</button>
</li>
);
}

return (
<li className="border group flex flex-col items-center gap-2 rounded-lg bg-white text-neutral-90 dark:bg-neutral-800/40 border-neutral-200 dark:border-neutral-700">
<div className="flex px-4 pt-2 md:px-8 md:pt-3 items-center gap-6 ">
<div className="overflow-hidden hidden sm:block flex-none w-[150px] rounded">
<li
onClick={() => {
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"
>
<div className="flex items-center gap-5 p-4 sm:gap-6 sm:p-5">
<div className="hidden w-[140px] shrink-0 overflow-hidden rounded-lg border border-neutral-100 sm:block dark:border-neutral-700">
<img
className="group-hover:scale-105 transition-all duration-500 aspect-[15/8] h-20 object-cover"
src="/img/thumbnail.png"
alt=""
className="aspect-[16/9] h-full w-full object-cover"
src={post.metadata.image}
alt={post.metadata.title}
/>
</div>
<div>
<h2 className="mb-1 text-base sm:text-lg break-words text-neutral-900 dark:text-neutral-100 font-medium line-clamp-2">
<div className="flex min-w-0 flex-1 flex-col justify-center">
<h2 className="mb-1.5 line-clamp-2 text-pretty typo-18-b text-neutral-900 group-hover:text-blue-600 dark:text-neutral-100 dark:group-hover:text-blue-400">
<HighlightText text={post.metadata.title} query={searchText} />
</h2>
<div className="mb-2 sm:mb-2 text-neutral-600 dark:text-neutral-400 text-sm sm:text-base overflow-hidden text-ellipsis break-all line-clamp-2 leading-normal">

<div className="mb-3 line-clamp-2 text-pretty typo-14-body-m text-neutral-600 dark:text-neutral-400">
<HighlightText text={post.excerpt} query={searchText} />
</div>
<div className="relative z-[1] flex w-full items-center gap-x-6 gap-y-2">
<div className="text-xs sm:text-sm text-neutral-600 dark:text-neutral-400">
{post.metadata.date}
</div>
<div className="flex flex-row gap-3 text-neutral-600 dark:text-neutral-400">

<div className="flex items-center gap-2 typo-13-m text-neutral-500 dark:text-neutral-400">
<span className="shrink-0">{post.metadata.date}</span>
{post.metadata.tags.length > 0 && (
<span className="size-1 shrink-0 rounded-full bg-neutral-300 dark:bg-neutral-600"></span>
)}
<div className="flex gap-2 truncate">
{post.metadata.tags.map((tag) => (
<span key={tag} className="text-sm line-clamp-1">
# {tag}
<span key={tag} className="truncate">
#{tag}
</span>
))}
</div>
</div>
</div>
</div>
<div
onClick={() => {
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"
>
<ArrowHeadSVG className="size-6 " />
<div className="flex w-full items-center justify-center border-t border-neutral-100 py-2 dark:border-neutral-700/50">
<ArrowHeadSVG className="size-5 shrink-0 text-neutral-400 transition-transform group-hover:translate-y-1 dark:text-neutral-500" />
</div>
</li>
);
Expand Down
Loading
Loading