Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: db migration #97

Merged
merged 4 commits into from
Jan 29, 2025
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
21 changes: 14 additions & 7 deletions apps/blog/app/(blog)/posts/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { MdxRemote } from "@repo/mdx";
import { Box, Flex, Stack } from "@xionwcfm/xds";
import { Chip } from "@xionwcfm/xds/chip";
import { last } from "es-toolkit";
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { getAllPosts, getPost } from "~/entities/post/model/post.service";
import { getAllPosts } from "~/entities/post/api/getAllPosts";
import { getPostBySlug } from "~/entities/post/api/getPostBySlug";
import { PostDetailAuthorAndDate } from "~/entities/post/ui/post/PostDetailAuthorAndDate";
import { PostDetailAuthorWithChar } from "~/entities/post/ui/post/PostDetailAuthorWithChar";
import { PostDetailTitle } from "~/entities/post/ui/post/PostDetailTitle";
Expand All @@ -20,22 +22,25 @@ type PostProps = {

export default async function Post({ params }: PostProps) {
const slug = (await params).slug;
const post = await getPost(slug);
const lastSlug = last(slug) ?? "";
const post = await getPostBySlug(lastSlug);

if (!post) {
return redirect("/");
}

return (
<Stack as="main" px={{ initial: "16", md: "0" }}>
<Box my="16">
<PostDetailTitle>{post.title}</PostDetailTitle>
</Box>

<Flex>
<Chip>{post.categories}</Chip>
<Chip>{post.category}</Chip>
</Flex>

<Box my="16">
<PostDetailAuthorAndDate date={post.releaseDate} />
<PostDetailAuthorAndDate date={post.release_date} />
</Box>

<Border className=" my-16" />
Expand All @@ -57,19 +62,21 @@ export default async function Post({ params }: PostProps) {

export const generateMetadata = async ({ params }: PostProps): Promise<Metadata> => {
const slug = (await params).slug;
const post = await getPost(slug);
const lastSlug = last(slug) ?? "";
const post = await getPostBySlug(lastSlug);
if (!post) {
throw new Error("Post not found");
}
const url = `${BASE_SITE_URL}/posts/${post.filePath.join("/")}`;

const url = `${BASE_SITE_URL}/posts/${post.category}/${post.slug}`;
const metaData = createMetadata({ description: post.description, title: post.title, url });
return metaData;
};

export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.filePath,
slug: [post.category, post.slug],
}));
}

Expand Down
21 changes: 18 additions & 3 deletions apps/blog/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { Paragraph, Stack } from "@xionwcfm/xds";
import { getAllPostsSortedByReleaseDate } from "~/entities/post/model/post.service";
import { compareDesc, format, isAfter, parseISO } from "date-fns";
import { getAllPosts } from "~/entities/post/api/getAllPosts";
import { PostCard } from "~/entities/post/ui/post/PostCard";
import { AUTHOR_NICKNAME } from "~/shared/constants";
import { ROUTES } from "~/shared/routes";
import { Border } from "~/shared/ui/common/Border";
import { MainTitle } from "~/shared/ui/common/MainTitle";
import { Footer } from "~/widgets/footer";
import { StaticHeader } from "~/widgets/header/static-header";

