From 2f08e488883fe36465532e21d98e994a3e12310c Mon Sep 17 00:00:00 2001 From: naseullee Date: Sat, 15 Aug 2026 15:39:19 +0900 Subject: [PATCH 01/15] feat(admin): notices UI flow KD3-88 --- apps/oop/src/app/components/AdminShell.css.ts | 17 +++ apps/oop/src/app/components/AdminShell.tsx | 53 +++++++ apps/oop/src/app/constants/routes.ts | 4 + apps/oop/src/app/routeTree.gen.ts | 141 +++++++++++++++++- apps/oop/src/app/routes/admin.index.lazy.tsx | 7 + apps/oop/src/app/routes/admin.lazy.tsx | 5 +- .../routes/admin.notices.$noticeId.lazy.tsx | 7 + .../app/routes/admin.notices.index.lazy.tsx | 7 + .../oop/src/app/routes/admin.notices.lazy.tsx | 5 + .../src/app/routes/admin.notices.new.lazy.tsx | 7 + .../admin-dashboard/AdminHomeDashboard.tsx | 30 ++-- .../admin-notices/AdminNoticePages.css.ts | 30 ++++ .../admin-notices/AdminNoticePages.tsx | 96 ++++++++++++ 13 files changed, 383 insertions(+), 26 deletions(-) create mode 100644 apps/oop/src/app/components/AdminShell.css.ts create mode 100644 apps/oop/src/app/components/AdminShell.tsx create mode 100644 apps/oop/src/app/routes/admin.index.lazy.tsx create mode 100644 apps/oop/src/app/routes/admin.notices.$noticeId.lazy.tsx create mode 100644 apps/oop/src/app/routes/admin.notices.index.lazy.tsx create mode 100644 apps/oop/src/app/routes/admin.notices.lazy.tsx create mode 100644 apps/oop/src/app/routes/admin.notices.new.lazy.tsx create mode 100644 apps/oop/src/widgets/admin-notices/AdminNoticePages.css.ts create mode 100644 apps/oop/src/widgets/admin-notices/AdminNoticePages.tsx diff --git a/apps/oop/src/app/components/AdminShell.css.ts b/apps/oop/src/app/components/AdminShell.css.ts new file mode 100644 index 00000000..fd84d368 --- /dev/null +++ b/apps/oop/src/app/components/AdminShell.css.ts @@ -0,0 +1,17 @@ +import { tokens } from '@aics/design-system'; +import { globalStyle, style } from '@vanilla-extract/css'; + +export const shell = style({ background: tokens.color.background.body, display: 'grid', gridTemplateColumns: '240px minmax(0, 1fr)', minHeight: '100dvh', '@media': { '(max-width: 900px)': { gridTemplateColumns: '1fr' } } }); +export const sidebar = style({ background: tokens.color.text.primary, color: tokens.color.background.surface, display: 'flex', flexDirection: 'column', minHeight: '100%', padding: '24px 16px 16px' }); +export const brand = style({ display: 'flex', flexDirection: 'column', gap: 2, padding: '0 10px' }); +export const nav = style({ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 58 }); +const navBase = { alignItems: 'center', background: 'transparent', border: 0, borderRadius: tokens.radius.container, color: tokens.color.background.surface, cursor: 'pointer', display: 'flex', font: 'inherit', justifyContent: 'space-between', minHeight: 42, padding: '0 12px', textAlign: 'left' as const, textDecoration: 'none' }; +export const navItem = style(navBase); +export const activeNav = style({ ...navBase, background: 'rgba(255, 255, 255, 0.06)' }); +export const count = style({ alignItems: 'center', background: tokens.color.accent, borderRadius: tokens.radius.full, color: tokens.color.background.surface, display: 'inline-flex', fontSize: 11, height: 20, justifyContent: 'center', width: 20 }); +export const account = style({ alignItems: 'center', background: 'rgba(255, 255, 255, 0.08)', borderRadius: tokens.radius.container, display: 'flex', gap: 10, marginTop: 'auto', padding: 12 }); +export const avatar = style({ background: tokens.color.icon.disabled, borderRadius: '50%', display: 'block', height: 28, width: 28 }); +export const main = style({ minWidth: 0 }); +globalStyle(brand + ' span, ' + account + ' span', { color: tokens.color.text.disabled, fontSize: 12 }); +globalStyle(brand + ' small', { color: tokens.color.text.disabled, fontSize: 11, marginTop: 8 }); +globalStyle(account + ' div', { display: 'flex', flexDirection: 'column', gap: 2 }); diff --git a/apps/oop/src/app/components/AdminShell.tsx b/apps/oop/src/app/components/AdminShell.tsx new file mode 100644 index 00000000..a5b33b8d --- /dev/null +++ b/apps/oop/src/app/components/AdminShell.tsx @@ -0,0 +1,53 @@ +import { Link, Outlet, useRouterState } from '@tanstack/react-router'; + +import { ROUTES } from '~/app/constants/routes'; + +import * as styles from './AdminShell.css'; + +const menuItems = [ + { label: '홈', to: ROUTES.ADMIN }, + { label: '공지사항', to: ROUTES.ADMIN_NOTICES }, +] as const; + +export default function AdminShell() { + const pathname = useRouterState({ select: state => state.location.pathname }); + + return ( +
+ +
+
+ ); +} diff --git a/apps/oop/src/app/constants/routes.ts b/apps/oop/src/app/constants/routes.ts index 0deef4c7..c9c9276b 100644 --- a/apps/oop/src/app/constants/routes.ts +++ b/apps/oop/src/app/constants/routes.ts @@ -1,6 +1,8 @@ export const ROUTES = { LOGIN: '/login', ADMIN: '/admin', + ADMIN_NOTICES: '/admin/notices', + ADMIN_NOTICE_NEW: '/admin/notices/new', STUDENT: { HOME: '/student', TEAM: '/student/team', @@ -26,5 +28,7 @@ export type OnboardingDestination = export type RouteDestination = | typeof ROUTES.LOGIN | typeof ROUTES.ADMIN + | typeof ROUTES.ADMIN_NOTICES + | typeof ROUTES.ADMIN_NOTICE_NEW | (typeof ROUTES.STUDENT)[keyof typeof ROUTES.STUDENT] | (typeof ROUTES.ONBOARDING)[keyof typeof ROUTES.ONBOARDING]; diff --git a/apps/oop/src/app/routeTree.gen.ts b/apps/oop/src/app/routeTree.gen.ts index 69c5f0c1..ac2ee45b 100644 --- a/apps/oop/src/app/routeTree.gen.ts +++ b/apps/oop/src/app/routeTree.gen.ts @@ -17,6 +17,7 @@ const LoginLazyRouteImport = createFileRoute('/login')() const AdminLazyRouteImport = createFileRoute('/admin')() const IndexLazyRouteImport = createFileRoute('/')() const StudentIndexLazyRouteImport = createFileRoute('/student/')() +const AdminIndexLazyRouteImport = createFileRoute('/admin/')() const StudentTeamLazyRouteImport = createFileRoute('/student/team')() const StudentSubmissionsLazyRouteImport = createFileRoute( '/student/submissions', @@ -33,8 +34,10 @@ const StudentMilestonesLazyRouteImport = createFileRoute( const StudentGradesLazyRouteImport = createFileRoute('/student/grades')() const StudentFeedbackLazyRouteImport = createFileRoute('/student/feedback')() const OnboardingTeamLazyRouteImport = createFileRoute('/onboarding/team')() +const AdminNoticesLazyRouteImport = createFileRoute('/admin/notices')() const OnboardingTeamIndexLazyRouteImport = createFileRoute('/onboarding/team/')() +const AdminNoticesIndexLazyRouteImport = createFileRoute('/admin/notices/')() const OnboardingTeamSurveyLazyRouteImport = createFileRoute( '/onboarding/team/survey', )() @@ -44,6 +47,10 @@ const OnboardingTeamResultLazyRouteImport = createFileRoute( const OnboardingTeamFirstMeetingLazyRouteImport = createFileRoute( '/onboarding/team/first-meeting', )() +const AdminNoticesNewLazyRouteImport = createFileRoute('/admin/notices/new')() +const AdminNoticesNoticeIdLazyRouteImport = createFileRoute( + '/admin/notices/$noticeId', +)() const StudentLazyRoute = StudentLazyRouteImport.update({ id: '/student', @@ -70,6 +77,11 @@ const StudentIndexLazyRoute = StudentIndexLazyRouteImport.update({ path: '/', getParentRoute: () => StudentLazyRoute, } as any).lazy(() => import('./routes/student.index.lazy').then((d) => d.Route)) +const AdminIndexLazyRoute = AdminIndexLazyRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AdminLazyRoute, +} as any).lazy(() => import('./routes/admin.index.lazy').then((d) => d.Route)) const StudentTeamLazyRoute = StudentTeamLazyRouteImport.update({ id: '/team', path: '/team', @@ -124,6 +136,11 @@ const OnboardingTeamLazyRoute = OnboardingTeamLazyRouteImport.update({ } as any).lazy(() => import('./routes/onboarding.team.lazy').then((d) => d.Route), ) +const AdminNoticesLazyRoute = AdminNoticesLazyRouteImport.update({ + id: '/notices', + path: '/notices', + getParentRoute: () => AdminLazyRoute, +} as any).lazy(() => import('./routes/admin.notices.lazy').then((d) => d.Route)) const OnboardingTeamIndexLazyRoute = OnboardingTeamIndexLazyRouteImport.update({ id: '/', path: '/', @@ -131,6 +148,13 @@ const OnboardingTeamIndexLazyRoute = OnboardingTeamIndexLazyRouteImport.update({ } as any).lazy(() => import('./routes/onboarding.team.index.lazy').then((d) => d.Route), ) +const AdminNoticesIndexLazyRoute = AdminNoticesIndexLazyRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => AdminNoticesLazyRoute, +} as any).lazy(() => + import('./routes/admin.notices.index.lazy').then((d) => d.Route), +) const OnboardingTeamSurveyLazyRoute = OnboardingTeamSurveyLazyRouteImport.update({ id: '/survey', @@ -155,12 +179,28 @@ const OnboardingTeamFirstMeetingLazyRoute = } as any).lazy(() => import('./routes/onboarding.team.first-meeting.lazy').then((d) => d.Route), ) +const AdminNoticesNewLazyRoute = AdminNoticesNewLazyRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => AdminNoticesLazyRoute, +} as any).lazy(() => + import('./routes/admin.notices.new.lazy').then((d) => d.Route), +) +const AdminNoticesNoticeIdLazyRoute = + AdminNoticesNoticeIdLazyRouteImport.update({ + id: '/$noticeId', + path: '/$noticeId', + getParentRoute: () => AdminNoticesLazyRoute, + } as any).lazy(() => + import('./routes/admin.notices.$noticeId.lazy').then((d) => d.Route), + ) export interface FileRoutesByFullPath { '/': typeof IndexLazyRoute - '/admin': typeof AdminLazyRoute + '/admin': typeof AdminLazyRouteWithChildren '/login': typeof LoginLazyRoute '/student': typeof StudentLazyRouteWithChildren + '/admin/notices': typeof AdminNoticesLazyRouteWithChildren '/onboarding/team': typeof OnboardingTeamLazyRouteWithChildren '/student/feedback': typeof StudentFeedbackLazyRoute '/student/grades': typeof StudentGradesLazyRoute @@ -169,15 +209,18 @@ export interface FileRoutesByFullPath { '/student/project-topic': typeof StudentProjectTopicLazyRoute '/student/submissions': typeof StudentSubmissionsLazyRoute '/student/team': typeof StudentTeamLazyRoute + '/admin/': typeof AdminIndexLazyRoute '/student/': typeof StudentIndexLazyRoute + '/admin/notices/$noticeId': typeof AdminNoticesNoticeIdLazyRoute + '/admin/notices/new': typeof AdminNoticesNewLazyRoute '/onboarding/team/first-meeting': typeof OnboardingTeamFirstMeetingLazyRoute '/onboarding/team/result': typeof OnboardingTeamResultLazyRoute '/onboarding/team/survey': typeof OnboardingTeamSurveyLazyRoute + '/admin/notices/': typeof AdminNoticesIndexLazyRoute '/onboarding/team/': typeof OnboardingTeamIndexLazyRoute } export interface FileRoutesByTo { '/': typeof IndexLazyRoute - '/admin': typeof AdminLazyRoute '/login': typeof LoginLazyRoute '/student/feedback': typeof StudentFeedbackLazyRoute '/student/grades': typeof StudentGradesLazyRoute @@ -186,18 +229,23 @@ export interface FileRoutesByTo { '/student/project-topic': typeof StudentProjectTopicLazyRoute '/student/submissions': typeof StudentSubmissionsLazyRoute '/student/team': typeof StudentTeamLazyRoute + '/admin': typeof AdminIndexLazyRoute '/student': typeof StudentIndexLazyRoute + '/admin/notices/$noticeId': typeof AdminNoticesNoticeIdLazyRoute + '/admin/notices/new': typeof AdminNoticesNewLazyRoute '/onboarding/team/first-meeting': typeof OnboardingTeamFirstMeetingLazyRoute '/onboarding/team/result': typeof OnboardingTeamResultLazyRoute '/onboarding/team/survey': typeof OnboardingTeamSurveyLazyRoute + '/admin/notices': typeof AdminNoticesIndexLazyRoute '/onboarding/team': typeof OnboardingTeamIndexLazyRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexLazyRoute - '/admin': typeof AdminLazyRoute + '/admin': typeof AdminLazyRouteWithChildren '/login': typeof LoginLazyRoute '/student': typeof StudentLazyRouteWithChildren + '/admin/notices': typeof AdminNoticesLazyRouteWithChildren '/onboarding/team': typeof OnboardingTeamLazyRouteWithChildren '/student/feedback': typeof StudentFeedbackLazyRoute '/student/grades': typeof StudentGradesLazyRoute @@ -206,10 +254,14 @@ export interface FileRoutesById { '/student/project-topic': typeof StudentProjectTopicLazyRoute '/student/submissions': typeof StudentSubmissionsLazyRoute '/student/team': typeof StudentTeamLazyRoute + '/admin/': typeof AdminIndexLazyRoute '/student/': typeof StudentIndexLazyRoute + '/admin/notices/$noticeId': typeof AdminNoticesNoticeIdLazyRoute + '/admin/notices/new': typeof AdminNoticesNewLazyRoute '/onboarding/team/first-meeting': typeof OnboardingTeamFirstMeetingLazyRoute '/onboarding/team/result': typeof OnboardingTeamResultLazyRoute '/onboarding/team/survey': typeof OnboardingTeamSurveyLazyRoute + '/admin/notices/': typeof AdminNoticesIndexLazyRoute '/onboarding/team/': typeof OnboardingTeamIndexLazyRoute } export interface FileRouteTypes { @@ -219,6 +271,7 @@ export interface FileRouteTypes { | '/admin' | '/login' | '/student' + | '/admin/notices' | '/onboarding/team' | '/student/feedback' | '/student/grades' @@ -227,15 +280,18 @@ export interface FileRouteTypes { | '/student/project-topic' | '/student/submissions' | '/student/team' + | '/admin/' | '/student/' + | '/admin/notices/$noticeId' + | '/admin/notices/new' | '/onboarding/team/first-meeting' | '/onboarding/team/result' | '/onboarding/team/survey' + | '/admin/notices/' | '/onboarding/team/' fileRoutesByTo: FileRoutesByTo to: | '/' - | '/admin' | '/login' | '/student/feedback' | '/student/grades' @@ -244,10 +300,14 @@ export interface FileRouteTypes { | '/student/project-topic' | '/student/submissions' | '/student/team' + | '/admin' | '/student' + | '/admin/notices/$noticeId' + | '/admin/notices/new' | '/onboarding/team/first-meeting' | '/onboarding/team/result' | '/onboarding/team/survey' + | '/admin/notices' | '/onboarding/team' id: | '__root__' @@ -255,6 +315,7 @@ export interface FileRouteTypes { | '/admin' | '/login' | '/student' + | '/admin/notices' | '/onboarding/team' | '/student/feedback' | '/student/grades' @@ -263,16 +324,20 @@ export interface FileRouteTypes { | '/student/project-topic' | '/student/submissions' | '/student/team' + | '/admin/' | '/student/' + | '/admin/notices/$noticeId' + | '/admin/notices/new' | '/onboarding/team/first-meeting' | '/onboarding/team/result' | '/onboarding/team/survey' + | '/admin/notices/' | '/onboarding/team/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexLazyRoute: typeof IndexLazyRoute - AdminLazyRoute: typeof AdminLazyRoute + AdminLazyRoute: typeof AdminLazyRouteWithChildren LoginLazyRoute: typeof LoginLazyRoute StudentLazyRoute: typeof StudentLazyRouteWithChildren OnboardingTeamLazyRoute: typeof OnboardingTeamLazyRouteWithChildren @@ -315,6 +380,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof StudentIndexLazyRouteImport parentRoute: typeof StudentLazyRoute } + '/admin/': { + id: '/admin/' + path: '/' + fullPath: '/admin/' + preLoaderRoute: typeof AdminIndexLazyRouteImport + parentRoute: typeof AdminLazyRoute + } '/student/team': { id: '/student/team' path: '/team' @@ -371,6 +443,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OnboardingTeamLazyRouteImport parentRoute: typeof rootRouteImport } + '/admin/notices': { + id: '/admin/notices' + path: '/notices' + fullPath: '/admin/notices' + preLoaderRoute: typeof AdminNoticesLazyRouteImport + parentRoute: typeof AdminLazyRoute + } '/onboarding/team/': { id: '/onboarding/team/' path: '/' @@ -378,6 +457,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OnboardingTeamIndexLazyRouteImport parentRoute: typeof OnboardingTeamLazyRoute } + '/admin/notices/': { + id: '/admin/notices/' + path: '/' + fullPath: '/admin/notices/' + preLoaderRoute: typeof AdminNoticesIndexLazyRouteImport + parentRoute: typeof AdminNoticesLazyRoute + } '/onboarding/team/survey': { id: '/onboarding/team/survey' path: '/survey' @@ -399,9 +485,52 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OnboardingTeamFirstMeetingLazyRouteImport parentRoute: typeof OnboardingTeamLazyRoute } + '/admin/notices/new': { + id: '/admin/notices/new' + path: '/new' + fullPath: '/admin/notices/new' + preLoaderRoute: typeof AdminNoticesNewLazyRouteImport + parentRoute: typeof AdminNoticesLazyRoute + } + '/admin/notices/$noticeId': { + id: '/admin/notices/$noticeId' + path: '/$noticeId' + fullPath: '/admin/notices/$noticeId' + preLoaderRoute: typeof AdminNoticesNoticeIdLazyRouteImport + parentRoute: typeof AdminNoticesLazyRoute + } } } +interface AdminNoticesLazyRouteChildren { + AdminNoticesNoticeIdLazyRoute: typeof AdminNoticesNoticeIdLazyRoute + AdminNoticesNewLazyRoute: typeof AdminNoticesNewLazyRoute + AdminNoticesIndexLazyRoute: typeof AdminNoticesIndexLazyRoute +} + +const AdminNoticesLazyRouteChildren: AdminNoticesLazyRouteChildren = { + AdminNoticesNoticeIdLazyRoute: AdminNoticesNoticeIdLazyRoute, + AdminNoticesNewLazyRoute: AdminNoticesNewLazyRoute, + AdminNoticesIndexLazyRoute: AdminNoticesIndexLazyRoute, +} + +const AdminNoticesLazyRouteWithChildren = + AdminNoticesLazyRoute._addFileChildren(AdminNoticesLazyRouteChildren) + +interface AdminLazyRouteChildren { + AdminNoticesLazyRoute: typeof AdminNoticesLazyRouteWithChildren + AdminIndexLazyRoute: typeof AdminIndexLazyRoute +} + +const AdminLazyRouteChildren: AdminLazyRouteChildren = { + AdminNoticesLazyRoute: AdminNoticesLazyRouteWithChildren, + AdminIndexLazyRoute: AdminIndexLazyRoute, +} + +const AdminLazyRouteWithChildren = AdminLazyRoute._addFileChildren( + AdminLazyRouteChildren, +) + interface StudentLazyRouteChildren { StudentFeedbackLazyRoute: typeof StudentFeedbackLazyRoute StudentGradesLazyRoute: typeof StudentGradesLazyRoute @@ -447,7 +576,7 @@ const OnboardingTeamLazyRouteWithChildren = const rootRouteChildren: RootRouteChildren = { IndexLazyRoute: IndexLazyRoute, - AdminLazyRoute: AdminLazyRoute, + AdminLazyRoute: AdminLazyRouteWithChildren, LoginLazyRoute: LoginLazyRoute, StudentLazyRoute: StudentLazyRouteWithChildren, OnboardingTeamLazyRoute: OnboardingTeamLazyRouteWithChildren, diff --git a/apps/oop/src/app/routes/admin.index.lazy.tsx b/apps/oop/src/app/routes/admin.index.lazy.tsx new file mode 100644 index 00000000..919f1c53 --- /dev/null +++ b/apps/oop/src/app/routes/admin.index.lazy.tsx @@ -0,0 +1,7 @@ +import { createLazyFileRoute } from '@tanstack/react-router'; + +import AdminHomeDashboard from '~/widgets/admin-dashboard/AdminHomeDashboard'; + +export const Route = createLazyFileRoute('/admin/')({ + component: AdminHomeDashboard, +}); diff --git a/apps/oop/src/app/routes/admin.lazy.tsx b/apps/oop/src/app/routes/admin.lazy.tsx index 20bb4aba..ebe7e2e8 100644 --- a/apps/oop/src/app/routes/admin.lazy.tsx +++ b/apps/oop/src/app/routes/admin.lazy.tsx @@ -1,11 +1,10 @@ import { Navigate, createLazyFileRoute } from '@tanstack/react-router'; +import AdminShell from '~/app/components/AdminShell'; import { ROUTES } from '~/app/constants/routes'; import { useAuthStore } from '~/features/auth/authStore'; -import AdminHomeDashboard from '~/widgets/admin-dashboard/AdminHomeDashboard'; - export const Route = createLazyFileRoute('/admin')({ component: AdminHomePage, }); @@ -22,5 +21,5 @@ function AdminHomePage() { return ; } - return ; + return ; } diff --git a/apps/oop/src/app/routes/admin.notices.$noticeId.lazy.tsx b/apps/oop/src/app/routes/admin.notices.$noticeId.lazy.tsx new file mode 100644 index 00000000..a12ae0a1 --- /dev/null +++ b/apps/oop/src/app/routes/admin.notices.$noticeId.lazy.tsx @@ -0,0 +1,7 @@ +import { createLazyFileRoute } from '@tanstack/react-router'; + +import { AdminNoticeDetailPage } from '~/widgets/admin-notices/AdminNoticePages'; + +export const Route = createLazyFileRoute('/admin/notices/$noticeId')({ + component: AdminNoticeDetailPage, +}); diff --git a/apps/oop/src/app/routes/admin.notices.index.lazy.tsx b/apps/oop/src/app/routes/admin.notices.index.lazy.tsx new file mode 100644 index 00000000..9ba8ea88 --- /dev/null +++ b/apps/oop/src/app/routes/admin.notices.index.lazy.tsx @@ -0,0 +1,7 @@ +import { createLazyFileRoute } from '@tanstack/react-router'; + +import { AdminNoticeListPage } from '~/widgets/admin-notices/AdminNoticePages'; + +export const Route = createLazyFileRoute('/admin/notices/')({ + component: AdminNoticeListPage, +}); diff --git a/apps/oop/src/app/routes/admin.notices.lazy.tsx b/apps/oop/src/app/routes/admin.notices.lazy.tsx new file mode 100644 index 00000000..34938754 --- /dev/null +++ b/apps/oop/src/app/routes/admin.notices.lazy.tsx @@ -0,0 +1,5 @@ +import { Outlet, createLazyFileRoute } from '@tanstack/react-router'; + +export const Route = createLazyFileRoute('/admin/notices')({ + component: Outlet, +}); diff --git a/apps/oop/src/app/routes/admin.notices.new.lazy.tsx b/apps/oop/src/app/routes/admin.notices.new.lazy.tsx new file mode 100644 index 00000000..3e377caa --- /dev/null +++ b/apps/oop/src/app/routes/admin.notices.new.lazy.tsx @@ -0,0 +1,7 @@ +import { createLazyFileRoute } from '@tanstack/react-router'; + +import { AdminNoticeNewPage } from '~/widgets/admin-notices/AdminNoticePages'; + +export const Route = createLazyFileRoute('/admin/notices/new')({ + component: AdminNoticeNewPage, +}); diff --git a/apps/oop/src/widgets/admin-dashboard/AdminHomeDashboard.tsx b/apps/oop/src/widgets/admin-dashboard/AdminHomeDashboard.tsx index 7c17642f..9aa7d379 100644 --- a/apps/oop/src/widgets/admin-dashboard/AdminHomeDashboard.tsx +++ b/apps/oop/src/widgets/admin-dashboard/AdminHomeDashboard.tsx @@ -1,37 +1,33 @@ import { Button, Heading } from '@aics/design-system'; +import { Link } from '@tanstack/react-router'; + +import { ROUTES } from '~/app/constants/routes'; import * as styles from './AdminHomeDashboard.css'; const schedules = [ - ['1151(월)', '48명 / 7팀', '~10/8\n제출 7팀\n회의록 3건', '~10/29\n제출 6팀\n회의록 3건', '~11/12\n제출 6팀\n회의록 1건', '~11/19\n제출 2팀\n회의록 0건', '11/27\n시작 전', '-', '3건'], - ['1152(월)', '46명 / 7팀', '~10/8\n제출 7팀\n회의록 4건', '~10/29\n제출 6팀\n회의록 3건', '~11/12\n제출 6팀\n회의록 2건', '~11/19\n제출 4팀\n회의록 1건', '11/27\n시작 전', '-', '4건'], - ['1153(월)', '47명 / 6팀', '~10/8\n제출 6팀\n회의록 3건', '~10/29\n제출 5팀\n회의록 2건', '~11/12\n제출 1팀\n회의록 0건', '~11/19\n제출 0팀\n회의록 0건', '11/27\n시작 전', '-', '5건'], + ['1151(월6)', '48명 / 7팀', '~10/8\n제출 7팀\n회의록 3건', '~10/29\n제출 6팀\n회의록 3건', '~11/12\n제출 6팀\n회의록 1건', '~11/19\n제출 2팀\n회의록 0건', '11/27\n시작 전', '-', '3건'], + ['1152(월7)', '46명 / 7팀', '~10/8\n제출 7팀\n회의록 4건', '~10/29\n제출 6팀\n회의록 3건', '~11/12\n제출 6팀\n회의록 2건', '~11/19\n제출 4팀\n회의록 1건', '11/27\n시작 전', '-', '4건'], + ['1153(월8)', '47명 / 6팀', '~10/8\n제출 6팀\n회의록 3건', '~10/29\n제출 5팀\n회의록 2건', '~11/12\n제출 1팀\n회의록 0건', '~11/19\n제출 0팀\n회의록 0건', '11/27\n시작 전', '-', '5건'], ] as const; -const notices = [['1151반', '전체 접수 공지', '2025-12-17'], ['1152반', '프로젝트 산출물 제출 안내', '2025-12-17'], ['1153반', '기말 필기 시험 접수 공지 (수정 12/16)', '2025-12-15']] as const; +const notices = [['1151(월6)', '전체 접수 공지', '2025-12-17'], ['1152(월7)', '프로젝트 산출물 제출 안내', '2025-12-17'], ['1153(월8)', '기말 필기 시험 접수 공지 (수정 12/16)', '2025-12-15']] as const; const minutes = [['1151반-A팀', '와이어프레임 기획 논의', '2025-12-17'], ['1152반-A팀', '기획 논의', '2025-12-17'], ['1152반-B팀', '주제 투표하기', '2025-12-15'], ['1153반-C팀', '주제 투표하기', '2025-12-15']] as const; const inbox = [['1152-A팀', '김민준 교수님, 화면설계서 1차 첨부했는데 확인 부탁드려요!', '20분 전'], ['1152-B팀', '이은정 교수님, 회의록을 수정 과정에서 저장됐어요.', '1시간 전'], ['1153-B팀', '박서연 발표자료 초안 오늘 밤까지 올릴게요.', '1시간 전']] as const; -function List({ items }: { items: readonly (readonly [string, string, string])[] }) { - return
    {items.map(([section, title, date]) =>
  • {section}{title}
  • )}
; +function List({ isNoticeList = false, items }: { isNoticeList?: boolean; items: readonly (readonly [string, string, string])[] }) { + return
    {items.map(([section, title, date], index) =>
  • {section}{isNoticeList ? {title} : {title}}
  • )}
; } -function Panel({ title, items, action }: { title: string; items: readonly (readonly [string, string, string])[]; action?: boolean }) { - return
{title}
{action ?
: null}
; +function Panel({ title, items, action, isNoticePanel = false }: { title: string; items: readonly (readonly [string, string, string])[]; action?: boolean; isNoticePanel?: boolean }) { + return
{title}{isNoticePanel ? 전체보기 › : }
{action ?
{isNoticePanel ?
: null}
; } export default function AdminHomeDashboard() { - return
- -
+ return
홈
분반별 진행 일정
{['분반', '인원/팀 수', '제안서', '중간 점검', '발표 제출', '발표 평가', '최종 보고서', '상호 평가', '쪽지'].map(header => )}{schedules.map(row => {row.map(cell => )})}
{header}
{cell}
-
+
-
; } diff --git a/apps/oop/src/widgets/admin-notices/AdminNoticePages.css.ts b/apps/oop/src/widgets/admin-notices/AdminNoticePages.css.ts new file mode 100644 index 00000000..411710e5 --- /dev/null +++ b/apps/oop/src/widgets/admin-notices/AdminNoticePages.css.ts @@ -0,0 +1,30 @@ +import { tokens } from '@aics/design-system'; +import { globalStyle, style } from '@vanilla-extract/css'; + +export const page = style({ display: 'flex', flexDirection: 'column', gap: 20, margin: '0 auto', maxWidth: 1240, padding: '28px clamp(20px, 5vw, 48px) 56px', width: '100%' }); +export const titleRow = style({ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }); +export const backLink = style({ color: tokens.color.text.accent, fontSize: 13, textDecoration: 'none' }); +export const filters = style({ display: 'flex', gap: 8 }); +const filterBase = { background: tokens.color.background.gray, border: 0, borderRadius: tokens.radius.full, color: tokens.color.text.primary, cursor: 'pointer', font: 'inherit', padding: '7px 12px' }; +export const filter = style(filterBase); +export const filterActive = style({ ...filterBase, background: tokens.color.text.primary, color: tokens.color.background.surface }); +export const tableCard = style({ overflowX: 'auto', padding: 0 }); +export const table = style({ borderCollapse: 'collapse', minWidth: 660, width: '100%' }); +export const titleLink = style({ color: tokens.color.text.primary, textDecoration: 'none' }); +export const emptyCell = style({ color: tokens.color.text.secondary, padding: '42px 16px !important', textAlign: 'center' }); +export const listFooter = style({ alignItems: 'center', display: 'flex', justifyContent: 'space-between' }); +export const pagination = style({ display: 'flex', gap: 8, margin: '0 auto' }); +globalStyle(pagination + ' button', { background: 'transparent', border: 0, cursor: 'pointer', font: 'inherit', minWidth: 28, padding: 5 }); +export const pageActive = style({ background: tokens.color.accent + ' !important', borderRadius: tokens.radius.element, color: tokens.color.background.surface }); +export const detailCard = style({ display: 'flex', flexDirection: 'column', gap: 16, padding: 28 }); +export const formCard = style({ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 760, padding: 28 }); +export const meta = style({ fontSize: 13 }); +export const divider = style({ background: tokens.color.border.base, height: 1, width: '100%' }); +export const attachment = style({ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 8 }); +export const actions = style({ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 8 }); +export const fields = style({ display: 'flex', flexDirection: 'column', gap: 18 }); +export const fieldGroup = style({ display: 'flex', flexDirection: 'column', gap: 8 }); +export const selectButton = style({ background: tokens.color.background.surface, border: '1px solid ' + tokens.color.border.base, borderRadius: tokens.radius.element, cursor: 'pointer', padding: '10px 12px', textAlign: 'left', width: 160 }); +globalStyle(table + ' th', { background: tokens.color.background.gray, color: tokens.color.text.secondary, fontSize: 13, fontWeight: 500, padding: '12px 16px', textAlign: 'left' }); +globalStyle(table + ' td', { borderTop: '1px solid ' + tokens.color.border.base, fontSize: 14, padding: '14px 16px' }); +globalStyle(attachment + ' a', { color: tokens.color.text.accent, fontSize: 13 }); diff --git a/apps/oop/src/widgets/admin-notices/AdminNoticePages.tsx b/apps/oop/src/widgets/admin-notices/AdminNoticePages.tsx new file mode 100644 index 00000000..6885c11e --- /dev/null +++ b/apps/oop/src/widgets/admin-notices/AdminNoticePages.tsx @@ -0,0 +1,96 @@ +import { Button, Card, Heading, Text, TextArea, TextInput } from '@aics/design-system'; +import { Link } from '@tanstack/react-router'; +import { useState } from 'react'; + +import { ROUTES } from '~/app/constants/routes'; + +import * as styles from './AdminNoticePages.css'; + +const notices = [ + { date: '2025-12-17', id: '1', section: '1151(월6)', title: '전체 접수 공지', writer: '이은정' }, + { date: '2025-12-17', id: '2', section: '1152(월7)', title: '프로젝트 산출물 제출 안내', writer: '이은정' }, + { date: '2025-12-15', id: '3', section: '1153(월8)', title: '10주차 발표 안내', writer: '이은정' }, + { date: '2025-12-17', id: '4', section: '1151(월6)', title: '전체 접수 공지', writer: '이은정' }, + { date: '2025-12-17', id: '5', section: '1152(월7)', title: '프로젝트 산출물 제출 안내', writer: '이은정' }, + { date: '2025-12-15', id: '6', section: '1153(월8)', title: '기말 필기 시험 접수 공지 (수정 12/16)', writer: '이은정' }, + { date: '2025-12-12', id: '7', section: '1151(월6)', title: '프로젝트 중간 점검 일정 안내', writer: '이은정' }, + { date: '2025-12-10', id: '8', section: '1152(월7)', title: '발표 자료 제출 전 확인 사항', writer: '이은정' }, + { date: '2025-12-08', id: '9', section: '1153(월8)', title: '상호 평가 진행 안내', writer: '이은정' }, +] as const; + +const sectionFilters = ['전체', '1151(월6)', '1152(월7)', '1153(월8)'] as const; +const noticesPerPage = 3; + +function BackToList() { + return ← 공지사항 목록으로; +} + +export function AdminNoticeListPage() { + const [currentPage, setCurrentPage] = useState(1); + const [selectedSection, setSelectedSection] = useState<(typeof sectionFilters)[number]>('전체'); + const filteredNotices = selectedSection === '전체' ? notices : notices.filter(notice => notice.section === selectedSection); + const totalPages = Math.max(1, Math.ceil(filteredNotices.length / noticesPerPage)); + const displayedNotices = filteredNotices.slice( + (currentPage - 1) * noticesPerPage, + currentPage * noticesPerPage, + ); + + function selectSection(section: (typeof sectionFilters)[number]) { + setSelectedSection(section); + setCurrentPage(1); + } + + return
+ 공지사항 +
+ {sectionFilters.map(label => )} +
+ + + + {displayedNotices.length > 0 ? displayedNotices.map(notice => ) : } +
날짜 ↕분반제목작성자
{notice.date}{notice.section}{notice.title}{notice.writer}
{selectedSection}에 등록된 공지사항이 없어요.
+
+
+ {Array.from({ length: totalPages }, (_, index) => index + 1).map(page => )} + +
+
; +} + +export function AdminNoticeDetailPage() { + return
+
공지사항 > 10주차 발표 안내
+ + 10주차 발표 안내 + 작성일 : 2026-07-07 14:30 + 공개 범위 : 1151(월6), 1152(월7), 1153(월8) +
+ 10주차 중간고사 10번 문제 발표는 코드, 시연(다양한 앱 예제 활용), ppt 발표자료를 준비해 주세요. + 발표 점수는 만점 15점이고 발표 수준에 따라 점수를 부여합니다. + 추가로 발표 자료에는 코딩과 함께 구현한 화면을 포함해 주세요. + +
+ +
; +} + +export function AdminNoticeNewPage() { + const [content, setContent] = useState(''); + const [title, setTitle] = useState(''); + + return
+
공지사항 작성
+ + 공지사항 작성 + 작성일 : 2026-07-07 14:30 +
+ +
분반
+