Skip to content

Commit

Permalink
댓글 기능 구현 (#44)
Browse files Browse the repository at this point in the history
* feat: react-hook-form 설치

* fix: 퀴즈 데이터 호출에서 유저 api 호출 방식 변경

* fix: 헤더 뒤로가기 컴포넌트 수정

* chore: 퀴즈 관련 페이지 레이아웃 구성

* feat: 정답 페이지 기본 기능 구성
정답 api 호출
에러 및 로딩 페이지 구성
임시 스타일 적용

* fix: 헤더 유저 프로필 라우팅 변경

* refactor: 테이블 난이도 텍스트 변경

* feat: 댓글 기본 구조 작업
스타일 및 form 적용

* feat: 댓글 가져오기 기능 구현

* feat: 댓글 작성 기능

* style: 줄바꿈 적용

* fix: 쿼리키 및 댓글 추가 로딩 관련 코드 수정

* fix: api 병렬 처리 진행

* fix: 퀴즈 테이블 관련 api 호출 방식 변경
  • Loading branch information
jgjgill authored Dec 27, 2023
1 parent 1d906ec commit fc9f523
Show file tree
Hide file tree
Showing 33 changed files with 604 additions and 80 deletions.
6 changes: 3 additions & 3 deletions app/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export default async function Page() {
const cookieStore = cookies();
const supabase = createClient(cookieStore);

await queryClient.prefetchQuery(quizOptions.all());

const {
data: { user },
} = await supabase.auth.getUser();

await queryClient.prefetchQuery(quizOptions.all(user?.id));

const signOut = async () => {
'use server';

Expand Down Expand Up @@ -57,7 +57,7 @@ export default async function Page() {
rightArea={HeaderRightArea}
/>

<QuizTable />
<QuizTable userId={user?.id} />

<div className="h-16">
<div className="fixed bottom-0 h-16 w-[28rem] bg-white">
Expand Down
8 changes: 6 additions & 2 deletions app/(main)/quiz-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { columns } from '@/components/quiz/table/columns';
import DataTable from '@/components/quiz/table/data-table';
import { useGetQuizzes } from '@/services/quiz/hooks';

export default function QuizTable() {
const { data: quizzes } = useGetQuizzes();
type QuizTableProps = {
userId?: string;
};

export default function QuizTable({ userId }: QuizTableProps) {
const { data: quizzes } = useGetQuizzes(userId);

return <div>{quizzes && <DataTable columns={columns} data={quizzes} />}</div>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default function ChoiceForm({ quizId, children }: ChoiceFormProps) {
choiceId: choice.id,
},
{
onSuccess: () => router.push(`/quizzes/${quizId}/answer`),
onSuccess: () => router.push(`/quizzes/${quizId}/answers`),
onError: (error) => setErrorMessage(error.message),
}
);
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
7 changes: 0 additions & 7 deletions app/quizzes/[id]/answer/page.tsx

This file was deleted.

108 changes: 108 additions & 0 deletions app/quizzes/[id]/answers/_components/comments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use client';

import FullButton from '@/components/common/buttons/full-button';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Textarea } from '@/components/ui/textarea';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { Skeleton } from '@/components/ui/skeleton';
import {
useGetCommentsOfQuiz,
usePostCommentOfQuiz,
} from '@/services/comment/hooks';
import dayjs from 'dayjs';
import { useQueryClient } from '@tanstack/react-query';
import commentOptions from '@/services/comment/options';

type CommentsProps = {
disable: boolean;
quizId: number;
userId?: string;
};

const FormSchema = z.object({
content: z.string().min(1, {
message: '댓글을 입력해주세요.',
}),
});

export default function Comments({ disable, quizId, userId }: CommentsProps) {
const queryClient = useQueryClient();

const { data: comments = [] } = useGetCommentsOfQuiz(quizId);

const { mutate: postCommentOfQuiz, isPending } = usePostCommentOfQuiz();

const {
handleSubmit,
register,
formState: { errors },
setValue,
} = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
reValidateMode: 'onSubmit',
});

const onSubmit = (data: z.infer<typeof FormSchema>) => {
if (!userId) {
return;
}

const { content } = data;
postCommentOfQuiz(
{ content, quizId, userId },
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [...commentOptions.quiz(quizId).queryKey],
});
setValue('content', '');
},
}
);
};

