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
13 changes: 5 additions & 8 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { Button } from '@workspace/ui/components/button';
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';
import { getInfiniteScrollData } from '@/features/post/post-list';
import { PostInfiniteScroll } from '@/features/post/post-list';

export default async function Home() {
const { data, nextCursor, hasMore } = await getInfiniteScrollData('', 10);

return (
<>
<Button
divClassName="text-right mr-8"
buttonClassName="bg-black text-white font-semibold text-base p-2 rounded"
>
<Button asChild>
<Link href="/posts/write">새 글 작성</Link>
</Button>

<InfiniteScroll
<PostInfiniteScroll
postList={data}
lastPostId={nextCursor}
hasMore={hasMore}
Expand Down
8 changes: 7 additions & 1 deletion apps/web/entities/post/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export { createPost, getPostById, getPosts } from './api/post-api';
export { ERROR_MESSAGES } from './lib/error-messages';
export { validateFormField } from './lib/form-validation';
export type { CreatePostDTO, Post, UpdatePostDTO } from './model/post-types';
export type {
CreatePostDTO,
GetPostsCursor,
Post,
UpdatePostDTO,
} from './model/post-types';
export { mapPostToViewModel } from './model/post-view-model';
7 changes: 7 additions & 0 deletions apps/web/entities/post/model/post-view-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Post } from '@/entities/post';
import { formatToLocaleDate } from '@/shared/lib';

export const mapPostToViewModel = (post: Post) => ({
...post,
localeCreatedAt: formatToLocaleDate(post.createdAt),
});
25 changes: 7 additions & 18 deletions apps/web/features/post/post-list/api/post-infinite-scroll.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
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');
import { GetPostsCursor } from '@/entities/post';
import { httpClient } from '@/shared/api';

export async function getInfiniteScrollData(
cursor?: string,
limit?: number
): Promise<GetPostsCursor> {
const followingURL = `/posts/infinite?${new URLSearchParams({
...(cursor && { cursor }), // cursor가 있으면 추가, undefined or null이면 추가 안 함
...(limit && { limit: limit.toString() }), // limit가 있으면 추가
})}`;

const response = await fetch(`${BASE_URL}${followingURL}`, {
return httpClient<GetPostsCursor>('/posts/infinite', {
method: 'GET',
headers: commonHeaders,
queryParams: {
cursor,
limit,
},
});

if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}

return response.json();
}
3 changes: 3 additions & 0 deletions apps/web/features/post/post-list/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { getInfiniteScrollData } from './api/post-infinite-scroll';
export { PostInfiniteScroll } from './ui/post-infinite-scroll';
export { PostItem } from './ui/post-item';
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, 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';
import { mapPostToViewModel } from '@/entities/post';
import { Post } from '@/entities/post';
import { getInfiniteScrollData } from '@/features/post/post-list';
import { PostItem } from '@/features/post/post-list';

type InfiniteScrollProps = {
type PostInfiniteScrollProps = {
postList: Post[];
lastPostId: string | null;
hasMore: boolean;
};

export function InfiniteScroll({
export function PostInfiniteScroll({
postList,
lastPostId,
hasMore,
}: InfiniteScrollProps) {
}: PostInfiniteScrollProps) {
const [loading, setLoading] = useState(false);
const [posts, setPosts] = useState<Post[]>(postList);
const [cursor, setCursor] = useState(lastPostId ? lastPostId : null);
Expand Down Expand Up @@ -55,24 +55,26 @@ export function InfiniteScroll({
};
}, [loadMorePosts]);

const postViewModels = useMemo(
() => posts.map((post) => mapPostToViewModel(post)),
[posts]
);

return (
<div>
{posts.map(({ id, title, content, author, createdAt }) => {
const localeCreatedAt = formatToLocaleDate(createdAt);
return (
<PostItem
key={id}
linkPostId={id}
title={title}
content={content}
author={author}
localeCreatedAt={localeCreatedAt}
/>
);
})}
{postViewModels.map((postViewModel) => (
<PostItem
key={postViewModel.id}
linkPostId={postViewModel.id}
title={postViewModel.title}
content={postViewModel.content}
author={postViewModel.author}
localeCreatedAt={postViewModel.localeCreatedAt}
/>
))}
<h3
ref={target}
className="mx-8 mb-4 mt-8 text-center text-9xl font-semibold"
className="mx-8 mb-4 mt-8 text-center text-2xl font-semibold"
>
{posts.at(-1)?.id === cursor
? '*************더 많은 게시글 로딩 중****************'
Expand Down
2 changes: 1 addition & 1 deletion apps/web/features/post/post-list/ui/post-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function PostItem({
<h3 className="pb-4 font-semibold">{title}</h3>
<hr />
<p className="pt-4">{content}</p>
<br></br>
<br />
<div className="flex flex-col gap-2">
<span className="rounded border border-gray-300 bg-gray-300 px-2 py-0.5 italic">
{author}
Expand Down
19 changes: 17 additions & 2 deletions apps/web/shared/api/http-client.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
export const BASE_URL = 'http://localhost:4000/api';

interface HttpClientOptions extends RequestInit {
queryParams?: Record<string, string | number | undefined>;
}

export const httpClient = async <T>(
endpoint: string,
options?: RequestInit
options?: HttpClientOptions
): Promise<T> => {
const response = await fetch(`${BASE_URL}${endpoint}`, {
const url = new URL(`${BASE_URL}${endpoint}`);

// queryParams가 있을 경우 URL에 추가
if (options?.queryParams) {
Object.entries(options.queryParams).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, String(value));
}
});
}

const response = await fetch(url.toString(), {
headers: {
'Content-Type': 'application/json',
},
Expand Down
27 changes: 0 additions & 27 deletions apps/web/shared/ui/button.tsx

This file was deleted.