From f229558d2234d314c8c73ae4fd6804edf861cec3 Mon Sep 17 00:00:00 2001 From: Minkyu Lee Date: Fri, 9 Aug 2024 19:19:58 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20problem=20API=20v2=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/problem.ts | 30 +- src/apis/utility.ts | 5 +- .../solve/CodeEditor/useLanguage.tsx | 12 +- src/components/solve/TestResultConsoleV2.tsx | 77 +++++ src/main.tsx | 2 +- src/pages/SolveV2.tsx | 281 ++++++++++++++++++ src/types/apiTypes.ts | 26 ++ 7 files changed, 428 insertions(+), 5 deletions(-) create mode 100644 src/components/solve/TestResultConsoleV2.tsx create mode 100644 src/pages/SolveV2.tsx diff --git a/src/apis/problem.ts b/src/apis/problem.ts index 2135835..6a6d526 100644 --- a/src/apis/problem.ts +++ b/src/apis/problem.ts @@ -1,9 +1,11 @@ import { Problem, ProblemSubmissionRequest, + ProblemSubmissionRequestV2, ProblemSubmissionResult, + ProblemSubmissionResultV2, } from "../types/apiTypes"; -import { getRequest, sseRequest } from "./utility"; +import { getRequest, postRequest, sseRequest } from "./utility"; export const getProblemById = (problem_id: number) => getRequest(`/v1/problems/${problem_id}`); @@ -21,3 +23,29 @@ export const postProblemSubmission = ( {}, // headers true, // authorized ); + +export const getProblemByIdV2 = (problem_id: number) => + getRequest(`/v2/problems/${problem_id}`); + +export const postProblemSubmissionV2 = ( + problemSubmission: ProblemSubmissionRequestV2, +) => + postRequest<{ message: string }>( + "/v2/problems/submission", + problemSubmission, + {}, // headers + true, // authorized + ); + +export const getProblemSubmissionV2 = (problem_id: number) => + sseRequest< + | { type: "skip"; data: { items: [] } } + | { type: "message"; data: { items: ProblemSubmissionResultV2[] } } + | { type: "error"; data: { detail: string } } + >( + `/v2/problems/${problem_id}/submission`, + {}, // body + {}, // headers + true, // authorized + "GET", // method + ); diff --git a/src/apis/utility.ts b/src/apis/utility.ts index 2968b84..9c081b7 100644 --- a/src/apis/utility.ts +++ b/src/apis/utility.ts @@ -105,17 +105,18 @@ export const sseRequest = ( body: object, header: HeadersInit = {}, authorized = true, + method: "GET" | "POST" = "POST", ): AsyncIterable => ({ async *[Symbol.asyncIterator]() { const response = await fetch(`${BASE_URL}${url}`, { - method: "POST", + method, headers: { ...defaultCommonHeader, ...defaultPostHeader, ...header, ...(authorized ? authorizedHeader(getSsoToken()) : {}), }, - body: JSON.stringify(body), + body: method === "POST" ? JSON.stringify(body) : undefined, }); if (!response.ok) { throw response; diff --git a/src/components/solve/CodeEditor/useLanguage.tsx b/src/components/solve/CodeEditor/useLanguage.tsx index cdc1887..f0faa68 100644 --- a/src/components/solve/CodeEditor/useLanguage.tsx +++ b/src/components/solve/CodeEditor/useLanguage.tsx @@ -6,7 +6,7 @@ import { LanguageSupport, StreamLanguage } from "@codemirror/language"; import { java } from "@codemirror/lang-java"; import { c, kotlin } from "@codemirror/legacy-modes/mode/clike"; import { swift } from "@codemirror/legacy-modes/mode/swift"; -import { LanguageCode } from "../../../types/apiTypes"; +import { LanguageCode, LanguageCodeV2 } from "../../../types/apiTypes"; export const languages = [ "C", @@ -41,6 +41,16 @@ export const languageCodes: Record = { Swift: LanguageCode.SWFIT, }; +export const languageCodesV2: Record = { + C: LanguageCodeV2.C, + "C++": LanguageCodeV2.CPP, + Java: LanguageCodeV2.JAVA, + Javascript: LanguageCodeV2.JAVASCRIPT, + Python: LanguageCodeV2.PYTHON, + Kotlin: LanguageCodeV2.KOTLIN, + Swift: LanguageCodeV2.SWIFT, +}; + export const boilerplates: Record = { C: `#include #include diff --git a/src/components/solve/TestResultConsoleV2.tsx b/src/components/solve/TestResultConsoleV2.tsx new file mode 100644 index 0000000..595b695 --- /dev/null +++ b/src/components/solve/TestResultConsoleV2.tsx @@ -0,0 +1,77 @@ +import styled from "styled-components"; +import { ProblemSubmissionResultV2 } from "../../types/apiTypes.ts"; +import { LegacyRef } from "react"; + +type Props = { + results: ProblemSubmissionResultV2[]; + error: string[]; + ulRef?: LegacyRef; +}; + +export default function TestResultConsole(props: Props) { + return ( +
+ Console +
    + {props.results.map((result) => ( +
  • +

    {result.num}번 테스트케이스

    +

    시간: {result.time}초

    +

    메모리: {result.memory}KB

    + 결과: {result.status} + {result.stdout && ( +
    + 출력 +
    {result.stdout}
    +
    + )} +
  • + ))} + {props.error.map((err, i) => ( +
  • +

    에러 {i + 1}

    +
    {err}
    +
  • + ))} +
+
+ ); +} + +const Section = styled.section` + border: 0.4rem solid #373737; + border-top-width: 0.2rem; + border-radius: 0.5rem; + + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + + h3 { + font-weight: bold; + } + + ul { + display: flex; + flex-direction: column; + gap: 1rem; + line-height: 1.2; + overflow: auto; + + h4 { + font-weight: bold; + } + } + + /* Solve page layout */ + flex: 1; +`; + +const SectionTitle = styled.h3` + font-size: 1.6rem; +`; + +const Status = styled.p<{ $status: string }>` + color: ${(props) => (props.$status === "CORRECT" ? "#2fa500" : "#ff0000")}; +`; diff --git a/src/main.tsx b/src/main.tsx index 1426cab..5d0d7a8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,7 +6,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import GlobalStyles from "./GlobalStyles"; import Home from "./pages/Home"; -import Solve from "./pages/Solve"; +import Solve from "./pages/SolveV2"; import Resume from "./pages/Resume"; import Recruit from "./pages/Recruit"; import Dashboard from "./pages/Dashboard"; diff --git a/src/pages/SolveV2.tsx b/src/pages/SolveV2.tsx new file mode 100644 index 0000000..9cafeaa --- /dev/null +++ b/src/pages/SolveV2.tsx @@ -0,0 +1,281 @@ +import { Link, useParams } from "react-router-dom"; +import styled from "styled-components"; +import ProblemDescription from "../components/solve/ProblemDescription/ProblemDescription.tsx"; +import CodeEditor from "../components/solve/CodeEditor/index.tsx"; +import TestResultConsole from "../components/solve/TestResultConsoleV2.tsx"; +import DragResizable from "../components/solve/DragResizable.tsx"; +import { useRef, useState } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + getProblemById, + getProblemSubmissionV2, + postProblemSubmissionV2, +} from "../apis/problem.ts"; +import { + boilerplates, + languageCodesV2, + useLanguage, +} from "../components/solve/CodeEditor/useLanguage.tsx"; +import { useCodeRef } from "../components/solve/CodeEditor/useCode.tsx"; +import { useCustomTestCases } from "../components/solve/ProblemDescription/useCustomTestCases.tsx"; +import { ProblemSubmissionResultV2 } from "../types/apiTypes.ts"; +import { unreachable } from "../lib/unreachable.ts"; +import { flushSync } from "react-dom"; + +export default function Solve() { + const params = useParams(); + const queryClient = useQueryClient(); + const problemNumber = Number(params.problem_number); + const { + data: problem, + isLoading, + isError, + } = useQuery({ + queryKey: ["problem", problemNumber], + queryFn: () => getProblemById(problemNumber), + staleTime: 1000 * 60 * 60, + retry: 1, + }); + const [isFullScreen, setIsFullScreen] = useState(false); + const [language, setLanguage] = useLanguage(); + // const [code, setCode] = useCode(language, problemNumber); + const codeRef = useCodeRef(language, problemNumber); + const [customTestcases, setCustomTestcases] = + useCustomTestCases(problemNumber); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [testResults, setTestResults] = useState( + [], + ); + const [submitError, setSubmitError] = useState([]); + const testConsoleRef = useRef(null); + + const handleSubmit = async (is_example: boolean) => { + if (!codeRef.current) { + alert("코드를 입력해주세요"); + return; + } + + queryClient.invalidateQueries(["recruiting"]); + setIsSubmitting(true); + setTestResults([]); + setSubmitError([]); + const res = postProblemSubmissionV2({ + problem_id: problemNumber, + language: languageCodesV2[language], + source_code: codeRef.current, + is_example, + extra_testcases: is_example + ? customTestcases.map((t) => ({ + stdin: t.input, + expected_output: t.output, + })) + : [], + }); + res + .catch((e) => { + alert("제출 중 오류가 발생했습니다."); + }) + .then(() => getProblemSubmissionV2(problemNumber)) + .then(async (res) => { + for await (const { data, type } of res) { + switch (type) { + case "skip": + break; + case "message": + flushSync(() => { + setTestResults((prev) => [...prev, ...data.items]); + }); + if (testConsoleRef.current) { + testConsoleRef.current.lastElementChild?.scrollIntoView({ + behavior: "smooth", + }); + } + break; + case "error": + console.log(data); + flushSync(() => { + setSubmitError((prev) => [...prev, data.detail]); + }); + if (testConsoleRef.current) { + testConsoleRef.current.lastElementChild?.scrollIntoView({ + behavior: "smooth", + }); + } + break; + default: + unreachable(type); + } + } + }) + .catch((e) => { + console.error(e); + alert("모집이 마감되었습니다."); + }); + setIsSubmitting(false); + }; + + /** + * @TODO 에러처리 + */ + + if (isLoading) { + return
loading...
; + } + + if (isError) { + return
problem not found
; + } + + return ( + +
+ + + ← + Back + + + + + ({ + input: t.stdin, + output: t.expected_output, + }))} + customTestCases={customTestcases} + setCustomTestCases={setCustomTestcases} + /> + + + + { + codeRef.current = newCode; + }} + language={language} + setLanguage={setLanguage} + /> + + + + + + handleSubmit(false)} + disabled={isSubmitting} + $primary + > + 제출하기 + + handleSubmit(true)} + disabled={isSubmitting} + > + 테스트 실행 + + { + if (!confirm("정말로 코드를 초기화하시겠습니까?")) return; + codeRef.current = boilerplates[language]; + }} + > + 코드 초기화 + + + + +
+
+ ); +} + +const Container = styled.div` + display: flex; + height: 100vh; + padding: 3rem; + box-sizing: border-box; + background: #fff7e9; +`; +const Main = styled.main` + display: flex; + flex-direction: column; + gap: 1.6rem; + flex: 1; + overflow: hidden; + border: 0.4rem solid #373737; + box-shadow: 1rem 1rem #373737; + border-radius: 0.5rem; + background: white; +`; +const TopNav = styled.nav` + display: flex; + align-items: center; + padding: 1.4rem; + background: #f0745f; + border-bottom: 0.4rem solid #373737; + + a { + display: flex; + align-items: center; + gap: 0.8rem; + font-weight: bold; + font-size: 1.6rem; + color: #000000; + text-decoration: none; + } +`; +const Row = styled.div<{ $collapseLeft?: boolean }>` + display: flex; + flex: 1; + gap: ${(props) => (props.$collapseLeft ? "0" : "1.6rem")}; + padding: 0 1.6rem 1.6rem; + min-height: 0; + & > :first-child { + ${(props) => + props.$collapseLeft && + ` + flex: 0; + opacity: 0;`} + transition: ease 0.3s; + } +`; +const Col = styled.div` + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + min-width: 0; +`; +const BottomNav = styled.nav` + display: flex; + align-items: center; + justify-content: flex-end; + gap: 1.6rem; + padding: 1.6rem 0 0; +`; +const SubmitButton = styled.button<{ $primary?: boolean }>` + padding: 0.9rem 2rem; + border: 0.4rem solid #373737; + border-radius: 0.5rem; + box-shadow: 0.4rem 0.4rem #323232; + font-size: 1.8rem; + background: ${(props) => (props.$primary ? "#f0745f" : "#ededed")}; + cursor: pointer; + &:active { + box-shadow: 0.2rem 0.2rem #323232; + transform: translate(0.2rem, 0.2rem); + } + &:disabled { + background: #c4c4c4; + } +`; diff --git a/src/types/apiTypes.ts b/src/types/apiTypes.ts index 48c301b..558dc22 100644 --- a/src/types/apiTypes.ts +++ b/src/types/apiTypes.ts @@ -99,6 +99,16 @@ export enum LanguageCode { SWFIT = 83, } +export enum LanguageCodeV2 { + C = "c", + CPP = "c++", + JAVA = "java", + JAVASCRIPT = "javascript", + PYTHON = "python", + KOTLIN = "kotlin", + SWIFT = "swift", +} + export type ProblemSubmissionRequest = { problem_id: number; language: LanguageCode; @@ -107,6 +117,14 @@ export type ProblemSubmissionRequest = { extra_testcases?: ApiTestCase[]; }; +export type ProblemSubmissionRequestV2 = { + problem_id: number; + language: LanguageCodeV2; + source_code: string; + is_example?: boolean; + extra_testcases?: ApiTestCase[]; +}; + export enum ProblemSubmissionStatusCode { IN_QUEUE = 1, PROCESSING = 2, @@ -135,6 +153,14 @@ export type ProblemSubmissionResult = { memory: number; }; +export type ProblemSubmissionResultV2 = { + num: number; + status: string; + stdout: string | null; + time: number; + memory: number; +}; + /** * Resume */