return (
<div>
<h3 className="mb-4 text-lg font-semibold">댓글 ({comments?.length})</h3>

<form className="mb-8" onSubmit={handleSubmit(onSubmit)}>
<Textarea
{...register('content')}
disabled={disable}
placeholder={
disable ? '회원만 이용 가능합니다.' : '자유롭게 의견을 남겨주세요.'
}
/>
<FullButton disabled={disable || isPending}>제출</FullButton>
<p className="mt-4 text-destructive">{errors.content?.message}</p>
</form>

<div className="flex flex-col gap-8 whitespace-break-spaces">
{comments?.map((comment) => (
<div className="flex flex-col gap-4" key={comment.id}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Avatar>
<Skeleton className="h-10 w-10 rounded-full" />
<AvatarFallback>
<Skeleton className="h-10 w-10 rounded-full" />
</AvatarFallback>
</Avatar>
<span className="max-w-[150px] truncate">
{comment.users?.name}
</span>
</div>
<span className="text-sm text-slate-500">
{dayjs('2023-12-26T14:05:45.449Z').format('MMM DD, YYYY')}
</span>
</div>

<p>{comment.content}</p>
</div>
))}
</div>
</div>
);
}
29 changes: 29 additions & 0 deletions app/quizzes/[id]/answers/_components/quiz-answer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client';

import MarkDown from '@/components/ui/markdown';
import { useGetAnswersOfQuiz } from '@/services/quiz/hooks';
import { dracula } from 'react-syntax-highlighter/dist/cjs/styles/prism';

type QuizAnswerProps = {
quizId: number;
};

export default function QuizAnswer({ quizId }: QuizAnswerProps) {
const { data: quizAnswer } = useGetAnswersOfQuiz(quizId);

return (
<div className="flex flex-col gap-6">
<div>
<h2 className="text-xl font-bold">정답</h2>
<MarkDown style={dracula}>{quizAnswer.description}</MarkDown>
</div>

<div>
<h3 className="text-lg font-semibold">해설</h3>
<MarkDown style={dracula}>
{quizAnswer.answer_description ?? ''}
</MarkDown>
</div>
</div>
);
}
23 changes: 23 additions & 0 deletions app/quizzes/[id]/answers/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';

import Button from '@/components/common/buttons/button';
import { useParams, useRouter } from 'next/navigation';

