Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 24 additions & 0 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"features": {
"ghcr.io/devcontainers-extra/features/pnpm:2": {
"version": "2.0.5",
"resolved": "ghcr.io/devcontainers-extra/features/pnpm@sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6",
"integrity": "sha256:694c2b6182435c9e9c06f6071728b087c1181b38c18cecf0defe2ab8c11cddd6"
},
"ghcr.io/devcontainers/features/go:1": {
"version": "1.3.4",
"resolved": "ghcr.io/devcontainers/features/go@sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032",
"integrity": "sha256:d85e921f91b41340055bb12b325d9d551170ed04b3b832e33530bf42f167c032"
},
"ghcr.io/devcontainers/features/java:1": {
"version": "1.8.0",
"resolved": "ghcr.io/devcontainers/features/java@sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a",
"integrity": "sha256:9663ce0219ff85786e87901ce5f0a59f488edd5f99b46015192cda48468b233a"
},
"ghcr.io/devcontainers/features/terraform:1": {
"version": "1.4.2",
"resolved": "ghcr.io/devcontainers/features/terraform@sha256:250d08e39c99d9e959e091e74527c22c04eeae728b424da99f002423c7902f82",
"integrity": "sha256:250d08e39c99d9e959e091e74527c22c04eeae728b424da99f002423c7902f82"
}
}
}
1 change: 1 addition & 0 deletions apps/backend/apps/admin/src/group/group.resolver.ts

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.

create 안된다했는데 어떤 상황인지 좀만더 구체적으로 알려주세요...! 백엔드 문제인건가유?

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.

지금 prisma가 업데이트 되면서 설정이 달라졌는데, 그래서 tsconfig에서 조금 문제가 생긴거같아요 이 pr뿐만 아니라 다른 곳에서도 문제 될거같은데 왜 아무도 모르지...? 한번 알아볼게요

@Choi-Jung-Hyeon Choi-Jung-Hyeon Apr 3, 2026

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.

일단 백엔드 상관없는 프론트엔드 에러부터 수정할 수 있는거 먼저 하기! gemini가 리뷰해준거 크리티컬부터! 그리고 에러난다는거 에러문이나 스크린샷 좀 공유해주세요

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

넵 오늘 저녁에 공유드리겠습니다!

Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class GroupResolver {
}

@Resolver(() => CourseNotice)
@UseGroupLeaderGuard()
export class CourseNoticeResolver {
constructor(private readonly courseNoticeService: CourseNoticeService) {}

Expand Down
4 changes: 1 addition & 3 deletions apps/backend/apps/client/src/group/group.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1235,9 +1235,7 @@ export class GroupService {
) {
comment = {
...comment,
content: '',
createdBy: null,
createdById: null
content: ''
}
}
if (!comment.replyOnId) {
Expand Down
10 changes: 4 additions & 6 deletions apps/backend/libs/auth/src/roles/group-leader.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,10 @@ export class GroupLeaderGuard implements CanActivate {
let groupId: number
if (context.getType<GqlContextType>() === 'graphql') {
request = GqlExecutionContext.create(context).getContext().req
groupId = parseInt(
context
.getArgs()
.find((arg) => typeof arg === 'object' && 'groupId' in arg)
?.groupId ?? 0
)
const gqlArg = context
.getArgs()
.find((arg) => typeof arg === 'object' && arg !== null)
groupId = parseInt(gqlArg?.groupId ?? gqlArg?.input?.groupId ?? 0)
} else {
request = context.switchToHttp().getRequest()
groupId = parseInt(request.params.groupId)
Expand Down
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,192 @@
'use client'

import {
DataTable,
DataTablePagination,
DataTableRoot
} from '@/app/admin/_components/table'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/shadcn/dropdown-menu'
import { cn, safeFetcherWithAuth } from '@/libs/utils'
import ArrowDownIcon from '@/public/icons/arrow-down.svg'
import type {
CourseNoticeListItem,
CourseNoticeListResponse
} from '@/types/type'
import { useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
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],
queryFn: async () => {
const [fixedRes, normalRes] = await Promise.all([
safeFetcherWithAuth
.get(`course/${courseId}/notice/all`, {
searchParams: {
take: '100',
fixed: 'true',
readFilter: 'all',
order: 'createTime-desc'
}
})
.json<CourseNoticeListResponse>(),
safeFetcherWithAuth
.get(`course/${courseId}/notice/all`, {
searchParams: {
take: '100',
fixed: 'false',
readFilter: 'all',
order: 'createTime-desc'
}
})
.json<CourseNoticeListResponse>()
])
return [...fixedRes.data, ...normalRes.data]
},
enabled: Boolean(courseId)
})

const tableData: CourseNoticeRow[] = useMemo(() => {
const filtered =
filterType === 'unread' ? notices.filter((n) => !n.isRead) : notices
const noMap = new Map(
[...filtered]
.sort((a, b) => getTime(a) - getTime(b))
.map((n, i) => [n.id, i + 1])
)
return [...filtered]
.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((n) => ({
id: n.id,
no: String(noMap.get(n.id) ?? 0).padStart(2, '0'),
title: n.title,
createdBy: n.createdBy ?? 'Unknown',
date: n.createTime ?? n.updateTime ?? '',
isRead: n.isRead,
isFixed: n.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="text-color-neutral-50 w-30 border-line flex h-[46px] items-center justify-center gap-2 rounded-full border bg-white text-sm font-medium leading-[22.4px] tracking-[-0.48px] outline-none"
>
<span>{orderLabel}</span>
<ArrowDownIcon className="h-4 w-4" />
</button>
</DropdownMenuTrigger>

<DropdownMenuContent
align="end"
className="border-line 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="border-line flex h-[46px] w-[250px] items-center rounded-full border bg-white p-[5px]">
{(['all', 'unread'] as const).map((type) => (
<button
key={type}
type="button"
onClick={() => setFilterType(type)}
className={cn(
'text-body1_m_16 w-30 flex h-9 items-center justify-center rounded-full',
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