diff --git a/app/(main)/quiz-table.tsx b/app/(main)/quiz-table.tsx
index 83774cf..c8e09ea 100644
--- a/app/(main)/quiz-table.tsx
+++ b/app/(main)/quiz-table.tsx
@@ -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
{quizzes && }
;
}
diff --git a/app/quizzes/[id]/choice-form.tsx b/app/quizzes/[id]/(trying)/choice-form.tsx
similarity index 97%
rename from app/quizzes/[id]/choice-form.tsx
rename to app/quizzes/[id]/(trying)/choice-form.tsx
index 4b55534..6ef5175 100644
--- a/app/quizzes/[id]/choice-form.tsx
+++ b/app/quizzes/[id]/(trying)/choice-form.tsx
@@ -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),
}
);
diff --git a/app/quizzes/[id]/layout.tsx b/app/quizzes/[id]/(trying)/layout.tsx
similarity index 100%
rename from app/quizzes/[id]/layout.tsx
rename to app/quizzes/[id]/(trying)/layout.tsx
diff --git a/app/quizzes/[id]/loading.tsx b/app/quizzes/[id]/(trying)/loading.tsx
similarity index 100%
rename from app/quizzes/[id]/loading.tsx
rename to app/quizzes/[id]/(trying)/loading.tsx
diff --git a/app/quizzes/[id]/page.tsx b/app/quizzes/[id]/(trying)/page.tsx
similarity index 100%
rename from app/quizzes/[id]/page.tsx
rename to app/quizzes/[id]/(trying)/page.tsx
diff --git a/app/quizzes/[id]/questions/page.tsx b/app/quizzes/[id]/(trying)/questions/page.tsx
similarity index 100%
rename from app/quizzes/[id]/questions/page.tsx
rename to app/quizzes/[id]/(trying)/questions/page.tsx
diff --git a/app/quizzes/[id]/answer/page.tsx b/app/quizzes/[id]/answer/page.tsx
deleted file mode 100644
index 00003a0..0000000
--- a/app/quizzes/[id]/answer/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function Page() {
- return (
- <>
-
{'수정 예정'}
- >
- );
-}
diff --git a/app/quizzes/[id]/answers/_components/comments.tsx b/app/quizzes/[id]/answers/_components/comments.tsx
new file mode 100644
index 0000000..f83ffb4
--- /dev/null
+++ b/app/quizzes/[id]/answers/_components/comments.tsx
@@ -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
>({
+ resolver: zodResolver(FormSchema),
+ reValidateMode: 'onSubmit',
+ });
+
+ const onSubmit = (data: z.infer) => {
+ if (!userId) {
+ return;
+ }
+
+ const { content } = data;
+ postCommentOfQuiz(
+ { content, quizId, userId },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [...commentOptions.quiz(quizId).queryKey],
+ });
+ setValue('content', '');
+ },
+ }
+ );
+ };
+
+ return (
+
+
댓글 ({comments?.length})
+
+
+
+
+ {comments?.map((comment) => (
+
+
+
+
+
+
+
+
+
+
+ {comment.users?.name}
+
+
+
+ {dayjs('2023-12-26T14:05:45.449Z').format('MMM DD, YYYY')}
+
+
+
+
{comment.content}
+
+ ))}
+
+
+ );
+}
diff --git a/app/quizzes/[id]/answers/_components/quiz-answer.tsx b/app/quizzes/[id]/answers/_components/quiz-answer.tsx
new file mode 100644
index 0000000..1cad734
--- /dev/null
+++ b/app/quizzes/[id]/answers/_components/quiz-answer.tsx
@@ -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 (
+
+
+
정답
+ {quizAnswer.description}
+
+
+
+
해설
+
+ {quizAnswer.answer_description ?? ''}
+
+
+
+ );
+}
diff --git a/app/quizzes/[id]/answers/error.tsx b/app/quizzes/[id]/answers/error.tsx
new file mode 100644
index 0000000..8e2dfc9
--- /dev/null
+++ b/app/quizzes/[id]/answers/error.tsx
@@ -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 (
+
+
{error.message}
+
+
+ );
+}
diff --git a/app/quizzes/[id]/answers/layout.tsx b/app/quizzes/[id]/answers/layout.tsx
new file mode 100644
index 0000000..ce3b4fd
--- /dev/null
+++ b/app/quizzes/[id]/answers/layout.tsx
@@ -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 (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/app/quizzes/[id]/answers/loading.tsx b/app/quizzes/[id]/answers/loading.tsx
new file mode 100644
index 0000000..5f62160
--- /dev/null
+++ b/app/quizzes/[id]/answers/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return loading...
;
+}
diff --git a/app/quizzes/[id]/answers/page.tsx b/app/quizzes/[id]/answers/page.tsx
new file mode 100644
index 0000000..7ae1440
--- /dev/null
+++ b/app/quizzes/[id]/answers/page.tsx
@@ -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 (
+
+
+
+
+
+ );
+}
diff --git a/app/quizzes/[id]/quiz.tsx b/app/quizzes/[id]/quiz.tsx
deleted file mode 100644
index 85aa1f0..0000000
--- a/app/quizzes/[id]/quiz.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client';
-
-import Button from '@/components/common/buttons/button';
-import { useGetQuiz } from '@/services/quiz/hooks';
-
-export function Quiz({ id }: { id: number }) {
- const { data: quiz } = useGetQuiz(id);
- const openModal = () => {
- open();
- };
-
- return (
-
-
나는 클라이언트 컴포넌트에서 불러온 값!
- {JSON.stringify(quiz)}
-
-
- );
-}
diff --git a/components/common/buttons/back-button.tsx b/components/common/buttons/back-button.tsx
new file mode 100644
index 0000000..b8f580d
--- /dev/null
+++ b/components/common/buttons/back-button.tsx
@@ -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 (
+
+ );
+}
diff --git a/components/common/headers/back-header.tsx b/components/common/headers/back-header.tsx
index 0738653..f913dac 100644
--- a/components/common/headers/back-header.tsx
+++ b/components/common/headers/back-header.tsx
@@ -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 (
<>
-
-
- }
+ leftArea={}
centerArea={
TypeTime
}
diff --git a/components/common/headers/profile.tsx b/components/common/headers/profile.tsx
index 73a5724..e9c41bc 100644
--- a/components/common/headers/profile.tsx
+++ b/components/common/headers/profile.tsx
@@ -13,7 +13,7 @@ export default async function Profile() {
} = await supabase.auth.getUser();
return (
-
+
[] = [
function Filter({ column }: { column: Column }) {
const onClickToggle =
- (difficulty: '하' | '중' | '상') =>
+ (difficulty: '쉬움' | '보통' | '어려움') =>
(e: React.MouseEvent) => {
if (e.currentTarget.dataset.state === 'checked') {
column.setFilterValue((olds: string[]) =>
@@ -84,31 +84,32 @@ function Filter({ column }: { column: Column }) {
}
};
- const filterValue = (column.getFilterValue() as ['하' | '중' | '상']) ?? [];
+ const filterValue =
+ (column.getFilterValue() as ['쉬움' | '보통' | '어려움']) ?? [];
return (
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000..66aada3
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import * as React from 'react';
+import * as AvatarPrimitive from '@radix-ui/react-avatar';
+import { cn } from '@/libs/utils';
+
+const Avatar = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
new file mode 100644
index 0000000..08a3f83
--- /dev/null
+++ b/components/ui/separator.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import * as React from 'react';
+import * as SeparatorPrimitive from '@radix-ui/react-separator';
+import { cn } from '@/libs/utils';
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = 'horizontal', decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+);
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export { Separator };
diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx
new file mode 100644
index 0000000..ce94489
--- /dev/null
+++ b/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from '@/libs/utils';
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ );
+}
+
+export { Skeleton };
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
new file mode 100644
index 0000000..b3296f1
--- /dev/null
+++ b/components/ui/textarea.tsx
@@ -0,0 +1,23 @@
+import { cn } from '@/libs/utils';
+import * as React from 'react';
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ );
+ }
+);
+Textarea.displayName = 'Textarea';
+
+export { Textarea };
diff --git a/libs/database.types.ts b/libs/database.types.ts
index 197315c..66982b1 100644
--- a/libs/database.types.ts
+++ b/libs/database.types.ts
@@ -41,6 +41,48 @@ export interface Database {
},
];
};
+ comments: {
+ Row: {
+ content: string;
+ created_at: string;
+ id: string;
+ quiz_id: number;
+ updated_at: string;
+ user_id: string;
+ };
+ Insert: {
+ content: string;
+ created_at?: string;
+ id?: string;
+ quiz_id: number;
+ updated_at?: string;
+ user_id: string;
+ };
+ Update: {
+ content?: string;
+ created_at?: string;
+ id?: string;
+ quiz_id?: number;
+ updated_at?: string;
+ user_id?: string;
+ };
+ Relationships: [
+ {
+ foreignKeyName: 'comments_quiz_id_fkey';
+ columns: ['quiz_id'];
+ isOneToOne: false;
+ referencedRelation: 'quizzes';
+ referencedColumns: ['id'];
+ },
+ {
+ foreignKeyName: 'comments_user_id_fkey';
+ columns: ['user_id'];
+ isOneToOne: false;
+ referencedRelation: 'users';
+ referencedColumns: ['id'];
+ },
+ ];
+ };
hints: {
Row: {
description: string;
diff --git a/libs/models.ts b/libs/models.ts
index 91a664d..0644321 100644
--- a/libs/models.ts
+++ b/libs/models.ts
@@ -6,9 +6,9 @@ export const QuizSchema = z.object({
summary: z.string(),
description: z.string(),
difficulty: z.union([
- z.literal('easy').transform(() => '하'),
- z.literal('medium').transform(() => '중'),
- z.literal('hard').transform(() => '상'),
+ z.literal('easy').transform(() => '쉬움'),
+ z.literal('medium').transform(() => '보통'),
+ z.literal('hard').transform(() => '어려움'),
]),
created_at: z.string(),
updated_at: z.string(),
diff --git a/package-lock.json b/package-lock.json
index 779c12c..09820b2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,9 +6,12 @@
"": {
"hasInstallScript": true,
"dependencies": {
+ "@hookform/resolvers": "^3.3.2",
"@radix-ui/react-accordion": "^1.1.2",
+ "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-separator": "^1.0.3",
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@tailwindcss/typography": "^0.5.10",
@@ -18,6 +21,7 @@
"autoprefixer": "10.4.15",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
+ "dayjs": "^1.11.10",
"geist": "^1.0.0",
"lucide-react": "^0.299.0",
"next": "latest",
@@ -25,6 +29,7 @@
"prettier": "^3.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-hook-form": "^7.49.2",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0",
@@ -173,6 +178,14 @@
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
},
+ "node_modules/@hookform/resolvers": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.2.tgz",
+ "integrity": "sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==",
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
@@ -492,6 +505,32 @@
}
}
},
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz",
+ "integrity": "sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1",
+ "@radix-ui/react-use-layout-effect": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-checkbox": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.0.4.tgz",
@@ -918,6 +957,29 @@
}
}
},
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz",
+ "integrity": "sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
@@ -2430,6 +2492,11 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
+ "node_modules/dayjs": {
+ "version": "1.11.10",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
+ "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
+ },
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -6290,6 +6357,22 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.49.2",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz",
+ "integrity": "sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==",
+ "engines": {
+ "node": ">=18",
+ "pnpm": "8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/package.json b/package.json
index 361f522..50a73b1 100644
--- a/package.json
+++ b/package.json
@@ -9,9 +9,12 @@
"lint-staged": "lint-staged"
},
"dependencies": {
+ "@hookform/resolvers": "^3.3.2",
"@radix-ui/react-accordion": "^1.1.2",
+ "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-separator": "^1.0.3",
"@supabase/ssr": "latest",
"@supabase/supabase-js": "latest",
"@tailwindcss/typography": "^0.5.10",
@@ -21,6 +24,7 @@
"autoprefixer": "10.4.15",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
+ "dayjs": "^1.11.10",
"geist": "^1.0.0",
"lucide-react": "^0.299.0",
"next": "latest",
@@ -28,6 +32,7 @@
"prettier": "^3.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
+ "react-hook-form": "^7.49.2",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0",
diff --git a/services/comment/api.ts b/services/comment/api.ts
new file mode 100644
index 0000000..816dc0f
--- /dev/null
+++ b/services/comment/api.ts
@@ -0,0 +1,34 @@
+import { createClient } from '@/utils/supabase/client';
+import { SupabaseClient } from '@supabase/supabase-js';
+
+const commentAPI = {
+ getCommentsOfQuiz: async (quizId: number) => {
+ const supabase: SupabaseClient = createClient();
+
+ const { data } = await supabase
+ .from('comments')
+ .select('*, users (name)')
+ .eq('quiz_id', quizId)
+ .order('updated_at', { ascending: false });
+
+ return data;
+ },
+
+ postCommentOfQuiz: async ({
+ userId,
+ quizId,
+ content,
+ }: {
+ userId: string;
+ quizId: number;
+ content: string;
+ }) => {
+ const supabase: SupabaseClient = createClient();
+
+ await supabase
+ .from('comments')
+ .insert({ user_id: userId, quiz_id: quizId, content });
+ },
+};
+
+export default commentAPI;
diff --git a/services/comment/hooks.ts b/services/comment/hooks.ts
new file mode 100644
index 0000000..6871a09
--- /dev/null
+++ b/services/comment/hooks.ts
@@ -0,0 +1,13 @@
+import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
+import commentOptions from './options';
+import commentAPI from './api';
+
+export function useGetCommentsOfQuiz(quizId: number) {
+ return useSuspenseQuery(commentOptions.quiz(quizId));
+}
+
+export function usePostCommentOfQuiz() {
+ return useMutation({
+ mutationFn: commentAPI.postCommentOfQuiz,
+ });
+}
diff --git a/services/comment/options.ts b/services/comment/options.ts
new file mode 100644
index 0000000..d85299b
--- /dev/null
+++ b/services/comment/options.ts
@@ -0,0 +1,14 @@
+import { queryOptions } from '@tanstack/react-query';
+import commentAPI from './api';
+
+const commentOptions = {
+ default: ['comments'] as const,
+
+ quiz: (quizId: number) =>
+ queryOptions({
+ queryKey: [...commentOptions.default, 'quiz', quizId],
+ queryFn: () => commentAPI.getCommentsOfQuiz(quizId),
+ }),
+};
+
+export default commentOptions;
diff --git a/services/quiz/api.ts b/services/quiz/api.ts
index 515edb2..bbedb1c 100644
--- a/services/quiz/api.ts
+++ b/services/quiz/api.ts
@@ -3,24 +3,20 @@ import { createClient } from '@/utils/supabase/client';
import { SupabaseClient } from '@supabase/supabase-js';
const quizAPI = {
- getQuizzes: async () => {
+ getQuizzes: async (userId?: string) => {
const supabase: SupabaseClient = createClient();
const { data: quizzes } = await supabase
.from('quizzes')
.select('*')
.order('id', { ascending: true });
- const {
- data: { session },
- } = await supabase.auth.getSession();
- const { data: userquizSubmissions } = await supabase
- .from('quizsubmissions')
- .select('*')
- .eq('user_id', session?.user.id ?? '');
- if (!quizzes) {
- throw new Error('잘못된 접근');
- }
+ const { data: userquizSubmissions } = userId
+ ? await supabase
+ .from('quizsubmissions')
+ .select('*')
+ .eq('user_id', userId ?? '')
+ : { data: null };
const quizzesTable = await QuizTableSchema.transform((quiz) => {
const success = userquizSubmissions?.find(
@@ -81,6 +77,24 @@ const quizAPI = {
return data;
},
+ getAnswersOfQuiz: async (quizId: number) => {
+ const supabase: SupabaseClient = createClient();
+
+ const { data } = await supabase
+ .from('choices')
+ .select('description, answer_description')
+ .eq('quiz_id', quizId)
+ .eq('answer', true)
+ .limit(1)
+ .single();
+
+ if (!data) {
+ throw new Error('정답이 존재하지 않습니다.');
+ }
+
+ return data;
+ },
+
postQuizSubmission: async (params: { quizId: number; choiceId: number }) => {
const { quizId, choiceId } = params;
diff --git a/services/quiz/hooks.ts b/services/quiz/hooks.ts
index b0efbbf..4812202 100644
--- a/services/quiz/hooks.ts
+++ b/services/quiz/hooks.ts
@@ -1,14 +1,9 @@
-import { useSuspenseQuery, useMutation, useQuery } from '@tanstack/react-query';
+import { useSuspenseQuery, useMutation } from '@tanstack/react-query';
import quizOptions from './options';
import quizAPI from './api';
-/**
- * @note useGetQuizzes에 useSuspenseQuery를 사용할 시 간혈적으로 문제 풀이 여부 데이터를 받지 못합니다.
- * 현재 추측으로는 해당 함수 내 여러 비동기가 동작하는데 하나의 비동기 동작(await)이 완료되면 suspense에서 완료로 감지(?) 하지 않나 싶습니다.
- */
-
-export function useGetQuizzes() {
- return useQuery(quizOptions.all());
+export function useGetQuizzes(userId?: string) {
+ return useSuspenseQuery(quizOptions.all(userId));
}
export function useGetQuiz(quizId: number) {
@@ -23,6 +18,10 @@ export function useGetChoicesOfQuiz(quizId: number) {
return useSuspenseQuery(quizOptions.choices(quizId));
}
+export function useGetAnswersOfQuiz(quizId: number) {
+ return useSuspenseQuery(quizOptions.answers(quizId));
+}
+
export function useSubmitQuiz() {
return useMutation({
mutationFn: quizAPI.postQuizSubmission,
diff --git a/services/quiz/options.ts b/services/quiz/options.ts
index b7772fa..4bb43f8 100644
--- a/services/quiz/options.ts
+++ b/services/quiz/options.ts
@@ -4,10 +4,10 @@ import quizAPI from './api';
const quizOptions = {
default: ['quizzes'] as const,
- all: () =>
+ all: (userId?: string) =>
queryOptions({
- queryKey: [...quizOptions.default],
- queryFn: () => quizAPI.getQuizzes(),
+ queryKey: [...quizOptions.default, userId],
+ queryFn: () => quizAPI.getQuizzes(userId),
}),
detail: (quizId: number) =>
@@ -33,6 +33,11 @@ const quizOptions = {
queryKey: [...quizOptions.detail(quizId).queryKey, 'hints'],
queryFn: () => quizAPI.getHintsOfQuiz(quizId),
}),
+ answers: (quizId: number) =>
+ queryOptions({
+ queryKey: [...quizOptions.detail(quizId).queryKey, 'answers'],
+ queryFn: () => quizAPI.getAnswersOfQuiz(quizId),
+ }),
};
export default quizOptions;