diff --git a/web-app/app/app.css b/web-app/app/app.css
index 1b651e1..5ba9722 100644
--- a/web-app/app/app.css
+++ b/web-app/app/app.css
@@ -1,9 +1,62 @@
@import "tailwindcss";
-@import "pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css";
+
+@font-face {
+ font-family: "Pretendard";
+ font-weight: 400;
+ font-style: normal;
+ src: url("~/shared/assets/fonts/Pretendard-Regular.subset.woff") format("woff");
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Pretendard";
+ font-weight: 500;
+ font-style: normal;
+ src: url("~/shared/assets/fonts/Pretendard-Medium.subset.woff") format("woff");
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Pretendard";
+ font-weight: 600;
+ font-style: normal;
+ src: url("~/shared/assets/fonts/Pretendard-SemiBold.subset.woff") format("woff");
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Pretendard";
+ font-weight: 700;
+ font-style: normal;
+ src: url("~/shared/assets/fonts/Pretendard-Bold.subset.woff") format("woff");
+ font-display: swap;
+}
@theme {
- --font-sans: "Pretendard Variable", ui-sans-serif, system-ui, sans-serif,
+ --font-sans: "Pretendard", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+
+ /* Primary (Navy) */
+ --color-primary: var(--color-dabeeo-navy-main);
+ --color-primary-secondary: var(--color-dabeeo-navy-secondary);
+ --color-primary-tertiary: var(--color-dabeeo-navy-tertiary);
+ --color-primary-tertiary01: var(--color-dabeeo-navy-tertiary01);
+ --color-primary-tertiary02: var(--color-dabeeo-navy-tertiary02);
+
+ /* Navy */
+ --color-dabeeo-navy-main: #00387d;
+ --color-dabeeo-navy-secondary: #032651;
+ --color-dabeeo-navy-tertiary: #5c84b4;
+ --color-dabeeo-navy-tertiary01: #d4dde9;
+ --color-dabeeo-navy-tertiary02: #f0f3f7;
+
+ /* Gray */
+ --color-dabeeo-gray-44: #444444;
+ --color-dabeeo-gray-99: #999999;
+ --color-dabeeo-gray-be: #bebebe;
+ --color-dabeeo-gray-da: #dadada;
+ --color-dabeeo-gray-eb: #ebebeb;
+ --color-dabeeo-gray-f9: #f9f9f9;
}
html,
diff --git a/web-app/app/routes.ts b/web-app/app/routes.ts
index c059ada..8e23db0 100644
--- a/web-app/app/routes.ts
+++ b/web-app/app/routes.ts
@@ -7,6 +7,7 @@ export default [
route('login', './routes/login/page.tsx'),
]),
layout('./routes/layout.tsx', [
+ index('./routes/page.tsx'),
...prefix('imagery', [
...prefix('aerial', [
index('./routes/imagery/aerial/page.tsx'),
diff --git a/web-app/app/routes/code/page.tsx b/web-app/app/routes/code/page.tsx
index e69de29..8c2de45 100644
--- a/web-app/app/routes/code/page.tsx
+++ b/web-app/app/routes/code/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return
- 기본 레이아웃
-
+
);
}
diff --git a/web-app/app/routes/log/audit/page.tsx b/web-app/app/routes/log/audit/page.tsx
index e69de29..d0d2b32 100644
--- a/web-app/app/routes/log/audit/page.tsx
+++ b/web-app/app/routes/log/audit/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return
감사 로그
;
+}
diff --git a/web-app/app/routes/log/system/page.tsx b/web-app/app/routes/log/system/page.tsx
index e69de29..0e57abc 100644
--- a/web-app/app/routes/log/system/page.tsx
+++ b/web-app/app/routes/log/system/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return
시스템 로그
;
+}
diff --git a/web-app/app/routes/model/[id]/page.tsx b/web-app/app/routes/model/[id]/page.tsx
index e69de29..b436d5f 100644
--- a/web-app/app/routes/model/[id]/page.tsx
+++ b/web-app/app/routes/model/[id]/page.tsx
@@ -0,0 +1,5 @@
+import type { Route } from './+types/page';
+
+export default function Page({ params }: Route.ComponentProps) {
+ return
모델 상세 id: {params.modelId}
;
+}
diff --git a/web-app/app/routes/model/page.tsx b/web-app/app/routes/model/page.tsx
index e69de29..0806a74 100644
--- a/web-app/app/routes/model/page.tsx
+++ b/web-app/app/routes/model/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return
모델 목록
;
+}
diff --git a/web-app/app/routes/page.tsx b/web-app/app/routes/page.tsx
new file mode 100644
index 0000000..94417df
--- /dev/null
+++ b/web-app/app/routes/page.tsx
@@ -0,0 +1,5 @@
+import { Navigate } from 'react-router';
+
+export default function Page() {
+ return
;
+}
diff --git a/web-app/app/routes/schedule/page.tsx b/web-app/app/routes/schedule/page.tsx
index e69de29..1f7ed6d 100644
--- a/web-app/app/routes/schedule/page.tsx
+++ b/web-app/app/routes/schedule/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return
스케줄 목록
;
+}
diff --git a/web-app/app/shared/assets/fonts/Pretendard-Bold.subset.woff b/web-app/app/shared/assets/fonts/Pretendard-Bold.subset.woff
new file mode 100644
index 0000000..06ba102
Binary files /dev/null and b/web-app/app/shared/assets/fonts/Pretendard-Bold.subset.woff differ
diff --git a/web-app/app/shared/assets/fonts/Pretendard-Medium.subset.woff b/web-app/app/shared/assets/fonts/Pretendard-Medium.subset.woff
new file mode 100644
index 0000000..f97a78f
Binary files /dev/null and b/web-app/app/shared/assets/fonts/Pretendard-Medium.subset.woff differ
diff --git a/web-app/app/shared/assets/fonts/Pretendard-Regular.subset.woff b/web-app/app/shared/assets/fonts/Pretendard-Regular.subset.woff
new file mode 100644
index 0000000..174736a
Binary files /dev/null and b/web-app/app/shared/assets/fonts/Pretendard-Regular.subset.woff differ
diff --git a/web-app/app/shared/assets/fonts/Pretendard-SemiBold.subset.woff b/web-app/app/shared/assets/fonts/Pretendard-SemiBold.subset.woff
new file mode 100644
index 0000000..ee2fa3d
Binary files /dev/null and b/web-app/app/shared/assets/fonts/Pretendard-SemiBold.subset.woff differ
diff --git a/web-app/app/shared/components/menu/LayoutMenu.tsx b/web-app/app/shared/components/menu/LayoutMenu.tsx
new file mode 100644
index 0000000..9b886a8
--- /dev/null
+++ b/web-app/app/shared/components/menu/LayoutMenu.tsx
@@ -0,0 +1,22 @@
+import { useCallback } from 'react';
+import { useLocation, useNavigate } from 'react-router';
+
+import { Menu, type MenuItemChildrenType, type MenuItemType } from './Menu';
+
+interface Props {
+ items: MenuItemType[];
+}
+
+export const LayoutMenu = ({ items }: Props) => {
+ const { pathname } = useLocation();
+ const navigate = useNavigate();
+
+ const onSelectionChange = useCallback(
+ (menu: MenuItemChildrenType) => {
+ navigate(menu.menuUrl);
+ },
+ [navigate],
+ );
+
+ return
;
+};
diff --git a/web-app/app/shared/components/menu/Menu.tsx b/web-app/app/shared/components/menu/Menu.tsx
new file mode 100644
index 0000000..1576e3f
--- /dev/null
+++ b/web-app/app/shared/components/menu/Menu.tsx
@@ -0,0 +1,124 @@
+import { useCallback, useEffect, useId, useState } from 'react';
+
+export type MenuItemChildrenType = {
+ id: string;
+ name: string;
+ menuUrl: string;
+};
+export type MenuItemType = {
+ id: string;
+ name: string;
+ menuUrl?: null;
+ children: MenuItemChildrenType[];
+};
+export interface MenuProps {
+ items: MenuItemType[];
+ onSelectionChange: (menu: MenuItemChildrenType) => void;
+ currentPath: string;
+}
+export const Menu = (props: MenuProps) => {
+ const { items, currentPath, onSelectionChange } = props;
+ const id = useId();
+
+ const getCurrentPathItem = useCallback(() => {
+ return items.find((i) => i.children.some((c) => currentPath.includes(c.menuUrl)));
+ }, [items, currentPath]);
+
+ const [expandedItemKeys, setExpandedItemKeys] = useState
(() => {
+ const item = getCurrentPathItem();
+ return item ? [item.id] : [];
+ });
+
+ useEffect(() => {
+ const item = getCurrentPathItem();
+ setExpandedItemKeys((prev) => {
+ if (item && prev.length === 1 && prev[0] === item.id) {
+ return prev;
+ }
+ return item ? [item.id] : [];
+ });
+ }, [getCurrentPathItem]);
+
+ return (
+
+ );
+};
diff --git a/web-app/app/shared/components/menu/index.ts b/web-app/app/shared/components/menu/index.ts
new file mode 100644
index 0000000..2ffc9a2
--- /dev/null
+++ b/web-app/app/shared/components/menu/index.ts
@@ -0,0 +1,2 @@
+export { Menu } from './Menu';
+export type { MenuProps, MenuItemType, MenuItemChildrenType } from './Menu';
diff --git a/web-app/app/shared/constants/menu.ts b/web-app/app/shared/constants/menu.ts
new file mode 100644
index 0000000..77ddbac
--- /dev/null
+++ b/web-app/app/shared/constants/menu.ts
@@ -0,0 +1,53 @@
+import type { MenuItemType } from '~/shared/components/menu';
+
+export const MENU_ITEMS: MenuItemType[] = [
+ {
+ id: 'imagery',
+ name: '영상데이터관리',
+ children: [
+ { id: 'aerial', name: '항공영상관리', menuUrl: '/imagery/aerial' },
+ { id: 'satellite', name: '위성영상관리', menuUrl: '/imagery/satellite' },
+ { id: 'drone', name: '드론영상관리', menuUrl: '/imagery/drone' },
+ ],
+ },
+ {
+ id: 'inference',
+ name: '추론관리',
+ children: [{ id: 'inference-list', name: '추론 목록', menuUrl: '/inference' }],
+ },
+ {
+ id: 'model',
+ name: '모델관리',
+ children: [{ id: 'model-list', name: '모델 목록', menuUrl: '/model' }],
+ },
+ {
+ id: 'labeling',
+ name: '라벨링',
+ children: [
+ { id: 'label', name: '라벨링 작업', menuUrl: '/labeling/label' },
+ { id: 'review', name: '라벨링 검수', menuUrl: '/labeling/review' },
+ ],
+ },
+ {
+ id: 'log',
+ name: '로그',
+ children: [
+ { id: 'audit', name: '감사 로그', menuUrl: '/log/audit' },
+ { id: 'system', name: '시스템 로그', menuUrl: '/log/system' },
+ ],
+ },
+ {
+ id: 'schedule',
+ name: '스케줄관리',
+ children: [{ id: 'schedule-list', name: '스케줄 목록', menuUrl: '/schedule' }],
+ },
+ {
+ id: 'system',
+ name: '시스템관리',
+ children: [
+ { id: 'code', name: '공통코드 관리', menuUrl: '/code' },
+ { id: 'hyper-parameter', name: '하이퍼파라미터 설정', menuUrl: '/hyper-parameter' },
+ { id: 'user', name: '사용자 관리', menuUrl: '/user' },
+ ],
+ },
+];
diff --git a/web-app/eslint.config.js b/web-app/eslint.config.js
index 069448c..dd9ac71 100644
--- a/web-app/eslint.config.js
+++ b/web-app/eslint.config.js
@@ -21,6 +21,8 @@ export default tseslint.config(
{
rules: {
'@stylistic/jsx-one-expression-per-line': 'off',
+ '@stylistic/multiline-ternary': 'off',
+ 'react-hooks/set-state-in-effect': 'off',
},
},
);