Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
682078e
feat(fe): create problem tabs
Mar 9, 2026
24f8596
fix(fe): fix width
Mar 13, 2026
63e1245
feat(fe): remake tabs design
Mar 13, 2026
8c33722
fix(fe): remove unusables
Mar 13, 2026
ad06aa3
Merge branch 'main' into t2567-create-problem-tab
sONg20NOW Mar 14, 2026
688af8d
fix(fe): change text color and leaf node
Mar 16, 2026
67877e1
fix(fe): change tab design
Mar 16, 2026
1dfa205
fix(fe): capitalize problem letters
Mar 16, 2026
a3a8983
Merge branch 'main' into t2567-create-problem-tab
sONg20NOW Mar 17, 2026
8d5201d
fix(fe): change letters
Mar 18, 2026
be94113
Merge branch 't2567-create-problem-tab' of https://github.com/skkudin…
Mar 18, 2026
0c747b6
fix(fe): fix page design
Mar 19, 2026
3f6fe76
fix(fe): change table design
Mar 26, 2026
67f1827
fix(fe): korean patch
Mar 26, 2026
1619581
fix(fe): change table design
Mar 27, 2026
8f8da6c
fix(fe): change table design
Mar 27, 2026
1aea414
fix(fe): change table design
Mar 27, 2026
e019871
Update apps/frontend/app/(client)/(main)/problem/_components/ProblemD…
woo943 Mar 27, 2026
ec5dd0f
feat(fe): impl basic page design
Mar 30, 2026
36021e8
feat(fe): change card design
Mar 30, 2026
ce0835b
feat(fe): add icons
Mar 30, 2026
8cc971c
fix(fe): common change
Mar 30, 2026
7702ee7
feat(fe): infinite scroll
Mar 30, 2026
80c1675
feat(fe): impl empty page
Apr 1, 2026
04fc60d
fix(fe): add state button on published tab
Apr 1, 2026
3f6deaf
Merge branch 'main' into t2650-create-myproblem-tab
woo943 Apr 1, 2026
dd38462
Update apps/frontend/app/(client)/(main)/problem/_components/ProblemD…
woo943 Apr 1, 2026
060472a
Update apps/frontend/app/(client)/(main)/problem/_components/MyProble…
woo943 Apr 1, 2026
2439662
fix(fe): fix gemini code review
Apr 1, 2026
00620b7
Merge branch 't2650-create-myproblem-tab' of https://github.com/skkud…
Apr 1, 2026
1e40e6c
fix(fe): fix commented design
Apr 3, 2026
069aa5f
chore(fe): erase unused varibales
May 8, 2026
d16efbb
Merge remote-tracking branch 'origin/main' into t2650-create-myproble…
May 8, 2026
5284994
fix(fe): change search bar height
May 8, 2026
269b530
Merge branch 'main' into t2650-create-myproblem-tab
sONg20NOW May 12, 2026
a630201
fix(fe): change about comments
May 12, 2026
017ce60
fix(fe): change state design
May 12, 2026
aa9018a
chore(fe): Delete .codex
woo943 May 13, 2026
9546e6e
feat(fe): impl state filter
May 16, 2026
976783a
fix(fe): delete empty page
May 16, 2026
8dba293
Merge branch 'main' into t2650-create-myproblem-tab
sONg20NOW May 28, 2026
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
@@ -0,0 +1,95 @@
'use client'

import { Skeleton } from '@/components/shadcn/skeleton'
import type { Problem } from '@/types/type'
import { useSearchParams } from 'next/navigation'
import { MyProblemDataTable } from './MyProblemDataTable'

export interface MyProblemCardItem extends Problem {
state: 'Draft' | 'Ready' | 'Published'
timeLimit: number
memoryLimit: number
updatedAt: string
}

interface MyProblemProps {
forceEmpty?: boolean
}

const MOCK_MY_PROBLEMS: MyProblemCardItem[] = Array.from(
{ length: 48 },
(_, index) => {
const id = 3000 + index + 1
const difficulty = `Level${(index % 5) + 1}` as Problem['difficulty']
const submissionCount = 24 + index * 7
const acceptedRate = ((index % 9) + 2) / 12
const states: MyProblemCardItem['state'][] = ['Published', 'Draft', 'Ready']

return {
id,
title: `글자 수 상관 없음. 단, 한 줄로만 노출되는 Mock My Problem ${index + 1}`,
difficulty,
submissionCount,
acceptedRate,
tags: [],
languages: ['C', 'Cpp', 'Java', 'Python3'],
hasPassed: null,
state: states[index % states.length],
timeLimit: 1000 + (index % 4) * 500,
memoryLimit: 128 + (index % 3) * 64,
updatedAt: `2024-01-${String((index % 24) + 1).padStart(2, '0')} 19:00`
}
}
)
Comment thread
woo943 marked this conversation as resolved.

