diff --git a/apps/frontend/app/(client)/(code-editor)/_components/ContestProblemDropdown.tsx b/apps/frontend/app/(client)/(code-editor)/_components/ContestProblemDropdown.tsx new file mode 100644 index 0000000000..335dab63c2 --- /dev/null +++ b/apps/frontend/app/(client)/(code-editor)/_components/ContestProblemDropdown.tsx @@ -0,0 +1,69 @@ +'use client' + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/shadcn/dropdown-menu' +import { cn, convertToLetter, fetcherWithAuth } from '@/libs/utils' +import checkIcon from '@/public/icons/check-green.svg' +import type { ContestProblem, ProblemDetail } from '@/types/type' +import { useQuery } from '@tanstack/react-query' +import Image from 'next/image' +import Link from 'next/link' +import { FaSortDown } from 'react-icons/fa' + +interface ContestProblemsResponse { + data: ContestProblem[] + total: number +} + +interface ContestProblemDropdownProps { + problem: ProblemDetail + problemId: number + contestId: number +} + +export default function ContestProblemDropdown({ + problem, + problemId, + contestId +}: ContestProblemDropdownProps) { + const { data: contestProblems } = useQuery< + ContestProblemsResponse | undefined + >({ + queryKey: ['contest', contestId, 'problems'], + queryFn: () => + fetcherWithAuth.get(`contest/${contestId}/problem?take=20`).json() + }) + + return ( + + +

{`${convertToLetter(contestProblems?.data.find((item) => item.id === Number(problemId))?.order as number)}. ${problem.title}`}

