Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bullet이랑 텍스트 사이 너비가 피그마 상보다 좁아요! 이 페이지의 bullet 크기가 8x8이 아니어서 그런 것 같습니당!

<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
Expand Up @@ -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() {
// 스켈레톤 확인을 위한 더미코드
Expand Down Expand Up @@ -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',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SNB 수정하시고 추가적으로 스켈레톤 코드도 이에 맞게 수정해야 할 것 같아용!

text: '문제 생성 도구',
subText: '생성 및 입력 검증, 특수 채점',
Component: ToolsPage
},
{
Icon: BsPeopleFill,
Expand Down Expand Up @@ -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
})}
Expand Down
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' && (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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={[
'업로드된 입력 검증 프로그램이 없습니다.',
'기본 모드에서는 없이도 배포할 수 있으며, 커스텀 채점이 필요하면 추가해주세요.'
]}
/>
)
}
Loading
Loading