diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/CheckerPage.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/CheckerPage.tsx deleted file mode 100644 index 2d84d471c2..0000000000 --- a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/CheckerPage.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function CheckerPage() { - return
This is Checker page
-} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/FileUpload.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/FileUpload.tsx new file mode 100644 index 0000000000..f10242b74b --- /dev/null +++ b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/FileUpload.tsx @@ -0,0 +1,125 @@ +'use client' + +import { cn } from '@/libs/utils' +import InfoIconGray from '@/public/icons/info-icon-gray.svg' +import TrashIcon from '@/public/icons/trashcan2-gray.svg' +import { useRef, useState } from 'react' +import { AiFillFile } from 'react-icons/ai' +import { TbCode } from 'react-icons/tb' + +interface FileUploadProps { + primaryText: string + secondaryText: string + onFilesChange: (files: File[]) => void + multiple?: boolean + className?: string +} + +function formatFileSize(bytes: number): string { + if (bytes < 1024) { + return `${bytes}B` + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)}KB` + } + return `${(bytes / (1024 * 1024)).toFixed(1)}MB` +} + +export function FileUpload({ + primaryText, + secondaryText, + onFilesChange, + multiple, + className +}: FileUploadProps) { + const [files, setFiles] = useState([]) + const inputRef = useRef(null) + + const handleFiles = (incoming: FileList | null) => { + if (!incoming) { + return + } + const next = Array.from(incoming) + setFiles(next) + onFilesChange(next) + } + + const removeFile = (index: number) => { + const next = files.filter((_, i) => i !== index) + setFiles(next) + onFilesChange(next) + } + + return ( +
+ {files.length > 0 && ( +
    + {files.map((f, i) => { + const isZip = f.name.endsWith('.zip') + return ( +
  • +
    +
    + {isZip ? ( + + ) : ( + + )} +
    +
    + {f.name} + + {formatFileSize(f.size)} + +
    +
    + +
  • + ) + })} +
+ )} + {files.length === 0 && ( +
inputRef.current?.click()} + onDragOver={(e) => e.preventDefault()} + onDrop={(e) => { + e.preventDefault() + handleFiles(e.dataTransfer.files) + }} + className="bg-color-neutral-99 flex cursor-pointer flex-col items-center justify-center rounded-[12px] py-20" + > + handleFiles(e.target.files)} + /> +
+ +
+

{primaryText}

+

{secondaryText}

+
+
+
+ )} +
+ ) +} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/GeneratorPage.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/GeneratorPage.tsx deleted file mode 100644 index 62f8c54ae2..0000000000 --- a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/GeneratorPage.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useSuspenseQuery } from '@tanstack/react-query' - -export function GeneratorPage() { - // 스켈레톤 확인을 위한 더미코드 - useSuspenseQuery({ - queryKey: ['SongJunGyu'], - queryFn: () => - new Promise((resolve) => { - setTimeout(() => { - resolve('') - }, 5000) - }) - }) - - return
This is Generator page
-} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/ProblemCreateContainer.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/ProblemCreateContainer.tsx index 48bc5936ad..88ebd144f4 100644 --- a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/ProblemCreateContainer.tsx +++ b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/ProblemCreateContainer.tsx @@ -5,24 +5,19 @@ import { Button } from '@/components/shadcn/button' import { cn } from '@/libs/utils' import ArrowRightNarrowIcon from '@/public/icons/arrow-right-narrow.svg' import CheckCircleIcon from '@/public/icons/check-circle.svg' -import PenIcon from '@/public/icons/pen.svg' import { ErrorBoundary, Suspense } from '@suspensive/react' import { useSuspenseQuery } from '@tanstack/react-query' import { useState } from 'react' import { AiFillFile } from 'react-icons/ai' import { BsPeopleFill } from 'react-icons/bs' import { FaBook } from 'react-icons/fa' -import { FaSquareCheck } from 'react-icons/fa6' -import { PiMagnifyingGlassFill, PiWrenchFill } from 'react-icons/pi' -import { CheckerPage } from './CheckerPage' +import { PiWrenchFill } from 'react-icons/pi' import { CollaborationPage } from './CollaborationPage' -import { GeneratorPage } from './GeneratorPage' import { ProblemCreateContentSkeleton } from './ProblemCreateSkeletons' -import { SolutionPage } from './SolutionPage' import { StatementPage } from './StatementPage' +import { TcManagePage } from './TcManagePage' import { TestsPage } from './TestsPage' import { UploadButton } from './UploadButton' -import { ValidatorPage } from './ValidatorPage' export function ProblemCreateContainer() { // 스켈레톤 확인을 위한 더미코드 @@ -53,33 +48,12 @@ export function ProblemCreateContainer() { subText: '입력 및 정답 (Input & Output)', Component: TestsPage }, - { - Icon: PenIcon, - label: 'Solution', - text: '솔루션', - subText: '솔루션 업로드 및 테스트 검증', - Component: SolutionPage - }, { Icon: PiWrenchFill, - label: 'Generator', - text: '테스트 생성', - subText: '테스트 입력 생성', - Component: GeneratorPage - }, - { - Icon: PiMagnifyingGlassFill, - label: 'Validator', - text: '입력 검증', - subText: '입력 및 검증', - Component: ValidatorPage - }, - { - Icon: FaSquareCheck, - label: 'Checker', - text: '특수 채점', - subText: '특수 채점 기능', - Component: CheckerPage + label: 'TcManage', + text: '테스트 케이스 관리', + subText: '생성 및 입력 검증, 특수 채점', + Component: TcManagePage }, { Icon: BsPeopleFill, @@ -179,7 +153,7 @@ export function ProblemCreateContainer() { height={15} className={cn({ 'scale-x-[-1]': - label === 'Generator' || label === 'Collaboration', + label === 'TcManage' || label === 'Collaboration', 'text-color-cool-neutral-40': curTab, 'text-color-cool-neutral-70': !curTab })} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/SolutionPage.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/SolutionPage.tsx deleted file mode 100644 index 354bd6eeb4..0000000000 --- a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/SolutionPage.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { Button } from '@/components/shadcn/button' -import { Separator } from '@/components/shadcn/separator' -import { cn } from '@/libs/utils' -import CheckCircleIcon from '@/public/icons/check-circle.svg' -import CodingIcon from '@/public/icons/coding.svg' -import InfoIconGray from '@/public/icons/info-icon-gray.svg' -import TrashCanIconGray from '@/public/icons/trashcan2-gray.svg' -import { useRef, useState } from 'react' - -const LANGUAGE_DISPLAY_MAP: Record = { - cpp: 'C++17', - c: 'C11', - cc: 'C++17', - 'c++': 'C++20', - java: 'Java 11', - kt: 'Kotlin 1.9', - kts: 'Kotlin Script', - py: 'Python 3.11', - ipynb: 'Jupyter Notebook', - js: 'JavaScript (Node.js)', - ts: 'TypeScript 5.0', - jsx: 'React JS', - tsx: 'React TS', - go: 'Go 1.21', - rs: 'Rust 1.75', - swift: 'Swift 5.9', - sh: 'Shell Script', - rb: 'Ruby 3.2', - cs: 'C# 12 (.NET 8)', - php: 'PHP 8.2', - sql: 'SQL (PostgreSQL)', - html: 'HTML5', - css: 'CSS3', - json: 'JSON Meta', - md: 'Markdown' -} - -interface SolutionFile { - file: File - testResult: { - isPassed: boolean - passedInfo: { - date: string - passNo: number - totalPassed: number - } | null - } -} - -export function SolutionPage() { - // TODO: DB에서 가져온 기존 파일들로 초기화해야함 - const [solutionFiles, setSolutionFiles] = useState([]) - const inputFileRef = useRef(null) - - const UplaodFile = (inputFiles: FileList) => { - const solutionFiles = Array.from(inputFiles).map( - (file) => - ({ - file, - testResult: { isPassed: false } - }) as SolutionFile - ) - setSolutionFiles((prev) => prev.concat(solutionFiles)) - } - - const DeleteFile = (solutionFile: SolutionFile) => { - setSolutionFiles((prev) => prev.filter((f) => f !== solutionFile)) - } - - // TODO: TestFile 함수 백엔드 함수와 통신하도록 변경해야 함 (제출 후 버튼 disable 로직 추가 필요) - const TestFile = (solutionFile: SolutionFile) => { - setSolutionFiles((prev) => - prev.map((f) => - f === solutionFile - ? ({ - file: f.file, - testResult: { - isPassed: true, - passedInfo: { - date: '2019. 01. 01', - passNo: 1, - totalPassed: 2 - } - } - } as SolutionFile) - : f - ) - ) - } - - return ( -
- { - event.target.files && UplaodFile(event.target.files) - }} - ref={inputFileRef} - multiple - className="hidden" - type="file" - /> -
-
-

솔루션 파일 업로드

-

- 참고 솔루션 (정답 코드)을 업로드하고, 테스트 케이스와 함께 - 검증하세요 -

-
- -
- -
- {solutionFiles.length === 0 ? ( -
- -

- { - '아직 솔루션이 업로드 되지 않았습니다.\n배포/Ready 상태 전환을 위해 솔루션 파일 업로드가 필수입니다.' - } -

-
- ) : ( - solutionFiles.map((solutionFile, idx) => { - const file = solutionFile.file - const fileName = file.name - const fileExtension = fileName - .substring(fileName.lastIndexOf('.') + 1) - .toLowerCase() - const fileSize = Math.floor((file.size / 1000) * 10) / 10 - - // TODO: 아래 데이터들 실제 백엔드 API 통신 값으로 바꾸기 - // (특히 테스트 통과 시 정보들을 ) - const testGroupCnt = 0 - const matchedCnt = 0 - const missingCnt = 0 - - const { isPassed: testPassed, passedInfo: passedInfo } = - solutionFile.testResult - - return ( -
-
-
- -
-
-
-

{fileName}

-
-

- {fileSize}KB -

-

-

- {LANGUAGE_DISPLAY_MAP[fileExtension]} -

-
-
- -
-
-
-
-

- 테스트 그룹 -

-

{testGroupCnt}

-
- -
-

Matched

-

{matchedCnt}

-
- -
-

Missing

-

{missingCnt}

-
-
-
-
- {testPassed ? ( - - ) : ( - - )} -
-

- {testPassed ? '테스트 통과' : '테스트 실행 필요'} -

-
- {testPassed && passedInfo ? ( -
-

{passedInfo.date}

-

-

- {passedInfo.passNo}/{passedInfo.totalPassed}{' '} - 솔루션 테스트 통과 완료 -

-
- ) : ( -

솔루션 테스트를 실행해주세요

- )} -
-
-
- -
-
- ) - }) - )} -
-
- ) -} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/TcManagePage.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/TcManagePage.tsx new file mode 100644 index 0000000000..fcbbf4b292 --- /dev/null +++ b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/TcManagePage.tsx @@ -0,0 +1,51 @@ +'use client' + +import { Tabs, TabsList, TabsTrigger } from '@/components/shadcn/tabs' +import { useSearchParams } from 'next/navigation' +import { useState } from 'react' +import { CheckerTab } from './tabs/CheckerTab' +import { TcAutoTab } from './tabs/TcAutoTab' +import { TcInputTab } from './tabs/TcInputTab' +import { TcManualTab } from './tabs/TcManualTab' +import { TcOutputTab } from './tabs/TcOutputTab' +import { ValidationTab } from './tabs/ValidationTab' + +const GENERAL_TABS = [TcAutoTab, TcManualTab, ValidationTab] +const SPECIAL_TABS = [TcInputTab, TcOutputTab, ValidationTab, CheckerTab] + +type TabValue = + | (typeof GENERAL_TABS)[number]['value'] + | (typeof SPECIAL_TABS)[number]['value'] + +export function TcManagePage() { + const searchParams = useSearchParams() + const isSpecial = searchParams.get('type') === 'special' + const tabs = isSpecial ? SPECIAL_TABS : GENERAL_TABS + + const [activeValue, setActiveValue] = useState(tabs[0].value) + const active = tabs.find((t) => t.value === activeValue) ?? tabs[0] + + const boxStyle = 'border-cool-neutral-40 rounded-[16px] border px-6 py-7' + + return ( +
+ setActiveValue(v as TabValue)} + > + + {tabs.map((t) => ( + + {t.label} + + ))} + + + {active.cards.map((Card, i) => ( +
+ +
+ ))} +
+ ) +} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/ValidatorPage.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/ValidatorPage.tsx deleted file mode 100644 index b11e40a173..0000000000 --- a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/ValidatorPage.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function ValidatorPage() { - return
This is Validator page
-} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/CheckerTab.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/CheckerTab.tsx new file mode 100644 index 0000000000..245deb28c8 --- /dev/null +++ b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/CheckerTab.tsx @@ -0,0 +1,9 @@ +function CheckerCard1() { + return
특수 채점 first div content
+} + +export const CheckerTab = { + value: 'checker' as const, + label: '특수 채점', + cards: [CheckerCard1] +} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcAutoTab.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcAutoTab.tsx new file mode 100644 index 0000000000..4e064c3408 --- /dev/null +++ b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcAutoTab.tsx @@ -0,0 +1,21 @@ +function TcAutoCard1() { + return
TC 자동 생성 first div content
+} + +function TcAutoCard2() { + return
TC 자동 생성 second div content
+} + +function TcAutoCard3() { + return
TC 자동 생성 third div content
+} + +function TcAutoCard4() { + return
TC 자동 생성 fourth div content
+} + +export const TcAutoTab = { + value: 'tc-auto' as const, + label: 'TC 자동 생성', + cards: [TcAutoCard1, TcAutoCard2, TcAutoCard3, TcAutoCard4] +} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcInputTab.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcInputTab.tsx new file mode 100644 index 0000000000..0d1112570a --- /dev/null +++ b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcInputTab.tsx @@ -0,0 +1,26 @@ +'use client' + +import { FileUpload } from '@/app/(client)/(main)/(create)/problem/create/_components/FileUpload' +import { useState } from 'react' + +function TcInputCard1() { + const [, setFiles] = useState([]) + return ( + + ) +} + +function TcInputCard2() { + return
TC 인풋 생성 second div content
+} + +export const TcInputTab = { + value: 'tc-input' as const, + label: 'TC 인풋 생성', + cards: [TcInputCard1, TcInputCard2] +} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcManualTab.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcManualTab.tsx new file mode 100644 index 0000000000..121767ae5e --- /dev/null +++ b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcManualTab.tsx @@ -0,0 +1,13 @@ +function TcManualCard1() { + return
TC 수동 생성 first div content
+} + +function TcManualCard2() { + return
TC 수동 생성 second div content
+} + +export const TcManualTab = { + value: 'tc-manual' as const, + label: 'TC 수동 생성', + cards: [TcManualCard1, TcManualCard2] +} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcOutputTab.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcOutputTab.tsx new file mode 100644 index 0000000000..b234f7dbb2 --- /dev/null +++ b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/TcOutputTab.tsx @@ -0,0 +1,17 @@ +function TcOutputCard1() { + return
TC 아웃풋 생성 first div content
+} + +function TcOutputCard2() { + return
TC 아웃풋 생성 second div content
+} + +function TcOutputCard3() { + return
TC 아웃풋 생성 third div content
+} + +export const TcOutputTab = { + value: 'tc-output' as const, + label: 'TC 아웃풋 생성', + cards: [TcOutputCard1, TcOutputCard2, TcOutputCard3] +} diff --git a/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/ValidationTab.tsx b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/ValidationTab.tsx new file mode 100644 index 0000000000..71dc3bdb2b --- /dev/null +++ b/apps/frontend/app/(client)/(main)/(create)/problem/create/_components/tabs/ValidationTab.tsx @@ -0,0 +1,13 @@ +function ValidationCard1() { + return
입력 검증 first div content
+} + +function ValidationCard2() { + return
입력 검증 second div content
+} + +export const ValidationTab = { + value: 'validation' as const, + label: '입력 검증', + cards: [ValidationCard1, ValidationCard2] +} diff --git a/apps/frontend/components/shadcn/tabs.tsx b/apps/frontend/components/shadcn/tabs.tsx index 54edcaf0c6..2216194f70 100644 --- a/apps/frontend/components/shadcn/tabs.tsx +++ b/apps/frontend/components/shadcn/tabs.tsx @@ -14,7 +14,8 @@ const tabsListVariants = cva('inline-flex', { outline: 'gap-0 rounded-full border-2 border-color-line p-1', problem: 'items-center gap-0 rounded-full border border-color-line bg-white p-1', - editor: 'gap-1 rounded bg-editor-background-1 py-1 px-1.5' // 수정: 넓은 간격, 둥근 모서리, 어두운 배경, 패딩 + editor: 'gap-1 rounded bg-editor-background-1 py-1 px-1.5', // 수정: 넓은 간격, 둥근 모서리, 어두운 배경, 패딩 + underline: 'gap-0 border-b border-color-line w-full' } }, defaultVariants: { @@ -46,6 +47,11 @@ const tabsTriggerVariants = cva( 'rounded-sm text-xs font-normal px-2 py-1', 'data-[state=active]:bg-[#334155] data-[state=active]:text-primary-light', 'data-[state=inactive]:bg-transparent data-[state=inactive]:text-slate-100' + ], + underline: [ + 'px-3 pb-4 text-sub3_sb_16 border-b-2', + 'data-[state=active]:border-primary data-[state=active]:text-primary', + 'data-[state=inactive]:border-transparent data-[state=inactive]:text-color-cool-neutral-40' ] } },