From 3abaf80597986ffadc0319aae288a8108e67cb9e Mon Sep 17 00:00:00 2001 From: rmkim7 Date: Fri, 21 Feb 2025 01:34:36 +0900 Subject: [PATCH 1/4] =?UTF-8?q?modify:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/components/stykle.css | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/web/components/stykle.css diff --git a/apps/web/components/stykle.css b/apps/web/components/stykle.css deleted file mode 100644 index e69de29..0000000 From 1eae9429022ccdcbd55ff4501abdf59dc5e94950 Mon Sep 17 00:00:00 2001 From: rmkim7 Date: Mon, 24 Feb 2025 15:54:22 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20Post=20=EA=B3=B5=EC=9A=A9=20fetch?= =?UTF-8?q?=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/entities/post/model/api.ts | 40 +++++++++++++++++++++++++++++ apps/web/shared/api/http-client.ts | 19 ++++++++++++++ apps/web/shared/types/post-types.ts | 26 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 apps/web/entities/post/model/api.ts create mode 100644 apps/web/shared/api/http-client.ts create mode 100644 apps/web/shared/types/post-types.ts diff --git a/apps/web/entities/post/model/api.ts b/apps/web/entities/post/model/api.ts new file mode 100644 index 0000000..0982991 --- /dev/null +++ b/apps/web/entities/post/model/api.ts @@ -0,0 +1,40 @@ +import { httpClient } from '@/shared/api/http-client'; +import { CreatePostDTO, Post, UpdatePostDTO } from '@/shared/types/post-types'; + +// 게시글 생성 (POST /posts) +export const createPost = async (postData: CreatePostDTO): Promise => { + return httpClient('/posts', { + method: 'POST', + body: JSON.stringify(postData), + }); +}; + +// 모든 게시글 조회 (GET /posts) +export const getPosts = async (): Promise => { + return httpClient('/posts'); +}; + +// 특정 게시글 조회 (GET /posts/{id}) +export const getPostById = async (postId: string): Promise => { + return httpClient(`/posts/${postId}`); +}; + +// 게시글 수정 (PUT /posts/{id}) +export const updatePost = async ( + postId: string, + updatedData: UpdatePostDTO +): Promise => { + return httpClient(`/posts/${postId}`, { + method: 'PUT', + body: JSON.stringify(updatedData), + }); +}; + +// 게시글 삭제 (DELETE /posts/{id}) +export const deletePost = async ( + postId: string +): Promise<{ message: string }> => { + return httpClient<{ message: string }>(`/posts/${postId}`, { + method: 'DELETE', + }); +}; diff --git a/apps/web/shared/api/http-client.ts b/apps/web/shared/api/http-client.ts new file mode 100644 index 0000000..ad9962d --- /dev/null +++ b/apps/web/shared/api/http-client.ts @@ -0,0 +1,19 @@ +export const BASE_URL = 'http://localhost:4000/api'; + +export const httpClient = async ( + endpoint: string, + options?: RequestInit +): Promise => { + const response = await fetch(`${BASE_URL}${endpoint}`, { + headers: { + 'Content-Type': 'application/json', + }, + ...options, + }); + + if (!response.ok) { + throw new Error(`API 요청 실패: ${response.status}`); + } + + return response.json(); +}; diff --git a/apps/web/shared/types/post-types.ts b/apps/web/shared/types/post-types.ts new file mode 100644 index 0000000..3889be9 --- /dev/null +++ b/apps/web/shared/types/post-types.ts @@ -0,0 +1,26 @@ +export interface Post { + id: string; + title: string; + content: string; + author: string; + createdAt: string; + updatedAt?: string; +} + +export type CreatePostDTO = Pick; +export type UpdatePostDTO = Partial; + +interface CursorPaginationResponse { + data: T[]; + nextCursor: string | null; + hasMore: boolean; +} +interface OffsetPaginationResponse { + data: T[]; + currentPage: number; + totalPages: number; + hasMore: boolean; +} + +export type GetPostsCursor = CursorPaginationResponse; +export type GetPostsOffset = OffsetPaginationResponse; From ac7905b59cfb1bf9a7a9eb8166b379576c6ddf2e Mon Sep 17 00:00:00 2001 From: rmkim7 Date: Mon, 24 Feb 2025 15:56:44 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9A=A9=20UI=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9C=A0=ED=8B=B8=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/shared/lib/format-date.ts | 4 ++++ apps/web/shared/ui/button.tsx | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 apps/web/shared/lib/format-date.ts create mode 100644 apps/web/shared/ui/button.tsx diff --git a/apps/web/shared/lib/format-date.ts b/apps/web/shared/lib/format-date.ts new file mode 100644 index 0000000..5e8f304 --- /dev/null +++ b/apps/web/shared/lib/format-date.ts @@ -0,0 +1,4 @@ +export function formatToLocaleDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleString('ko-KR'); +} diff --git a/apps/web/shared/ui/button.tsx b/apps/web/shared/ui/button.tsx new file mode 100644 index 0000000..60d410e --- /dev/null +++ b/apps/web/shared/ui/button.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { MouseEventHandler } from 'react'; + +interface ButtonProps { + divClassName?: string; + buttonClassName?: string; + onClick?: MouseEventHandler; + children: React.ReactNode; +} + +const Button = ({ + divClassName = '', + buttonClassName = '', + onClick, + children, +}: ButtonProps) => { + return ( +
+ +
+ ); +}; + +export { Button }; From 8ca94b4e720a1900f62888914762603725ce0a59 Mon Sep 17 00:00:00 2001 From: rmkim7 Date: Mon, 24 Feb 2025 15:58:04 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/page.tsx | 35 +++++--- .../post-list/api/post-infinite-scroll.ts | 26 ++++++ .../post/post-list/ui/infinite-scroll.tsx | 83 +++++++++++++++++++ .../features/post/post-list/ui/post-item.tsx | 26 ++++++ 4 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 apps/web/features/post/post-list/api/post-infinite-scroll.ts create mode 100644 apps/web/features/post/post-list/ui/infinite-scroll.tsx create mode 100644 apps/web/features/post/post-list/ui/post-item.tsx diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 58ee290..473bade 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,15 +1,26 @@ -export default function Page() { +import Link from 'next/link'; + +import { getInfiniteScrollData } from '@/features/post/post-list/api/post-infinite-scroll'; +import { InfiniteScroll } from '@/features/post/post-list/ui/infinite-scroll'; +import { Button } from '@/shared/ui/button'; + +export default async function Home() { + const { data, nextCursor, hasMore } = await getInfiniteScrollData('', 10); + return ( -
-
-

- Hello World -

- {/* */} -
- Test -
-
-
+ <> + + + + ); } diff --git a/apps/web/features/post/post-list/api/post-infinite-scroll.ts b/apps/web/features/post/post-list/api/post-infinite-scroll.ts new file mode 100644 index 0000000..00ef53c --- /dev/null +++ b/apps/web/features/post/post-list/api/post-infinite-scroll.ts @@ -0,0 +1,26 @@ +import { BASE_URL } from '@/shared/api/http-client'; +import { GetPostsCursor } from '@/shared/types/post-types'; + +const commonHeaders = new Headers(); +commonHeaders.append('Content-Type', 'application/json'); + +export async function getInfiniteScrollData( + cursor?: string, + limit?: number +): Promise { + const followingURL = `/posts/infinite?${new URLSearchParams({ + ...(cursor && { cursor }), // cursor가 있으면 추가, undefined or null이면 추가 안 함 + ...(limit && { limit: limit.toString() }), // limit가 있으면 추가 + })}`; + + const response = await fetch(`${BASE_URL}${followingURL}`, { + method: 'GET', + headers: commonHeaders, + }); + + if (!response.ok) { + throw new Error(`${response.status} ${response.statusText}`); + } + + return response.json(); +} diff --git a/apps/web/features/post/post-list/ui/infinite-scroll.tsx b/apps/web/features/post/post-list/ui/infinite-scroll.tsx new file mode 100644 index 0000000..768d82b --- /dev/null +++ b/apps/web/features/post/post-list/ui/infinite-scroll.tsx @@ -0,0 +1,83 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { getInfiniteScrollData } from '@/features/post/post-list/api/post-infinite-scroll'; +import { PostItem } from '@/features/post/post-list/ui/post-item'; +import { formatToLocaleDate } from '@/shared/lib/format-date'; +import { Post } from '@/shared/types/post-types'; + +type InfiniteScrollProps = { + postList: Post[]; + lastPostId: string | null; + hasMore: boolean; +}; + +export function InfiniteScroll({ + postList, + lastPostId, + hasMore, +}: InfiniteScrollProps) { + const [loading, setLoading] = useState(false); + const [posts, setPosts] = useState(postList); + const [cursor, setCursor] = useState(lastPostId ? lastPostId : null); + + const loadMorePosts = useCallback(async () => { + if (!hasMore || loading || !cursor) return; + + setLoading(true); + + try { + const { data } = await getInfiniteScrollData(cursor, 10); + setPosts((prevPosts) => [...prevPosts, ...data]); + const lastItem = data.at(-1); + setCursor(lastItem ? lastItem.id : null); + } catch (error) { + console.error('Failed to fetch more posts:', error); + } finally { + setLoading(false); + } + }, [cursor, hasMore, loading]); + + const target = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver((entries) => { + const firstEntry = entries[0]; + if (firstEntry?.isIntersecting) loadMorePosts(); + }); + + const currentTarget = target.current; + if (currentTarget) observer.observe(currentTarget); + + return () => { + if (currentTarget) observer.unobserve(currentTarget); + }; + }, [loadMorePosts]); + + return ( +
+ {posts.map(({ id, title, content, author, createdAt }) => { + const localeCreatedAt = formatToLocaleDate(createdAt); + return ( + + ); + })} +

+ {posts.at(-1)?.id === cursor + ? '*************더 많은 게시글 로딩 중****************' + : ''} +

+
+ ); +} diff --git a/apps/web/features/post/post-list/ui/post-item.tsx b/apps/web/features/post/post-list/ui/post-item.tsx new file mode 100644 index 0000000..b499695 --- /dev/null +++ b/apps/web/features/post/post-list/ui/post-item.tsx @@ -0,0 +1,26 @@ +import Link from 'next/link'; + +export function PostItem({ + linkPostId = '', + title = '', + content = '', + author = '', + localeCreatedAt = '', +}) { + return ( +
+ +

{title}

+
+

{content}

+

+
+ + {author} + + +
+ +
+ ); +}