Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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,165 @@
import infoIcon from '@/public/icons/file-info-gray.svg'
import fileIcon from '@/public/icons/file_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 } from 'react'
import { useState, useRef } from 'react'

interface UploadedFile {
id: string
name: string
size: string
}

interface FileUploadSectionProps {
title: string
description: string
emptyMessages: string[]
accept: string
}

const FORMAT_EXAMPLES = [
'Generator : generator.cpp / generator.py',
'Validator : validator.cpp (testlib 등)',
'Checker : checker.cpp (특수 채점)'
]

export function FileUploadSection({
title,
description,
emptyMessages,
accept
}: FileUploadSectionProps) {
const [files, setFiles] = useState<UploadedFile[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)

const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const selectedFiles = e.target.files
if (!selectedFiles) {
return
}

const allowedExtensions = accept.split(',').map((ext) => ext.trim())

const newFiles: UploadedFile[] = Array.from(selectedFiles)
.filter((file) =>
allowedExtensions.some((ext) => file.name.endsWith(ext))
)
.map((file) => {
const uniqueId = `${file.name}-${file.size}-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`

return {
id: uniqueId,
name: file.name,
size: `${(file.size / 1024).toFixed(1)}KB`
}
})

if (newFiles.length === 0 && selectedFiles.length > 0) {
alert(`${accept} 파일만 업로드 가능합니다.`)
return
}

setFiles((prev) => [...prev, ...newFiles])
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}

const handleDelete = (id: string) => {
setFiles((prev) => prev.filter((file) => file.id !== id))
}

return (
<div className="border-color-cool-neutral-90 rounded-[16px] border px-6 py-7">
<div className="flex justify-between border-b pb-5">
<div>
<p className="text-head5_sb_24 mb-1">{title}</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]">
{files.length === 0 ? (
<div className="bg-color-neutral-99 flex flex-col items-center rounded-[12px] py-20 text-center">
<Image
src={infoIcon}
Comment thread
sONg20NOW marked this conversation as resolved.
Outdated
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 flex-col gap-3">
{files.map((file) => (
<div
key={file.id}
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={20} height={20} />
Comment thread
sONg20NOW marked this conversation as resolved.
Outdated
</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(file.id)}
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>
)}
</div>

<p className="text-sub1_sb_18 mb-2">포맷 예시</p>
Comment thread
sONg20NOW marked this conversation as resolved.
Outdated
<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>
</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
@@ -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={[
'업로드된 입력 검증 프로그램이 없습니다.',
'기본 모드에서는 없이도 배포할 수 있으며, 커스텀 채점이 필요하면 추가해주세요.'
]}
/>
)
}
3 changes: 3 additions & 0 deletions apps/frontend/public/icons/trashcan2-gray.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/frontend/public/icons/upload-blue.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading