Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
88b66fd
feat(fe): make notice frame
egg-zz Mar 27, 2026
9e35026
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon Mar 27, 2026
82adec6
feat(be): secure CourseNoticeResolver
Choi-Jung-Hyeon Mar 27, 2026
7d9e397
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon Mar 29, 2026
a2ed1a0
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon Mar 30, 2026
b090163
feat(fe): modify importnoticemodal
egg-zz Apr 2, 2026
be9d50a
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon Apr 2, 2026
9ba210f
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon Apr 3, 2026
e8bbe8a
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon Apr 3, 2026
ca4902c
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon Apr 4, 2026
41ed0c7
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon May 6, 2026
4be3d17
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon May 6, 2026
405ba3c
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon May 11, 2026
6189e53
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon May 16, 2026
832cd95
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon May 19, 2026
685c149
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon May 20, 2026
19b993b
feat(be): improve group ID extraction in GroupLeaderGuard for GraphQL…
Choi-Jung-Hyeon May 20, 2026
06d55d8
feat(fe): modity course notice client
egg-zz May 21, 2026
9c1a1ac
feat(fe): delete useless annotation
egg-zz May 21, 2026
58f17f6
Merge branch 'main' into t2604-make-notice-page
Choi-Jung-Hyeon May 21, 2026
63be054
feat(be): remove unnecessary fields from comment object
Choi-Jung-Hyeon May 21, 2026
9ab8e52
feat(fe): update current modification
egg-zz May 26, 2026
e717483
Merge branch 'main' into t2604-make-notice-page
egg-zz May 26, 2026
af757ba
feat(fe): make notice frame
egg-zz Mar 27, 2026
a754e5f
feat(fe): modity course notice client
egg-zz May 21, 2026
cc2cc12
feat(fe): solve conflict
egg-zz May 27, 2026
4eada12
Merge branch 'main' into t2604-make-notice-page
seoeun9 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,66 @@
'use client'

import { cn, dateFormatter } from '@/libs/utils'
import type { ColumnDef } from '@tanstack/react-table'

export interface CourseNoticeRow {
id: number
no: string
title: string
createdBy: string
date: string
isRead: boolean
isFixed: boolean
}

