-
Notifications
You must be signed in to change notification settings - Fork 16
feat(fe): make generator, checker, validator page #3491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
36d20ff
dec47f8
03beac4
bf7607a
2ae5c21
f05fc1d
3a7d2d1
aa203ac
98369bf
7bb1ca3
7b9d0b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,15 @@ | ||
| import { FileUploadSection } from './FileUploadSection' | ||
|
|
||
| export function CheckerPage() { | ||
| return <div>This is Checker page</div> | ||
| return ( | ||
| <FileUploadSection | ||
| title="특수 채점" | ||
| description="부동소수 오차, 특수 채점 등의 출력 비교 로직을 업로드 해주세요" | ||
| accept=".cpp" | ||
| emptyMessages={[ | ||
| '업로드된 특수 채점 프로그램이 없습니다.', | ||
| '기본 모드에서는 없이도 배포할 수 있으며, 커스텀 채점이 필요하면 추가해주세요.' | ||
| ]} | ||
| /> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| import { Badge } from '@/components/shadcn/badge' | ||
| import { cn } from '@/libs/utils' | ||
| import fileIcon from '@/public/icons/file_gray.svg' | ||
| import InfoIcon from '@/public/icons/info-icon-gray.svg' | ||
| import trashcanIcon from '@/public/icons/trashcan2-gray.svg' | ||
| import uploadIcon from '@/public/icons/upload-blue.svg' | ||
| import Image from 'next/image' | ||
| import type { ChangeEvent, ReactNode } from 'react' | ||
| import { useState, useRef } from 'react' | ||
|
|
||
| interface UploadedFile { | ||
| name: string | ||
| size: string | ||
| } | ||
|
|
||
| interface FileUploadSectionProps { | ||
| title: string | ||
| description: string | ||
| emptyMessages: string[] | ||
| accept: string | ||
| optional?: boolean | ||
| children?: ReactNode | ||
| className?: string | ||
| } | ||
|
|
||
| const FORMAT_EXAMPLES = [ | ||
| 'Generator : generator.cpp / generator.py', | ||
| 'Validator : validator.cpp (testlib 등)', | ||
| 'Checker : checker.cpp (특수 채점)' | ||
| ] | ||
|
|
||
| export function FileUploadSection({ | ||
| title, | ||
| description, | ||
| emptyMessages, | ||
| accept, | ||
| optional, | ||
| children, | ||
| className | ||
| }: FileUploadSectionProps) { | ||
| const [file, setFile] = useState<UploadedFile | null>(null) | ||
| const fileInputRef = useRef<HTMLInputElement>(null) | ||
|
|
||
| const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => { | ||
| const selectedFiles = e.target.files | ||
| if (!selectedFiles || selectedFiles.length === 0) { | ||
| return | ||
| } | ||
|
|
||
| const allowedExtensions = accept.split(',').map((ext) => ext.trim()) | ||
| const selected = selectedFiles[0] | ||
|
|
||
| if (!allowedExtensions.some((ext) => selected.name.endsWith(ext))) { | ||
| alert(`${accept} 파일만 업로드 가능합니다.`) | ||
| if (fileInputRef.current) { | ||
| fileInputRef.current.value = '' | ||
| } | ||
| return | ||
| } | ||
|
|
||
| setFile({ | ||
| name: selected.name, | ||
| size: `${(selected.size / 1024).toFixed(1)}KB` | ||
| }) | ||
| if (fileInputRef.current) { | ||
| fileInputRef.current.value = '' | ||
| } | ||
| } | ||
|
|
||
| const handleDelete = () => { | ||
| setFile(null) | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn( | ||
| 'border-color-cool-neutral-90 rounded-[16px] border px-6 py-7', | ||
| className | ||
| )} | ||
| > | ||
| <div className="flex justify-between border-b pb-5"> | ||
| <div> | ||
| <p className="text-head5_sb_24 mb-1 flex items-center gap-2"> | ||
| {title} | ||
| {optional && ( | ||
| <Badge className="text-primary hover:bg-color-blue-95 bg-color-blue-95 text-caption1_m_13 rounded-[4px] px-[10px] py-1"> | ||
| 선택 | ||
| </Badge> | ||
| )} | ||
| </p> | ||
| <p className="text-body2_m_14 text-color-cool-neutral-40"> | ||
| {description} | ||
| </p> | ||
| </div> | ||
| <input | ||
| type="file" | ||
| ref={fileInputRef} | ||
| onChange={handleFileChange} | ||
| accept={accept} | ||
| multiple | ||
| className="hidden" | ||
| /> | ||
| <button | ||
| onClick={() => fileInputRef.current?.click()} | ||
| className="border-primary-light text-primary text-sub4_sb_14 flex h-fit items-center gap-[6px] rounded-[8px] border-[1.4px] px-3 py-[10px] transition-colors hover:bg-blue-50" | ||
| > | ||
| <Image src={uploadIcon} alt="upload" width={20} height={20} /> | ||
| 파일 업로드 | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="my-5 min-h-[160px]"> | ||
| {file === null ? ( | ||
| <div className="bg-color-neutral-99 flex flex-col items-center rounded-[12px] py-20 text-center"> | ||
| <Image | ||
| src={InfoIcon} | ||
| alt="info" | ||
| width={24} | ||
| height={24} | ||
| className="mb-2" | ||
| /> | ||
| {emptyMessages.map((msg, idx) => ( | ||
| <p | ||
| key={idx} | ||
| className="text-body1_m_16 text-color-cool-neutral-50" | ||
| > | ||
| {msg} | ||
| </p> | ||
| ))} | ||
| </div> | ||
| ) : ( | ||
| <div className="flex items-center justify-between rounded-[12px] bg-white p-4 shadow-[0_4px_20px_0_rgba(53,78,116,0.10)]"> | ||
| <div className="flex gap-4"> | ||
| <span className="bg-color-neutral-99 flex h-12 w-12 items-center justify-center rounded-[6.4px] border"> | ||
| <Image src={fileIcon} alt="file" width={24} height={24} /> | ||
| </span> | ||
| <div> | ||
| <p className="text-sub1_sb_18">{file.name}</p> | ||
| <p className="text-sub4_sb_14 text-color-cool-neutral-40"> | ||
| {file.size} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| <button | ||
| onClick={handleDelete} | ||
| className="border-color-neutral-90 bg-color-neutral-99 hover:bg-color-neutral-95/80 flex h-9 w-12 items-center justify-center rounded-full border transition-all" | ||
| > | ||
| <Image src={trashcanIcon} alt="trash" width={16} height={16} /> | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| <p className="text-sub1_sb_18 text-color-cool-neutral-30 mb-2"> | ||
| 포맷 예시 | ||
| </p> | ||
| <div className="bg-color-neutral-99 border-color-cool-neutral-90 flex flex-col gap-2 rounded-[8px] border p-4"> | ||
| {FORMAT_EXAMPLES.map((example, index) => ( | ||
| <div key={index} className="flex items-center gap-2"> | ||
| <span className="bg-color-neutral-30 h-1 w-1 shrink-0 rounded-full" /> | ||
| <p className="text-body3_r_16 text-color-neutral-30">{example}</p> | ||
| </div> | ||
| ))} | ||
| </div> | ||
|
|
||
| {children} | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,15 @@ | ||
| import { useSuspenseQuery } from '@tanstack/react-query' | ||
| import { FileUploadSection } from './FileUploadSection' | ||
|
|
||
| export function GeneratorPage() { | ||
| // 스켈레톤 확인을 위한 더미코드 | ||
| useSuspenseQuery({ | ||
| queryKey: ['SongJunGyu'], | ||
| queryFn: () => | ||
| new Promise((resolve) => { | ||
| setTimeout(() => { | ||
| resolve('') | ||
| }, 5000) | ||
| }) | ||
| }) | ||
|
|
||
| return <div>This is Generator page</div> | ||
| return ( | ||
| <FileUploadSection | ||
| title="테스트 생성" | ||
| description="테스트 입력을 생성하는 프로그램 및 스크립트를 업로드하세요" | ||
| accept=".cpp, .py" | ||
| emptyMessages={[ | ||
| '업로드 된 테스트 생성 프로그램이 없습니다.', | ||
| '커스텀 채점이 필요하면 추가해주세요.' | ||
| ]} | ||
| /> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,17 +12,14 @@ import { useState } from 'react' | |
| import { AiFillFile } from 'react-icons/ai' | ||
| import { BsPeopleFill } from 'react-icons/bs' | ||
| import { FaBook, FaPen } 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 { TestsPage } from './TestsPage' | ||
| import { ToolsPage } from './ToolsPage' | ||
| import { UploadButton } from './UploadButton' | ||
| import { ValidatorPage } from './ValidatorPage' | ||
|
|
||
| export function ProblemCreateContainer() { | ||
| // 스켈레톤 확인을 위한 더미코드 | ||
|
|
@@ -62,24 +59,10 @@ export function ProblemCreateContainer() { | |
| }, | ||
| { | ||
| 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: 'Tools', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SNB 수정하시고 추가적으로 스켈레톤 코드도 이에 맞게 수정해야 할 것 같아용! |
||
| text: '문제 생성 도구', | ||
| subText: '생성 및 입력 검증, 특수 채점', | ||
| Component: ToolsPage | ||
| }, | ||
| { | ||
| Icon: BsPeopleFill, | ||
|
|
@@ -184,7 +167,7 @@ export function ProblemCreateContainer() { | |
| height={15} | ||
| className={cn({ | ||
| 'scale-x-[-1]': | ||
| label === 'Generator' || label === 'Collaboration', | ||
| label === 'Tools' || label === 'Collaboration', | ||
| 'text-color-cool-neutral-40': curTab, | ||
| 'text-color-cool-neutral-70': !curTab | ||
| })} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| export function ScriptSection() { | ||
| return ( | ||
| <div className="mt-5"> | ||
| <p className="text-sub1_sb_18 text-color-cool-neutral-30 mb-2"> | ||
| 스크립트 | ||
| </p> | ||
| <textarea className="bg-editor-background-1 h-[174px] w-full rounded-[8px] p-4 font-mono text-sm text-white" /> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| 'use client' | ||
|
|
||
| import { cn } from '@/libs/utils' | ||
| import { useState } from 'react' | ||
| import { FileUploadSection } from './FileUploadSection' | ||
| import { ScriptSection } from './ScriptSection' | ||
|
|
||
| type TabType = 'generator' | 'validator' | 'checker' | ||
|
|
||
| const TABS: { key: TabType; label: string }[] = [ | ||
| { key: 'generator', label: '테스트 생성' }, | ||
| { key: 'validator', label: '입력 검증' }, | ||
| { key: 'checker', label: '특수 채점' } | ||
| ] | ||
|
|
||
| const TAB_CONFIG = { | ||
| generator: { | ||
| title: '테스트 생성', | ||
| description: | ||
| '테스트 입력을 생성하는 프로그램 및 스크립트를 업로드하세요 (최대 한 개의 파일만 업로드 가능)', | ||
| accept: '.cpp, .py', | ||
| emptyMessages: [ | ||
| '업로드 된 테스트 생성 프로그램이 없습니다.', | ||
| '기본 모드에서는 없이도 배포할 수 있으며, 커스텀 채점이 필요하면 추가해주세요.' | ||
| ] | ||
| }, | ||
| validator: { | ||
| title: '입력 검증', | ||
| description: | ||
| '잘못된 테스트를 걸러내는 용도의 입력 제약조건 검증하세요 (최대 한 개의 파일만 업로드 가능)', | ||
| accept: '.cpp', | ||
| emptyMessages: [ | ||
| '업로드된 입력 검증 프로그램이 없습니다.', | ||
| '기본 모드에서는 없이도 배포할 수 있으며, 커스텀 채점이 필요하면 추가해주세요.' | ||
| ] | ||
| }, | ||
| checker: { | ||
| title: '특수 채점', | ||
| description: | ||
| '부동소수 오차, 특수 채점 등의 출력 비교 로직을 업로드 해주세요 (최대 한 개의 파일만 업로드 가능)', | ||
| accept: '.cpp', | ||
| emptyMessages: [ | ||
| '업로드된 특수 채점 프로그램이 없습니다.', | ||
| '기본 모드에서는 없이도 배포할 수 있으며, 커스텀 채점이 필요하면 추가해주세요.' | ||
| ] | ||
| } | ||
| } | ||
|
|
||
| export function ToolsPage() { | ||
| const [activeTab, setActiveTab] = useState<TabType>('generator') | ||
|
|
||
| return ( | ||
| <div className="border-color-cool-neutral-90 rounded-[16px] border px-6 py-7"> | ||
| <div className="flex border-b"> | ||
| {TABS.map(({ key, label }) => ( | ||
| <button | ||
| key={key} | ||
| onClick={() => setActiveTab(key)} | ||
| className={cn( | ||
| 'text-sub3_sb_16 w-31 pb-4', | ||
| activeTab === key | ||
| ? 'border-primary text-primary border-b-2' | ||
| : 'text-color-cool-neutral-40' | ||
| )} | ||
| > | ||
| {label} | ||
| </button> | ||
| ))} | ||
| </div> | ||
|
|
||
| <div className="px-6 py-7"> | ||
| {activeTab === 'generator' && ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요기서 특정 탭에서 파일 업로드를 하고 다른 탭으로 가면 업로드했던 그 파일이 사라지는 문제가 있어요! 현재 코드는 하나의 FileUploadSection 컴포넌트만 살려두게 되어있는데 세 개 모두 살려두되 선택되지 않은 탭 페이지에 대해서는 hidden으로 두던가 해야할 것 같아요~ |
||
| <FileUploadSection | ||
| {...TAB_CONFIG.generator} | ||
| className="rounded-none border-0 p-0" | ||
| > | ||
| <ScriptSection /> | ||
| </FileUploadSection> | ||
| )} | ||
| {activeTab === 'validator' && ( | ||
| <FileUploadSection | ||
| {...TAB_CONFIG.validator} | ||
| className="rounded-none border-0 p-0" | ||
| /> | ||
| )} | ||
| {activeTab === 'checker' && ( | ||
| <FileUploadSection | ||
| {...TAB_CONFIG.checker} | ||
| className="rounded-none border-0 p-0" | ||
| optional | ||
| /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,15 @@ | ||
| import { FileUploadSection } from './FileUploadSection' | ||
|
|
||
| export function ValidatorPage() { | ||
| return <div>This is Validator page</div> | ||
| return ( | ||
| <FileUploadSection | ||
| title="입력 검증" | ||
| description="잘못된 테스트를 걸러내는 용도의 입력 제약조건 검증하세요" | ||
| accept=".cpp" | ||
| emptyMessages={[ | ||
| '업로드된 입력 검증 프로그램이 없습니다.', | ||
| '기본 모드에서는 없이도 배포할 수 있으며, 커스텀 채점이 필요하면 추가해주세요.' | ||
| ]} | ||
| /> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bullet이랑 텍스트 사이 너비가 피그마 상보다 좁아요! 이 페이지의 bullet 크기가 8x8이 아니어서 그런 것 같습니당!