+ +
+ + {contestProblems?.data.map((p) => ( + + + {`${convertToLetter(p.order)}. ${p.title}`} + {p.submissionTime && ( +
+ check +
+ )} +
+ + ))} +
+
+ ) +} diff --git a/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.tsx b/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.tsx index c16cafbf03..917ecbfdf1 100644 --- a/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.tsx +++ b/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.tsx @@ -36,6 +36,7 @@ import type { Submission, Template } from '@/types/type' +import { useQueryClient } from '@tanstack/react-query' import JSConfetti from 'js-confetti' import { Save } from 'lucide-react' import type { Route } from 'next' @@ -79,10 +80,12 @@ export default function Editor({ ) const { currentModal, showSignIn } = useAuthModalStore((state) => state) const [showModal, setShowModal] = useState(false) - const pushed = useRef(false) + //const pushed = useRef(false) const whereToPush = useRef('') const isModalConfrimed = useRef(false) + const queryClient = useQueryClient() + useInterval( async () => { const res = await fetcherWithAuth(`submission/${submissionId}`, { @@ -99,7 +102,7 @@ export default function Editor({ ? `/contest/${contestId}/problem/${problem.id}/submission/${submissionId}` : `/problem/${problem.id}/submission/${submissionId}` router.replace(href as Route) - window.history.pushState(null, '', '') + //window.history.pushState(null, '', window.location.href) if (submission.result === 'Accepted') { confetti?.addConfetti() } @@ -186,6 +189,9 @@ export default function Editor({ storeCodeToLocalStorage(code) const submission: Submission = await res.json() setSubmissionId(submission.id) + queryClient.refetchQueries({ + queryKey: ['contest', contestId, 'problems'] + }) } else { setIsSubmitting(false) if (res.status === 401) { @@ -243,22 +249,26 @@ export default function Editor({ contestId ) - const handlePopState = () => { - if (!checkSaved()) { - whereToPush.current = contestId ? `/contest/${contestId}` : '/problem' - setShowModal(true) - } else window.history.back() - } - if (!pushed.current) { - window.history.pushState(null, '', '') - pushed.current = true - } + // TODO: 배포 후 뒤로 가기 로직 재구현 + + // const handlePopState = () => { + // if (!checkSaved()) { + // whereToPush.current = contestId + // ? `/contest/${contestId}/problem` + // : '/problem' + // setShowModal(true) + // } else window.history.back() + // } + // if (!pushed.current) { + // window.history.pushState(null, '', window.location.href) + // pushed.current = true + // } window.addEventListener('beforeunload', handleBeforeUnload) - window.addEventListener('popstate', handlePopState) + //window.addEventListener('popstate', handlePopState) return () => { window.removeEventListener('beforeunload', handleBeforeUnload) - window.removeEventListener('popstate', handlePopState) + //window.removeEventListener('popstate', handlePopState) } }, []) diff --git a/apps/frontend/app/(client)/(code-editor)/_components/EditorLayout.tsx b/apps/frontend/app/(client)/(code-editor)/_components/EditorLayout.tsx index 6c81a48496..4e67da54ef 100644 --- a/apps/frontend/app/(client)/(code-editor)/_components/EditorLayout.tsx +++ b/apps/frontend/app/(client)/(code-editor)/_components/EditorLayout.tsx @@ -1,21 +1,14 @@ import ContestStatusTimeDiff from '@/components/ContestStatusTimeDiff' import HeaderAuthPanel from '@/components/auth/HeaderAuthPanel' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger -} from '@/components/shadcn/dropdown-menu' import { auth } from '@/libs/auth' -import { cn, convertToLetter, fetcher, fetcherWithAuth } from '@/libs/utils' -import checkIcon from '@/public/icons/check-green.svg' +import { fetcher, fetcherWithAuth } from '@/libs/utils' import codedangLogo from '@/public/logos/codedang-editor.svg' -import type { Contest, ContestProblem, ProblemDetail } from '@/types/type' +import type { Contest, ProblemDetail } from '@/types/type' import type { Route } from 'next' import Image from 'next/image' import Link from 'next/link' import { redirect } from 'next/navigation' -import { FaSortDown } from 'react-icons/fa' +import ContestProblemDropdown from './ContestProblemDropdown' import EditorMainResizablePanel from './EditorResizablePanel' interface EditorLayoutProps { @@ -24,25 +17,16 @@ interface EditorLayoutProps { children: React.ReactNode } -interface ContestProblemProps { - data: ContestProblem[] - total: number -} - export default async function EditorLayout({ contestId, problemId, children }: EditorLayoutProps) { - let problems: ContestProblemProps | undefined let contest: Contest | undefined let problem: ProblemDetail if (contestId) { // for getting contest info and problems list - problems = await fetcherWithAuth - .get(`contest/${contestId}/problem?take=20`) - .json() const res = await fetcherWithAuth( `contest/${contestId}/problem/${problemId}` ) @@ -72,46 +56,17 @@ export default async function EditorLayout({
{contest ? <>Contest : Problem}

/

- {contest ? ( + {contest && contestId ? ( <> {contest.title}

/

- - -

{`${convertToLetter(problems?.data.find((item) => item.id === Number(problemId))?.order as number)}. ${problem.title}`}

- -
- - {problems?.data.map((p: ContestProblem) => ( - - - {`${convertToLetter(p.order)}. ${p.title}`} - {p.submissionTime && ( -
- check -
- )} -
- - ))} -
-
+ ) : (

{`#${problem.id}. ${problem.title}`}

diff --git a/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/TestcasePanel.tsx b/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/TestcasePanel.tsx index b8d9d05b93..a1eae46c0a 100644 --- a/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/TestcasePanel.tsx +++ b/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/TestcasePanel.tsx @@ -20,15 +20,15 @@ export default function TestcasePanel() { .concat(result) .filter( (item, index, self) => - index === self.findIndex((t) => t.id === item.id) + index === self.findIndex((t) => t.originalId === item.originalId) ) ) - setCurrentTab(result.id) + setCurrentTab(result.originalId) } const removeTab = (testcaseId: number) => { setTestcaseTabList((state) => - state.filter((item) => item.id !== testcaseId) + state.filter((item) => item.originalId !== testcaseId) ) if (currentTab === testcaseId) { setCurrentTab(0) @@ -56,7 +56,7 @@ export default function TestcasePanel() { setCurrentTab(0)} - nextTab={testcaseTabList[0]?.id} + nextTab={testcaseTabList[0]?.originalId} className="flex-shrink-0" > {testcaseTabList.length < 7 ? 'Testcase Result' : 'TC Res'} @@ -72,12 +72,12 @@ export default function TestcasePanel() { {testcaseTabList.map((testcase, index) => ( setCurrentTab(testcase.id)} - onClickCloseButton={() => removeTab(testcase.id)} - testcaseId={testcase.id} - key={testcase.id} + prevTab={testcaseTabList[index - 1]?.originalId} + nextTab={testcaseTabList[index + 1]?.originalId} + onClickTab={() => setCurrentTab(testcase.originalId)} + onClickCloseButton={() => removeTab(testcase.originalId)} + testcaseId={testcase.originalId} + key={testcase.originalId} > { (testcaseTabList.length < 7 @@ -117,7 +117,7 @@ export default function TestcasePanel() {
) : ( item.id === currentTab)} + data={processedData.find((item) => item.originalId === currentTab)} /> )} @@ -200,9 +200,9 @@ function TestSummary({ const total = data.length const notAcceptedTestcases = data - .map((testcase, index) => + .map((testcase) => testcase.result !== 'Accepted' - ? `${testcase.isUserTestcase ? 'User' : 'Sample'} #${index + 1}` + ? `${testcase.isUserTestcase ? 'User' : 'Sample'} #${testcase.id}` : -1 ) .filter((index) => index !== -1) diff --git a/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/useTestResults.ts b/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/useTestResults.ts index bf3cac0be9..1d18395b27 100644 --- a/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/useTestResults.ts +++ b/apps/frontend/app/(client)/(code-editor)/_components/TestcasePanel/useTestResults.ts @@ -85,13 +85,20 @@ export const useTestResults = () => { }) const testcases = useTestcaseStore((state) => state.getTestcases()) + let userTestcaseCount = 1 + let sampleTestcaseCount = 1 const testResults = data.length > 0 ? testcases.map((testcase, index) => { const testResult = data.find((item) => item.id === testcase.id) + if (testcase.isUserTestcase) { + testcase.id = userTestcaseCount++ + } else { + testcase.id = sampleTestcaseCount++ + } return { - id: index + 1, - originalId: testcase.id, + id: testcase.id, + originalId: index + 1, input: testcase.input, expectedOutput: testcase.output, output: testResult?.output ?? '', diff --git a/apps/frontend/app/admin/contest/[contestId]/(overall)/@tabs/submission/_components/SubmissionTable.tsx b/apps/frontend/app/admin/contest/[contestId]/(overall)/@tabs/submission/_components/SubmissionTable.tsx index a76aabfc6f..5187ff9a3b 100644 --- a/apps/frontend/app/admin/contest/[contestId]/(overall)/@tabs/submission/_components/SubmissionTable.tsx +++ b/apps/frontend/app/admin/contest/[contestId]/(overall)/@tabs/submission/_components/SubmissionTable.tsx @@ -6,12 +6,12 @@ import DataTablePagination from '@/app/admin/_components/table/DataTablePaginati import DataTableProblemFilter from '@/app/admin/_components/table/DataTableProblemFilter' import DataTableRoot from '@/app/admin/_components/table/DataTableRoot' import DataTableSearchBar from '@/app/admin/_components/table/DataTableSearchBar' +import SubmissionDetailAdmin from '@/app/admin/contest/[contestId]/_components/SubmissionDetailAdmin' import { Dialog, DialogContent } from '@/components/shadcn/dialog' import { GET_CONTEST_SUBMISSIONS } from '@/graphql/submission/queries' import { useSuspenseQuery } from '@apollo/client' import { useState } from 'react' import { columns } from './Columns' -import SubmissionDetailAdmin from './SubmissionDetailAdmin' export function SubmissionTable({ contestId }: { contestId: number }) { const { data } = useSuspenseQuery(GET_CONTEST_SUBMISSIONS, { diff --git a/apps/frontend/app/admin/contest/[contestId]/(overall)/layout.tsx b/apps/frontend/app/admin/contest/[contestId]/(overall)/layout.tsx index 7d3e281a82..c6c425af70 100644 --- a/apps/frontend/app/admin/contest/[contestId]/(overall)/layout.tsx +++ b/apps/frontend/app/admin/contest/[contestId]/(overall)/layout.tsx @@ -13,12 +13,10 @@ import ContestOverallTabs from '../_components/ContestOverallTabs' export default function Layout({ params, - tabs, - userId + tabs }: { params: { contestId: string } tabs: React.ReactNode - userId: number }) { const { contestId } = params @@ -60,7 +58,7 @@ export default function Layout({ content={contestData?.description} classname="prose mb-4 w-full max-w-full border-y-2 border-y-gray-300 p-5 py-12" /> - + {tabs} ) diff --git a/apps/frontend/app/admin/contest/[contestId]/_components/ContestOverallTabs.tsx b/apps/frontend/app/admin/contest/[contestId]/_components/ContestOverallTabs.tsx index 484e697d19..b705044b44 100644 --- a/apps/frontend/app/admin/contest/[contestId]/_components/ContestOverallTabs.tsx +++ b/apps/frontend/app/admin/contest/[contestId]/_components/ContestOverallTabs.tsx @@ -30,15 +30,22 @@ interface SubmissionSummary { } export default function ContestOverallTabs({ - contestId, - userId + contestId }: { contestId: string - userId: number }) { const id = parseInt(contestId, 10) const pathname = usePathname() + const { data: userData } = useQuery<{ + getUserIdByContest: { userId: number } + }>(GET_CONTEST_SCORE_SUMMARIES, { + variables: { contestId: id }, + skip: !contestId + }) + + const userId = userData?.getUserIdByContest?.userId + const { data: scoreData } = useQuery<{ getContestScoreSummaries: ScoreSummary[] }>(GET_CONTEST_SCORE_SUMMARIES, { diff --git a/apps/frontend/app/admin/contest/[contestId]/(overall)/@tabs/submission/_components/SubmissionDetailAdmin.tsx b/apps/frontend/app/admin/contest/[contestId]/_components/SubmissionDetailAdmin.tsx similarity index 89% rename from apps/frontend/app/admin/contest/[contestId]/(overall)/@tabs/submission/_components/SubmissionDetailAdmin.tsx rename to apps/frontend/app/admin/contest/[contestId]/_components/SubmissionDetailAdmin.tsx index 23fc081669..63dee729f7 100644 --- a/apps/frontend/app/admin/contest/[contestId]/(overall)/@tabs/submission/_components/SubmissionDetailAdmin.tsx +++ b/apps/frontend/app/admin/contest/[contestId]/_components/SubmissionDetailAdmin.tsx @@ -1,6 +1,7 @@ 'use client' import CodeEditor from '@/components/CodeEditor' +import { Alert, AlertDescription, AlertTitle } from '@/components/shadcn/alert' import { ScrollArea, ScrollBar } from '@/components/shadcn/scroll-area' import { Table, @@ -15,6 +16,7 @@ import { GET_SUBMISSION } from '@/graphql/submission/queries' import { dateFormatter, getResultColor } from '@/libs/utils' import type { Language } from '@/types/type' import { useLazyQuery, useQuery } from '@apollo/client' +import { IoWarning } from 'react-icons/io5' export default function SubmissionDetailAdmin({ submissionId @@ -125,9 +127,10 @@ export default function SubmissionDetailAdmin({ - {submission?.testcaseResult.length !== 0 && ( -
-

Testcase

+ +

Testcase

+ {submission?.testcaseResult.length !== 0 ? ( +
@@ -201,16 +204,22 @@ export default function SubmissionDetailAdmin({
+ ) : ( + + + Testcase Judge Results Not Available + + The testcases have been recently updated and are now outdated. + + )} -
-

Source Code

- -
+

Source Code

+
)} diff --git a/apps/frontend/app/admin/contest/[contestId]/participant/[userId]/_components/SubmissionTable.tsx b/apps/frontend/app/admin/contest/[contestId]/participant/[userId]/_components/SubmissionTable.tsx index 8414716c6e..2f9aa00d82 100644 --- a/apps/frontend/app/admin/contest/[contestId]/participant/[userId]/_components/SubmissionTable.tsx +++ b/apps/frontend/app/admin/contest/[contestId]/participant/[userId]/_components/SubmissionTable.tsx @@ -5,8 +5,11 @@ import DataTableFallback from '@/app/admin/_components/table/DataTableFallback' import DataTablePagination from '@/app/admin/_components/table/DataTablePagination' import DataTableProblemFilter from '@/app/admin/_components/table/DataTableProblemFilter' import DataTableRoot from '@/app/admin/_components/table/DataTableRoot' +import SubmissionDetailAdmin from '@/app/admin/contest/[contestId]/_components/SubmissionDetailAdmin' +import { Dialog, DialogContent } from '@/components/shadcn/dialog' import { GET_CONTEST_SUBMISSION_SUMMARIES_OF_USER } from '@/graphql/contest/queries' import { useSuspenseQuery } from '@apollo/client' +import { useState } from 'react' import { submissionColumns } from './SubmissionColumns' export function SubmissionTable({ @@ -22,20 +25,37 @@ export function SubmissionTable({ variables: { contestId, userId, take: 1000 } } ) + const [isSubmissionDialogOpen, setIsSubmissionDialogOpen] = useState(false) + const [submissionId, setSubmissionId] = useState(0) const submissionsData = submissions.data.getContestSubmissionSummaryByUserId.submissions return ( - - - - - + <> + + + { + setSubmissionId(row.original.id) + setIsSubmissionDialogOpen(true) + }} + /> + + + + + + + + ) } diff --git a/apps/frontend/app/admin/contest/_components/ContestProblemColumns.tsx b/apps/frontend/app/admin/contest/_components/ContestProblemColumns.tsx index 60e7881628..b881e0d65d 100644 --- a/apps/frontend/app/admin/contest/_components/ContestProblemColumns.tsx +++ b/apps/frontend/app/admin/contest/_components/ContestProblemColumns.tsx @@ -92,7 +92,7 @@ export const createColumns = ( row.original) .reduce((total, problem) => total + problem.score, 0)} diff --git a/apps/frontend/app/admin/problem/[problemId]/edit/_components/BelongedContestTable.tsx b/apps/frontend/app/admin/problem/[problemId]/edit/_components/BelongedContestTable.tsx index 524e29ec24..83490305c7 100644 --- a/apps/frontend/app/admin/problem/[problemId]/edit/_components/BelongedContestTable.tsx +++ b/apps/frontend/app/admin/problem/[problemId]/edit/_components/BelongedContestTable.tsx @@ -24,7 +24,9 @@ export function BelongedContestTable({ const { data } = useSuspenseQuery(GET_BELONGED_CONTESTS, { variables: { problemId - } + }, + // TODO: 필요시 refetch 하도록 수정 + fetchPolicy: 'network-only' }) useEffect(() => { diff --git a/apps/frontend/app/admin/problem/[problemId]/edit/_components/BelongedContestTableColumns.tsx b/apps/frontend/app/admin/problem/[problemId]/edit/_components/BelongedContestTableColumns.tsx index 0577d56a50..b5d19dfe4b 100644 --- a/apps/frontend/app/admin/problem/[problemId]/edit/_components/BelongedContestTableColumns.tsx +++ b/apps/frontend/app/admin/problem/[problemId]/edit/_components/BelongedContestTableColumns.tsx @@ -44,7 +44,7 @@ export const columns: ColumnDef[] = [ ), cell: ({ row }) => ( -

+

{row.getValue('title')}

), @@ -57,7 +57,9 @@ export const columns: ColumnDef[] = [

State

), cell: ({ row }) => ( -

{row.getValue('state')}

+

+ {row.getValue('state')} +

) }, { @@ -68,7 +70,7 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => (

@@ -84,7 +86,7 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => (

diff --git a/apps/frontend/app/admin/problem/[problemId]/edit/_components/ScoreCautionDialog.tsx b/apps/frontend/app/admin/problem/[problemId]/edit/_components/ScoreCautionDialog.tsx index 85a883ff8d..85c6bed62a 100644 --- a/apps/frontend/app/admin/problem/[problemId]/edit/_components/ScoreCautionDialog.tsx +++ b/apps/frontend/app/admin/problem/[problemId]/edit/_components/ScoreCautionDialog.tsx @@ -36,7 +36,7 @@ export function ScoreCautionDialog({ return (

- + Are you sure you want to edit this problem? diff --git a/apps/frontend/components/shadcn/alert.tsx b/apps/frontend/components/shadcn/alert.tsx new file mode 100644 index 0000000000..808f3865f1 --- /dev/null +++ b/apps/frontend/components/shadcn/alert.tsx @@ -0,0 +1,58 @@ +import { cn } from '@/libs/utils' +import { cva, type VariantProps } from 'class-variance-authority' +import * as React from 'react' + +const alertVariants = cva( + 'relative w-full rounded-lg border border-gray-200 px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-gray-950 [&>svg~*]:pl-7 dark:border-gray-800 dark:[&>svg]:text-gray-50', + { + variants: { + variant: { + default: 'bg-white text-gray-950 dark:bg-gray-950 dark:text-gray-50', + destructive: + 'border-red-500/50 text-red-500 dark:border-red-500 [&>svg]:text-red-500 dark:border-red-900/50 dark:text-red-900 dark:dark:border-red-900 dark:[&>svg]:text-red-900' + } + }, + defaultVariants: { + variant: 'default' + } + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = 'Alert' + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = 'AlertTitle' + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = 'AlertDescription' + +export { Alert, AlertTitle, AlertDescription } diff --git a/apps/frontend/stores/editor.ts b/apps/frontend/stores/editor.ts index d80aedc3ac..699ebc0861 100644 --- a/apps/frontend/stores/editor.ts +++ b/apps/frontend/stores/editor.ts @@ -23,6 +23,7 @@ export const useLanguageStore = (problemId: number, contestId?: number) => { ) ) } + interface CodeState { code: string setCode: (code: string) => void diff --git a/apps/frontend/types/type.ts b/apps/frontend/types/type.ts index 63a5aec25c..47147ea6de 100644 --- a/apps/frontend/types/type.ts +++ b/apps/frontend/types/type.ts @@ -184,6 +184,7 @@ export interface TestResultDetail extends TestResult { input: string expectedOutput: string isUserTestcase: boolean + originalId: number } export interface SettingsFormat { diff --git a/apps/infra/production/codedang/message_queue_aws.tf b/apps/infra/production/codedang/message_queue_aws.tf index b4d7c940d9..6a0473c891 100644 --- a/apps/infra/production/codedang/message_queue_aws.tf +++ b/apps/infra/production/codedang/message_queue_aws.tf @@ -23,7 +23,7 @@ resource "aws_mq_broker" "judge_queue" { broker_name = "Codedang-JudgeQueue" engine_type = "RabbitMQ" - engine_version = "3.10.20" + engine_version = "3.11.28" host_instance_type = "mq.t3.micro" subnet_ids = [aws_subnet.private_mq.id] publicly_accessible = true