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
16 changes: 16 additions & 0 deletions apps/admin/app/notice/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SidebarProvider } from '@dpm-core/shared';

import { AppSidebar } from '@/components/app-sidebar';

export default function NoticeLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<SidebarProvider>
<AppSidebar />
{children}
</SidebarProvider>
);
}
150 changes: 150 additions & 0 deletions apps/admin/app/notice/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use client';

import { useState } from 'react';
import { Button, Plus, SidebarInset, TeamTabBar } from '@dpm-core/shared';

import { NoticeCard } from '@/components/notice/notice-card';
import { NoticeHeader } from '@/components/notice/notice-header';

type NoticeType = 'all' | 'general' | 'assignment' | 'etc';

interface Notice {
id: string;
title: string;
date: string;
readCount: number;
tags: Array<'default' | 'assignment' | 'individual' | 'team' | 'etc'>;
}

const NoticePage = () => {
const [selectedType, setSelectedType] = useState<NoticeType>('all');

// 목업 데이터
const notices: Notice[] = [
{
id: '1',
title:
'{공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다.}',
date: '(YYYY.MM.DD)',
readCount: 0,
tags: ['default'],
},
{
id: '2',
title:
'{공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다.}',
date: '(YYYY.MM.DD)',
readCount: 0,
tags: ['assignment', 'individual'],
},
{
id: '3',
title:
'{공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다.}',
date: '(YYYY.MM.DD)',
readCount: 0,
tags: ['assignment', 'team'],
},
{
id: '4',
title:
'{공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다.}',
date: '(YYYY.MM.DD)',
readCount: 0,
tags: ['default'],
},
{
id: '5',
title:
'{공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다. 공지 제목이 들어갑니다.}',
date: '(YYYY.MM.DD)',
readCount: 0,
tags: ['assignment', 'team'],
},
];

// 선택된 타입에 따라 필터링
const filteredNotices = notices.filter((notice) => {
if (selectedType === 'all') return true;
if (selectedType === 'general') return notice.tags.includes('default');
if (selectedType === 'assignment') return notice.tags.includes('assignment');
if (selectedType === 'etc') return notice.tags.includes('etc');
return true;
});

const handleNoticeClick = (noticeId: string) => {
console.log('공지 클릭:', noticeId);
// TODO: 공지 상세 페이지로 이동
};

const handleCreateNotice = () => {
console.log('공지 등록 클릭');
// TODO: 공지 등록 페이지로 이동
};

return (
<SidebarInset>
<div className="flex h-screen max-w-full flex-col items-center">
{/* Header */}
<NoticeHeader title="공지" className="w-full" />

{/* Body */}
<div className="flex w-full flex-1 flex-col items-start bg-background-normal">
{/* Title + Button */}
<div className="flex w-full items-center justify-between px-10 pt-6">
<div className="flex items-center gap-2">
<h2 className="font-bold text-label-normal text-title1">공지 목록</h2>
<span className="font-medium text-body1 text-primary-normal">
{filteredNotices.length}
</span>
</div>
<Button
size="lg"
className="h-12 gap-1.5 rounded-lg bg-background-inverse px-5 py-3"
onClick={handleCreateNotice}
>
<Plus className="size-5 text-icon-noraml" />
공지 등록
</Button>
</div>

{/* Tab Bar */}
<TeamTabBar
tabs={[
{ id: 'all', label: '전체' },
{ id: 'general', label: '일반' },
{ id: 'assignment', label: '과제' },
{ id: 'etc', label: '기타' },
]}
activeTabId={selectedType}
onTabChange={(tabId) => setSelectedType(tabId as NoticeType)}
className="w-full px-10"
/>

{/* Notice Card List */}
<div className="flex w-full flex-col gap-3 px-10 py-6">
{filteredNotices.map(({ id, title, date, readCount, tags }) => (
<NoticeCard
key={id}
title={title}
date={date}
readCount={readCount}
tags={tags}
onClick={() => handleNoticeClick(id)}
/>
))}
</div>

{/* Empty State */}
{filteredNotices.length === 0 && (
<div className="flex min-h-100 w-full items-center justify-center">
<p className="text-body1 text-label-alternative">등록된 공지가 없습니다.</p>
</div>
)}
</div>
</div>
</SidebarInset>
);
};

export default NoticePage;
5 changes: 5 additions & 0 deletions apps/admin/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ const SIDEBAR_ITEMS = [
href: '/',
icon: IconHome,
},
{
title: '공지',
href: '/notice',
icon: IconHome, // [TODO]: 아이콘 준비되면 교체 필요
},
{
title: '출석',
href: `/attendance/search/session?week=${SESSION_ID}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/components/notice/notice-tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const noticeTagVariants = cva(
);

const labelMap: Record<string, string> = {
default: '일반 공지',
default: '필수 공지',
assignment: '과제 공지',
individual: '개인 과제',
team: '팀 과제',
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { Clock } from './clock';
export { CopyIcon } from './copy';
export { DpmText } from './dpm-text';
export { KakaoLogo } from './kakao-logo';
export { Plus } from './plus';
export { TextLogo } from './text-logo';
export { UserAvatar } from './user-avatar';
export { XCircle } from './x-circle';
Expand Down
33 changes: 33 additions & 0 deletions packages/shared/src/components/icons/plus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { forwardRef } from 'react';

const Plus = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>((props, ref) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
ref={ref}
{...props}
>
<title>추가</title>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.41699 10.0039C3.41699 9.58969 3.75278 9.25391 4.16699 9.25391H15.8337C16.2479 9.25391 16.5837 9.58969 16.5837 10.0039C16.5837 10.4181 16.2479 10.7539 15.8337 10.7539H4.16699C3.75278 10.7539 3.41699 10.4181 3.41699 10.0039Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 3.4209C10.4142 3.4209 10.75 3.75668 10.75 4.1709V15.8376C10.75 16.2518 10.4142 16.5876 10 16.5876C9.58579 16.5876 9.25 16.2518 9.25 15.8376V4.1709C9.25 3.75668 9.58579 3.4209 10 3.4209Z"
fill="currentColor"
/>
</svg>
);
});

Plus.displayName = 'Plus';

export { Plus };
2 changes: 1 addition & 1 deletion packages/shared/src/components/team-tab-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const TeamTabBar = ({
return (
<div
className={cn(
'flex h-full items-end border-line-normal border-b bg-background-normal pt-3',
'flex items-end border-line-normal border-b bg-background-normal pt-3',
className,
)}
{...props}
Expand Down