다크 모드
Admin Dashboard (React/Vite)
truloop-core-admin-dashboard는 내부 관리자용 CMS 웹 애플리케이션입니다. 사용자 관리, 커버 템플릿 CRUD, 샘플 룹 관리, 주요 지표 대시보드 기능을 제공합니다.
기술 스택
| 카테고리 | 기술 | 버전 |
|---|---|---|
| 프레임워크 | React | 19.2.x |
| 빌드 | Vite | 7.3.x |
| 라우팅 | TanStack Router | 1.166.x |
| 서버 상태 | TanStack React Query | 5.90.x |
| 테이블 | TanStack React Table | 8.21.x |
| 클라이언트 상태 | Zustand | 5.0.x |
| HTTP | ky | 1.14.x |
| 폼 | React Hook Form + Zod 4 | 7.71.x / 4.3.x |
| 스타일 | Tailwind CSS 4 + Radix UI | 4.2.x |
| 차트 | Recharts | 3.8.x |
| OAuth | @react-oauth/google | 0.13.x |
| 토스트 | Sonner | 2.0.x |
| 아이콘 | Lucide React | 0.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.tsx의 beforeLoad 훅에서 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.tsx의 beforeLoad에서 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-proxy | VITE_API_BASE_URL/core | API 호출 (CORS 우회) |
/s3-upload-proxy | S3 Presigned URL | 이미지 업로드 (S3 CORS 우회) |
/api-proxy는 Vite의 내장 proxy 기능을 사용하고, /s3-upload-proxy는 커스텀 Vite 플러그인(s3UploadProxy)으로 X-Upload-Url 헤더에 지정된 S3 URL로 PUT 요청을 중계합니다.
상태 관리
| 상태 유형 | 도구 | 용도 |
|---|---|---|
| 서버 상태 | TanStack React Query | API 데이터 캐싱, 갱신, 무효화 |
| 클라이언트 상태 | Zustand | 인증 정보 (user, accessToken) |
| UI 상태 | React useState/useContext | Dialog 열림/닫힘, 폼 상태, 기간 선택 등 |
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'] }),
});
}관련 문서
- 비즈니스 규칙: 어드민 유저 관리
- 백엔드 API: API 설계 — Admin API 라우트
- Admin 인증 체계: API 설계 — Admin 인증
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-03-16 | 초기 문서 작성 |