export default function Error({
error,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const router = useRouter();
const { id: quizId } = useParams();

return (
<div className="flex flex-col items-center gap-6">
<h2 className="text-xl font-semibold">{error.message}</h2>
<Button onClick={() => router.push(`/quizzes/${quizId}`)}>
문제로 이동하기
</Button>
</div>
);
}
16 changes: 16 additions & 0 deletions app/quizzes/[id]/answers/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import BackHeader from '@/components/common/headers/back-header';
import React from 'react';

type LayoutProps = {
children: React.ReactNode;
};

export default function Layout({ children }: LayoutProps) {
return (
<>
<BackHeader />

<section className="p-4">{children}</section>
</>
);
}
3 changes: 3 additions & 0 deletions app/quizzes/[id]/answers/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Loading() {
return <div>loading...</div>;
}
37 changes: 37 additions & 0 deletions app/quizzes/[id]/answers/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import quizOptions from '@/services/quiz/options';
import {
HydrationBoundary,
QueryClient,
dehydrate,
} from '@tanstack/react-query';
import QuizAnswer from './_components/quiz-answer';
import Comments from './_components/comments';
import { cookies } from 'next/headers';
import { createClient } from '@/utils/supabase/server';
import { Separator } from '@/components/ui/separator';
import commentOptions from '@/services/comment/options';

export default async function Page({ params }: { params: { id: string } }) {
const queryClient = new QueryClient();
const quizId = Number(params.id) ?? 0;

const cookieStore = cookies();
const supabase = createClient(cookieStore);

const {
data: { session },
} = await supabase.auth.getSession();

await Promise.all([
queryClient.prefetchQuery(quizOptions.answers(quizId)),
queryClient.prefetchQuery(commentOptions.quiz(quizId)),
]);

return (
<HydrationBoundary state={dehydrate(queryClient)}>
<QuizAnswer quizId={quizId} />
<Separator className="my-4" />
<Comments disable={!session} quizId={quizId} userId={session?.user.id} />
</HydrationBoundary>
);
}
19 changes: 0 additions & 19 deletions app/quizzes/[id]/quiz.tsx

This file was deleted.

15 changes: 15 additions & 0 deletions components/common/buttons/back-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';

import Image from 'next/image';
import BackIcon from '@/assets/images/back-icon.png';
import { useRouter } from 'next/navigation';

export default function BackButton() {
const router = useRouter();

return (
<button className="ml-4 flex items-center" onClick={() => router.back()}>
<Image src={BackIcon} width={30} height={30} alt="뒤로 가기" />
</button>
);
}
16 changes: 2 additions & 14 deletions components/common/headers/back-header.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import BackButton from '../buttons/back-button';
import Header from './header';
import Image from 'next/image';
import NavLink from '../link/nav-link';
import BackIcon from '@/assets/images/back-icon.png';
import Profile from './profile';

export default function BackHeader() {
return (
<>
<Header
leftArea={
<NavLink href="/" scroll={false}>
<Image
src={BackIcon}
width={30}
height={30}
className="ml-4"
alt="뒤로 가기"
/>
</NavLink>
}
leftArea={<BackButton />}
centerArea={
<h1 className="text-xl font-bold text-blue-primary">TypeTime</h1>
}
Expand Down
2 changes: 1 addition & 1 deletion components/common/headers/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default async function Profile() {
} = await supabase.auth.getUser();

return (
<Link href={user ? '/' : '/auth'} scroll={false}>
<Link href={user ? `users/${user.id}` : '/auth'} scroll={false}>
<Image
src={UserProfile}
className="mr-4"
Expand Down
17 changes: 9 additions & 8 deletions components/quiz/table/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const columns: ColumnDef<QuizTable>[] = [

function Filter({ column }: { column: Column<Quiz> }) {
const onClickToggle =
(difficulty: '' | '' | '') =>
(difficulty: '쉬움' | '보통' | '어려움') =>
(e: React.MouseEvent<HTMLButtonElement>) => {
if (e.currentTarget.dataset.state === 'checked') {
column.setFilterValue((olds: string[]) =>
Expand All @@ -84,31 +84,32 @@ function Filter({ column }: { column: Column<Quiz> }) {
}
};

const filterValue = (column.getFilterValue() as ['하' | '중' | '상']) ?? [];
const filterValue =
(column.getFilterValue() as ['쉬움' | '보통' | '어려움']) ?? [];

return (
<div className="flex flex-col">
<div className="flex items-center gap-2">
<Checkbox
id="easy-check"
checked={filterValue.includes('')}
onClick={onClickToggle('')}
checked={filterValue.includes('쉬움')}
onClick={onClickToggle('쉬움')}
/>
<label htmlFor="easy-check">쉬움</label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="medium-check"
checked={filterValue.includes('')}
onClick={onClickToggle('')}
checked={filterValue.includes('보통')}
onClick={onClickToggle('보통')}
/>
<label htmlFor="medium-check">보통</label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="hard-check"
checked={filterValue.includes('')}
onClick={onClickToggle('')}
checked={filterValue.includes('어려움')}
onClick={onClickToggle('어려움')}
/>
<label htmlFor="hard-check">어려움</label>
</div>
Expand Down
Loading

0 comments on commit fc9f523

Please sign in to comment.