export default async function RootPage() {
const posts = await getAllPostsSortedByReleaseDate();
const rawPosts = await getAllPosts();
const posts = rawPosts
.filter((post) => post.authority === "viewer" && isAfter(new Date(), parseISO(post.release_date)))
.sort((a, b) => compareDesc(parseISO(a.release_date), parseISO(b.release_date)));

const currentPostTitle = `${AUTHOR_NICKNAME}의 최신 포스트 보기`;

return (
Expand All @@ -26,7 +33,15 @@ export default async function RootPage() {
</Stack>
<Stack my={"28"} gap={"16"}>
{posts.map((post) => (
<PostCard key={post.title} post={post} />
<PostCard
key={post.title}
title={post.title}
category={post.category}
description={post.description}
href={ROUTES.postDetail([post.category, post.slug])}
authorNickname={AUTHOR_NICKNAME}
date={format(parseISO(post.release_date), "yyyy.MM.dd. HH:mm")}
/>
))}
</Stack>
</Stack>
Expand Down
10 changes: 6 additions & 4 deletions apps/blog/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { MetadataRoute } from "next";
import { cookies } from "next/headers";
import { getAllPosts } from "~/entities/post/api/getAllPosts";
import { BASE_SITE_URL } from "~/shared/constants";
import { ROUTES } from "~/shared/routes";
import { getAllPosts } from "../src/entities/post/model/post.service";
import { BASE_SITE_URL } from "../src/shared/constants";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
const cookieStore = await cookies();
const posts = await getAllPosts(cookieStore);

const postUrls = posts.map((post) => ({
url: `${BASE_SITE_URL}${ROUTES.postDetail(post.filePath)}`,
url: `${BASE_SITE_URL}${ROUTES.postDetail([post.category, post.slug])}`,
lastModified: new Date(),
Comment on lines +8 to 13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and caching strategy

The sitemap generation lacks error handling and could benefit from caching to improve performance.

+ import { cache } from 'react';
+
+ const CACHE_REVALIDATE_SECONDS = 3600; // 1 hour
+
+ const getCachedPosts = cache(async (cookieStore: Awaited<ReturnType<typeof cookies>>) => {
+   try {
+     return await getAllPosts(cookieStore);
+   } catch (error) {
+     console.error('Failed to generate sitemap:', error);
+     return [];
+   }
+ });

  export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
    const cookieStore = await cookies();
-   const posts = await getAllPosts(cookieStore);
+   const posts = await getCachedPosts(cookieStore);

Committable suggestion skipped: line range outside the PR's diff.

}));

Expand Down
8 changes: 4 additions & 4 deletions apps/blog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"es-toolkit": "^1.31.0",
"motion": "^12.0.5",
"next": "15.1.3",
"motion": "^12.0.6",
"next": "15.1.6",
"next-mdx-remote": "^5.0.0",
"react": "catalog:react18",
"react-dom": "catalog:react18",
Expand Down Expand Up @@ -70,14 +70,14 @@
"postcss": "^8",
"react-fast-marquee": "^1.6.5",
"sharp": "^0.33.5",
"shiki": "^1.24.0",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5",
"vite": "^6.0.11",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.0.4",
"vitest-fetch-mock": "^0.4.3",
"ws": "^8.18.0",
"shiki": "^1.24.0"
"ws": "^8.18.0"
}
}
2 changes: 1 addition & 1 deletion apps/blog/posts/frontend/single-flight.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,4 @@ const tanstack = useMutation({

이번에는 프론트엔드에서 발생할 수 있는 사용자 행동에 의한 API 호출 문제를 다루어보았습니다. 개인적으로는 해당 문제를 일정의 압박에 쫓기며 머리를 짜내 useRef를 통해서 구현했었던 경험이 있는데요 동작은 잘됐지만 코드복잡도가 크게 올라갔던 기억이 있어 다른 방법들을 탐구해보았습니다.

그럼 오늘은 이만 마치도록 하겠습니다. **읽어주셔서 감사합니다.**
그럼 오늘은 이만 마치도록 하겠습니다. **읽어주셔서 감사합니다.**
14 changes: 14 additions & 0 deletions apps/blog/src/entities/post/api/getAllPosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "server-only";
import { createServerSupabaseClient } from "@repo/database/server";
import type { cookies } from "next/headers";

export const getAllPosts = async (cookieStore?: Awaited<ReturnType<typeof cookies>>) => {
const supabase = await createServerSupabaseClient(cookieStore);
const { data, error } = await supabase.from("posts").select("*");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding pagination and column selection

The current implementation fetches all posts and all columns at once, which could lead to performance issues as the dataset grows.

Consider:

  1. Adding pagination parameters
  2. Explicitly selecting only needed columns
- const { data, error } = await supabase.from("posts").select("*");
+ const { data, error } = await supabase
+   .from("posts")
+   .select('id, title, slug, category, created_at, updated_at')
+   .order('created_at', { ascending: false })
+   .range(0, 9);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data, error } = await supabase.from("posts").select("*");
const { data, error } = await supabase
.from("posts")
.select('id, title, slug, category, created_at, updated_at')
.order('created_at', { ascending: false })
.range(0, 9);


if (error) {
throw error;
}

return data;
};
14 changes: 14 additions & 0 deletions apps/blog/src/entities/post/api/getPostBySlug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "server-only";
import { createServerSupabaseClient } from "@repo/database/server";
import type { cookies } from "next/headers";

export const getPostBySlug = async (slug: string, cookieStore?: Awaited<ReturnType<typeof cookies>>) => {
const supabase = await createServerSupabaseClient(cookieStore);
const { data, error } = await supabase.from("posts").select("*").eq("slug", slug).single();

if (error) {
throw error;
}

return data;
};
Comment on lines +5 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type safety and input validation

The function lacks type safety for the returned data and input validation for the slug parameter.

+ import { type Post } from './types';
+ 
+ const isValidSlug = (slug: string) => {
+   return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug);
+ };
+
- export const getPostBySlug = async (slug: string, cookieStore?: Awaited<ReturnType<typeof cookies>>) => {
+ export const getPostBySlug = async (
+   slug: string,
+   cookieStore?: Awaited<ReturnType<typeof cookies>>
+ ): Promise<Post> => {
+   if (!isValidSlug(slug)) {
+     throw new Error('Invalid slug format');
+   }
+
    const supabase = await createServerSupabaseClient(cookieStore);
-   const { data, error } = await supabase.from("posts").select("*").eq("slug", slug).single();
+   const { data, error } = await supabase
+     .from("posts")
+     .select('id, title, slug, category, content, created_at, updated_at')
+     .eq("slug", slug)
+     .single();

    if (error) {
-     throw error;
+     throw new Error(`Failed to fetch post with slug ${slug}: ${error.message}`);
    }

    return data;
  };

Committable suggestion skipped: line range outside the PR's diff.

12 changes: 0 additions & 12 deletions apps/blog/src/entities/post/model/post-service.test.ts

This file was deleted.

21 changes: 0 additions & 21 deletions apps/blog/src/entities/post/model/post.model.ts

This file was deleted.

102 changes: 0 additions & 102 deletions apps/blog/src/entities/post/model/post.service.ts

This file was deleted.

28 changes: 14 additions & 14 deletions apps/blog/src/entities/post/ui/post/PostCard.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { formatDate } from "@repo/date/format-date";

import { Box, Paragraph, Stack } from "@xionwcfm/xds";
import Link from "next/link";
import { AUTHOR_NICKNAME } from "~/shared/constants";
import { ROUTES } from "~/shared/routes";
import type { PostWithFrontmatterType } from "../../model/post.model";

type PostCardProps = {
post: PostWithFrontmatterType;
date: string;
title: string;
category: string;
description: string;
href: string;
authorNickname: string;
};

export const PostCard = (props: PostCardProps) => {
const { post } = props;
const date = formatDate(post.releaseDate, "yyyy.MM.dd. HH:mm");
const label = `ReadMore : ${post.title}`;
const { date, title, category, description, href, authorNickname } = props;
const label = `ReadMore : ${title}`;

return (
<Stack className=" py-16 px-12 transition-all rounded-[14px] duration-300 hover:opacity-80 hover:bg-neutral-200 active:opacity-56 active:scale-[0.99] ">
<Box>
<Paragraph color={"gray-600"} size={"3"} responsive={true}>
{post.categories}
{category}
</Paragraph>
</Box>
<Link href={ROUTES.postDetail(post.filePath)} aria-label={label} title={label}>
<Link href={href} aria-label={label} title={label}>
<Stack>
<Paragraph
as="h2"
Expand All @@ -31,13 +31,13 @@ export const PostCard = (props: PostCardProps) => {
responsive={true}
overflow={"ellipsis"}
>
{post.title}
{title}
</Paragraph>
<Paragraph responsive={true} my="12" leading={"loose"} weight={"thin"} size={"4"} color={"neutral-600"}>
{post.description}
{description}
</Paragraph>
<Paragraph responsive={true} mt="4" size={"4"} weight={"medium"} color={"neutral-700"}>
{AUTHOR_NICKNAME}
{authorNickname}
</Paragraph>
<Paragraph responsive={true} as="time" size={"3"} weight={"thin"} color={"neutral-600"}>
{date}
Expand Down
Loading
Loading