Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions .agent/rules/code-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
## Accessibility and UI states

- Prefer semantic elements such as `header`, `nav`, `main`, `section`, and `article`.
- Do not nest interactive elements such as `Button` inside `Link`.
- Use `Link` for text navigation. For button-styled navigation, follow the existing `Button` with `useNavigate` pattern.
- Use visible labels when possible; use `aria-label` when an accessible name is otherwise unavailable.
- Preserve visible focus, errors, descriptions, and understandable loading/empty/error/missing-prerequisite states.
- Custom composite widgets such as tabs must implement the relevant ARIA roles and keyboard interaction and receive an interaction test.
Expand Down
92 changes: 92 additions & 0 deletions apps/oop/src/app/components/AdminShell.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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,
});
65 changes: 65 additions & 0 deletions apps/oop/src/app/components/AdminShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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;

function isMenuItemActive(pathname: string, itemPath: string) {
if (itemPath === ROUTES.ADMIN) return pathname === itemPath;

return pathname === itemPath || pathname.startsWith(`${itemPath}/`);
}

export default function AdminShell() {
const pathname = useRouterState({ select: state => state.location.pathname });

return (
<div className={styles.shell}>
<aside className={styles.sidebar}>
<div className={styles.brand}>
<strong>AICS Team</strong>
<span>팀 프로젝트 운영 플랫폼</span>
<small>2026-2 · 객체지향프로그래밍</small>
</div>
<nav aria-label='관리자 메뉴' className={styles.nav}>
{menuItems.map(item => {
const isActive = isMenuItemActive(pathname, item.to);

return (
<Link
className={isActive ? styles.activeNav : styles.navItem}
key={item.label}
to={item.to}
>
{item.label}
</Link>
);
})}
{['수강생/팀 관리', '분반별 제출물', '회의록'].map(item => (
<button className={styles.navItem} key={item} type='button'>
{item}
</button>
))}
<button className={styles.navItem} type='button'>
쪽지함 <span className={styles.count}>12</span>
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
</nav>
<div className={styles.account}>
<span aria-hidden='true' className={styles.avatar} />
<div>
<strong>어드민 계정</strong>
<span>로그아웃 / 권한 변경</span>
</div>
</div>
</aside>
<main className={styles.main}>
<Outlet />
</main>
</div>
);
}
3 changes: 3 additions & 0 deletions apps/oop/src/app/constants/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export const ROUTES = {
LOGIN: '/login',
ADMIN: '/admin',
ADMIN_NOTICES: '/admin/notices',
ADMIN_NOTICE_NEW: '/admin/notices/new',
ADMIN_NOTICE_EDIT: '/admin/notices/$noticeId/edit',
STUDENT: {
HOME: '/student',
TEAM: '/student/team',
Expand Down
Loading
Loading