-
Notifications
You must be signed in to change notification settings - Fork 16
feat(fe): make notice page #3514
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
88b66fd
9e35026
82adec6
7d9e397
a2ed1a0
b090163
be9d50a
9ba210f
e8bbe8a
ca4902c
41ed0c7
4be3d17
405ba3c
6189e53
832cd95
685c149
19b993b
06d55d8
9c1a1ac
58f17f6
63be054
9ab8e52
e717483
af757ba
a754e5f
cc2cc12
4eada12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| } | ||
| } | ||
| } |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| )} | ||
| > | ||
| {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> | ||
|
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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic inside this
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 |
||
|
|
||
| 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]' | ||
|
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> | ||
| ) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
create 안된다했는데 어떤 상황인지 좀만더 구체적으로 알려주세요...! 백엔드 문제인건가유?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지금 prisma가 업데이트 되면서 설정이 달라졌는데, 그래서 tsconfig에서 조금 문제가 생긴거같아요 이 pr뿐만 아니라 다른 곳에서도 문제 될거같은데 왜 아무도 모르지...? 한번 알아볼게요
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
일단 백엔드 상관없는 프론트엔드 에러부터 수정할 수 있는거 먼저 하기! gemini가 리뷰해준거 크리티컬부터! 그리고 에러난다는거 에러문이나 스크린샷 좀 공유해주세요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 오늘 저녁에 공유드리겠습니다!