export function MyProblem({ forceEmpty = false }: MyProblemProps) {
const searchParams = useSearchParams()
const search = searchParams.get('search') ?? ''
const normalizedSearch = search.trim().toLowerCase()

const filteredProblems = MOCK_MY_PROBLEMS.filter((problem) => {
if (!normalizedSearch) {
return true
}

return (
problem.title.toLowerCase().includes(normalizedSearch) ||
String(problem.id).includes(normalizedSearch)
)
})

const data = forceEmpty ? [] : filteredProblems

return <MyProblemDataTable data={data} search={search} />
}

export function MyProblemFallback() {
return (
<div className="flex w-full flex-col gap-6">
<div className="flex items-center justify-between">
<Skeleton className="h-10 w-40 rounded-xl" />
<div className="flex gap-3">
<Skeleton className="h-12 w-28 rounded-full" />
<Skeleton className="h-12 w-72 rounded-full" />
<Skeleton className="h-12 w-36 rounded-full" />
</div>
</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
{[...Array(8)].map((_, i) => (
<div
key={i}
className="border-line overflow-hidden rounded-[24px] border"
>
<Skeleton className="h-40 w-full rounded-none" />
<div className="space-y-4 p-5">
<Skeleton className="h-7 w-20 rounded-md" />
<Skeleton className="h-7 w-full rounded-md" />
<Skeleton className="h-6 w-2/3 rounded-md" />
<Skeleton className="h-5 w-1/2 rounded-md" />
</div>
</div>
))}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
'use client'

import { SearchBar } from '@/components/SearchBar'
import { Button } from '@/components/shadcn/button'
import { cn } from '@/libs/utils'
import ClockGray from '@/public/icons/clock_gray.svg'
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.

clock_gray 대신에 이번에 clock icon 써서 svgr 활용해서 구현해보세요!

import Memory from '@/public/icons/memory.svg'
import PlusCircle from '@/public/icons/plus-circle-blue.svg'
import type { Route } from 'next'
import Image from 'next/image'
import Link from 'next/link'
import { useState } from 'react'
import type { MyProblemCardItem } from './MyProblem'
import {
MyProblemStateFilter,
STATE_FILTER_OPTIONS
} from './MyProblemStateFilter'

interface MyProblemDataTableProps {
data: MyProblemCardItem[]
search: string
}

const stateBadgeClassName = {
Draft: {
container: 'border border-line bg-color-neutral-99',
text: 'text-color-neutral-70'
},
Ready: {
container: 'border border-color-green-50 bg-white',
text: 'text-color-green-40'
},
Published: {
container: 'bg-[#EDF4FF]',
text: 'text-primary'
}
} as const

function getStateBadgeClassName(state: string) {
if (state === 'Draft' || state === 'DRAFT') {
return stateBadgeClassName.Draft
}

if (state === 'Ready' || state === 'READY') {
return stateBadgeClassName.Ready
}

return stateBadgeClassName.Published
}

export function MyProblemDataTable({ data, search }: MyProblemDataTableProps) {
const [selectedStates, setSelectedStates] = useState<
MyProblemCardItem['state'][]
>([])

const filteredData =
selectedStates.length === 0 ||
selectedStates.length === STATE_FILTER_OPTIONS.length
? data
: data.filter((problem) => selectedStates.includes(problem.state))

return (
<div className="flex w-full flex-col items-center">
<div className="flex w-full items-center justify-between self-stretch">
<div className="flex shrink-0 items-center justify-start">
<p className="text-head3_sb_28 whitespace-nowrap">내가 만든 문제</p>
</div>
<div className="flex w-full flex-col gap-3 lg:flex-row lg:items-center lg:justify-end">
<MyProblemStateFilter
selectedStates={selectedStates}
onSelectedStatesChange={setSelectedStates}
/>
<SearchBar className="w-60" sizeVariant="lg" />
<Button
asChild
className="text-body1_m_16 bg-primary h-[46px] shrink-0 whitespace-nowrap rounded-full px-[22px] py-2.5"
>
<Link
href="/problem/create"
className="flex items-center justify-center gap-1.5 whitespace-nowrap"
>
<Image
src={PlusCircle}
alt="plus circle"
width={20}
height={20}
/>
새 문제 생성
</Link>
</Button>
</div>
</div>
{filteredData.length ? (
<div className="mb-30 mt-5 grid w-full grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
{filteredData.map((problem) => {
const href =
`/problem/my-problem/${problem.id}${search ? `?search=${search}` : ''}` as Route
const badgeClassName = getStateBadgeClassName(problem.state)

return (
<Link
key={problem.id}
href={href}
className="bg-background outline-color-cool-neutral-90 inline-flex w-full flex-col items-start gap-5 rounded-2xl p-5 outline outline-1 outline-offset-[-1px] transition-transform duration-200 hover:-translate-y-1"
>
<div className="flex w-full flex-col items-start gap-3">
<div
className={cn(
'inline-flex items-center justify-center gap-2.5 rounded px-2.5 py-1',
badgeClassName.container
)}
>
<span
className={cn(
'text-caption1_m_13 text-center',
badgeClassName.text
)}
>
{problem.state}
</span>
</div>
<p className="text-title1_sb_20 line-clamp-1 self-stretch">
{problem.title}
</p>
</div>
<div className="flex w-full flex-col items-start gap-2">
<div className="inline-flex items-start justify-start gap-2 self-stretch">
<div className="flex items-center justify-start gap-1">
<Image
src={ClockGray}
alt="clock gray"
width={20}
height={20}
/>
<span className="text-caption3_r_13">
{problem.timeLimit}ms
</span>
</div>
<div className="flex items-center justify-start gap-1">
<Image src={Memory} alt="memory" width={20} height={20} />
<span className="text-caption3_r_13">
{problem.memoryLimit}MB
</span>
</div>
</div>
<div className="inline-flex items-center justify-start gap-1">
<span className="text-caption3_r_13 text-color-neutral-90 justify-start text-right">
Last Modified:
</span>
<span className="text-caption3_r_13 justify-start text-right text-gray-500">
{problem.updatedAt}
</span>
</div>
</div>
</Link>
)
})}
</div>
) : (
<MyProblemEmptyState />
)}
</div>
)
}

function MyProblemEmptyState() {
return (
<div className="border-line mb-30 mt-5 flex h-[266px] w-full items-center justify-center rounded-xl border bg-white py-20">
<div className="flex flex-col items-center gap-[16px] text-center">
<p className="text-sub1_sb_18 text-color-neutral-30">
내가 만든 문제가
<br />
존재하지 않습니다.
</p>
<Button
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.

이거 일단 problem/create 페이지 완성되기 전까지는 disable로 막아주세요!

asChild
className="text-sub4_sb_14 bg-primary h-10 shrink-0 whitespace-nowrap rounded-lg px-3 py-2.5"
>
<Link href="/problem/create">새 문제 생성하기</Link>
</Button>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use client'

import { Badge } from '@/components/shadcn/badge'
import { Button } from '@/components/shadcn/button'
import { Checkbox } from '@/components/shadcn/checkbox'
import {
Command,
CommandGroup,
CommandItem,
CommandList
} from '@/components/shadcn/command'
import {
Popover,
PopoverContent,
PopoverTrigger
} from '@/components/shadcn/popover'
import { Separator } from '@/components/shadcn/separator'
import { IoFilter } from 'react-icons/io5'
import type { MyProblemCardItem } from './MyProblem'

export const STATE_FILTER_OPTIONS: MyProblemCardItem['state'][] = [
'Published',
'Ready',
'Draft'
]

interface MyProblemStateFilterProps {
selectedStates: MyProblemCardItem['state'][]
onSelectedStatesChange: (states: MyProblemCardItem['state'][]) => void
}

export function MyProblemStateFilter({
selectedStates,
onSelectedStatesChange
}: MyProblemStateFilterProps) {
const selectedValues = new Set(selectedStates)

const handleFilterSelect = (value: MyProblemCardItem['state']) => {
const nextSelectedValues = new Set(selectedValues)

if (nextSelectedValues.has(value)) {
nextSelectedValues.delete(value)
} else {
nextSelectedValues.add(value)
}

onSelectedStatesChange(Array.from(nextSelectedValues))
}

return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="border-line text-body1_m_16 h-[46px] min-w-28 justify-center rounded-full border px-5 py-[11px] text-black"
>
<IoFilter className="text-color-cool-neutral-30 mr-2 h-5 w-5" />
State
{selectedValues.size > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<div className="flex gap-1">
{selectedValues.size === STATE_FILTER_OPTIONS.length ? (
<Badge
variant="secondary"
className="rounded-xs px-1 font-normal"
>
All
</Badge>
) : (
STATE_FILTER_OPTIONS.filter((state) =>
selectedValues.has(state)
).map((state) => (
<Badge
key={state}
variant="secondary"
className="rounded-xs px-1 font-normal"
>
{state}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>

<PopoverContent className="w-[112px] p-0" align="start">
<Command>
<CommandList>
<CommandGroup>
{STATE_FILTER_OPTIONS.map((state) => (
<CommandItem
key={state}
value={state}
className="gap-x-1"
onSelect={() => handleFilterSelect(state)}
>
<Checkbox checked={selectedValues.has(state)} />
{state}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
Loading
Loading