export const courseNoticeColumns: ColumnDef<CourseNoticeRow>[] = [
{
accessorKey: 'no',
header: 'NO',
cell: ({ row }) => (
<div
className={cn(
'relative w-full text-center text-sm text-[#666666]',
row.original.isFixed &&
"before:bg-primary before:absolute before:left-[-16px] before:top-[-18px] before:h-[57px] before:w-[3px] before:rounded-full before:content-['']"
Comment on lines +24 to +25

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

These before: pseudo-element styles are complex and contain magic numbers (e.g., left-[-16px], top-[-18px]). This makes the code hard to read and maintain. Consider extracting this into a separate, well-named utility class in your Tailwind configuration for better readability and reusability.

)}
>
{row.original.no}
</div>
),
enableSorting: false
},
{
accessorKey: 'title',
header: 'Title',
cell: ({ row }) => (
<div className="flex items-center justify-start gap-2 overflow-hidden text-sm text-black">
<span className="line-clamp-1">{row.original.title}</span>
{!row.original.isRead && (
<span className="bg-primary h-[6px] w-[6px] shrink-0 rounded-full" />
)}
</div>
),
enableSorting: false
},
{
accessorKey: 'date',
header: 'Date',
cell: ({ row }) => (
<span className="text-sm text-[#666666]">
{row.original.date
? dateFormatter(row.original.date, 'YY-MM-DD HH:mm')
: '-'}
</span>
),
enableSorting: false
},
{
accessorKey: 'createdBy',
header: 'Writer',
cell: ({ row }) => (
<span className="text-sm text-[#666666]">{row.original.createdBy}</span>
Comment thread
egg-zz marked this conversation as resolved.
),
enableSorting: false
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
'use client'

import {
DataTable,
DataTablePagination,
DataTableRoot
} from '@/app/admin/_components/table'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/shadcn/dropdown-menu'
import { cn } from '@/libs/utils'
import arrowDownIcon from '@/public/icons/arrow-down.svg'
import type { CourseNoticeListItem } from '@/types/type'
import { useQuery } from '@tanstack/react-query'
import Image from 'next/image'
import { useMemo, useState } from 'react'
import { mockCourseNotices } from '../notice/_components/mock'
import {
courseNoticeColumns,
type CourseNoticeRow
} from './CourseNoticeColumns'

type FilterType = 'all' | 'unread'
type OrderType = 'latest' | 'oldest'

interface CourseNoticeTableProps {
courseId: number
}

const getTime = (notice: CourseNoticeListItem) =>
new Date(notice.createTime ?? notice.updateTime ?? 0).getTime()

export function CourseNoticeTable({ courseId }: CourseNoticeTableProps) {
const [filterType, setFilterType] = useState<FilterType>('all')
const [orderType, setOrderType] = useState<OrderType | undefined>()

let orderLabel = 'Order'

if (orderType === 'latest') {
orderLabel = 'Latest'
} else if (orderType === 'oldest') {
orderLabel = 'Oldest'
}

const { data: notices = [] } = useQuery<CourseNoticeListItem[]>({
queryKey: ['courseNotices', courseId, filterType, orderType],
queryFn: () => mockCourseNotices,
enabled: Boolean(courseId),
retry: false
})

const tableData: CourseNoticeRow[] = useMemo(() => {
const filteredNotices =
filterType === 'unread'
? notices.filter((notice) => !notice.isRead)
: notices

const noMap = new Map(
[...filteredNotices]
.sort((a, b) => getTime(a) - getTime(b))
.map((notice, index) => [notice.id, index + 1])
)

return [...filteredNotices]
.sort((a, b) => {
if (a.isFixed !== b.isFixed) {
return a.isFixed ? -1 : 1
}
return orderType === 'oldest'
? getTime(a) - getTime(b)
: getTime(b) - getTime(a)
})
.map((notice) => ({
id: notice.id,
no: String(noMap.get(notice.id) ?? 0).padStart(2, '0'),
title: notice.title,
createdBy: notice.createdBy ?? 'Unknown',
date: notice.createTime ?? notice.updateTime ?? '',
isRead: notice.isRead,
isFixed: notice.isFixed
}))
}, [notices, filterType, orderType])
Comment on lines +79 to +105

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

The logic inside this useMemo hook for preparing tableData could be more efficient. It currently involves multiple sorts and iterations over the notices array:

  1. The filteredNotices are sorted to create noMap.
  2. filteredNotices are sorted again to apply the final display order.

This can be optimized by reducing the number of sorts. For instance, you could perform the final sort first, and then map over the sorted array to generate the no value based on the index.


return (
<DataTableRoot
data={tableData}
columns={courseNoticeColumns}
defaultPageSize={10}
defaultSortState={[]}
>
<div className="mb-6 flex items-center justify-between">
<span className="text-2xl font-semibold leading-[33.6px] tracking-[-0.48px]">
NOTICE
</span>

<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex h-[46px] min-w-[108px] items-center justify-center gap-2 rounded-full border bg-white text-sm leading-[22.4px] text-neutral-500 outline-none"
>
<span>{orderLabel}</span>
<Image
src={arrowDownIcon}
alt="arrow down"
className="h-4 w-4"
/>
</button>
</DropdownMenuTrigger>

<DropdownMenuContent
align="end"
className="border-neutral-95 min-w-[108px] rounded-[16px] border bg-white p-1"
>
<DropdownMenuItem
onClick={() => setOrderType('latest')}
className="cursor-pointer rounded-[10px] text-sm leading-[22.4px] text-neutral-500"
>
Latest
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setOrderType('oldest')}
className="cursor-pointer rounded-[10px] text-sm leading-[22.4px] text-neutral-500"
>
Oldest
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

<div className="flex h-[46px] items-center rounded-full border p-[5px]">
{(['all', 'unread'] as const).map((type) => (
<button
key={type}
type="button"
onClick={() => setFilterType(type)}
className={cn(
'text-body1_m_16 h-9 rounded-full px-8 py-[6px]',
filterType === type
? 'bg-primary text-white'
: 'text-[#808080]'
Comment thread
egg-zz marked this conversation as resolved.
)}
>
{type === 'all' ? 'All' : 'Unread'}
</button>
))}
</div>
</div>
</div>

<DataTable
size="md"
headerStyle={{
no: 'w-[80px]',
title: '',
date: 'w-[180px]',
createdBy: 'w-[110px]'
}}
bodyStyle={{
no: 'text-center',
title: 'justify-start',
date: 'text-center',
createdBy: 'text-center'
}}
getHref={(row) => `/course/${courseId}/notice/${row.id}`}
/>

<div className="mt-10">
<DataTablePagination showRowsPerPage={false} />
</div>
</DataTableRoot>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { usePathname } from 'next/navigation'
import { useState } from 'react'
import { FaAnglesLeft, FaAnglesRight } from 'react-icons/fa6'
import {
NoticeIcon,
AssignmentIcon,
ExerciseIcon,
QnaIcon
Expand All @@ -24,6 +25,11 @@ export function CourseSidebar({ courseId }: CourseSidebarProps) {
const pathname = usePathname()

const navItems = [
{
name: 'Notice',
path: `/course/${courseId}/notice` as const,
icon: NoticeIcon
},
{
name: 'Assignment',
path: `/course/${courseId}/assignment` as const,
Expand Down
Loading
Loading