- 협상카드 성공률(사용 세션 대비 타결) 집계 추가, 견적 생성 카드선택 순위 정렬·통계 TOP5 - 상품 엑셀: 품목코드·모델명 라벨화, 리드타임 suffix 제거, 공급사 컬럼 신설(등록 후 supplier_items 매핑) - 설정 화면 JSON 불러오기/내보내기(빈 값은 미갱신), IMK 설정 JSON 보관 - 설정·회원 페이지 새로고침 forbidden 수정(자식 loader 가 initAuth 대기) - 협력사·견적 목록도 포털형 카드(툴바+테이블 결합)로 통일
111 lines
3.6 KiB
TypeScript
111 lines
3.6 KiB
TypeScript
import {createBrowserRouter, redirect} from 'react-router';
|
|
import {initAuth} from '../features/auth/service';
|
|
import {isLoggedIn, hasRole} from '../stores/auth';
|
|
import {hasSeenOnboarding} from '../features/onboarding/storage';
|
|
import AuthenticatedLayout from '@/components/layout/AuthenticatedLayout';
|
|
import LoginPage from '../pages/login';
|
|
import DashboardPage from '../pages/dashboard';
|
|
import StatisticsPage from '../pages/statistics';
|
|
import ForbiddenPage from '../pages/forbidden';
|
|
import NotFoundPage from '../pages/not-found';
|
|
import ProductsPage from '../pages/products';
|
|
import PartnersPage from '../pages/partners';
|
|
import QuotationPage from '../pages/quotation';
|
|
import CardsPage from '../pages/cards';
|
|
import MembersPage from '../pages/members';
|
|
import SettingsPage from '../pages/settings';
|
|
import NotificationsPage from '../pages/notifications';
|
|
import OnboardingPage from '../pages/onboarding';
|
|
|
|
export const router = createBrowserRouter([
|
|
// dev 전용: import.meta.env.DEV가 false인 프로덕션 빌드에선 이 배열 항목과
|
|
// 내부 import()가 통째로 트리셰이킹되어 번들/라우트에 포함되지 않는다.
|
|
...(import.meta.env.DEV
|
|
? [
|
|
{
|
|
path: 'dev/design',
|
|
lazy: async () => ({
|
|
Component: (await import('../pages/dev/dev-design')).default,
|
|
}),
|
|
},
|
|
]
|
|
: []),
|
|
{
|
|
index: true,
|
|
loader: () => redirect('/dashboard'),
|
|
},
|
|
{
|
|
path: 'login',
|
|
loader: async () => {
|
|
await initAuth();
|
|
if (isLoggedIn()) return redirect('/dashboard');
|
|
return null;
|
|
},
|
|
Component: LoginPage,
|
|
},
|
|
{
|
|
path: 'forbidden',
|
|
Component: ForbiddenPage,
|
|
},
|
|
{
|
|
path: 'onboarding',
|
|
loader: async ({request}) => {
|
|
await initAuth();
|
|
if (!isLoggedIn()) {
|
|
const url = new URL(request.url);
|
|
const from = encodeURIComponent(url.pathname + url.search);
|
|
return redirect(`/login?redirect=${from}`);
|
|
}
|
|
return null;
|
|
},
|
|
Component: OnboardingPage,
|
|
},
|
|
{
|
|
loader: async ({request}) => {
|
|
await initAuth();
|
|
if (!isLoggedIn()) {
|
|
const url = new URL(request.url);
|
|
const from = encodeURIComponent(url.pathname + url.search);
|
|
return redirect(`/login?redirect=${from}`);
|
|
}
|
|
// 최초 로그인 유저는 온보딩으로. 완료·건너뛰기 시 flag 저장되어 이후엔 통과.
|
|
if (!hasSeenOnboarding()) {
|
|
return redirect('/onboarding');
|
|
}
|
|
return null;
|
|
},
|
|
Component: AuthenticatedLayout,
|
|
children: [
|
|
{path: 'dashboard', Component: DashboardPage},
|
|
{path: 'statistics', Component: StatisticsPage},
|
|
{path: 'products', Component: ProductsPage},
|
|
{path: 'partners', Component: PartnersPage},
|
|
{path: 'quotation', Component: QuotationPage},
|
|
{path: 'cards', Component: CardsPage},
|
|
{path: 'notifications', Component: NotificationsPage},
|
|
{
|
|
// 최고관리자 전용. 자식 loader 는 부모와 병렬 실행되므로 여기서도 initAuth 를 기다린다(멱등).
|
|
path: 'members',
|
|
loader: async () => {
|
|
await initAuth();
|
|
return hasRole('최고관리자') ? null : redirect('/forbidden');
|
|
},
|
|
Component: MembersPage,
|
|
},
|
|
{
|
|
// 최고관리자 전용. 회사 브랜딩/용어/커스텀필드 설정. (자식 loader 는 부모와 병렬 → initAuth 대기 필수)
|
|
path: 'settings',
|
|
loader: async () => {
|
|
await initAuth();
|
|
return hasRole('최고관리자') ? null : redirect('/forbidden');
|
|
},
|
|
Component: SettingsPage,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
path: '*',
|
|
Component: NotFoundPage,
|
|
},
|
|
]);
|