diff --git a/app/study/[id]/page.tsx b/app/study/[id]/page.tsx index 8f91d16..9847905 100644 --- a/app/study/[id]/page.tsx +++ b/app/study/[id]/page.tsx @@ -6,21 +6,41 @@ import { useEffect, useState } from 'react'; import StudyDashBoard from '@/components/study/dashboard/dashboard'; import JoinStudyDialog from '@/components/study/study-join-dialog'; +import { Button } from '@/components/ui/button/button'; import Spinner from '@/components/ui/spinner/spinner'; import getStudyDetails from '@/lib/api/study/get-details'; +import startStudy from '@/lib/api/study/start'; import { userState } from '@/recoil/userAtom'; -import { AlgorithmRound, StudyDetails } from '@/types/study/study-detail'; +import { + AlgorithmRound, + StudyDetails, + StudyStatus +} from '@/types/study/study-detail'; +import { toast } from 'react-toastify'; import { useRecoilState } from 'recoil'; export default function StudyPage() { const params = useParams(); - const studyId = params.id.toString(); + + const studyId = Number(params.id); const [details, setDetails] = useState(); const [round, setRound] = useState(); const [isParticipant, setIsParticipant] = useState(false); + const [canStart, setCanStart] = useState(false); const [myData, setMyData] = useRecoilState(userState); + const [trigger, setTrigger] = useState(Date.now()); + const handleStart = async () => { + try { + const response = await startStudy(studyId); + toast.success('스터디를 시작하였습니다.'); + refresh(); + } catch (error: any) { + toast.error(error.response.data.error); + console.error(error); + } + }; useEffect(() => { async function fetchStudyDetails() { try { @@ -35,26 +55,53 @@ export default function StudyPage() { break; } } + + if ( + studyDetailsAndRound.details.leaderId === myData?.id && + studyDetailsAndRound.details.status === StudyStatus.READY + ) { + setCanStart(true); + } else { + setCanStart(false); + } } catch (error) { console.error('Failed to fetch study details:', error); } } fetchStudyDetails(); - }, [studyId]); + }, [studyId, trigger]); + const refresh = () => { + setTrigger(Date.now()); + }; if (!details || !round) { return ; } + return (
- {isParticipant ? '' : } + {isParticipant ? ( + canStart ? ( + + ) : ( + '' + ) + ) : ( + + )}
diff --git a/components/study/dashboard/dashboard.tsx b/components/study/dashboard/dashboard.tsx index 5a26f64..9e07013 100644 --- a/components/study/dashboard/dashboard.tsx +++ b/components/study/dashboard/dashboard.tsx @@ -51,7 +51,7 @@ function DashBoardBody({ studyId: number; }) { const [my, _] = useRecoilState(userState); - const myTasks = round.users[my!.id].tasks; + const myTasks = round.users[my!.id]?.tasks; return (
@@ -63,7 +63,7 @@ function DashBoardBody({

{problem.title}

- {myTasks[Number(problemId)] && ( + {myTasks?.[Number(problemId)] && ( void; }) { const params = useParams(); - const studyId = params.id.toString(); + const studyId = Number(params.id); const handleRoundChange = async (value: string) => { - const newRound = await getRound(parseInt(studyId), parseInt(value)); + const newRound = await getRound(studyId, parseInt(value)); setRound(newRound); }; const renderSelectContent = () => { diff --git a/components/study/difficulty-level-dialog.tsx b/components/study/difficulty-level-dialog.tsx index da25dde..54edee1 100644 --- a/components/study/difficulty-level-dialog.tsx +++ b/components/study/difficulty-level-dialog.tsx @@ -1,5 +1,9 @@ 'use client'; -import { useState } from 'react'; +import { + MAX_DIFFICULTY_LEVEL, + MIN_DIFFICULTY_LEVEL +} from '@/constants/study/study'; +import { DialogDescription } from '@radix-ui/react-dialog'; import { Dialog, DialogContent, @@ -7,9 +11,7 @@ import { DialogTitle } from '../ui/dialog/dialog'; import { RadioGroup } from '../ui/radio/radio-group'; -import { MAX_DIFFICULTY_LEVEL } from '@/constants/study/study'; import TierRadioItem from './tier'; -import { DialogDescription } from '@radix-ui/react-dialog'; export default function DifficultyLevelDialog({ open, @@ -37,11 +39,17 @@ export default function DifficultyLevelDialog({ }} >
- {Array.from({ length: MAX_DIFFICULTY_LEVEL + 1 }).map((x, i) => ( + {Array.from({ + length: MAX_DIFFICULTY_LEVEL - MIN_DIFFICULTY_LEVEL + 1 + }).map((x, i) => ( ))}
diff --git a/components/study/study-create-modal.tsx b/components/study/study-create-modal.tsx index d45247e..8902352 100644 --- a/components/study/study-create-modal.tsx +++ b/components/study/study-create-modal.tsx @@ -45,6 +45,7 @@ import { DAYS_PER_WEEK, MAX_DIFFICULTY_LEVEL, MAX_WEEKS, + MIN_DIFFICULTY_LEVEL, StudyType } from '@/constants/study/study'; import registerAlgorithmStudy from '@/lib/api/study/create-algorithm-study'; @@ -88,7 +89,7 @@ export default function StudyCreateModal({ }) { const [open, setOpen] = useState(false); const [user, setUser] = useRecoilState(userState); - const [difficultyBegin, setDifficultyBegin] = useState(0); + const [difficultyBegin, setDifficultyBegin] = useState(MIN_DIFFICULTY_LEVEL); const [weeks, setWeeks] = useState(1); const [difficultyEnd, setDifficultyEnd] = useState(MAX_DIFFICULTY_LEVEL); const [studyType, setStudyType] = useState(''); diff --git a/components/study/study-grid.tsx b/components/study/study-grid.tsx index 9daa967..6be6661 100644 --- a/components/study/study-grid.tsx +++ b/components/study/study-grid.tsx @@ -35,6 +35,7 @@ export default function StudyGrid({ trigger }: { trigger: number }) { {studyPage?.contents.map((study: Study) => { return ( { router.push(`/study/${study.id}`); diff --git a/components/study/study-group.tsx b/components/study/study-group.tsx index 63ca383..7bc87b5 100644 --- a/components/study/study-group.tsx +++ b/components/study/study-group.tsx @@ -28,17 +28,11 @@ function TierBadge({ difficultyLevel }: { difficultyLevel: number }) { function AlgorithmStudyInfo(algorithmStudy: AlgorithmStudy) { const as = algorithmStudy; - let difficultyAvg = - as.difficultyDp + - as.difficultyDs + - as.difficultyGeometry + - as.difficultyGraph + - as.difficultyGreedy + - as.difficultyImpl + - as.difficultyMath + - as.difficultyString; - difficultyAvg /= 8; + const difficultyAvg = + Object.values(as.difficultySpreadMap).reduce((a, b) => a + b.left, 0) / + Object.keys(as.difficultySpreadMap).length; const difficultyBegin = Math.round(difficultyAvg); + const difficultyEnd = difficultyBegin + as.difficultyGap; return (
diff --git a/components/study/study-join-dialog.tsx b/components/study/study-join-dialog.tsx index 79fd1a9..f6c9153 100644 --- a/components/study/study-join-dialog.tsx +++ b/components/study/study-join-dialog.tsx @@ -13,11 +13,20 @@ import { StudyDetails } from '@/types/study/study-detail'; import { useState } from 'react'; import { toast } from 'react-toastify'; -export default function JoinStudyDialog(details: StudyDetails, key: string) { +export default function JoinStudyDialog({ + details, + studyId, + refresh +}: { + details: StudyDetails; + studyId: number; + refresh: () => void; +}) { const handleSubmit = async () => { try { - const response = await joinStudy(parseInt(key)); + const response = await joinStudy(studyId); toast.success('스터디에 참여하였습니다.'); + refresh(); } catch (error: any) { toast.error(error.response.data.error); console.error(error); diff --git a/components/study/tier.tsx b/components/study/tier.tsx index 6e56430..6f1fc0c 100644 --- a/components/study/tier.tsx +++ b/components/study/tier.tsx @@ -1,22 +1,20 @@ -import StudyGroup from './study-group'; -import { Study, StudyPage } from '../../types/study/study'; -import { RadioGroupItem } from '@radix-ui/react-radio-group'; -import { Label } from '@radix-ui/react-label'; -import { ShieldIcon } from '../ui/icon/icon'; import { - MAX_DIFFICULTY_LEVEL, + MIN_DIFFICULTY_LEVEL, Tier, colorClassMap } from '@/constants/study/study'; -import { ITier } from '@/types/study/tier'; import { cn } from '@/lib/utils'; +import { ITier } from '@/types/study/tier'; +import { Label } from '@radix-ui/react-label'; +import { RadioGroupItem } from '@radix-ui/react-radio-group'; +import { ShieldIcon } from '../ui/icon/icon'; export function getTierInfo(difficultyLevel: number): ITier { - difficultyLevel = Math.min(difficultyLevel, MAX_DIFFICULTY_LEVEL); - const tierIndex = Math.floor(difficultyLevel / 5); + const difficultyLevelFromMin = difficultyLevel - MIN_DIFFICULTY_LEVEL; + const tierIndex = Math.floor(difficultyLevelFromMin / 5); const tier: string = Tier[tierIndex]; const colorClass = colorClassMap[tierIndex]; - const division = 5 - (difficultyLevel % 5); + const division = 5 - (difficultyLevelFromMin % 5); return { colorClass, tier, division }; } export function TierIcon({ colorClass, tier, division }: ITier) { diff --git a/constants/study/study.ts b/constants/study/study.ts index 702fbfe..935f228 100644 --- a/constants/study/study.ts +++ b/constants/study/study.ts @@ -30,7 +30,8 @@ export enum StudyType { BOOK = '기술서적 스터디' } -export const MAX_DIFFICULTY_LEVEL = 29; +export const MAX_DIFFICULTY_LEVEL = 30; +export const MIN_DIFFICULTY_LEVEL = 1; export const MAX_RELIABILITY_LIMIT = 100; export const MAX_PENALTY = 100_000; export const MAX_WEEKS = 52; diff --git a/lib/api/study/get-details.ts b/lib/api/study/get-details.ts index af21d04..2a2db07 100644 --- a/lib/api/study/get-details.ts +++ b/lib/api/study/get-details.ts @@ -5,7 +5,7 @@ import axios from 'axios'; * 스터디 디테일 조회 API */ export default async function getStudyDetails( - id: string + id: number ): Promise { return axios .get(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/api/v1/studies/` + id) diff --git a/lib/api/study/start.ts b/lib/api/study/start.ts new file mode 100644 index 0000000..a63bee1 --- /dev/null +++ b/lib/api/study/start.ts @@ -0,0 +1,22 @@ +import axios from 'axios'; + +/** + * 스터디 시작 API + * body : studyId + */ + +export default function startStudy(studyId: number) { + const token = localStorage.getItem('accessToken'); + + return axios.post( + `${process.env.NEXT_PUBLIC_API_SERVER_URL}/api/v1/studies/start`, + { + studyId: studyId + }, + { + headers: { + Authorization: `Bearer ${token}` + } + } + ); +} diff --git a/types/study/register-study.ts b/types/study/register-study.ts index 9c9b1fe..e60dc4d 100644 --- a/types/study/register-study.ts +++ b/types/study/register-study.ts @@ -7,6 +7,7 @@ import { MAX_PROBLEM_COUNT, MAX_RELIABILITY_LIMIT, MAX_WEEKS, + MIN_DIFFICULTY_LEVEL, StudyType } from '@/constants/study/study'; import { RawCreateParams, z } from 'zod'; @@ -102,15 +103,21 @@ export function getStudySchema(user: User) { studyType: z.literal(StudyType.ALGORITHM), difficultyBegin: z .number() - .min(0, '0 이상이여야 합니다') + .min( + MIN_DIFFICULTY_LEVEL, + `${MIN_DIFFICULTY_LEVEL} 이상이여야 합니다` + ) .max( MAX_DIFFICULTY_LEVEL, `${MAX_DIFFICULTY_LEVEL} 이하여야 합니다.` ) - .default(0), + .default(1), difficultyEnd: z .number() - .min(0, '0 이상이여야 합니다') + .min( + MIN_DIFFICULTY_LEVEL, + `${MIN_DIFFICULTY_LEVEL} 이상이여야 합니다` + ) .max( MAX_DIFFICULTY_LEVEL, `${MAX_DIFFICULTY_LEVEL} 이하여야 합니다.` @@ -135,7 +142,8 @@ export function getStudySchema(user: User) { studyType: z.literal(StudyType.BOOK), isbn: z.number({ required_error: '필수입니다.' - })}) + }) + }) ], { errorMap: (issue, ctx) => { diff --git a/types/study/study-detail.ts b/types/study/study-detail.ts index 7dfa6f6..e799463 100644 --- a/types/study/study-detail.ts +++ b/types/study/study-detail.ts @@ -10,10 +10,16 @@ export interface StudyDetails { headCount: number; capacity: number; penalty: number; + leaderId: number; reliabilityLimit: number; startDate: Date; weeks: number; - status: string; + status: StudyStatus; +} +export enum StudyStatus { + READY = 'READY', + RUNNING = 'RUNNING', + END = 'END' } export interface AlgorithmRound { diff --git a/types/study/study.ts b/types/study/study.ts index eb9e27b..5716554 100644 --- a/types/study/study.ts +++ b/types/study/study.ts @@ -16,22 +16,13 @@ export interface Study { studyType: string; } -export interface AlgorithmStudy extends Study { - difficultyMath: number; - - difficultyDp: number; - - difficultyGreedy: number; - - difficultyImpl: number; - - difficultyGraph: number; - - difficultyGeometry: number; - - difficultyDs: number; +interface Spread { + left: number; + right: number; +} - difficultyString: number; +export interface AlgorithmStudy extends Study { + difficultySpreadMap: { [key: string]: Spread }; difficultyGap: number;