Skip to content

대댓글 수정 및 수정 폼 추가 #34

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

Open
wants to merge 7 commits into
base: feature-group/comment
Choose a base branch
from
Open
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 .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm lint-staged
pnpm lint-staged
32 changes: 1 addition & 31 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1 @@
import '@workspace/ui/globals.css';

import { Geist, Geist_Mono } from 'next/font/google';

import { Providers } from '@/components/providers';

const fontSans = Geist({
subsets: ['latin'],
variable: '--font-sans',
});

const fontMono = Geist_Mono({
subsets: ['latin'],
variable: '--font-mono',
});

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased`}
>
<Providers>{children}</Providers>
</body>
</html>
);
}
export { RootLayout as default } from '@/src/app/layout';
16 changes: 1 addition & 15 deletions apps/web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1 @@
export default function Page() {
return (
<div className="min-h-svh flex items-center justify-center">
<div className="flex flex-col items-center justify-center gap-4">
<h1 className="flex flex-col justify-center text-2xl font-bold">
Hello World
</h1>
{/* <Button size="sm">Button</Button> */}
<div className="flex items-center bg-blue-500 p-4 hover:bg-blue-700">
Test
</div>
</div>
</div>
);
}
export { HomePage as default } from '@/src/app/main';
27 changes: 27 additions & 0 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import '@workspace/ui/globals.css';

import { Geist, Geist_Mono } from 'next/font/google';

import { Providers } from '@/src/app/providers/providers';

const fontSans = Geist({
subsets: ['latin'],
variable: '--font-sans',
});

const fontMono = Geist_Mono({
subsets: ['latin'],
variable: '--font-mono',
});

export function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${fontSans.variable} ${fontMono.variable} font-sans antialiased`}
>
<Providers>{children}</Providers>
</body>
</html>
);
}
12 changes: 12 additions & 0 deletions apps/web/src/app/main/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ReplyForm } from '@/src/feature/reply/ui/reply-form';
import { ReplyList } from '@/src/feature/reply/ui/reply-list';
export function HomePage() {
return (
<div className="flex min-h-svh items-center justify-center">
<div className="flex flex-col items-center justify-center gap-4">
<ReplyForm />
<ReplyList />
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions apps/web/src/app/providers/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client';

import { ThemeProvider as NextThemesProvider } from 'next-themes';
import * as React from 'react';

export function Providers({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
>
{children}
</NextThemesProvider>
);
}
22 changes: 22 additions & 0 deletions apps/web/src/entity/reply/model/reply-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export interface ReplyType {
id: string;
postId: string;
content: string;
author: string;
createdAt: string;
updatedAt: string;
parentCommentId: string;
}
export interface CreateReplyDTO {
content: string;
author: string;
}
export interface UpdateReplyDTO {
content: string;
author: string;
}
export interface ReplyCursorPaginationResponse {
data: ReplyType[];
nextCursor: string; // default : true
hasMore: boolean;
}
38 changes: 38 additions & 0 deletions apps/web/src/feature/comment/ui/comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Button } from '@workspace/ui/components/button';
import Image from 'next/image';

import { profile } from '@/src/shared/assets';
import { formatTimeAgo } from '@/src/shared/lib';
export function Comment({
author,
content,
createdAt,
updatedAt,
onEdit,
}: {
author: string;
content: string;
createdAt: string;
updatedAt: string;
onEdit: () => void;
}) {
return (
<div className="flex space-x-4 py-[100px]">
<div className="relative size-12 overflow-hidden rounded-full">
<Image src={profile} alt="profile" fill={true} objectFit="cover" />
</div>
<div className="flex flex-col items-start space-y-2">
<div className="flex items-center space-x-2">
<h3 className="font-semibold">{author}</h3>
<span className="text-sm text-gray-500">
{formatTimeAgo(updatedAt ? updatedAt : createdAt)}
</span>
</div>
<p className="text-gray-700">{content}</p>
<div className="flex space-x-2">
<Button onClick={onEdit}>수정</Button>
</div>
</div>
</div>
);
}
55 changes: 55 additions & 0 deletions apps/web/src/feature/reply/api/reply-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { fetchWrapper } from '@/src/shared/api';

import {
CreateReplyDTO,
ReplyCursorPaginationResponse,
UpdateReplyDTO,
} from '../../../entity/reply/model/reply-type';

export const getReply = (
postId: string,
commentId: string,
cursor?: string,
limit?: number
): Promise<ReplyCursorPaginationResponse> => {
const queryParams = new URLSearchParams();
if (cursor) queryParams.append('cursor', cursor);
if (limit) queryParams.append('limit', limit.toString());

return fetchWrapper.get(
`posts/${postId}/comments/${commentId}/replies?${queryParams.toString()}`
);
};

export const createReply = (
postId: string,
commentId: string,
content: CreateReplyDTO
): Promise<CreateReplyDTO> => {
return fetchWrapper.post(
`/posts/${postId}/comments/${commentId}/replies`,
content
);
};

export const deleteReply = (
postId: string,
commentId: string,
replyId: string
): Promise<void> => {
return fetchWrapper.delete(
`/posts/${postId}/comments/${commentId}/replies/${replyId}`
);
};

export const updateReply = (
postId: string,
commentId: string,
replyId: string,
content: UpdateReplyDTO
): Promise<UpdateReplyDTO> => {
return fetchWrapper.put(
`/posts/${postId}/comments/${commentId}/replies/${replyId}`,
content
);
};
20 changes: 20 additions & 0 deletions apps/web/src/feature/reply/model/reply-add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use server';

import { createReply } from '@/src/feature/reply/api/reply-api';
type State = {
message?: string | null;
};

export async function replyAdd(prevState: State, queryData: FormData) {
const postId = queryData.get('postId') as string;
const commentId = queryData.get('commentId') as string;
const content = queryData.get('content') as string;
// const author = queryData.get("author") as string;
const author = 'guest';
if (!content) {
return { message: '모든 빈 칸을 입력해주세요' };
}

await createReply(postId, commentId, { content, author });
return { message: '댓글 작성 완료' };
}
Empty file.
38 changes: 38 additions & 0 deletions apps/web/src/feature/reply/model/reply-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use clients';
import { useCallback, useState } from 'react';

import type { ReplyType } from '@/src/entity/reply/model/reply-type';
import { getReply } from '@/src/feature/reply/api/reply-api';
import { useInfiniteScroll } from '@/src/shared/lib/useInfiniteScroll';

export function useReplyFetch() {
const [data, setData] = useState<ReplyType[]>([]);
const [hasMore, setHasMore] = useState<boolean>(true);
const [nextCursor, setNextCursor] = useState<string | undefined>(undefined);
const [isLoading, setIsLoading] = useState(false);

const fetchReplies = useCallback(async () => {
if (!hasMore || isLoading) return;

setIsLoading(true);
try {
const response = await getReply(
'17358013402371',
'1738039148628',
nextCursor,
10
);
setData((prev) => [...prev, ...response.data]);
setHasMore(response.hasMore);
setNextCursor(response.nextCursor);
} catch (error) {
throw new Error(error as string);
} finally {
setIsLoading(false);
}
}, [nextCursor, hasMore, isLoading]);

const { targetRef } = useInfiniteScroll(fetchReplies, { threshold: 1 });

return { data, targetRef, isLoading };
}
27 changes: 27 additions & 0 deletions apps/web/src/feature/reply/model/reply-modify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use server';

import { updateReply } from '../api/reply-api';

type State = {
message?: string | null;
};

export async function replymodify(prevState: State, queryData: FormData) {
const postId = queryData.get('postId') as string;
const commentId = queryData.get('commentId') as string;
const replyId = queryData.get('replyId') as string;
const content = queryData.get('content') as string;
const author = 'guest';

if (!content.trim()) {
return { message: '모든 빈 칸을 입력해주세요' };
}

try {
await updateReply(postId, commentId, replyId, { content, author });
return { message: '수정 완료' };
} catch (error) {
console.error('댓글 수정 오류:', error);
return { message: '수정 실패' };
}
}
28 changes: 28 additions & 0 deletions apps/web/src/feature/reply/ui/reply-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';
import { Button } from '@workspace/ui/components/button';
import { useSearchParams } from 'next/navigation';
import { useActionState } from 'react';

import { replyAdd } from '@/src/feature/reply/model/reply-add';

export function ReplyForm() {
const [state, dispatch] = useActionState(replyAdd, { message: '' }, '/');
const searchParams = useSearchParams();
const parentId = searchParams.get('postId');
const postId = searchParams.get('parentCommentId');

return (
<form action={dispatch}>
<textarea
className="border-input flex min-h-[80px] w-full rounded-md border bg-white px-3 py-2 text-sm placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
name="content"
placeholder="댓글을 작성해 주세요..."
defaultValue={''}
/>
<input type="hidden" name="postId" value={postId || ''} />
<input type="hidden" name="parentCommentId" value={parentId || ''} />
<Button variant={'default'}>댓글 달기</Button>
<p>{state.message}</p>
</form>
);
}
Loading