Skip to content

Admin Dashboard (React/Vite)

truloop-core-admin-dashboard는 내부 관리자용 CMS 웹 애플리케이션입니다. 사용자 관리, 커버 템플릿 CRUD, 샘플 룹 관리, 주요 지표 대시보드 기능을 제공합니다.


기술 스택

카테고리기술버전
프레임워크React19.2.x
빌드Vite7.3.x
라우팅TanStack Router1.166.x
서버 상태TanStack React Query5.90.x
테이블TanStack React Table8.21.x
클라이언트 상태Zustand5.0.x
HTTPky1.14.x
React Hook Form + Zod 47.71.x / 4.3.x
스타일Tailwind CSS 4 + Radix UI4.2.x
차트Recharts3.8.x
OAuth@react-oauth/google0.13.x
토스트Sonner2.0.x
아이콘Lucide React0.577.x

프로젝트 구조

src/
├── components/          # 공통 UI 컴포넌트
│   ├── layout/          # AuthenticatedLayout, Header, Main, Sidebar
│   └── ui/              # Radix UI 기반 UI 프리미티브 (Button, Card, Dialog, Sheet 등)
├── context/             # React Context (Layout, Search, Theme)
├── features/            # Feature 모듈 (도메인별)
│   ├── auth/            # Google OAuth 인증
│   ├── cover-templates/ # 커버 템플릿 CRUD
│   ├── dashboard/       # 대시보드 지표/차트
│   ├── errors/          # 에러 페이지 (401, 404, 500)
│   ├── sample-loops/    # 샘플 룹 관리
│   └── users/           # 유저 관리
├── hooks/               # 커스텀 훅
├── lib/                 # 유틸리티
│   ├── api-client.ts    # ky 기반 HTTP 클라이언트
│   ├── cookies.ts       # 쿠키 유틸리티
│   └── utils.ts         # 공용 유틸리티
├── routes/              # TanStack Router 파일 기반 라우트
├── stores/              # Zustand 스토어
│   └── auth-store.ts    # 인증 상태 (user, accessToken)
└── styles/              # 전역 스타일

각 feature 모듈은 동일한 내부 구조를 따릅니다:

features/{feature}/
├── api/                 # API 호출 함수 + React Query hooks
│   ├── {feature}-api.ts       # ky 기반 API 호출 함수
│   └── {feature}-queries.ts   # queryOptions, useMutation 정의
├── components/          # Feature 전용 UI 컴포넌트
├── data/                # 스키마, 타입, 상수
│   ├── schema.ts        # Zod 스키마 + TypeScript 타입
│   └── data.ts          # 상수, enum 라벨 매핑
└── index.tsx            # Feature 루트 컴포넌트 (페이지)

라우팅

TanStack Router의 파일 기반 라우팅을 사용합니다. Vite 플러그인(@tanstack/router-plugin/vite)이 src/routes/ 디렉토리를 스캔하여 라우트 트리를 자동 생성합니다. autoCodeSplitting: true 옵션으로 라우트별 코드 분할이 자동 적용됩니다.

라우트 트리

__root.tsx                          # Root layout (Toaster, NavigationProgress, DevTools)
├── (auth)/sign-in                  # /sign-in — 로그인 (비인증)
├── (errors)/401 | 404 | 500       # 에러 페이지
└── _authenticated/                 # Layout Route — 인증 필요
    ├── index                       # / → Dashboard
    ├── users/                      # /users → 유저 관리
    ├── cover-templates/            # /cover-templates → 커버 템플릿
    └── sample-loops/               # /sample-loops → 샘플 룹

인증 가드

_authenticated/route.tsxbeforeLoad 훅에서 3단계 인증 검증을 수행합니다:

토큰 존재 확인

Zustand auth store에서 accessToken 존재 여부를 확인합니다. 없으면 /sign-in으로 redirect합니다.

User State Hydration

토큰은 있지만 user 상태가 없는 경우(새로고침 등), JWT payload를 디코딩하여 email, name, picture, exp를 추출하고 user state를 복원합니다. 디코딩 실패 시 auth 초기화 후 redirect합니다.

토큰 만료 확인

JWT의 exp 클레임(Unix timestamp)을 현재 시간과 비교합니다. 만료 시 auth store를 초기화하고 /sign-in으로 redirect합니다.


인증 흐름

Google OAuth + Backend Token Validation 방식입니다. Google ID Token을 자체 JWT 교환 없이 직접 Bearer Token으로 사용합니다.

정보

이후 모든 API 요청에 Authorization: Bearer <Google ID Token>이 자동 첨부됩니다. 토큰은 쿠키(truloop_admin_token, max-age 7일)에 저장하여 새로고침 시에도 유지됩니다.

주의

Google ID Token의 수명은 약 1시간(exp 클레임 기준)입니다. 쿠키 max-age(7일)보다 토큰 자체가 먼저 만료되므로, _authenticated/route.tsxbeforeLoad에서 exp 검증이 실질적 만료 처리를 담당합니다.

도메인 제한

서버 측(truloop-core)의 validateGoogleOidcPayload()에서 Google Workspace butbeautifulco.com 도메인 계정만 허용합니다. 일반 Google 계정으로는 validate-token 호출이 실패합니다.


API Client

typescript
const baseUrl = import.meta.env.DEV
  ? '/api-proxy'                                        // 개발: Vite 프록시
  : `${import.meta.env.VITE_API_BASE_URL}/core`;        // 프로덕션: 직접 호출

export const apiClient = ky.create({
  prefixUrl: baseUrl,
  timeout: 30000,
  hooks: {
    beforeRequest: [
      (request) => {
        const token = useAuthStore.getState().auth.accessToken;
        if (token) {
          request.headers.set('Authorization', `Bearer ${token}`);
        }
      },
    ],
  },
});

개발 환경 프록시

Vite dev server에서 두 가지 프록시를 설정합니다:

프록시 경로대상용도
/api-proxyVITE_API_BASE_URL/coreAPI 호출 (CORS 우회)
/s3-upload-proxyS3 Presigned URL이미지 업로드 (S3 CORS 우회)

/api-proxy는 Vite의 내장 proxy 기능을 사용하고, /s3-upload-proxy는 커스텀 Vite 플러그인(s3UploadProxy)으로 X-Upload-Url 헤더에 지정된 S3 URL로 PUT 요청을 중계합니다.


상태 관리

상태 유형도구용도
서버 상태TanStack React QueryAPI 데이터 캐싱, 갱신, 무효화
클라이언트 상태Zustand인증 정보 (user, accessToken)
UI 상태React useState/useContextDialog 열림/닫힘, 폼 상태, 기간 선택 등

Feature별 API 패턴

각 feature는 api/ 디렉토리에서 API 호출과 React Query hooks를 분리합니다:

typescript
// ky 기반 API 호출 함수 — 순수 Promise 반환
export const featureApi = {
  list: (params): Promise<ListResponse> =>
    apiClient.get('admin/v1/...', { searchParams }).json(),
  create: (body): Promise<Item> =>
    apiClient.post('admin/v1/...', { json: body }).json(),
};
typescript
// React Query options + mutation hooks
export const featureListOptions = (params) =>
  queryOptions({
    queryKey: ['feature', params],
    queryFn: () => featureApi.list(params),
  });

export function useCreateFeature() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: featureApi.create,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['feature'] }),
  });
}

관련 문서


변경 이력

날짜내용
2026-03-16초기 문서 작성