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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<File[]>([])
const inputRef = useRef<HTMLInputElement | null>(null)

const handleFiles = (incoming: FileList | null) => {
if (!incoming) {
return
}
const next = Array.from(incoming)
setFiles(next)
onFilesChange(next)
}
Comment on lines +38 to +45
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.

medium

onDrop을 통해 여러 파일이 한꺼번에 들어올 수 있으므로, multiple 옵션이 false인 경우에는 첫 번째 파일만 선택되도록 로직을 보강하는 것이 안전합니다.

  const handleFiles = (incoming: FileList | null) => {
    if (!incoming) {
      return
    }
    let next = Array.from(incoming)
    if (!multiple && next.length > 0) {
      next = [next[0]]
    }
    setFiles(next)
    onFilesChange(next)
  }


const removeFile = (index: number) => {
const next = files.filter((_, i) => i !== index)
setFiles(next)
onFilesChange(next)
}

return (
<div className={cn('flex flex-col gap-2', className)}>
{files.length > 0 && (
<ul className="flex flex-col gap-2">
{files.map((f, i) => {
const isZip = f.name.endsWith('.zip')
return (
<li
key={`${f.name}-${i}`}
className="flex items-center justify-between rounded-[12px] px-4 py-3 shadow-[0_4px_20px_0_rgba(53,78,116,0.10)]"
>
<div className="flex items-center gap-3">
<div className="border-line bg-color-neutral-99 grid size-12 place-items-center rounded-[6.4px] border-[0.8px]">
{isZip ? (
<TbCode
size={24}
className="text-color-cool-neutral-50"
/>
) : (
<AiFillFile
size={24}
className="text-color-cool-neutral-50"
/>
)}
</div>
<div className="flex flex-col">
<span className="text-sub1_sb_18 mb-[2px]">{f.name}</span>
<span className="text-sub4_sb_14 text-color-cool-neutral-50">
{formatFileSize(f.size)}
</span>
</div>
</div>
<button
type="button"
onClick={() => removeFile(i)}
className="border-color-neutral-90 bg-color-neutral-99 flex items-center justify-center rounded-full border px-4 py-[10px]"
>
<TrashIcon className="h-4 w-4" />
</button>
</li>
)
})}
</ul>
)}
{files.length === 0 && (
<div
onClick={() => 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"
>
<input
ref={inputRef}
type="file"
multiple={multiple}
className="hidden"
onChange={(e) => handleFiles(e.target.files)}
/>
<div className="flex flex-col items-center gap-[10px]">
<InfoIconGray className="mb-2" height={24} width={24} />
<div className="text-body1_m_16 text-color-cool-neutral-50 whitespace-pre-wrap text-center">
<p>{primaryText}</p>
<p>{secondaryText}</p>
</div>
</div>
</div>
)}
</div>
)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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() {
// 스켈레톤 확인을 위한 더미코드
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
})}
Expand Down
Loading
Loading