Skip to content

Commit

Permalink
메인 페이지 퀴즈 컴포넌트 작업 (#38)
Browse files Browse the repository at this point in the history
* feat: pre-push 설정 추가

* @tanstack/react-table 설치

* feat: 테이블 관련 컴포넌트 설치
table, dropdwon, checkbox 추가

* feat: 테이블 관련 작업
기본 데이터 테이블 구성
난이도 필터 적용
드롭다운 적용

* feat: 검색 기능 구현
커스텀 필터 적용

* feat: 페이지네이션 기능 추가

* feat: 퀴즈 데이터 호출
타입 관련 수정

* feat: zod 적용 및 getQuizzes 호출 방식 변경
quiz 관련 스키마 정의
getQuizzes 쿼리 방식 useQuery로 변경

* refactor: 코드 정리
안쓰는 코드 제거
마크다운 관련 스타일 변경

* fix: 난이도 필터 스키마 변경

* chore: 콘솔 제거

* fix: 수정된 쿼리키 적용
  • Loading branch information
jgjgill authored Dec 26, 2023
1 parent 1b47e13 commit 1d906ec
Show file tree
Hide file tree
Showing 17 changed files with 1,269 additions and 97 deletions.
4 changes: 4 additions & 0 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run build
105 changes: 29 additions & 76 deletions app/(main)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { createClient } from '@/utils/supabase/server';
import { cookies } from 'next/headers';
import KakaoButton from '../auth/kakao-button';
import { QuizCard } from '@/components/quiz/quiz-card';
import FullButton from '@/components/common/buttons/full-button';
import TypeTimeLogo from '@/assets/images/type-time-logo.png';
import Image from 'next/image';
import Button from '@/components/common/buttons/button';
import BaseHeader from '@/components/common/headers/base-header';
import {
HydrationBoundary,
QueryClient,
dehydrate,
} from '@tanstack/react-query';
import quizOptions from '@/services/quiz/options';
import QuizTable from './quiz-table';
import KakaoButton from '../auth/kakao-button';
import Header from '@/components/common/headers/header';

export default async function Page() {
const queryClient = new QueryClient();

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

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

const {
data: { user },
} = await supabase.auth.getUser();
Expand All @@ -32,85 +43,27 @@ export default async function Page() {
);

return (
<>
<BaseHeader />
<div>
{/* TODO: 임시 구현-유저 페이지에 로그아웃 구현되면 제거 예정 */}
{HeaderRightArea}
</div>
<main>
<div className="flex flex-col gap-6">
<QuizCard
status="correct"
difficulty="easy"
title="2 Get Return Type"
summary="함수의 반환 타입을 만들어주세요"
/>
<QuizCard
status="wrong"
difficulty="hard"
title="2 Get Return Type"
summary="함수의 반환 타입을 만들어주세요"
/>
<QuizCard
difficulty="medium"
title="2 Get Return Type"
summary="함수의 반환 타입을 만들어주세요"
<HydrationBoundary state={dehydrate(queryClient)}>
<Header
leftArea={
<Image
src={TypeTimeLogo}
alt="타입타임 로고"
width={158}
height={63}
priority
/>
<QuizCard
difficulty="medium"
title="Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam iure
illum inventore hic perspiciatis, voluptatem eos corrupti mollitia
eveniet ad rem quam tenetur vel, repellat cupiditate sint facilis
aspernatur laudantium."
summary="함수의 반환 타입을 만들어주세요"
/>
<QuizCard
difficulty="medium"
title="2 Get Return Type"
summary="Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam iure
illum inventore hic perspiciatis, voluptatem eos corrupti mollitia
eveniet ad rem quam tenetur vel, repellat cupiditate sint facilis
aspernatur laudantium."
/>
<QuizCard
difficulty="medium"
title="2 Get Return Type"
summary="Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam iure
illum inventore hic perspiciatis, voluptatem eos corrupti mollitia
eveniet ad rem quam tenetur vel, repellat cupiditate sint facilis
aspernatur laudantium."
/>
<QuizCard
difficulty="medium"
title="Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam iure
illum inventore hic perspiciatis, voluptatem eos corrupti mollitia
eveniet ad rem quam tenetur vel, repellat cupiditate sint facilis
aspernatur laudantium."
summary="함수의 반환 타입을 만들어주세요"
/>
<QuizCard
status="wrong"
difficulty="medium"
title="Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam iure
illum inventore hic perspiciatis, voluptatem eos corrupti mollitia
eveniet ad rem quam tenetur vel, repellat cupiditate sint facilis
aspernatur laudantium."
summary="Lorem ipsum dolor sit amet consectetur adipisicing elit. Ullam iure
illum inventore hic perspiciatis, voluptatem eos corrupti mollitia
eveniet ad rem quam tenetur vel, repellat cupiditate sint facilis
aspernatur laudantium."
/>
</div>
}
rightArea={HeaderRightArea}
/>

<FullButton>제출하기(너비 100%)</FullButton>
</main>
<QuizTable />

<div className="h-16">
<div className="fixed bottom-0 h-16 w-[28rem] bg-white">
혹시 몰라서 추가해 본 바텀시트...
</div>
</div>
</>
</HydrationBoundary>
);
}
11 changes: 11 additions & 0 deletions app/(main)/quiz-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client';

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();

return <div>{quizzes && <DataTable columns={columns} data={quizzes} />}</div>;
}
13 changes: 5 additions & 8 deletions app/api/quiz-submission/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,11 @@ export async function POST(request: NextRequest) {
const cookieStore = cookies();
const supabase = createClient(cookieStore);

const validateBody = await request.json().then((body) =>
z
.object({
quizId: z.number(),
choiceId: z.number(),
})
.safeParse(body)
);
const validateBody = await request
.json()
.then((body) =>
z.object({ quizId: z.number(), choiceId: z.number() }).safeParse(body)
);

if (!validateBody.success) {
return NextResponse.json(
Expand Down
2 changes: 1 addition & 1 deletion components/common/buttons/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { forwardRef, type Ref } from 'react';
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof button>;

const button = cva(['rounded text-white'], {
const button = cva(['rounded text-white disabled:bg-slate-300'], {
variants: {
intent: {
primary: ['bg-blue-primary', 'hover:bg-blue-primary'],
Expand Down
117 changes: 117 additions & 0 deletions components/quiz/table/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
'use client';

import { Checkbox } from '@/components/ui/checkbox';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import type { Quiz, QuizTable } from '@/libs/models';
import { Column, ColumnDef } from '@tanstack/react-table';
import Link from 'next/link';

export const columns: ColumnDef<QuizTable>[] = [
{
accessorKey: 'success',
header: () => <div className="text-left">상태</div>,
cell: ({ row }) => {
const { success } = row.original;

return <div>{JSON.stringify(success)}</div>;
},
},
{
accessorKey: 'title',
header: '제목',
cell: ({ row }) => {
const { title, summary, id } = row.original;

return (
<div className="max-w-[13rem]">
<h3 className="truncate">
<Link className="text-base font-semibold" href={`/quizzes/${id}`}>
{title}
</Link>
</h3>
<p className="truncate text-blue-500">{summary}</p>
</div>
);
},
/**
* @description 참고 코드
* https://github.com/TanStack/table/blob/a334f66a82a9b3b0b4e99e7a0cc99ba077aaf167/packages/table-core/src/filterFns.ts#L3-L16
*/
filterFn: (row, columnId, filterValue) => {
const search = filterValue.toLowerCase();

return Boolean(
row.getValue<string | null>(columnId)?.toLowerCase().includes(search)
);
},
},
{
accessorKey: 'difficulty',
header: ({ column }) => (
<DropdownMenu>
<DropdownMenuTrigger className="w-full">난이도</DropdownMenuTrigger>
<DropdownMenuContent>
<Filter column={column} />
</DropdownMenuContent>
</DropdownMenu>
),
cell: ({ row }) => {
const difficulty = String(row.getValue('difficulty'));

return <div className="text-center">{difficulty}</div>;
},
filterFn: 'arrIncludesSome',
},
];

function Filter({ column }: { column: Column<Quiz> }) {
const onClickToggle =
(difficulty: '하' | '중' | '상') =>
(e: React.MouseEvent<HTMLButtonElement>) => {
if (e.currentTarget.dataset.state === 'checked') {
column.setFilterValue((olds: string[]) =>
olds.filter((old) => old !== difficulty)
);
} else {
column.setFilterValue((olds: string[]) => [
...(olds ?? []),
difficulty,
]);
}
};

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('하')}
/>
<label htmlFor="easy-check">쉬움</label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="medium-check"
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('상')}
/>
<label htmlFor="hard-check">어려움</label>
</div>
</div>
);
}
Loading

0 comments on commit 1d906ec

Please sign in to comment.