2025/20.개발산출물/02.개발문서(설계서,설치가이드) » IITP-DABT-Admin_FE상세설계서.md
/* PDF 변환 시 페이지 머리글 및 번호 표시 */
@page {
@top-left {
content: "IITP DABT Admin Frontend 설계서";
font-size: 10pt;
color: #666;
font-family: Arial, sans-serif;
}
@bottom-center {
content: counter(page) " / " counter(pages);
font-size: 10pt;
color: #666;
font-family: Arial, sans-serif;
}
}
IITP DABT Admin
Frontend 설계서
문서 버전: 1.0.0
작성일: 2025-11-14
(주)스위트케이
문서 History
| 버전 | 일자 | 작성자 | 변경 내용 |
|---|---|---|---|
| 1.0.0 | 2025-11-14 | (주)스위트케이 | 최초 작성 |
목차
-
- 1.1 개요
- 1.2 Frontend 역할 및 책임
- 1.3 기술 스택
- 1.4 Common 패키지 활용
- 1.5 참고 문서
-
- 2.1 Frontend 아키텍처 개요 (Component-Based Architecture)
- 2.2 계층 구조 상세
- 2.3 화면 로드 처리 흐름
- 2.4 UI 테마 및 스타일 (Material-UI)
- 2.5 의존성 관계도
-
- 3.1 전체 디렉토리 트리
- 3.2 빌드 및 설정 파일
-
- 4.1 JWT 토큰 관리
- 4.2 인증 상태 관리
- 4.3 사용자 정보 관리
- 4.4 User/Admin 독립 세션 관리 (핵심 로직)
- 4.5 권한 체크 유틸리티
- 4.6 라우트 가드
-
- 5.1 API 클라이언트 구조 및 공통 로직
- 5.2 API 모듈 목록 (테이블 형태)
-
- 6.1 라우트 정의
- 6.2 라우팅 Flow
-
- 7.1 공개 페이지 (Public) - 인증 불필요
- 7.2 사용자 페이지 (User) - 일반 사용자 인증 필요
- 7.3 관리자 페이지 (Admin) - 관리자 인증 필요
- 7.4 공통 페이지 (Common)
-
- 8.1 컴포넌트 분류 및 역할
- 8.2 컴포넌트 목록 (테이블 형태)
-
- 9.1 유틸리티 함수 목록 (테이블 형태)
- 9.2 Custom React Hooks (테이블 형태)
-
- 10.1 환경 변수 설정
- 10.2 빌드 설정
- 10.3 TypeScript 설정
- 10.4 빌드 및 배포
-
- 11.1 API 에러 처리
- 11.2 사용자 친화적 에러 메시지
- 11.3 토큰 갱신 실패 시 처리
- 11.4 권한 부족 시 UI 처리
-
- 12.1 코드 스플리팅 (Lazy Loading)
- 12.2 Vite 빌드 최적화
- 12.3 API 요청 최적화
-
- 13.1 토큰 보안
- 13.2 권한 체크
-
- 14.1 주요 npm 패키지 설명
- 14.2 공통 타입 정의
- 14.3 프로젝트 아키텍처 가이드 참조
- 14.4 Backend API 규격서 참조
1. Frontend 시스템 개요
1.1 개요
IITP DABT Admin Frontend는 React 기반의 Single Page Application(SPA)으로, 사용자와 관리자를 위한 직관적인 웹 인터페이스를 제공합니다.
주요 특징:
- Modern Stack: React 18, TypeScript, Vite, Material-UI
- Component-Based Architecture: 재사용 가능한 컴포넌트 구조
- Monorepo 구조: Common 패키지를 통한 BE/FE 코드 공유
- 독립 세션 관리: User/Admin 동시 로그인 지원
- Role-Based UI Control: 권한별 UI 차별화 (VIEWER/EDITOR/ADMIN/S-ADMIN)
1.2 Frontend 역할 및 책임
| 영역 | 책임 |
|---|---|
| UI/UX | 사용자 인터페이스 제공 및 사용자 경험 최적화 |
| Client-Side 검증 | 입력 데이터 형식 검증 (Common 패키지 활용) |
| 인증 상태 관리 | JWT 토큰 관리, 자동 갱신, 만료 체크 |
| 권한 기반 UI 제어 | 역할별 메뉴/버튼 표시 제어 (UX 목적) |
| API 통신 | Backend API 호출 및 응답 처리 |
| 에러 핸들링 | 사용자 친화적 에러 메시지 표시 |
| 라우팅 | SPA 라우팅 및 권한 기반 접근 제어 |
1.3 기술 스택
1.3.1 개발 환경 (필수)
| 환경 | 버전 | 용도 |
|---|---|---|
| Node.js | 22.x 이상 | 개발 서버 실행 및 프로덕션 빌드 (필수) |
| npm | 9.x 이상 | 패키지 관리 |
중요:
- 개발/빌드 시: Node.js 필수
- 실행(런타임) 시: 브라우저에서 실행 (Node.js 불필요)
- 프로젝트 시작 전 Node.js와 npm 버전을 먼저 확인하세요.
1.3.2 Core 라이브러리
| 패키지 | 버전 | 용도 |
|---|---|---|
| react | 18.2.0 | UI 라이브러리 |
| react-dom | 18.2.0 | React DOM 렌더링 |
| react-router-dom | 6.20.1 | SPA 라우팅 |
| typescript | 5.x | 정적 타입 체킹 |
1.3.3 UI 프레임워크
| 패키지 | 버전 | 용도 |
|---|---|---|
| @mui/material | 5.15.0 | Material-UI 컴포넌트 |
| @mui/icons-material | 5.15.0 | Material-UI 아이콘 |
| @emotion/react | 11.11.1 | CSS-in-JS (MUI 의존성) |
| @emotion/styled | 11.11.0 | Styled Components (MUI 의존성) |
1.3.4 HTTP 클라이언트 및 상태 관리
| 패키지 | 버전 | 용도 |
|---|---|---|
| axios | 1.11.0 | HTTP 클라이언트 (실제로는 fetch 사용) |
| jwt-decode | 4.0.0 | JWT 토큰 디코딩 |
참고: 실제 구현에서는
axios대신fetchAPI를 직접 사용합니다.
1.3.5 빌드 도구
| 패키지 | 버전 | 용도 |
|---|---|---|
| vite | 5.0.8 | 빌드 도구 및 개발 서버 |
| @vitejs/plugin-react | 4.2.1 | Vite React 플러그인 |
1.3.6 Common 패키지
| 패키지 | 위치 | 용도 |
|---|---|---|
| @iitp-dabt/common | ../packages/common |
BE/FE 공유 코드 |
Common 패키지 제공 기능:
- 검증 함수:
isValidEmail,isValidPassword,isValidName등 - ErrorCode 체계: 11xxx-22xxx 범위 에러 코드
- 공통 타입: API 요청/응답 타입
- 상수: API URL, 관리자 역할 코드 등
1.4 Common 패키지 활용
Frontend에서 Common 패키지를 다음과 같이 활용합니다:
import {
isValidEmail,
isValidPassword,
ErrorCode,
FULL_API_URLS,
CODE_SYS_ADMIN_ROLES
} from '@iitp-dabt/common';
활용 예시:
입력 검증:
if (!isValidEmail(email)) { return '유효하지 않은 이메일 형식입니다.'; }ErrorCode 매핑:
if (data.errorCode === ErrorCode.UNAUTHORIZED) { return '인증이 필요합니다.'; }API URL:
const url = `${API_BASE_URL}${FULL_API_URLS.AUTH.USER.LOGIN}`;관리자 역할 체크:
if (adminRole === CODE_SYS_ADMIN_ROLES.SUPER_ADMIN) { // S-Admin 전용 기능 }
1.5 참고 문서
- IITP DABT Admin 프로젝트 아키텍처 가이드 : 프로젝트 전체 아키텍처 설명
- IITP DABT Admin Backend 상세 설계서 : Backend 상세 설계서
- API 규격서 : API 스펙 상세
- 서버 배포 및 설치 가이드 : 서버 배포/설치/실행 가이드
2. Frontend 아키텍처
2.1 Frontend 아키텍처 개요 (Component-Based Architecture)
Frontend는 Component-Based Architecture를 기반으로 5개의 계층으로 구성됩니다.
2.1.1 계층 구조
┌─────────────────────────────────────────────────────────┐ │ Presentation Layer │ │ (Pages - 화면) │ ├─────────────────────────────────────────────────────────┤ │ Container Layer │ │ (Components - 재사용 컴포넌트) │ ├─────────────────────────────────────────────────────────┤ │ Business Logic Layer │ │ (Store + Utils - 상태 및 로직) │ ├─────────────────────────────────────────────────────────┤ │ Data Layer │ │ (API Client - Backend 통신) │ ├─────────────────────────────────────────────────────────┤ │ Routing Layer │ │ (React Router - URL → Page 매핑) │ └─────────────────────────────────────────────────────────┘
2.1.2 계층 간 데이터 흐름
Frontend의 각 계층은 단방향 데이터 흐름(Unidirectional Data Flow)을 따릅니다.
요청 흐름 (Request Flow):
Browser (URL 입력/링크 클릭)
↓
React Router (Route 매칭)
↓
Route Guard (인증 체크: PrivateRoute/AdminProtectedRoute)
↓
Page Component (Lazy Loading + Suspense)
↓
useDataFetching Hook (상태 관리)
↓
API Client (apiFetch/publicApiFetch)
↓
Backend API (HTTP Request)
응답 흐름 (Response Flow):
Backend API (HTTP Response)
↓
API Client (응답 파싱, 에러 처리, 토큰 갱신)
↓
useDataFetching Hook (상태 업데이트: loading → success/error)
↓
Page Component (리렌더링 트리거)
↓
Container Components (Props 전달)
↓
Presentation Layer (UI 렌더링 완료)
↓
Browser (화면 표시)
상태 관리 흐름:
- 로딩 중:
isLoading = true→LoadingSpinner표시 - 성공:
data업데이트 → 실제 데이터 UI 렌더링 - 에러:
isError = true→ErrorAlert표시 - 빈 데이터:
isEmpty = true→EmptyState표시
2.2 계층 구조 상세
2.2.1 프레젠테이션 계층 (Presentation Layer) - Pages
역할: 사용자에게 보여지는 실제 화면 구성
구성:
/src/pages/public- 공개 페이지 (로그인 불필요)/src/pages/user- 일반 사용자 페이지 (User 인증 필요)/src/pages/admin- 관리자 페이지 (Admin 인증 필요)
특징:
- 각 페이지는 Container 컴포넌트를 조합하여 구성
- Business Logic은 최소화하고 표현에 집중
- API 호출은 Data Layer를 통해 수행
예시:
// src/pages/admin/FaqList.tsx
export default function FaqList() {
const [faqs, setFaqs] = useState([]);
useEffect(() => {
fetchFaqs(); // Data Layer 호출
}, []);
return (
<Layout>
<PageHeader title="FAQ 관리" />
<DataTable data={faqs} />
</Layout>
);
}
2.2.2 컨테이너 계층 (Container Layer) - Components
역할: 재사용 가능한 UI 컴포넌트 제공
구성:
/src/components/common- 범용 컴포넌트 (DataTable, Pagination 등)/src/components/admin- 관리자 전용 컴포넌트 (SideNav, AdminPageHeader)/src/components- 기타 공통 컴포넌트 (Layout, Footer 등)
특징:
- Props를 통한 데이터 전달
- 재사용성 극대화
- 비즈니스 로직 포함 최소화
예시:
// src/components/common/DataTable.tsx
export function DataTable({ data, columns, onRowClick }) {
return (
<TableContainer>
<Table>
{/* 테이블 렌더링 */}
</Table>
</TableContainer>
);
}
2.2.3 비즈니스 로직 계층 (Business Logic Layer) - Store + Utils
역할: 상태 관리 및 비즈니스 로직 처리
구성:
/src/store/auth.ts- 인증 상태 관리 (토큰 저장/조회/갱신)/src/store/user.ts- 사용자 정보 관리/src/utils/auth.ts- 권한 체크 함수/src/utils/jwt.ts- JWT 토큰 처리/src/utils/date.ts- 날짜 포맷팅/src/utils/openApiStatus.ts- OpenAPI 상태 처리
특징:
- LocalStorage를 통한 상태 영속화
- User/Admin 독립 세션 관리 (prefix 분리)
- 순수 함수 중심
예시:
// src/store/auth.ts
export function saveTokens(accessToken: string, refreshToken: string) {
const prefix = getCurrentPrefix(); // user_ or admin_
localStorage.setItem(prefix + 'accessToken', accessToken);
localStorage.setItem(prefix + 'refreshToken', refreshToken);
}
2.2.4 데이터 계층 (Data Layer) - API Client
역할: Backend API 통신 및 데이터 처리
구성:
/src/api/api.ts- 공통 API 요청 함수 (apiFetch, publicApiFetch)/src/api/user.ts- 사용자 인증 API/src/api/admin.ts- 관리자 인증 API/src/api/faq.ts- FAQ API/src/api/qna.ts- QnA API- 등 (API 모듈별로 분리)
특징:
- 토큰 자동 갱신 (401 에러 시)
- ErrorCode 기반 에러 처리
- 타임아웃 및 재시도 로직
예시:
// src/api/faq.ts
export async function getFaqs(params) {
return await apiFetch('/api/admin/faq', {
method: 'GET',
// 자동 토큰 포함, 401 시 자동 갱신
});
}
2.2.5 라우팅 계층 (Routing Layer) - React Router
역할: URL과 Page 컴포넌트 매핑, 권한 기반 접근 제어
구성:
/src/routes/index.ts- 라우트 정의 (ROUTES 객체)/src/App.tsx- 라우트 설정 및 Guard 적용/src/components/ProtectedRoute.tsx- 권한 체크 Guard
특징:
- Public / User / Admin / Common 라우트 분리
PrivateRoute- 일반 사용자 인증 체크AdminProtectedRoute- 관리자 인증 체크- 인증 실패 시 로그인 페이지 리다이렉트
예시:
// src/App.tsx
<Route
path="/admin/faqs"
element={
<AdminProtectedRoute>
<FaqList />
</AdminProtectedRoute>
}
/>
2.3 화면 로드 처리 흐름
모든 화면은 통일된 처리 흐름을 따릅니다. 사용자가 URL을 입력하거나 링크를 클릭했을 때부터 화면이 완전히 렌더링될 때까지의 전체 과정을 설명합니다.
2.3.1 전체 처리 흐름 (예: FAQ 관리 페이지)
시나리오: 사용자가 /admin/faqs URL에 접근하여 FAQ 목록을 조회하는 경우
┌────────────────────────────────────────────────────────────┐
│ 사용자 액션 │
│ - URL 직접 입력: /admin/faqs │
│ - 또는 링크 클릭: <Link to="/admin/faqs">FAQ 관리</Link> │
└─────────────┬──────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ React Router (BrowserRouter) │
│ -------------------------------- │
│ - URL 파싱: /admin/faqs │
│ - Route 매칭: ROUTES.ADMIN.FAQ.LIST │
│ - Route 정의 확인: App.tsx │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Route Guard (AdminProtectedRoute) │
│ -------------------------------- │
│ - isAdminAuthenticated() 호출 │
│ │ │
│ ├─ admin_accessToken 유효? │
│ │ └─ YES → 다음 단계 진행 │
│ │ │
│ └─ NO → /admin/login 리다이렉트 │
│ (state: { from: '/admin/faqs' } 저장) │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Lazy Loading (Suspense) │
│ -------------------------------- │
│ - AdminFaqList 컴포넌트 동적 import │
│ - 로딩 중: <LoadingSpinner loading={true} /> 표시 │
│ - 로드 완료: 컴포넌트 마운트 │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Page Component (FaqList.tsx) │
│ -------------------------------- │
│ - 컴포넌트 마운트 (useEffect 실행) │
│ - useDataFetching Hook 초기화 │
│ - 상태 초기값: { status: 'loading' } │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ useDataFetching Hook │
│ -------------------------------- │
│ - fetchFunction: () => getAdminFaqList({ ... }) │
│ - autoFetch: true (자동 실행) │
│ - dependencies: [page, limit, searchTerm, ...] │
│ - 상태: { status: 'loading' } │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ API Client (apiFetch) │
│ -------------------------------- │
│ - URL: /api/admin/faq │
│ - Method: GET │
│ - Headers: │
│ - Authorization: Bearer {admin_accessToken} │
│ - Content-Type: application/json │
│ - Request Body: { page: 1, limit: 10, ... } │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Backend API (HTTP Request) │
│ -------------------------------- │
│ POST /api/admin/faq │
│ { page: 1, limit: 10, ... } │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Backend API (HTTP Response) │
│ -------------------------------- │
│ Status: 200 OK │
│ Body: { │
│ success: true, │
│ data: { │
│ items: [...], │
│ total: 50, │
│ page: 1, │
│ limit: 10, │
│ totalPages: 5 │
│ } │
│ } │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ API Client (응답 처리) │
│ -------------------------------- │
│ - 응답 파싱: res.json() │
│ - enhanceApiResponse() 호출 │
│ - 에러 체크: res.ok 확인 │
│ - 401 에러 시: 토큰 갱신 시도 (자동) │
│ - 성공: ApiResponse<T> 반환 │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ useDataFetching Hook (상태 업데이트) │
│ -------------------------------- │
│ - 응답 데이터 확인: response.success && response.data │
│ - 페이징 데이터 확인: items 배열 존재 여부 │
│ - 상태 업데이트: │
│ { status: 'success', data: response.data } │
│ - 컴포넌트 리렌더링 트리거 │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Page Component (조건부 렌더링) │
│ -------------------------------- │
│ - isLoading 체크: false (로딩 완료) │
│ - isError 체크: false (에러 없음) │
│ - data 확인: faqData?.items 존재 │
│ - isEmpty 체크: false (데이터 있음) │
│ - 실제 UI 렌더링: │
│ <ListScaffold> │
│ <DataTable data={faqData.items} /> │
│ <Pagination ... /> │
│ </ListScaffold> │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Container Components │
│ -------------------------------- │
│ - ListScaffold: 페이지 레이아웃 │
│ - DataTable: FAQ 목록 테이블 │
│ - Pagination: 페이지네이션 컨트롤 │
│ - 각 컴포넌트에 Props 전달 │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Presentation Layer (최종 렌더링) │
│ -------------------------------- │
│ - Material-UI 컴포넌트 렌더링 │
│ - 테마 적용 (Admin 테마) │
│ - 스타일 적용 (CSS-in-JS) │
│ - 화면 완전히 표시 완료 │
└─────────────┬───────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Browser (화면 표시) │
│ -------------------------------- │
│ - 사용자가 FAQ 목록 화면 확인 │
│ - 인터랙션 가능 (클릭, 검색, 페이지 변경 등) │
└─────────────────────────────────────────────────────────────┘
2.3.2 예외 처리 Flow
인증 실패 시:
Route Guard (AdminProtectedRoute)
│
├─ isAdminAuthenticated() = false
│ │
│ └─ Navigate to /admin/login
│ (state: { from: '/admin/faqs' } 저장)
│
└─ 로그인 성공 후 원래 페이지로 복원
API 에러 시:
Backend API (HTTP Response)
│
├─ Status: 401 Unauthorized
│ └─ API Client: 토큰 갱신 시도
│ ├─ 성공 → 원래 요청 재시도
│ └─ 실패 → 로그인 페이지 리다이렉트
│
├─ Status: 403 Forbidden
│ └─ useDataFetching: { status: 'error', error: '접근 권한이 없습니다.' }
│ └─ Page Component: <ErrorAlert error={error} />
│
└─ Status: 500 Internal Server Error
└─ useDataFetching: { status: 'error', error: '서버 오류가 발생했습니다.' }
└─ Page Component: <ErrorAlert error={error} />
로딩 상태 관리:
useDataFetching Hook
│
├─ status: 'loading'
│ └─ Page Component: <LoadingSpinner loading={true} />
│
├─ status: 'success' && data.items.length > 0
│ └─ Page Component: 실제 데이터 UI 렌더링
│
├─ status: 'success' && data.items.length === 0
│ └─ Page Component: <EmptyState message="데이터가 없습니다." />
│
└─ status: 'error'
└─ Page Component: <ErrorAlert error={error} />
2.3.3 주요 특징
1. Lazy Loading (코드 스플리팅)
- 관리자 페이지는
lazy()로 동적 import - 초기 로딩 시간 단축
Suspense로 로딩 상태 표시
2. 자동 데이터 페칭
useDataFetching의autoFetch: true로 마운트 시 자동 실행dependencies변경 시 자동 재조회
3. 상태 기반 렌더링
isLoading,isError,isEmpty상태에 따라 UI 분기- 사용자 경험 최적화
4. 토큰 자동 갱신
- API Client에서 401 에러 시 자동 토큰 갱신
- 사용자 개입 없이 세션 유지
2.4 UI 테마 및 스타일 (Material-UI)
Material-UI v5를 사용하여 User와 Admin 사이트의 독립적인 테마를 구축합니다.
2.4.1 테마 구분 및 색상 체계
파일: src/theme/index.ts, src/theme/mui.ts
Frontend는 User와 Admin 테마를 명확히 분리합니다:
User 테마 - 밝고 친근한 파란색 계열:
user: {
primary: '#0B5FFF', // 밝은 파란색
secondary: '#00B8D9', // 청록색
background: '#F5F7FB', // 연한 회색 배경
paper: '#FFFFFF', // 흰색 카드
text: '#0F172A', // 짙은 회색 텍스트
textSecondary: '#475569', // 중간 회색
}
Admin 테마 - 전문적이고 차분한 네이비 계열:
admin: {
primary: '#1E3A8A', // 짙은 네이비
secondary: '#3B82F6', // 밝은 파랑
background: '#F1F5F9', // 연한 회색 배경
paper: '#FFFFFF', // 흰색 카드
text: '#111827', // 짙은 회색 텍스트
textSecondary: '#6B7280', // 중간 회색
// Admin 전용 설정
spacing: 6, // 더 compact (User는 8)
borderRadius: 8, // 더 작은 radius (User는 10)
button size: 'small', // 더 작은 버튼 (User는 'medium')
}
Public 페이지 - User 테마 사용:
- 공개 페이지(/, /faq, /qna, /notice)는 User 테마 적용
- 일관된 사용자 경험 제공
테마 구분 방법:
// src/theme/index.ts
export const getThemeColors = (theme: 'user' | 'admin'): ThemeColors => {
return THEME_COLORS[theme];
};
// 사용 예시
const colors = getThemeColors('user'); // User 페이지
const colors = getThemeColors('admin'); // Admin 페이지
Material-UI 테마 생성:
// src/theme/mui.ts
export function createAppTheme(type: 'user' | 'admin', density = 'default') {
return createTheme({
...common, // 공통 설정
...(type === 'admin' ? adminTheme : userTheme), // 테마별 설정
...densityPreset // 밀도 설정 (선택)
});
}
2.4.2 테마별 시각적 차이
| 요소 | User 테마 | Admin 테마 | 목적 |
|---|---|---|---|
| Primary 색상 | #0B5FFF (밝은 파란색) | #1E3A8A (짙은 네이비) | 브랜드 구분 |
| Spacing | 8px | 6px | Admin은 더 compact |
| Border Radius | 10px | 8px | Admin은 더 각진 느낌 |
| Button Size | medium | small | Admin은 정보 밀도 높임 |
| TextField Size | medium | small | Admin은 정보 밀도 높임 |
| 느낌 | 밝고 친근한 | 전문적이고 차분한 | UX 차별화 |
페이지별 테마 적용:
// User 페이지
const colors = getThemeColors('user');
// Admin 페이지
const colors = getThemeColors('admin');
// Public 페이지 (User 테마 사용)
const colors = getThemeColors('user');
2.4.3 테마 스타일 유틸리티
파일: src/theme/index.ts
export const themeStyles = {
// 페이지 타이틀 스타일 (테마별)
pageTitle: (theme: 'user' | 'admin') => ({
color: THEME_COLORS[theme].primary,
fontWeight: 600,
borderBottom: `2px solid ${THEME_COLORS[theme].primary}20`
}),
// 카드 스타일 (테마별)
card: (theme: 'user' | 'admin') => ({
backgroundColor: THEME_COLORS[theme].paper,
boxShadow: `0 4px 12px ${THEME_COLORS[theme].primary}15`,
border: `1px solid ${THEME_COLORS[theme].border}`
}),
// 버튼 스타일 (테마별)
primaryButton: (theme: 'user' | 'admin') => ({
bgcolor: THEME_COLORS[theme].primary,
color: '#f8f9fa',
fontWeight: 'bold',
'&:hover': {
bgcolor: THEME_COLORS[theme].primary,
opacity: 0.9,
}
}),
};
활용 예시:
// User 페이지
<ThemedButton theme="user" /> // 파란색 버튼
<ThemedCard theme="user" /> // User 스타일 카드
// Admin 페이지
<ThemedButton theme="admin" /> // 네이비 버튼
<ThemedCard theme="admin" /> // Admin 스타일 카드
2.4.4 테마 프리뷰
/theme-preview 페이지에서 주요 컴포넌트 스타일을 확인할 수 있습니다.
파일: src/pages/ThemePreview.tsx
확인 가능 요소:
- 버튼 (primary, secondary, outlined, text)
- 카드 (paper, border)
- 칩 (status, type)
- 입력 필드 (textfield, select)
- 색상 팔레트 (primary, secondary, success, error, warning, info)
2.5 의존성 관계도
┌─────────────────────────────────────────────────────────┐
│ Pages (화면) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Public │ │ User │ │ Admin │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
└───────┼───────────┼─────────────┼───────────────────────┘
│ │ │
└───────────┴─────────────┘
│
┌───────────▼───────────────────────────────────┐
│ Components (재사용) │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Common │ │ Admin │ │ Layout │ │
│ └────┬───┘ └───┬────┘ └───┬────┘ │
└───────┼──────────┼───────────┼────────────────┘
│ │ │
┌───────┴──────────┴───────────┴───────────────┐
│ Store & Utils (비즈니스 로직) │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Auth │ │ User │ │ JWT │ │
│ └────┬───┘ └───┬────┘ └───┬────┘ │
└───────┼──────────┼───────────┼───────────────┘
│ │ │
┌───────┴──────────┴───────────┴───────────────┐
│ API Client (데이터) │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ User │ │ Admin │ │ FAQ │ ... │
│ └────┬───┘ └───┬────┘ └───┬────┘ │
└───────┼──────────┼───────────┼───────────────┘
│ │ │
└──────────┴───────────┘
│
┌──────────▼──────────────────┐
│ Backend API Server │
└─────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ @iitp-dabt/common (공유) │
│ ErrorCode, 검증 함수, 공통 타입, API URL │
└─────────────────────────────────────────────────────────┘
의존성 흐름:
- Pages → Components → Store/Utils → API Client → Backend
- 모든 계층 → Common 패키지 (공유 코드)
- React Router → Pages (라우팅)
3. 디렉토리 구조
3.1 전체 디렉토리 트리
3.1.1 /src/pages - 페이지 컴포넌트
Public 페이지 (로그인 불필요):
src/pages/
ThemePreview.tsx # UI 테마 프리뷰 페이지
public/
OpenApiAbout.tsx # OpenAPI 소개 페이지
Privacy.tsx # 개인정보 처리방침
Terms.tsx # 이용약관
User 페이지 (일반 사용자 인증 필요):
src/pages/user/ Dashboard.tsx # 사용자 대시보드 Home.tsx # 홈 페이지 (공개 + 선택적 인증) Login.tsx # 사용자 로그인 Register.tsx # 사용자 회원가입 UserProfile.tsx # 사용자 프로필 관리 FaqList.tsx # FAQ 목록 (사용자용) NoticeList.tsx # 공지사항 목록 (사용자용) NoticeDetail.tsx # 공지사항 상세 (사용자용) QnaList.tsx # QnA 목록 (사용자용, 공개) QnaDetail.tsx # QnA 상세 (사용자용, 공개) QnaCreate.tsx # QnA 생성 (인증 필요) QnaHistory.tsx # 나의 QnA 히스토리 OpenApiManagement.tsx # OpenAPI 키 관리 (사용자용)
Admin 페이지 (관리자 인증 필요):
src/pages/admin/ AdminLogin.tsx # 관리자 로그인 AdminDashboard.tsx # 관리자 대시보드 AdminProfile.tsx # 관리자 프로필 FaqList.tsx # FAQ 관리 (목록) FaqCreate.tsx # FAQ 생성 FaqDetail.tsx # FAQ 상세 FaqEdit.tsx # FAQ 수정 QnaManage.tsx # QnA 관리 (목록) QnaDetail.tsx # QnA 상세 (관리자용) QnaReply.tsx # QnA 답변 작성 QnaEdit.tsx # QnA 수정 NoticeManage.tsx # 공지사항 관리 (목록) NoticeCreate.tsx # 공지사항 생성 NoticeDetail.tsx # 공지사항 상세 (관리자용) NoticeEdit.tsx # 공지사항 수정 UserManagement.tsx # 사용자 관리 (목록) UserCreate.tsx # 사용자 생성 UserDetail.tsx # 사용자 상세 UserEdit.tsx # 사용자 수정 OperatorManagement.tsx # 운영자 관리 (목록, S-ADMIN 전용) OperatorCreate.tsx # 운영자 생성 (S-ADMIN 전용) OperatorDetail.tsx # 운영자 상세 (S-ADMIN 전용) OperatorEdit.tsx # 운영자 수정 (S-ADMIN 전용) OpenApiManage.tsx # OpenAPI 클라이언트 관리 OpenApiDetail.tsx # OpenAPI 클라이언트 상세 OpenApiEdit.tsx # OpenAPI 클라이언트 수정 OpenApiRequests.tsx # OpenAPI 키 승인 요청 목록 OpenApiRequestDetail.tsx # OpenAPI 키 승인 요청 상세 CodeManagement.tsx # 코드 관리 (목록, S-ADMIN 전용) CodeGroupDetail.tsx # 코드 그룹 상세 (S-ADMIN 전용) CodeCreate.tsx # 코드 생성 (S-ADMIN 전용) CodeDetail.tsx # 코드 상세 (S-ADMIN 전용)
3.1.2 /src/components - 공통 컴포넌트
레이아웃 컴포넌트:
src/components/ Layout.tsx # 전체 레이아웃 (Header + Content + Footer) AppBar.tsx # 사용자 상단 앱바 AppBarCommon.tsx # 공통 앱바 (로직 공유) AdminMenuBar.tsx # 관리자 메뉴바 Footer.tsx # 푸터 LoginForm.tsx # 로그인 폼 (공통) ProfileForm.tsx # 프로필 폼 (공통) ProtectedRoute.tsx # 권한 체크 Guard (PrivateRoute, AdminProtectedRoute) LoadingSpinner.tsx # 로딩 스피너 ErrorAlert.tsx # 에러 알림 CommonDialog.tsx # 공통 다이얼로그 CommonToast.tsx # 공통 토스트 ToastProvider.tsx # 토스트 프로바이더
Admin 전용 컴포넌트:
src/components/admin/ AdminPageHeader.tsx # 관리자 페이지 헤더 SideNav.tsx # 관리자 사이드 네비게이션
Common UI 컴포넌트:
src/components/common/ DataTable.tsx # 데이터 테이블 TableListBody.tsx # 테이블 리스트 본문 CardListBody.tsx # 카드 리스트 본문 ListItemCard.tsx # 리스트 아이템 카드 EmptyState.tsx # 빈 상태 표시 PageHeader.tsx # 페이지 헤더 PageTitle.tsx # 페이지 제목 ListHeader.tsx # 리스트 헤더 ListScaffold.tsx # 리스트 스캐폴드 ListTotal.tsx # 리스트 총 개수 SelectField.tsx # 셀렉트 필드 ThemedButton.tsx # 테마 버튼 ByteLimitHelper.tsx # 바이트 제한 헬퍼 StatusChip.tsx # 상태 칩 QnaTypeChip.tsx # QnA 타입 칩 Pagination.tsx # 페이지네이션 ThemedCard.tsx # 테마 카드 ExtendKeyDialog.tsx # API 키 연장 다이얼로그
3.1.3 /src/api - API 클라이언트
src/api/ api.ts # 공통 API 요청 함수 (apiFetch, publicApiFetch) index.ts # API 모듈 통합 Export user.ts # 사용자 인증 및 관리 API admin.ts # 관리자 인증 API account.ts # 관리자 계정 관리 API common.ts # 공통 API (JWT 설정 등) commonCode.ts # 공통 코드 관리 API faq.ts # FAQ 관리 API qna.ts # QnA 관리 API notice.ts # 공지사항 관리 API openApi.ts # OpenAPI 키 관리 API
3.1.4 /src/store - 상태 관리
src/store/ auth.ts # 인증 상태 관리 (토큰 저장/조회/갱신) user.ts # 사용자 정보 관리 (User/Admin 정보 분리)
3.1.5 /src/utils - 유틸리티 함수
src/utils/ auth.ts # 권한 체크 함수 (isSAdmin, hasContentEditPermission 등) jwt.ts # JWT 토큰 처리 (검증, 만료 체크, 갱신 판단) date.ts # 날짜 포맷팅 유틸리티 openApiStatus.ts # OpenAPI 상태 관리 유틸리티 apiResponseHandler.ts # API 응답 핸들러
3.1.6 /src/routes - 라우팅 설정
src/routes/
index.ts # 라우트 정의 (ROUTES, ROUTE_META, RouteUtils)
guards/
PublicRoute.tsx # 공개 라우트 가드 (향후 확장)
3.1.7 /src/theme - UI 테마
src/theme/ index.ts # 테마 색상 정의 (User/Admin 분리), themeStyles 유틸리티 mui.ts # Material-UI 테마 생성 (createAppTheme)
3.1.8 /src/constants - 상수 정의
src/constants/ spacing.ts # 페이지 간격 상수 (SPACING, PAGE_SPACING) pagination.ts # 페이지네이션 기본값 (DEFAULT_PAGE, DEFAULT_LIMIT) noticeTypes.ts # 공지사항 타입 상수
3.1.9 /src/hooks - Custom React Hooks
src/hooks/ useDataFetching.ts # 데이터 페칭 공통 훅 (로딩, 에러, 빈 상태 관리) usePasswordValidation.ts # 비밀번호 검증 훅 useQuerySync.ts # URL 쿼리 동기화 훅 (검색, 페이징) usePagination.ts # 페이지네이션 상태 관리 훅 useInputWithTrim.ts # 입력 값 trim 처리 훅 useErrorHandler.ts # 에러 핸들링 훅 useCommonCode.ts # 공통 코드 조회 훅
3.1.10 /src/types - 타입 정의
src/types/ api.ts # API 요청/응답 타입 정의 errorCodes.ts # ErrorCode 타입 (Common 패키지 재정의)
3.1.11 기타 파일
src/
App.tsx # 최상위 App 컴포넌트 (라우트 설정)
main.tsx # 엔트리 포인트 (ReactDOM.render)
config.ts # 환경 변수 설정 (API_BASE_URL 등)
vite-env.d.ts # Vite 환경 변수 타입 정의
App.css # 전역 CSS
index.css # 루트 CSS
assets/
react.svg # React 로고
3.1.12 /public - 정적 자산
public/ iitp_cms_logo_img_1.png # IITP 로고 (메인) iitp_cms_logo_img_2.png # IITP 로고 (서브) vite.svg # Vite 로고 index.html # HTML 템플릿
역할: 빌드 시 dist/로 복사되는 정적 파일
3.2 빌드 및 설정 파일
3.2.1 vite.config.ts - Vite 빌드 설정
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [react()],
base: env.VITE_BASE || '/',
server: {
port: Number(env.VITE_PORT) || 5173,
},
build: {
outDir: 'dist',
},
}
})
주요 설정:
- plugins:
@vitejs/plugin-react사용 - base: 서브 경로 배포 지원 (
VITE_BASE환경 변수) - server.port: 개발 서버 포트 (기본 5173)
- build.outDir: 빌드 출력 디렉토리 (
dist/)
3.2.2 tsconfig.json - TypeScript 설정
{
"files": [],
"references": [
{ "path": "../packages/common" },
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"packages/common/*": ["../packages/common/*"]
}
}
}
주요 설정:
- references: Project References로 Common 패키지 참조
- paths:
@iitp-dabt/common경로 매핑
3.2.3 package.json - 의존성 및 스크립트
주요 스크립트:
{
"scripts": {
"dev": "vite",
"build": "rimraf dist && tsc -b && vite build && node scripts/build-info.js",
"build:clean": "rimraf dist && tsc -b && vite build && node scripts/build-info.js",
"preview": "vite preview",
"prebuild": "node scripts/build-info.js"
}
}
- dev: 개발 서버 실행
- build: 프로덕션 빌드 (빌드 정보 생성 포함)
- build:clean: 클린 빌드
- preview: 빌드 결과물 미리보기
3.2.4 scripts/ - 빌드 스크립트
scripts/ build-info.js # 빌드 정보 생성 (version, buildDate) build.sh # 빌드 스크립트 (Linux/Mac) setup.sh # 초기 설정 스크립트
scripts/build-info.js:
// 빌드 정보 생성 스크립트
const buildInfo = {
version: pkg.version,
buildDate: getLocalDateTimeString()
};
fs.writeFileSync(path.join(distPath, 'build-info.json'),
JSON.stringify(buildInfo, null, 2));
역할: 빌드 버전 및 빌드 시각을 dist/build-info.json으로 출력
3.2.5 루트 설정 파일
fe/ .env # 환경 변수 설정 (로컬, git ignore) .env.sample # 환경 변수 샘플 파일 package.json # 의존성 및 스크립트 package-lock.json # 의존성 잠금 파일 vite.config.ts # Vite 빌드 설정 tsconfig.json # TypeScript 루트 설정 (Project References) tsconfig.app.json # App TypeScript 설정 tsconfig.node.json # Node TypeScript 설정 (Vite 빌드 스크립트용) eslint.config.js # ESLint 설정 index.html # HTML 엔트리 포인트 README.md # 프로젝트 설명
3.2.6 public/
public/ iitp_cms_logo_img_1.png # IITP 로고 (메인) iitp_cms_logo_img_2.png # IITP 로고 (서브) vite.svg # Vite 로고 index.html # HTML 템플릿 (빌드 시 사용)
역할: 빌드 시 dist/로 복사되는 정적 파일 (로고, 아이콘 등)
4. 인증 및 권한 체계
Frontend는 JWT 기반 인증과 Role-Based 권한 체크를 합니다.
중요: Frontend의 인증 및 권한 체크는 UX이며, 실제 보안은 Backend에서 담당합니다.
4.1 JWT 토큰 관리
4.1.1 토큰 검증 로직
파일: src/utils/jwt.ts
주요 함수:
import { jwtDecode } from 'jwt-decode';
// 토큰 형식 검증
export function isValidTokenFormat(token: string): boolean {
if (!token || typeof token !== 'string') return false;
const parts = token.split('.');
return parts.length === 3; // header.payload.signature
}
// 토큰 정보 추출
export function extractTokenInfo(token: string): TokenInfo | null {
try {
const decoded = jwtDecode(token) as any;
if (!decoded || !decoded.exp || !decoded.iat) return null;
return {
exp: decoded.exp,
iat: decoded.iat,
expiresIn: JWT_CONFIG.accessTokenExpiresIn,
};
} catch {
return null;
}
}
4.1.2 토큰 만료 확인
// 토큰 만료 여부 확인
export function isTokenExpired(token: string): boolean {
try {
const decoded = jwtDecode(token) as any;
if (!decoded || !decoded.exp) return true;
const currentTime = Math.floor(Date.now() / 1000);
return decoded.exp < currentTime;
} catch {
return true;
}
}
// 토큰 만료까지 남은 시간 (초)
export function getTokenTimeRemaining(token: string): number {
try {
const decoded = jwtDecode(token) as any;
if (!decoded || !decoded.exp) return -1;
const currentTime = Math.floor(Date.now() / 1000);
return decoded.exp - currentTime;
} catch {
return -1;
}
}
4.1.3 토큰 갱신 판단 (만료 5분 전)
// 토큰 갱신이 필요한지 확인 (기본값: 만료 5분 전)
export function shouldRefreshToken(
token: string,
bufferSeconds: number = 300 // 5분
): boolean {
const timeRemaining = getTokenTimeRemaining(token);
return timeRemaining > 0 && timeRemaining <= bufferSeconds;
}
갱신 판단 로직:
- 만료까지 5분 이하 남았을 때 갱신 필요
- 이미 만료된 경우도 갱신 시도
bufferSeconds파라미터로 갱신 시점 조정 가능
4.2 인증 상태 관리
4.2.1 토큰 저장/조회
파일: src/store/auth.ts
// User/Admin 독립적 저장을 위한 prefix 상수
const USER_PREFIX = 'user_';
const ADMIN_PREFIX = 'admin_';
// 현재 활성 사용자 타입에 맞는 prefix 자동 반환
function getCurrentPrefix(): string {
const userType = getUserType();
return userType === 'A' ? ADMIN_PREFIX : USER_PREFIX;
}
// 토큰 저장
export function saveTokens(accessToken: string, refreshToken: string) {
if (!isValidTokenFormat(accessToken) || !isValidTokenFormat(refreshToken)) {
console.warn('Invalid token format detected');
return;
}
const prefix = getCurrentPrefix();
localStorage.setItem(prefix + 'accessToken', accessToken);
localStorage.setItem(prefix + 'refreshToken', refreshToken);
}
// Access Token 가져오기
export function getAccessToken(): string | null {
const prefix = getCurrentPrefix();
const token = localStorage.setItem(prefix + 'accessToken');
if (!token || !isValidTokenFormat(token)) return null;
return token;
}
저장 위치: LocalStorage
- User 토큰:
user_accessToken,user_refreshToken - Admin 토큰:
admin_accessToken,admin_refreshToken
4.2.2 토큰 만료 확인
// Access Token 만료 여부
export function isAccessTokenExpired(): boolean {
const token = getAccessToken();
return !token || isTokenExpired(token);
}
// Refresh Token 만료 여부
export function isRefreshTokenExpired(): boolean {
const token = getRefreshToken();
return !token || isTokenExpired(token);
}
// Access Token 갱신 필요 여부
export function shouldRefreshAccessToken(): boolean {
const token = getAccessToken();
return token ? shouldRefreshToken(token) : true;
}
4.2.3 토큰 갱신 조건 및 Flow
갱신 조건:
- Access Token이 만료 5분 전일 때
- Access Token이 만료되었고 Refresh Token이 유효할 때
갱신 Flow:
// 토큰 상태 확인 및 갱신 (API 요청 전 호출)
export async function ensureValidToken(): Promise<string | null> {
validateAndCleanTokens(); // 유효성 검사 및 정리
const accessToken = getAccessToken();
const refreshToken = getRefreshToken();
// 토큰이 없으면 null 반환
if (!accessToken) {
// Access 없음 → Refresh로 갱신 시도
if (refreshToken && !isTokenExpired(refreshToken)) {
return await tryRefreshToken(refreshToken);
}
return null;
}
// Access Token이 유효하고 갱신 필요 없으면 그대로 사용
if (!isTokenExpired(accessToken) && !shouldRefreshToken(accessToken)) {
return accessToken;
}
// Access 만료 또는 만료 임박 → Refresh로 갱신 시도
if (refreshToken && !isTokenExpired(refreshToken)) {
return await tryRefreshToken(refreshToken);
}
return null; // 갱신 실패
}
// Refresh Token으로 Access/Refresh 재발급 시도
async function tryRefreshToken(refreshToken: string): Promise<string | null> {
try {
const userType = getUserType();
const url = userType === 'A'
? FULL_API_URLS.AUTH.ADMIN.REFRESH
: FULL_API_URLS.AUTH.USER.REFRESH;
const res = await fetch(`${API_BASE_URL}${url}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
if (!res.ok) throw new Error(`Refresh failed: ${res.status}`);
const data = await res.json();
const newAccess = data?.data?.token || data?.token;
const newRefresh = data?.data?.refreshToken || data?.refreshToken;
if (newAccess && newRefresh &&
isValidTokenFormat(newAccess) &&
isValidTokenFormat(newRefresh)) {
saveTokens(newAccess, newRefresh);
return newAccess;
}
throw new Error('Invalid refresh response');
} catch (error) {
console.error('Token refresh failed:', error);
removeTokens();
clearLoginInfo();
return null;
}
}
Flow 다이어그램:
API 요청 전
│
▼
ensureValidToken() 호출
│
├─ Access Token 없음?
│ └─ Refresh Token 있고 유효? → 갱신 시도
│ ├─ 성공 → 새 Access Token 반환
│ └─ 실패 → null 반환 (로그인 필요)
│
├─ Access Token 있고 유효? (만료 5분 초과)
│ └─ 그대로 사용
│
└─ Access Token 만료 또는 만료 임박? (5분 이내)
└─ Refresh Token으로 갱신 시도
├─ 성공 → 새 Access Token 반환
└─ 실패 → null 반환 (로그인 필요)
4.2.4 인증 상태 확인
// 기본 인증 상태 확인
export function isAuthenticated(): boolean {
const accessToken = getAccessToken();
const refreshToken = getRefreshToken();
return (!!accessToken && !isTokenExpired(accessToken)) ||
(!!refreshToken && !isTokenExpired(refreshToken));
}
// 일반 사용자 인증 상태 확인
export function isUserAuthenticated(): boolean {
const userAccessToken = localStorage.getItem('user_accessToken');
const userRefreshToken = localStorage.getItem('user_refreshToken');
const hasValidUserToken =
(userAccessToken && !isTokenExpired(userAccessToken)) ||
(userRefreshToken && !isTokenExpired(userRefreshToken));
const userInfo = localStorage.getItem('user_userInfo');
return !!(hasValidUserToken && userInfo);
}
// 관리자 인증 상태 확인
export function isAdminAuthenticated(): boolean {
const adminAccessToken = localStorage.getItem('admin_accessToken');
const adminRefreshToken = localStorage.getItem('admin_refreshToken');
const hasValidAdminToken =
(adminAccessToken && !isTokenExpired(adminAccessToken)) ||
(adminRefreshToken && !isTokenExpired(adminRefreshToken));
const adminInfo = localStorage.getItem('admin_userInfo');
return !!(hasValidAdminToken && adminInfo);
}
4.3 사용자 정보 관리
4.3.1 사용자 정보 저장/조회
파일: src/store/user.ts
interface UserInfo {
userId: number;
email: string;
name: string;
userType: 'U' | 'A';
role?: string; // 관리자의 경우 role 정보
roleName?: string; // 관리자의 경우 role 이름
}
// 사용자 정보 저장
export function saveUserInfo(userInfo: UserInfo) {
const prefix = getCurrentPrefix(userInfo.userType);
localStorage.setItem(prefix + 'userInfo', JSON.stringify(userInfo));
}
// 사용자 정보 가져오기
export function getUserInfo(): UserInfo | null {
const prefix = getCurrentPrefix();
const userInfoStr = localStorage.getItem(prefix + 'userInfo');
if (!userInfoStr) return null;
try {
return JSON.parse(userInfoStr);
} catch (error) {
console.error('Failed to parse user info:', error);
return null;
}
}
4.3.2 Admin Role 조회
// 권한 체크용 Admin Role 반환 (userInfo.role 사용)
export function getAdminRole(): string {
const adminInfoStr = localStorage.getItem('admin_userInfo');
if (!adminInfoStr) return '';
try {
const adminInfo = JSON.parse(adminInfoStr);
return adminInfo?.role || '';
} catch (error) {
console.error('Failed to parse admin info:', error);
return '';
}
}
// 화면 표시용 Admin Role Name 반환
export function getAdminRoleName(): string {
const adminInfoStr = localStorage.getItem('admin_userInfo');
if (!adminInfoStr) return '관리자';
try {
const adminInfo = JSON.parse(adminInfoStr);
return adminInfo?.roleName || '관리자';
} catch (error) {
console.error('Failed to parse admin info:', error);
return '관리자';
}
}
4.3.3 로그인 정보 저장/삭제
// 로그인 시 사용자 정보와 토큰을 함께 저장
export function saveLoginInfo(
userInfo: UserInfo,
accessToken: string,
refreshToken: string
) {
saveUserInfo(userInfo);
saveTokens(accessToken, refreshToken);
}
// 현재 활성 사용자 타입의 로그인 정보만 제거
export function clearLoginInfo() {
removeUserInfo(); // 현재 타입의 사용자 정보만 제거
removeTokens(); // 현재 타입의 토큰만 제거
}
// 특정 사용자 타입의 로그인 정보만 제거
export function clearLoginInfoByType(userType: 'U' | 'A') {
removeUserInfoByType(userType);
removeTokensByType(userType);
}
// 모든 타입의 로그인 정보 완전 제거
export function clearAllLoginInfo() {
removeAllUserInfo();
removeAllTokens();
}
4.4 User/Admin 독립 세션 관리 (핵심 로직)
Frontend는 User와 Admin의 독립적인 동시 로그인을 지원합니다.
4.4.1 LocalStorage prefix 분리 (user_, admin_)
저장 구조:
LocalStorage: user_accessToken # 일반 사용자 Access Token user_refreshToken # 일반 사용자 Refresh Token user_userInfo # 일반 사용자 정보 admin_accessToken # 관리자 Access Token admin_refreshToken # 관리자 Refresh Token admin_userInfo # 관리자 정보
장점:
- 독립적 세션: User와 Admin 세션이 서로 영향을 주지 않음
- 동시 로그인: 하나의 브라우저에서 User와 Admin 동시 로그인 가능
- 세션 전환: 로그아웃 없이 User ↔ Admin 전환 가능
4.4.2 동시 로그인 지원 메커니즘
현재 활성 사용자 타입 자동 판단:
// 현재 활성 사용자 타입 자동 판단 (Admin 우선)
export function getUserType(): 'U' | 'A' | null {
// Admin 정보 먼저 확인 (우선순위)
const adminInfo = localStorage.getItem('admin_userInfo');
if (adminInfo) {
try {
const parsed = JSON.parse(adminInfo);
if (parsed && parsed.userType === 'A') return 'A';
} catch {}
}
// User 정보 확인
const userInfo = localStorage.getItem('user_userInfo');
if (userInfo) {
try {
const parsed = JSON.parse(userInfo);
if (parsed && parsed.userType === 'U') return 'U';
} catch {}
}
return null; // 둘 다 없으면 null
}
우선순위: Admin > User
- Admin으로 로그인되어 있으면 Admin 세션 사용
- Admin이 없고 User만 있으면 User 세션 사용
- 둘 다 없으면
null(로그인 필요)
4.4.3 자동 사용자 타입 판단 (Admin 우선)
prefix 자동 결정:
function getCurrentPrefix(): string {
const userType = getUserType();
return userType === 'A' ? ADMIN_PREFIX : USER_PREFIX;
}
활용:
saveTokens(): 현재 활성 타입에 맞게 토큰 저장getAccessToken(): 현재 활성 타입의 토큰 조회saveUserInfo(): 현재 활성 타입의 사용자 정보 저장
4.4.4 세션 전환 Flow
시나리오 1: User → Admin 전환
1. User로 로그인 상태 - user_accessToken, user_userInfo 존재 2. Admin 로그인 (/admin/login) - admin_accessToken, admin_userInfo 생성 3. getUserType() 호출 - Admin 우선 → 'A' 반환 4. 이후 모든 API 요청은 admin_ prefix 사용 - User 세션은 유지됨 (로그아웃 불필요)
시나리오 2: Admin → User 전환
1. Admin으로 로그인 상태
- admin_accessToken, admin_adminInfo 존재
2. Admin 로그아웃 (clearLoginInfoByType('A'))
- admin_ prefix 데이터 삭제
3. getUserType() 호출
- Admin 없음 → User 확인 → 'U' 반환
4. User 세션으로 자동 전환
시나리오 3: 완전 로그아웃
1. User와 Admin 모두 로그인 상태 2. 완전 로그아웃 (clearAllLoginInfo()) - user_ prefix 데이터 삭제 - admin_ prefix 데이터 삭제 3. getUserType() 호출 - null 반환 (로그인 필요)
4.4.5 브라우저 저장 데이터 종합
Frontend는 LocalStorage와 SessionStorage를 사용하여 인증 및 사용자 정보를 저장합니다.
LocalStorage (영구 저장):
| 키 이름 | 데이터 형식 | 용도 | 예시 값 |
|---|---|---|---|
user_accessToken |
JWT 문자열 | 일반 사용자 Access Token | eyJhbGciOiJIUzI1NiIs... |
user_refreshToken |
JWT 문자열 | 일반 사용자 Refresh Token | eyJhbGciOiJIUzI1NiIs... |
user_userInfo |
JSON 문자열 | 일반 사용자 정보 | {"userId":1,"email":"user@example.com","name":"홍길동","userType":"U"} |
admin_accessToken |
JWT 문자열 | 관리자 Access Token | eyJhbGciOiJIUzI1NiIs... |
admin_refreshToken |
JWT 문자열 | 관리자 Refresh Token | eyJhbGciOiJIUzI1NiIs... |
admin_userInfo |
JSON 문자열 | 관리자 정보 (role 포함) | {"userId":1,"email":"admin@example.com","name":"관리자","userType":"A","role":"S-ADMIN","roleName":"Super Admin"} |
SessionStorage (탭 닫으면 삭제):
| 키 이름 | 데이터 형식 | 용도 |
|---|---|---|
returnTo |
문자열 (URL 경로) | 인증 실패 시 원래 페이지 경로 저장 (로그인 후 자동 복원) |
저장 데이터 특징:
- User와 Admin 완전 분리: prefix로 구분하여 독립 세션 지원
- 로그아웃 시 선택적 삭제:
clearLoginInfoByType('U')→ user_ prefix만 삭제clearLoginInfoByType('A')→ admin_ prefix만 삭제clearAllLoginInfo()→ 모든 데이터 삭제
- SessionStorage 활용: 페이지 복원을 위한 임시 데이터 (보안 강화)
- 자동 정리: 유효하지 않은 토큰은 자동 제거 (
validateAndCleanTokens)
보안 고려사항:
- LocalStorage는 XSS 공격에 취약 (JavaScript로 접근 가능)
- 토큰 형식 검증 (
isValidTokenFormat) 적용 - 토큰 만료 체크 (
isTokenExpired) 적용 - HTTPS 통신 필수 (프로덕션)
4.5 권한 체크 유틸리티
4.5.1 역할 기반 권한 체크 함수 (8개)
파일: src/utils/auth.ts
import { CODE_SYS_ADMIN_ROLES } from '@iitp-dabt/common';
// 1. S-Admin 권한 확인 (최고 권한)
export function isSAdmin(adminRole: string | null): boolean {
return adminRole === CODE_SYS_ADMIN_ROLES.SUPER_ADMIN;
}
// 2. 일반 Admin 권한 확인 (ADMIN, EDITOR, VIEWER 포함)
export function isAdmin(adminRole: string | null): boolean {
return adminRole === CODE_SYS_ADMIN_ROLES.SUPER_ADMIN
|| adminRole === CODE_SYS_ADMIN_ROLES.ADMIN
|| adminRole === CODE_SYS_ADMIN_ROLES.EDITOR
|| adminRole === CODE_SYS_ADMIN_ROLES.VIEWER;
}
// 3. 콘텐츠 편집 권한 확인 (S-ADMIN, ADMIN, EDITOR)
export function hasContentEditPermission(adminRole: string | null): boolean {
return adminRole === CODE_SYS_ADMIN_ROLES.SUPER_ADMIN
|| adminRole === CODE_SYS_ADMIN_ROLES.ADMIN
|| adminRole === CODE_SYS_ADMIN_ROLES.EDITOR;
}
// 4. 읽기 권한 확인 (모든 관리자)
export function hasReadPermission(adminRole: string | null): boolean {
return adminRole === CODE_SYS_ADMIN_ROLES.SUPER_ADMIN
|| adminRole === CODE_SYS_ADMIN_ROLES.ADMIN
|| adminRole === CODE_SYS_ADMIN_ROLES.EDITOR
|| adminRole === CODE_SYS_ADMIN_ROLES.VIEWER;
}
// 5. 운영자 계정 관리 권한 확인 (S-ADMIN만)
export function hasAccountManagementPermission(adminRole: string | null): boolean {
return adminRole === CODE_SYS_ADMIN_ROLES.SUPER_ADMIN;
}
// 6. 사용자 계정 조회 권한 확인 (모든 관리자)
export function hasUserAccountReadPermission(adminRole: string | null): boolean {
return adminRole === CODE_SYS_ADMIN_ROLES.SUPER_ADMIN
|| adminRole === CODE_SYS_ADMIN_ROLES.ADMIN
|| adminRole === CODE_SYS_ADMIN_ROLES.EDITOR
|| adminRole === CODE_SYS_ADMIN_ROLES.VIEWER;
}
// 7. 사용자 계정 편집 권한 확인 (S-ADMIN, ADMIN만)
export function hasUserAccountEditPermission(adminRole: string | null): boolean {
return adminRole === CODE_SYS_ADMIN_ROLES.SUPER_ADMIN
|| adminRole === CODE_SYS_ADMIN_ROLES.ADMIN;
}
// 8. 시스템 설정 권한 확인 (S-ADMIN만)
export function hasSystemConfigPermission(adminRole: string | null): boolean {
return adminRole === CODE_SYS_ADMIN_ROLES.SUPER_ADMIN;
}
권한 매트릭스:
| 기능 | VIEWER | EDITOR | ADMIN | S-ADMIN |
|---|---|---|---|---|
| 콘텐츠 조회 (FAQ, QnA, Notice) | O | O | O | O |
| 콘텐츠 편집 (생성/수정/삭제) | X | O | O | O |
| 사용자 계정 조회 | O | O | O | O |
| 사용자 계정 편집 | X | X | O | O |
| 운영자 계정 관리 | X | X | X | O |
| 코드 관리 | X | X | X | O |
| 시스템 설정 | X | X | X | O |
4.5.2 메뉴 접근 권한 체크
export function hasMenuAccess(adminRole: string | null, menuName: string): boolean {
switch (menuName) {
case 'dashboard':
case 'openapi':
case 'qna':
case 'faq':
case 'notice':
case 'user-management':
return hasReadPermission(adminRole);
case 'operator-management':
case 'code-management':
return hasAccountManagementPermission(adminRole);
default:
return hasReadPermission(adminRole);
}
}
활용: 사이드 네비게이션 메뉴 표시/숨김 제어
4.5.3 버튼/액션 권한 체크
export function hasActionPermission(adminRole: string | null, actionType: string): boolean {
switch (actionType) {
case 'create':
case 'update':
case 'delete':
return hasContentEditPermission(adminRole);
case 'user-create':
case 'user-update':
case 'user-delete':
return hasUserAccountEditPermission(adminRole);
case 'operator-create':
case 'operator-update':
case 'operator-delete':
return hasAccountManagementPermission(adminRole);
case 'code-create':
case 'code-update':
case 'code-delete':
return hasAccountManagementPermission(adminRole);
default:
return hasReadPermission(adminRole);
}
}
활용: 버튼 활성화/비활성화, 경고 메시지 표시
4.6 라우트 가드
4.6.1 PrivateRoute - 일반 사용자 인증 체크
파일: src/components/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useEffect } from 'react';
import { validateAndCleanTokens, isUserAuthenticated } from '../store/auth';
import { ROUTES } from '../routes';
// 인증 보호 라우트 컴포넌트 (일반 사용자용)
export function PrivateRoute({ children }: { children: React.ReactNode }) {
const isLoggedIn = isUserAuthenticated();
const location = useLocation();
// 토큰 유효성 검사 및 정리
useEffect(() => {
validateAndCleanTokens();
}, []);
if (!isLoggedIn) {
return <Navigate to={ROUTES.PUBLIC.LOGIN} state={{ from: location }} replace />;
}
return <>{children}</>;
}
동작:
isUserAuthenticated()호출로 User 토큰 유효성 확인- 유효하지 않으면
/login으로 리다이렉트 location.state에 원래 페이지 경로 저장 (로그인 후 복원)
적용 페이지:
/dashbd- 사용자 대시보드/profile- 프로필 관리/user/qna/create- QnA 생성/user/openapi- OpenAPI 키 관리
4.6.2 AdminProtectedRoute - 관리자 인증 체크
// 관리자 보호 라우트 컴포넌트
export function AdminProtectedRoute({ children }: { children: React.ReactNode }) {
const isLoggedIn = isAdminAuthenticated();
const location = useLocation();
// 토큰 유효성 검사 및 정리
useEffect(() => {
validateAndCleanTokens();
}, []);
if (!isLoggedIn) {
return <Navigate to={ROUTES.ADMIN.LOGIN} state={{ from: location }} replace />;
}
return <>{children}</>;
}
동작:
isAdminAuthenticated()호출로 Admin 토큰 유효성 확인- 유효하지 않으면
/admin/login으로 리다이렉트 location.state에 원래 페이지 경로 저장
적용 페이지:
/admin/dashbd- 관리자 대시보드/admin/faqs/*- FAQ 관리/admin/qnas/*- QnA 관리/admin/notices/*- 공지사항 관리/admin/users/*- 사용자 관리/admin/operators/*- 운영자 관리/admin/openapi/*- OpenAPI 관리/admin/code/*- 코드 관리
4.6.3 인증 실패 시 리다이렉트
Flow:
사용자가 보호된 페이지 접근
│
▼
PrivateRoute / AdminProtectedRoute
│
├─ 인증 성공?
│ └─ 페이지 렌더링
│
└─ 인증 실패?
└─ 로그인 페이지로 리다이렉트
├─ state: { from: location }
└─ 로그인 성공 후 원래 페이지로 복원
로그인 후 복원 예시:
// Login.tsx
const location = useLocation();
const from = location.state?.from?.pathname || '/dashbd';
const handleLoginSuccess = () => {
navigate(from, { replace: true });
};
5. API 클라이언트
5.1 API 클라이언트 구조 및 공통 로직
Frontend의 모든 Backend API 통신은 src/api/ 디렉토리의 모듈을 통해 이루어집니다.
주요 특징:
- 통합 요청 함수:
apiFetch()(인증 필요),publicApiFetch()(공개) - 자동 토큰 관리: 토큰 갱신, 만료 체크
- ErrorCode 기반 처리: 사용자 친화적 메시지 변환
- 타임아웃 및 재시도: 네트워크 오류 처리
5.1.1 apiFetch() vs publicApiFetch() 차이
| 항목 | apiFetch() |
publicApiFetch() |
|---|---|---|
| 토큰 필수 여부 | 필수 (없으면 null 반환) | 선택 (없어도 진행) |
| 토큰 갱신 | 요청 전 자동 갱신 | 401 에러 시 재시도 |
| 사용 페이지 | User/Admin 인증 페이지 | 공개 페이지 (FAQ, QnA, Notice) |
| Authorization 헤더 | 항상 포함 | 토큰 있을 때만 포함 |
5.1.2 토큰 자동 갱신 (401 에러 시)
apiFetch() 갱신 Flow:
API 요청 전 → ensureValidToken() 호출
├─ Access Token 유효 (만료 5분 초과)
│ └─ 그대로 사용
└─ Access Token 만료 또는 만료 임박 (5분 이내)
└─ Refresh Token으로 갱신 시도
├─ 성공 → 새 Access Token 사용
└─ 실패 → null 반환 (API 요청 중단)
publicApiFetch() 갱신 Flow:
API 요청 → 401 에러 발생 시
└─ Refresh Token으로 갱신 시도
├─ 성공 → 동일 요청 재시도 (1회만)
└─ 실패 → 에러 응답 반환
5.1.3 정상 응답 처리 (ApiResponse<T> 구조)
타입 정의: src/types/api.ts
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
errorCode?: number;
errorMessage?: string;
// FE 전용 확장 필드
showPopup?: boolean; // 팝업 표시 필요 여부
redirectTo?: string; // 리다이렉트 URL
autoLogout?: boolean; // 자동 로그아웃 필요 여부
details?: any; // 추가 상세 정보
}
5.1.4 에러 응답 처리 (ErrorCode → 사용자 친화적 메시지)
ErrorCode 기반 메시지 변환 (src/api/api.ts):
| ErrorCode | 사용자 메시지 |
|---|---|
UNAUTHORIZED (40101) |
인증이 필요합니다. 다시 로그인해주세요. |
TOKEN_EXPIRED (40102) |
로그인 세션이 만료되었습니다. 다시 로그인해주세요. |
INVALID_TOKEN (40103) |
유효하지 않은 인증 정보입니다. 다시 로그인해주세요. |
ACCESS_DENIED (40301) |
접근 권한이 없습니다. |
USER_NOT_FOUND (40401) |
사용자를 찾을 수 없습니다. |
LOGIN_FAILED (40001) |
로그인에 실패했습니다. 아이디와 비밀번호를 확인해주세요. |
NETWORK_ERROR (50301) |
네트워크 오류가 발생했습니다. |
REQUEST_TIMEOUT (50801) |
요청 시간이 초과되었습니다. |
응답 강화 (src/utils/apiResponseHandler.ts):
showPopup: 팝업 표시 필요 여부 자동 판단redirectTo: 리다이렉트 URL 자동 생성 (User/Admin 타입별)autoLogout: 자동 로그아웃 필요 여부 판단
5.1.5 재시도 로직 (타임아웃, 네트워크 오류)
타임아웃 설정:
- 기본 타임아웃:
10초(API_TIMEOUT) - 커스텀 타임아웃:
options.timeoutMs로 조정 가능
재시도 정책:
- 401 에러 (인증 실패): 토큰 갱신 후 1회 재시도
- 타임아웃: 재시도 없음 (에러 메시지 표시)
- 네트워크 오류: 재시도 없음 (에러 메시지 표시)
5.2 API 모듈 목록 (테이블 형태)
| 파일 | 주요 함수 | 설명 | 사용처 |
|---|---|---|---|
api.ts |
apiFetch, publicApiFetch |
공통 API 요청 함수 | 모든 API 모듈 |
user.ts |
login, register, getProfile, updateProfile, changePassword |
사용자 인증 및 관리 | 로그인, 회원가입, 프로필 페이지 |
admin.ts |
loginAdmin, refreshAdminToken, getAdminProfile |
관리자 인증 | 관리자 로그인, 프로필 페이지 |
account.ts |
getAdminAccounts, createAdminAccount, updateAdminAccount, deleteAdminAccount |
관리자 계정 관리 | 운영자 관리 페이지 (S-ADMIN) |
common.ts |
getJwtConfig, getSystemInfo |
공통 API | JWT 설정 조회, 시스템 정보 |
commonCode.ts |
getCommonCodes, getCodesByGroup, createCode, updateCode, deleteCode |
공통 코드 관리 | 코드 관리 페이지 (S-ADMIN) |
faq.ts |
getFaqs, getFaqById, createFaq, updateFaq, deleteFaq, batchDeleteFaq |
FAQ 관리 | FAQ 목록, 상세, 생성, 수정 페이지 |
qna.ts |
getQnas, getQnaById, createQna, updateQna, deleteQna, replyQna |
QnA 관리 | QnA 목록, 상세, 생성, 답변 페이지 |
notice.ts |
getNotices, getNoticeById, createNotice, updateNotice, deleteNotice |
공지사항 관리 | 공지사항 목록, 상세, 생성, 수정 페이지 |
openApi.ts |
getOpenApiClients, approveOpenApiKey, rejectOpenApiKey, extendOpenApiKey |
OpenAPI 키 관리 | OpenAPI 클라이언트 관리, 키 승인 페이지 |
특징:
- 일관된 네이밍:
get*,create*,update*,delete*,batchDelete* - 타입 안전: TypeScript 제네릭을 통한 타입 추론
- 에러 핸들링: 모든 함수는
ApiResponse<T>반환
6. 라우팅 구조
6.1 라우트 정의
6.1.1 ROUTES 객체 구조 (PUBLIC, USER, ADMIN, COMMON)
파일: src/routes/index.ts
Frontend는 4단계 라우트 분류를 사용합니다:
1. PUBLIC - 공개 페이지 (로그인 불필요)
HOME: '/' THEME_PREVIEW: '/theme-preview' NOTICE: '/notice' NOTICE_DETAIL: '/notice/:noticeId' FAQ: '/faq' QNA: '/qna' QNA_DETAIL: '/qna/:qnaId' ABOUT: '/about' # OpenAPI 소개 TERMS: '/terms' # 이용약관 PRIVACY: '/privacy' # 개인정보 처리방침 LOGIN: '/login' REGISTER: '/register'
2. USER - 일반 사용자 페이지 (User 인증 필요)
DASHBOARD: '/dashbd' PROFILE: '/profile' QNA_CREATE: '/user/qna/create' QNA_HISTORY: '/user/qna/history' OPEN_API_MANAGEMENT: '/user/openapi'
3. ADMIN - 관리자 페이지 (Admin 인증 필요)
| 기능 | 라우트 패턴 | 접근 권한 |
|---|---|---|
| 대시보드 | /admin/dashbd |
모든 관리자 |
| FAQ 관리 | /admin/faqs/* |
VIEWER+ (조회), EDITOR+ (편집) |
| QnA 관리 | /admin/qnas/* |
VIEWER+ (조회), EDITOR+ (편집) |
| 공지사항 관리 | /admin/notices/* |
VIEWER+ (조회), EDITOR+ (편집) |
| 사용자 관리 | /admin/users/* |
VIEWER+ (조회), ADMIN+ (편집) |
| 운영자 관리 | /admin/operators/* |
S-ADMIN 전용 |
| OpenAPI 관리 | /admin/openapi/clients/*, /admin/openapi/requests/* |
모든 관리자 (조회), ADMIN+ (승인/거부) |
| 코드 관리 | /admin/code/* |
S-ADMIN 전용 |
4. COMMON - 공통 페이지
NOT_FOUND: '/404' ERROR: '/error'
중첩 객체 구조:
ADMIN: {
FAQ: {
LIST: '/admin/faqs',
CREATE: '/admin/faqs/create',
EDIT: '/admin/faqs/:id/edit',
DETAIL: '/admin/faqs/:id',
},
// ... 다른 기능들
}
6.1.2 ROUTE_META - 네비게이션 메타데이터
메타데이터 구조:
export const ROUTE_META = {
[ROUTES.ADMIN.DASHBOARD]: {
title: '대시보드',
icon: 'Dashboard',
requiresAuth: true,
minRole: CODE_SYS_ADMIN_ROLES.VIEWER,
},
[ROUTES.ADMIN.OPERATORS.LIST]: {
title: '운영자 관리',
icon: 'AdminPanelSettings',
requiresAuth: true,
minRole: CODE_SYS_ADMIN_ROLES.SUPER_ADMIN, // S-ADMIN 전용
},
}
활용:
title: 페이지 제목 (네비게이션, 브레드크럼)icon: Material-UI 아이콘 이름requiresAuth: 인증 필요 여부minRole: 최소 요구 권한
6.1.3 RouteUtils
헬퍼 함수:
export const RouteUtils = {
// 동적 라우트 생성 (예: /faq/:id → /faq/123)
createDynamicRoute: (route: string, params: Record<string, string | number>): string => {
let result = route;
Object.entries(params).forEach(([key, value]) => {
result = result.replace(`:`, String(value));
});
return result;
},
// FAQ 상세 페이지
createFaqDetailRoute: (id: string | number): string => {
return RouteUtils.createDynamicRoute(ROUTES.USER.FAQ_DETAIL, { id });
},
// 관리자 FAQ 편집
createAdminFaqEditRoute: (id: string | number): string => {
return RouteUtils.createDynamicRoute(ROUTES.ADMIN.FAQ.EDIT, { id });
},
};
사용 예시:
// FAQ 상세 페이지로 이동
navigate(RouteUtils.createFaqDetailRoute(123)); // /user/faq/123
// 관리자 FAQ 편집 페이지로 이동
navigate(RouteUtils.createAdminFaqEditRoute(456)); // /admin/faqs/456/edit
6.2 라우팅 Flow
6.2.1 URL 접근 시 권한 체크 Flow
파일: src/App.tsx
<Routes>
<Route path="/" element={<Layout />}>
{/* 공개 페이지 (로그인 불필요) */}
<Route index element={<Home />} />
<Route path="/faq" element={<FaqList />} />
<Route path="/login" element={<Login />} />
{/* 일반 사용자 페이지 (PrivateRoute Guard) */}
<Route
path="/dashbd"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
{/* 관리자 페이지 (AdminProtectedRoute Guard) */}
<Route
path="/admin/faqs"
element={
<AdminProtectedRoute>
<AdminFaqList />
</AdminProtectedRoute>
}
/>
</Route>
</Routes>
권한 체크 Flow:
사용자가 URL 접근 (예: /admin/faqs)
│
▼
React Router - Route 매칭
│
▼
<AdminProtectedRoute> Guard
│
├─ isAdminAuthenticated() 호출
│ │
│ ├─ Admin 토큰 유효?
│ │ └─ YES → 페이지 렌더링
│ │
│ └─ Admin 토큰 없음/만료?
│ └─ NO → /admin/login 리다이렉트
│ (state: { from: '/admin/faqs' } 저장)
│
▼
페이지 컴포넌트 (AdminFaqList)
│
▼
UI 권한 체크 (hasContentEditPermission)
│
├─ VIEWER → 조회만 가능 (버튼 비활성화)
└─ EDITOR+ → 모든 기능 가능
6.2.2 인증 실패 시 리다이렉트
PrivateRoute 리다이렉트:
// src/components/ProtectedRoute.tsx
if (!isUserAuthenticated()) {
return <Navigate to={ROUTES.PUBLIC.LOGIN} state={{ from: location }} replace />;
}
AdminProtectedRoute 리다이렉트:
if (!isAdminAuthenticated()) {
return <Navigate to={ROUTES.ADMIN.LOGIN} state={{ from: location }} replace />;
}
로그인 후 복원:
// src/pages/user/Login.tsx
const location = useLocation();
const from = location.state?.from?.pathname || '/dashbd';
const handleLoginSuccess = () => {
navigate(from, { replace: true }); // 원래 페이지로 복원
};
시나리오:
1. 사용자가 /user/qna/create 접근 (비로그인)
2. PrivateRoute에서 인증 실패 감지
3. /login으로 리다이렉트 (state: { from: '/user/qna/create' })
4. 사용자 로그인 성공
5. /user/qna/create로 자동 복원
6.2.3 권한 부족 시 처리
UI 레벨 권한 체크:
// src/pages/admin/FaqList.tsx
const adminRole = getAdminRole();
const canEdit = hasContentEditPermission(adminRole);
return (
<div>
<DataTable data={faqs} />
{canEdit ? (
<Button onClick={handleCreate}>생성</Button>
) : (
<Tooltip title="편집 권한이 없습니다.">
<span>
<Button disabled>생성</Button>
</span>
</Tooltip>
)}
</div>
);
권한 부족 시 처리 방식:
- 버튼 비활성화: VIEWER는 생성/수정/삭제 버튼 비활성화
- 툴팁 표시: "편집 권한이 없습니다." 메시지
- 메뉴 숨김: S-ADMIN 전용 메뉴는 다른 역할에게 숨김
- 경고 메시지: 직접 URL 접근 시 "접근 권한이 없습니다." 표시
중요: Frontend의 권한 체크는 UX 개선 목적이며, Backend에서 실제 권한 검증을 수행합니다.
7. 주요 페이지 상세
페이지별 설명 템플릿:
- 페이지 목적
- URL 경로
- 접근 권한 (Public / User / Admin 역할별)
- 주요 기능
- 사용 API (API 규격서 참조)
- 권한별 UI 차이 (VIEWER/EDITOR/ADMIN/S-ADMIN)
- 예외 처리 (인증 실패, 권한 부족, API 오류)
7.1 공개 페이지 (Public) - 인증 불필요
7.1.1 홈 페이지 - /
페이지: 서비스 메인 페이지 (공개 + 선택적 인증)
URL 경로: /
파일: src/pages/user/Home.tsx
접근 권한:
- Public (로그인 불필요)
- 로그인 시 User 정보 표시 (선택적 인증)
주요 기능:
- 서비스 소개
- 최근 공지사항 미리보기
- FAQ 미리보기
- 로그인 시 대시보드 바로가기
API:
publicApiFetch사용 (토큰 선택적 포함)
Error:
- API 오류 → 에러 메시지 표시 (서비스 계속 이용 가능)
7.1.2 로그인 페이지 - /login
페이지: 일반 사용자 로그인
URL 경로: /login
파일: src/pages/user/Login.tsx
접근 권한:
- Public (로그인 없이 접근 가능)
- 이미 로그인된 경우 홈 또는 원래 페이지로 리다이렉트
주요 기능:
- 이메일/비밀번호 입력
- 로그인 처리 (
loginUserAPI) - 로그인 실패 시 에러 메시지 표시
- 로그인 성공 시 원래 페이지 복원 (
location.state.from) - Admin 로그인 정리 (User 우선권 확보)
API (상세는 API 규격서 참조):
POST /api/user/login- 사용자 로그인
주요 처리 Flow:
1. 이미 User 로그인? → 홈 또는 원래 페이지로 리다이렉트
2. 이메일/비밀번호 입력
3. Admin 세션 정리 (clearLoginInfoByType('A'))
4. loginUser() API 호출
5. 성공 시 → location.state.from 또는 '/dashbd'로 이동
6. 실패 시 → 에러 메시지 표시
Error:
- 로그인 실패 (
LOGIN_FAILED) → "아이디와 비밀번호를 확인해주세요." - 네트워크 오류 → "네트워크 오류가 발생했습니다."
7.1.3 회원가입 페이지 - /register
페이지: 일반 사용자 회원가입
URL 경로: /register
파일: src/pages/user/Register.tsx
접근 권한:
- Public (로그인 없이 접근 가능)
주요 기능:
- 이메일, 비밀번호, 이름 입력
- Common 패키지 검증 함수 사용
isValidEmail(email)isValidPassword(password)isValidName(name)
- 회원가입 처리
- 성공 시 로그인 페이지로 이동
API (상세는 API 규격서 참조):
POST /api/user/register- 사용자 회원가입
Error:
- 이메일 중복 → "이미 사용 중인 이메일입니다."
- 비밀번호 검증 실패 → "8자 이상, 영문/숫자/특수문자 포함"
- 네트워크 오류 → 에러 메시지 표시
7.1.4 FAQ 목록 (공개) - /faq
페이지: 자주 묻는 질문 목록 조회 (공개)
URL 경로: /faq
파일: src/pages/user/FaqList.tsx
접근 권한:
- Public (로그인 불필요)
주요 기능:
- FAQ 목록 조회 (페이징, 검색)
- 카테고리별 필터링
- FAQ 상세 보기
API (상세는 API 규격서 참조):
GET /api/public/faq- FAQ 목록 조회
Error:
- API 오류 → 에러 메시지 표시
- 빈 목록 → "등록된 FAQ가 없습니다." 표시
7.1.5 QnA 목록/상세 (공개) - /qna, /qna/:qnaId
페이지: 질문과 답변 목록 및 상세 조회 (공개)
URL 경로:
- 목록:
/qna - 상세:
/qna/:qnaId
파일:
src/pages/user/QnaList.tsxsrc/pages/user/QnaDetail.tsx
접근 권한:
- Public (로그인 불필요)
주요 기능:
- QnA 목록 조회 (페이징, 검색)
- 답변 완료/대기 상태별 필터링
- QnA 상세 보기 (질문 + 답변)
API (상세는 API 규격서 참조):
GET /api/public/qna- QnA 목록 조회GET /api/public/qna/:id- QnA 상세 조회
Error:
- QnA 없음 (404) → "질문을 찾을 수 없습니다."
- API 오류 → 에러 메시지 표시
7.1.6 공지사항 목록/상세 (공개) - /notice, /notice/:noticeId
페이지: 공지사항 목록 및 상세 조회 (공개)
URL 경로:
- 목록:
/notice - 상세:
/notice/:noticeId
파일:
src/pages/user/NoticeList.tsxsrc/pages/user/NoticeDetail.tsx
접근 권한:
- Public (로그인 불필요)
주요 기능:
- 공지사항 목록 조회 (페이징)
- 공지사항 상세 보기
API (상세는 API 규격서 참조):
GET /api/public/notice- 공지사항 목록 조회GET /api/public/notice/:id- 공지사항 상세 조회
Error:
- 공지사항 없음 (404) → "공지사항을 찾을 수 없습니다."
- API 오류 → 에러 메시지 표시
7.1.7 정적 페이지 - /about, /terms, /privacy
페이지: 정적 콘텐츠 페이지
URL 경로:
/about- OpenAPI 소개/terms- 이용약관/privacy- 개인정보 처리방침
파일:
src/pages/public/OpenApiAbout.tsxsrc/pages/public/Terms.tsxsrc/pages/public/Privacy.tsx
접근 권한:
- Public (로그인 불필요)
주요 기능:
- 정적 콘텐츠 표시
- API 호출 없음
7.2 사용자 페이지 (User) - 일반 사용자 인증 필요
7.2.1 사용자 대시보드 - /dashbd
페이지: 사용자 메인 대시보드
URL 경로: /dashbd
파일: src/pages/user/Dashboard.tsx
접근 권한:
- User 인증 필요 (
PrivateRoute)
주요 기능:
- 사용자 정보 표시
- My QnA 통계 (작성한 QnA 수, 답변 대기 중)
- My OpenAPI 키 통계 (활성 키, 승인 대기)
- 최근 공지사항
API (상세는 API 규격서 참조):
GET /api/user/stats- 사용자 통계 (향후)GET /api/user/qna- 나의 QnA 목록GET /api/user/open-api- 나의 API 키 목록
Error:
- 인증 실패 →
/login리다이렉트 - API 오류 → 해당 섹션만 에러 표시
7.2.2 프로필 관리 - /profile
페이지: 사용자 프로필 조회/수정, 비밀번호 변경
URL 경로: /profile
파일: src/pages/user/UserProfile.tsx
접근 권한:
- User 인증 필요 (
PrivateRoute)
주요 기능:
- 프로필 조회 (이메일, 이름)
- 프로필 수정 (이름)
- 비밀번호 변경
API (상세는 API 규격서 참조):
GET /api/user/profile- 프로필 조회PUT /api/user/profile- 프로필 수정PUT /api/user/password- 비밀번호 변경
Error:
- 인증 실패 →
/login리다이렉트 - 비밀번호 검증 실패 → "현재 비밀번호가 일치하지 않습니다."
- API 오류 → 에러 메시지 표시
7.2.3 My QnA 관리 - /user/qna/*
페이지: 나의 질문 작성, 조회, 관리
URL 경로:
- 생성:
/user/qna/create - 히스토리:
/user/qna/history
파일:
src/pages/user/QnaCreate.tsxsrc/pages/user/QnaHistory.tsx
접근 권한:
- User 인증 필요 (
PrivateRoute)
주요 기능:
- QnA 생성 (제목, 내용, 공개 여부)
- 나의 QnA 목록 조회
- QnA 상세 보기 (질문 + 관리자 답변)
API (상세는 API 규격서 참조):
POST /api/user/qna- QnA 생성GET /api/user/qna- 나의 QnA 목록GET /api/user/qna/:id- QnA 상세
Error:
- 인증 실패 →
/login리다이렉트 - 입력 검증 실패 → "제목과 내용을 입력해주세요."
- API 오류 → 에러 메시지 표시
7.2.4 OpenAPI 키 관리 - /user/openapi
페이지: OpenAPI 인증키 신청, 조회, 관리
URL 경로: /user/openapi
파일: src/pages/user/OpenApiManagement.tsx
접근 권한:
- �� User 인증 필요 (
PrivateRoute)
주요 기능:
- API 키 신청 (키 이름, 설명, 유효기간)
- API 키 목록 조회
- API 키 상세 (키 정보, 유효기간, 승인 상태)
- API 키 갱신 신청 (유효기간 연장)
- API 키 삭제 (논리 삭제)
- API 키 복사 (클립보드)
API (상세는 API 규격서 참조):
GET /api/user/open-api- 나의 API 키 목록POST /api/user/open-api- API 키 신청PUT /api/user/open-api/:id/extend- API 키 연장 신청DELETE /api/user/open-api/:id- API 키 삭제
API 키 상태:
- PENDING (
activeYn='N',activeAt=null) - 승인 대기 - ACTIVE (
activeYn='Y', 유효기간 내) - 활성 - EXPIRED (
activeYn='Y', 유효기간 만료) - 만료 - REJECTED (
keyRejectReason존재) - 거부됨
API 키 형식:
- 형식: 60자 길이의 16진수 문자열 (hex)
- 정규식:
/^[a-f0-9]{60}$/ - 생성 방식: Backend의
authKeyGenerator.generate()함수 사용crypto.randomBytes(30)기반 생성 (30 bytes = 60 hex characters)
- 예시:
a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456 - 표시: Frontend에서 키 복사 기능 제공 (클립보드 복사)
권한별 UI 차이:
- 모든 사용자 동일 (자신의 API 키만 관리)
Error:
- 인증 실패 →
/login리다이렉트 - 입력 검증 실패 → "키 이름과 설명을 입력해주세요."
- API 오류 → 에러 메시지 표시
7.2.5 공지사항 조회 - /user/notice/*
페이지: 사용자용 공지사항 목록/상세
URL 경로:
- 목록:
/user/notice - 상세:
/user/notice/:id
파일:
src/pages/user/NoticeList.tsxsrc/pages/user/NoticeDetail.tsx
접근 권한:
- User 인증 필요 (
PrivateRoute)
주요 기능:
- 공지사항 목록 조회 (페이징)
- 공지사항 상세 보기
API (상세는 API 규격서 참조):
GET /api/user/notice- 공지사항 목록GET /api/user/notice/:id- 공지사항 상세
Error:
- 인증 실패 →
/login리다이렉트 - 공지사항 없음 (404) → "공지사항을 찾을 수 없습니다."
7.3 관리자 페이지 (Admin) - 관리자 인증 필요
7.3.1 관리자 로그인 - /admin/login
페이지: 관리자 로그인
URL 경로: /admin/login
파일: src/pages/admin/AdminLogin.tsx
접근 권한:
- Public (로그인 없이 접근 가능)
- 이미 관리자 로그인된 경우
/admin/dashbd로 리다이렉트
주요 기능:
- 이메일/비밀번호 입력
- 관리자 로그인 처리 (
loginAdminAPI) - 로그인 성공 시 원래 페이지 복원
- User 로그인 정리 (Admin 우선권 확보)
API (상세는 API 규격서 참조):
POST /api/admin/login- 관리자 로그인
주요 처리 Flow:
1. 이미 Admin 로그인? → /admin/dashbd로 리다이렉트
2. 이메일/비밀번호 입력
3. User 세션 정리 (clearLoginInfoByType('U'))
4. loginAdmin() API 호출
5. 성공 시 → location.state.from 또는 '/admin/dashbd'로 이동
6. 실패 시 → 에러 메시지 표시
Error:
- 로그인 실패 → "관리자 정보를 확인해주세요."
- 네트워크 오류 → 에러 메시지 표시
7.3.2 관리자 대시보드 - /admin/dashbd
페이지: 관리자 메인 대시보드
URL 경로: /admin/dashbd
파일: src/pages/admin/AdminDashboard.tsx
접근 권한:
- Admin 인증 필요 (
AdminProtectedRoute) - 모든 관리자 역할 접근 가능 (VIEWER+)
주요 기능:
- OpenAPI 키 통계 (총 키, 활성 키, 만료 키, 승인 대기)
- QnA 통계 (총 QnA, 답변 완료, 답변 대기)
- 최근 활동 현황
API (상세는 API 규격서 참조):
GET /api/admin/openapi/stats- OpenAPI 통계GET /api/admin/qna/stats- QnA 통계
권한별 UI 차이:
- 모든 관리자 동일 (통계 조회만)
Error:
- 인증 실패 →
/admin/login리다이렉트 - API 오류 → "통계 데이터를 불러오는 중 오류가 발생했습니다."
7.3.3 관리자 프로필 - /admin/profile
페이지: 관리자 프로필 조회/수정, 비밀번호 변경
URL 경로: /admin/profile
파일: src/pages/admin/AdminProfile.tsx
접근 권한:
- Admin 인증 필요 (
AdminProtectedRoute)
주요 기능:
- 프로필 조회 (이메일, 이름, 역할)
- 프로필 수정 (이름)
- 비밀번호 변경
API (상세는 API 규격서 참조):
GET /api/admin/profile- 프로필 조회PUT /api/admin/profile- 프로필 수정PUT /api/admin/password- 비밀번호 변경
권한별 UI 차이:
- 모든 관리자 동일 (자신의 프로필만 수정)
Error:
- 인증 실패 →
/admin/login리다이렉트 - 비밀번호 검증 실패 → "현재 비밀번호가 일치하지 않습니다."
7.3.4 FAQ 관리 (EDITOR+) - /admin/faqs/*
페이지: FAQ 생성, 수정, 삭제, 관리
URL 경로:
- 목록:
/admin/faqs - 생성:
/admin/faqs/create - 상세:
/admin/faqs/:id - 수정:
/admin/faqs/:id/edit
파일:
src/pages/admin/FaqList.tsxsrc/pages/admin/FaqCreate.tsxsrc/pages/admin/FaqDetail.tsxsrc/pages/admin/FaqEdit.tsx
접근 권한:
- Admin 인증 필요 (
AdminProtectedRoute) - 조회: VIEWER+ (모든 관리자)
- 생성/수정/삭제: EDITOR+ (
hasContentEditPermission)
주요 기능:
- FAQ 목록 조회 (페이징, 검색, 카테고리 필터)
- FAQ 생성 (제목, 내용, 카테고리)
- FAQ 수정
- FAQ 삭제 (논리 삭제)
- FAQ 일괄 삭제
API (상세는 API 규격서 참조):
GET /api/admin/faq- FAQ 목록POST /api/admin/faq- FAQ 생성GET /api/admin/faq/:id- FAQ 상세PUT /api/admin/faq/:id- FAQ 수정DELETE /api/admin/faq/:id- FAQ 삭제DELETE /api/admin/faq/batch- FAQ 일괄 삭제
권한별 UI 차이:
- VIEWER: 목록/상세 조회만, 생성/수정/삭제 버튼 숨김
- EDITOR/ADMIN/S-ADMIN: 모든 기능 사용 가능
Error:
- 인증 실패 →
/admin/login리다이렉트 - 권한 부족 → 버튼 비활성화 + "편집 권한이 없습니다." 툴팁
- 입력 검증 실패 → "제목과 내용을 입력해주세요."
- API 오류 → 에러 메시지 표시
7.3.5 QnA 관리 (EDITOR+) - /admin/qnas/*
페이지: QnA 조회, 답변 작성, 관리
URL 경로:
- 목록:
/admin/qnas - 상세:
/admin/qnas/:id - 답변:
/admin/qnas/:id/reply - 수정:
/admin/qnas/:id/edit
파일:
src/pages/admin/QnaManage.tsxsrc/pages/admin/QnaDetail.tsxsrc/pages/admin/QnaReply.tsxsrc/pages/admin/QnaEdit.tsx
접근 권한:
- Admin 인증 필요 (
AdminProtectedRoute) - 조회: VIEWER+ (모든 관리자)
- 답변/수정/삭제: EDITOR+ (
hasContentEditPermission)
주요 기능:
- QnA 목록 조회 (페이징, 검색, 답변 상태 필터)
- QnA 상세 보기 (질문 + 답변)
- QnA 답변 작성
- QnA 수정
- QnA 삭제 (논리 삭제)
- QnA 일괄 삭제
API (상세는 API 규격서 참조):
GET /api/admin/qna- QnA 목록GET /api/admin/qna/:id- QnA 상세POST /api/admin/qna/:id/reply- 답변 작성PUT /api/admin/qna/:id- QnA 수정DELETE /api/admin/qna/:id- QnA 삭제DELETE /api/admin/qna/batch- QnA 일괄 삭제
권한별 UI 차이:
- VIEWER: 목록/상세 조회만
- EDITOR/ADMIN/S-ADMIN: 답변 작성, 수정, 삭제 가능
Error:
- 인증 실패 →
/admin/login리다이렉트 - 권한 부족 → 버튼 비활성화
- API 오류 → 에러 메시지 표시
7.3.6 공지사항 관리 (EDITOR+) - /admin/notices/*
페이지: 공지사항 생성, 수정, 삭제, 관리
URL 경로:
- 목록:
/admin/notices - 생성:
/admin/notices/create - 상세:
/admin/notices/:id - 수정:
/admin/notices/:id/edit
파일:
src/pages/admin/NoticeManage.tsxsrc/pages/admin/NoticeCreate.tsxsrc/pages/admin/NoticeDetail.tsxsrc/pages/admin/NoticeEdit.tsx
접근 권한:
- Admin 인증 필요 (
AdminProtectedRoute) - 조회: VIEWER+ (모든 관리자)
- 생성/수정/삭제: EDITOR+ (
hasContentEditPermission)
주요 기능:
- 공지사항 목록 조회 (페이징, 검색)
- 공지사항 생성 (제목, 내용)
- 공지사항 수정
- 공지사항 삭제 (논리 삭제)
- 공지사항 일괄 삭제
API (상세는 API 규격서 참조):
GET /api/admin/notice- 공지사항 목록POST /api/admin/notice- 공지사항 생성GET /api/admin/notice/:id- 공지사항 상세PUT /api/admin/notice/:id- 공지사항 수정DELETE /api/admin/notice/:id- 공지사항 삭제DELETE /api/admin/notice/batch- 공지사항 일괄 삭제
권한별 UI 차이:
- VIEWER: 목록/상세 조회만
- EDITOR/ADMIN/S-ADMIN: 모든 기능 사용 가능
Error:
- 인증 실패 →
/admin/login리다이렉트 - 권한 부족 → 버튼 비활성화
- API 오류 → 에러 메시지 표시
7.3.7 사용자 관리 (ADMIN+) - /admin/users/*
페이지: 일반 사용자 계정 조회, 생성, 수정, 삭제
URL 경로:
- 목록:
/admin/users - 생성:
/admin/users/create - 상세:
/admin/users/:id - 수정:
/admin/users/:id/edit
파일:
src/pages/admin/UserManagement.tsxsrc/pages/admin/UserCreate.tsxsrc/pages/admin/UserDetail.tsxsrc/pages/admin/UserEdit.tsx
접근 권한:
- Admin 인증 필요 (
AdminProtectedRoute) - 조회: VIEWER+ (
hasUserAccountReadPermission) - 생성/수정/삭제: ADMIN+ (
hasUserAccountEditPermission)
주요 기능:
- 사용자 목록 조회 (페이징, 검색, 상태 필터)
- 사용자 생성
- 사용자 상세 보기 (가입일, 최근 접속일, 보유 API 키 등)
- 사용자 수정 (이름, 상태)
- 사용자 삭제 (논리 삭제)
- 사용자 일괄 삭제
- 사용자 비밀번호 초기화
API (상세는 API 규격서 참조):
GET /api/admin/user-accounts- 사용자 목록POST /api/admin/user-accounts- 사용자 생성GET /api/admin/user-accounts/:id- 사용자 상세PUT /api/admin/user-accounts/:id- 사용자 수정DELETE /api/admin/user-accounts/:id- 사용자 삭제DELETE /api/admin/user-accounts/batch- 사용자 일괄 삭제
권한별 UI 차이:
- VIEWER/EDITOR: 목록/상세 조회만, 생성/수정/삭제 버튼 숨김
- ADMIN/S-ADMIN: 모든 기능 사용 가능
Error:
- 인증 실패 →
/admin/login리다이렉트 - 권한 부족 → 버튼 비활성화 + "편집 권한이 없습니다." 툴팁
- 이메일 중복 → "이미 사용 중인 이메일입니다."
- API 오류 → 에러 메시지 표시
7.3.8 관리자 계정 관리 (S-ADMIN) - /admin/operators/*
페이지: 운영자(관리자) 계정 생성, 수정, 삭제, 역할 관리
URL 경로:
- 목록:
/admin/operators - 생성:
/admin/operators/create - 상세:
/admin/operators/:id - 수정:
/admin/operators/:id/edit
파일:
src/pages/admin/OperatorManagement.tsxsrc/pages/admin/OperatorCreate.tsxsrc/pages/admin/OperatorDetail.tsxsrc/pages/admin/OperatorEdit.tsx
접근 권한:
- Admin 인증 필요 (
AdminProtectedRoute) - S-ADMIN 전용 (
hasAccountManagementPermission)
주요 기능:
- 운영자 목록 조회 (페이징, 검색, 역할 필터)
- 운영자 생성 (이메일, 비밀번호, 이름, 역할)
- 운영자 상세 보기
- 운영자 수정 (이름, 역할, 상태)
- 운영자 삭제 (논리 삭제)
- 운영자 일괄 삭제
- 이메일 중복 확인
- 비밀번호 변경
API (상세는 API 규격서 참조):
GET /api/admin/account- 운영자 목록POST /api/admin/account- 운영자 생성GET /api/admin/account/:id- 운영자 상세PUT /api/admin/account/:id- 운영자 수정DELETE /api/admin/account/:id- 운영자 삭제DELETE /api/admin/account/batch- 운영자 일괄 삭제POST /api/admin/account/check-email- 이메일 중복 확인
권한별 UI 차이:
- VIEWER/EDITOR/ADMIN: 메뉴 자체가 숨김
- S-ADMIN: 모든 기능 사용 가능
Error:
- 인증 실패 →
/admin/login리다이렉트 - 권한 부족 (S-ADMIN 아님) → "접근 권한이 없습니다."
- 이메일 중복 → "이미 사용 중인 이메일입니다."
- API 오류 → 에러 메시지 표시
7.3.9 OpenAPI 클라이언트 관리 (전체) - /admin/openapi/*
페이지: OpenAPI 클라이언트 및 API 키 승인 관리
URL 경로:
- 클라이언트 목록:
/admin/openapi/clients - 클라이언트 상세:
/admin/openapi/clients/:id - 클라이언트 수정:
/admin/openapi/clients/:id/edit - 승인 요청 목록:
/admin/openapi/requests - 승인 요청 상세:
/admin/openapi/requests/:id
파일:
src/pages/admin/OpenApiManage.tsx(클라이언트 목록)src/pages/admin/OpenApiDetail.tsx(클라이언트 상세)src/pages/admin/OpenApiEdit.tsx(클라이언트 수정)src/pages/admin/OpenApiRequests.tsx(승인 요청 목록)src/pages/admin/OpenApiRequestDetail.tsx(승인 요청 상세)
접근 권한:
- Admin 인증 필요 (
AdminProtectedRoute) - 조회: VIEWER+ (모든 관리자)
- 승인/거부/연장/삭제: ADMIN+ (
hasUserAccountEditPermission)
주요 기능:
- OpenAPI 클라이언트 목록 조회 (사용자별 API 키 현황)
- 클라이언트 상세 보기 (보유 API 키 목록)
- API 키 승인 요청 목록 (
activeYn='N',activeAt=null) - API 키 승인/거부
- API 키 유효기간 연장
- API 키 삭제
API (상세는 API 규격서 참조):
GET /api/admin/open-api- OpenAPI 클라이언트 목록GET /api/admin/open-api/:id- 클라이언트 상세PUT /api/admin/open-api/:id/approve- API 키 승인PUT /api/admin/open-api/:id/reject- API 키 거부PUT /api/admin/open-api/:id/extend- API 키 연장DELETE /api/admin/open-api/:id- API 키 삭제
API 키 형식:
- 형식: 60자 길이의 16진수 문자열 (hex)
- 정규식:
/^[a-f0-9]{60}$/ - 생성 방식: Backend의
authKeyGenerator.generate()함수 사용crypto.randomBytes(30)기반 생성 (30 bytes = 60 hex characters)
- 예시:
a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456
권한별 UI 차이:
- VIEWER/EDITOR: 목록/상세 조회만, 승인/거부/연장 버튼 숨김
- ADMIN/S-ADMIN: 모든 기능 사용 가능
Error:
- 인증 실패 →
/admin/login리다이렉트 - 권한 부족 → 버튼 비활성화
- API 오류 → 에러 메시지 표시
7.3.10 코드 관리 (S-ADMIN) - /admin/code/*
페이지: 공통 코드 그룹 및 코드 관리
URL 경로:
- 코드 목록:
/admin/code - 코드 그룹 상세:
/admin/code/group/:groupId - 코드 생성:
/admin/code/create - 코드 상세:
/admin/code/:id - 코드 수정:
/admin/code/:id/edit
파일:
src/pages/admin/CodeManagement.tsxsrc/pages/admin/CodeGroupDetail.tsxsrc/pages/admin/CodeCreate.tsxsrc/pages/admin/CodeDetail.tsx
접근 권한:
- Admin 인증 필요 (
AdminProtectedRoute) - S-ADMIN 전용 (
hasAccountManagementPermission)
주요 기능:
- 코드 그룹 목록 조회
- 코드 그룹별 상세 (하위 코드 목록)
- 코드 생성
- 코드 수정
- 코드 삭제
API (상세는 API 규격서 참조):
GET /api/admin/common-code- 코드 그룹 목록GET /api/admin/common-code/group/:id- 코드 그룹 상세POST /api/admin/common-code- 코드 생성PUT /api/admin/common-code/:id- 코드 수정DELETE /api/admin/common-code/:id- 코드 삭제
권한별 UI 차이:
- VIEWER/EDITOR/ADMIN: 메뉴 자체가 숨김
- S-ADMIN: 모든 기능 사용 가능
Error:
- 인증 실패 →
/admin/login리다이렉트 - 권한 부족 → "접근 권한이 없습니다."
- API 오류 → 에러 메시지 표시
7.4 공통 페이지 (Common)
7.4.1 404 페이지 - /404
페이지: 페이지를 찾을 수 없을 때 표시
URL 경로: /404 (자동 리다이렉트)
접근 권한:
- Public
주요 기능:
- "페이지를 찾을 수 없습니다." 메시지
- 홈으로 돌아가기 버튼
7.4.2 에러 페이지 - /error
페이지: 시스템 오류 발생 시 표시
URL 경로: /error
접근 권한:
- Public
주요 기능:
- "시스템 오류가 발생했습니다." 메시지
- 홈으로 돌아가기 버튼
8. 공통 컴포넌트
8.1 컴포넌트 분류 및 역할
Frontend의 공통 컴포넌트는 재사용성과 일관된 UI를 위해 체계적으로 분류됩니다.
분류 체계:
- 레이아웃 컴포넌트: 페이지 전체 구조 (Layout, AppBar, Footer 등)
- 관리자 전용 컴포넌트: 관리자 페이지 전용 (SideNav, AdminPageHeader)
- 공통 UI 컴포넌트: 범용 재사용 컴포넌트 (DataTable, Pagination, StatusChip 등)
- 폼 컴포넌트: 입력 폼 (LoginForm, ProfileForm)
- 피드백 컴포넌트: 로딩, 에러, 토스트 등
8.2 컴포넌트 목록 (테이블 형태)
| 컴포넌트명 | 경로 | 설명 | 주요 사용처 |
|---|---|---|---|
| 레이아웃 | |||
Layout.tsx |
/components/ |
전체 레이아웃 (Header + Content + Footer) | 모든 페이지 |
AppBar.tsx |
/components/ |
사용자 상단 앱바 | User 페이지 |
AppBarCommon.tsx |
/components/ |
공통 앱바 로직 | AppBar 공유 |
AdminMenuBar.tsx |
/components/ |
관리자 메뉴바 | Admin 페이지 |
Footer.tsx |
/components/ |
푸터 | 모든 페이지 |
| 관리자 전용 | |||
AdminPageHeader.tsx |
/components/admin/ |
관리자 페이지 헤더 | Admin 페이지 상단 |
SideNav.tsx |
/components/admin/ |
관리자 사이드 네비게이션 (권한별 메뉴 표시) | Admin 페이지 좌측 |
| 데이터 표시 | |||
DataTable.tsx |
/components/common/ |
데이터 테이블 (정렬, 클릭 이벤트) | 모든 목록 페이지 |
TableListBody.tsx |
/components/common/ |
테이블 리스트 본문 | 테이블 형식 목록 |
CardListBody.tsx |
/components/common/ |
카드 리스트 본문 | 카드 형식 목록 |
ListItemCard.tsx |
/components/common/ |
리스트 아이템 카드 | 카드 리스트 |
EmptyState.tsx |
/components/common/ |
빈 상태 표시 ("데이터가 없습니다.") | 빈 목록 페이지 |
| 페이지 구성 | |||
PageHeader.tsx |
/components/common/ |
페이지 헤더 | 페이지 상단 |
PageTitle.tsx |
/components/common/ |
페이지 제목 | 페이지 타이틀 |
ListHeader.tsx |
/components/common/ |
리스트 헤더 (검색, 필터) | 목록 페이지 상단 |
ListScaffold.tsx |
/components/common/ |
리스트 스캐폴드 (목록 구조 템플릿) | 목록 페이지 |
ListTotal.tsx |
/components/common/ |
리스트 총 개수 표시 | 목록 페이지 |
Pagination.tsx |
/components/common/ |
페이지네이션 | 목록 페이지 하단 |
| 입력 및 액션 | |||
SelectField.tsx |
/components/common/ |
셀렉트 필드 (드롭다운) | 검색 필터, 폼 입력 |
ThemedButton.tsx |
/components/common/ |
테마 버튼 (User/Admin 색상) | 모든 버튼 |
ByteLimitHelper.tsx |
/components/common/ |
바이트 제한 헬퍼 (텍스트 입력) | 폼 입력 |
| 피드백 | |||
StatusChip.tsx |
/components/common/ |
상태 칩 (활성/비활성/대기 등) | 상태 표시 |
QnaTypeChip.tsx |
/components/common/ |
QnA 타입 칩 (공개/비공개) | QnA 목록 |
ThemedCard.tsx |
/components/common/ |
테마 카드 | 대시보드, 통계 |
LoadingSpinner.tsx |
/components/ |
로딩 스피너 | API 호출 중 |
ErrorAlert.tsx |
/components/ |
에러 알림 | 에러 발생 시 |
| 다이얼로그 | |||
CommonDialog.tsx |
/components/ |
공통 다이얼로그 (확인/취소) | 삭제 확인 등 |
CommonToast.tsx |
/components/ |
공통 토스트 (알림) | 성공/실패 메시지 |
ToastProvider.tsx |
/components/ |
토스트 프로바이더 | 전역 토스트 관리 |
ExtendKeyDialog.tsx |
/components/common/ |
API 키 연장 다이얼로그 | OpenAPI 키 연장 |
| 폼 | |||
LoginForm.tsx |
/components/ |
로그인 폼 (공통) | User/Admin 로그인 |
ProfileForm.tsx |
/components/ |
프로필 폼 (공통) | User/Admin 프로필 |
| 라우트 가드 | |||
ProtectedRoute.tsx |
/components/ |
권한 체크 Guard (PrivateRoute, AdminProtectedRoute) | 보호된 라우트 |
총 30개 이상의 컴포넌트가 체계적으로 분류되어 재사용됩니다.
9. 유틸리티 함수
9.1 유틸리티 함수 목록 (테이블 형태)
| 파일 | 주요 함수 | 설명 | 사용처 |
|---|---|---|---|
auth.ts |
isSAdmin, isAdmin, hasContentEditPermission, hasUserAccountEditPermission, hasAccountManagementPermission, hasMenuAccess, hasActionPermission |
권한 체크 함수 (8개) | 모든 관리자 페이지 (버튼, 메뉴 제어) |
jwt.ts |
isValidTokenFormat, extractTokenInfo, isTokenExpired, getTokenTimeRemaining, shouldRefreshToken |
JWT 토큰 검증 및 만료 체크 | 인증 관련 모든 로직 |
apiResponseHandler.ts |
enhanceApiResponse, handleApiResponse, shouldShowPopup, shouldAutoLogout, getRedirectUrl |
API 응답 강화 및 에러 핸들링 | 모든 API 호출 |
date.ts |
formatYmd, formatYmdHm, formatRelativeTime |
날짜 포맷팅 (YYYY-MM-DD, YYYY-MM-DD HH:mm 등) | 목록, 상세 페이지 (날짜 표시) |
openApiStatus.ts |
getOpenApiKeyStatus |
OpenAPI 키 상태 판단 (PENDING, ACTIVE, EXPIRED, REJECTED) | OpenAPI 관리 페이지 |
9.2 Custom React Hooks (테이블 형태)
| Hook | 주요 기능 | 목적 |
|---|---|---|
useDataFetching |
API 호출, 로딩/에러/빈 상태 자동 관리, 자동/수동 페칭 | 목록 페이지의 반복 코드 제거 (로딩, 에러, 빈 상태 통합 관리) |
useQuerySync |
URL 쿼리 파라미터 동기화 (page, limit, search 등) | 검색/페이징 상태를 URL에 유지 (뒤로가기 지원, 공유 가능) |
usePagination |
페이지 상태 관리 (page, limit, total, totalPages) | 페이지네이션 로직 통합 |
usePasswordValidation |
비밀번호 실시간 검증 (8자 이상, 영문/숫자/특수문자 포함) | 회원가입, 비밀번호 변경 시 즉시 피드백 |
useInputWithTrim |
입력 값 자동 trim 처리 (앞뒤 공백 제거) | 사용자 입력 정규화 (이메일, 이름 등) |
useErrorHandler |
에러 상태 관리 및 자동 초기화 | 에러 처리 통합 (자동 닫기 타이머) |
useCommonCode |
공통 코드 조회 및 캐싱 | 드롭다운, 필터에서 코드 목록 재사용 |
10. 환경 설정 및 빌드
10.1 환경 변수 설정
10.1.1 .env 파일 구조
파일 위치: fe/.env (개발), fe/.env.production (프로덕션)
Vite 환경 변수 규칙:
- 모든 환경 변수는
VITE_prefix 필수 import.meta.env.VITE_*로 접근
10.1.2 주요 환경 변수
| 변수명 | 설명 | 기본값 | 예시 |
|---|---|---|---|
VITE_API_BASE_URL |
Backend API URL | http://localhost:30000 |
http://api.example.com |
VITE_API_TIMEOUT |
API 타임아웃 (ms) | 10000 (10초) |
30000 (30초) |
VITE_OPEN_API_DOC_URL |
OpenAPI 문서 URL | http://localhost:8080/api-docs |
http://api.example.com/docs |
VITE_OPEN_API_SERVER_URL |
OpenAPI 서버 URL | http://localhost:8080 |
http://api.example.com |
VITE_BASE |
Base URL (서브 경로 배포) | / |
/admin |
VITE_PORT |
개발 서버 포트 | 5173 |
3000 |
사용 예시 (src/config.ts):
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:30000';
export const API_TIMEOUT = Number(import.meta.env.VITE_API_TIMEOUT) || 10000;
export const OPEN_API_DOC_URL = import.meta.env.VITE_OPEN_API_DOC_URL || 'http://localhost:8080/api-docs';
10.2 빌드 설정
10.2.1 Vite 플러그인 설정
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [react()],
base: env.VITE_BASE || '/',
server: {
port: Number(env.VITE_PORT) || 5173,
},
build: {
outDir: 'dist',
},
}
})
플러그인:
@vitejs/plugin-react: React Fast Refresh, JSX 변환
10.2.2 개발 서버 설정
- 포트:
VITE_PORT(기본 5173) - HMR: Hot Module Replacement 자동 지원
- CORS: Backend API 프록시 필요 시 설정 가능
10.2.3 빌드 출력 설정
- 출력 디렉토리:
dist/ - 정적 자산:
dist/assets/(JS, CSS, 이미지 등) - 빌드 정보:
dist/build-info.json(버전, 빌드 시각)
10.3 TypeScript 설정
Project References:
{
"references": [
{ "path": "../packages/common" }, // Common 패키지 참조
{ "path": "./tsconfig.app.json" }, // App 설정
{ "path": "./tsconfig.node.json" } // Node 설정 (Vite)
]
}
경로 매핑:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"packages/common/*": ["../packages/common/*"]
}
}
}
10.4 빌드 및 배포
10.4.1 개발 환경 실행 (npm run dev)
cd fe
npm run dev
실행 결과:
VITE v5.0.8 ready in 500 ms ➜ Local: http://localhost:5173/ ➜ Network: http://192.168.x.x:5173/ ➜ press h to show help
10.4.2 프로덕션 빌드 (npm run build)
cd fe
npm run build
빌드 단계:
1. rimraf dist # 이전 빌드 삭제 2. tsc -b # TypeScript 컴파일 (타입 체크) 3. vite build # Vite 빌드 (번들링, 최적화) 4. node scripts/build-info.js # 빌드 정보 생성
빌드 결과:
dist/
index.html # 엔트리 HTML
build-info.json # 빌드 정보 (버전, 빌드 시각)
assets/
index-[hash].js # 번들 JS
index-[hash].css # 번들 CSS
logo-[hash].png # 이미지 자산
10.4.3 빌드 결과물 (/dist)
파일 구조:
dist/
index.html # SPA 엔트리 포인트
build-info.json # { version, buildDate }
assets/
index-[hash].js # Main JS Bundle
vendor-[hash].js # Vendor Bundle (React, MUI 등)
index-[hash].css # Main CSS Bundle
빌드 정보 예시 (build-info.json):
{
"version": "1.0.0",
"buildDate": "2025-11-07 14:30:45.123"
}
10.4.4 배포 시 주의사항
1. SPA 라우팅 설정 (Nginx fallback)
Nginx 설정 예시:
location / {
root /var/www/html/fe/dist;
try_files / /index.html; # SPA Fallback
}
이유: React Router는 클라이언트 라우팅이므로, 모든 경로를 index.html로 fallback 필요
2. 환경 변수 주입
프로덕션 빌드 전 .env.production 설정:
VITE_API_BASE_URL=https://api.production.com
VITE_API_TIMEOUT=30000
3. CORS 설정 확인
Backend API 서버에서 Frontend Origin 허용 필요:
// BE: src/index.ts
app.use(cors({
origin: 'https://admin.production.com', // Frontend URL
credentials: true
}));
4. 빌드 정보 표시 (scripts/build-info.js)
빌드 버전 및 빌드 시각을 dist/build-info.json으로 자동 생성하여 버전 추적 가능
11. 예외 처리 및 에러 핸들링
11.1 API 에러 처리
11.1.1 네트워크 오류, 타임아웃
네트워크 오류 처리:
// src/api/api.ts
try {
const res = await fetch(url, { signal: controller.signal });
// ...
} catch (e: any) {
if (e.name === 'AbortError') {
return enhanceApiResponse({
success: false,
errorMessage: '요청 시간이 초과되었습니다.',
errorCode: ErrorCode.REQUEST_TIMEOUT
});
}
return enhanceApiResponse({
success: false,
errorMessage: '네트워크 오류가 발생했습니다.',
errorCode: ErrorCode.NETWORK_ERROR
});
}
타임아웃 설정:
- 기본 타임아웃: 10초
AbortController를 통한 요청 취소
사용자 메시지:
- 타임아웃: "요청 시간이 초과되었습니다."
- 네트워크 오류: "네트워크 오류가 발생했습니다."
11.1.2 인증 오류 (401) - 자동 토큰 갱신 시도
401 에러 처리 Flow:
API 요청 → 401 Unauthorized
│
▼
Refresh Token 유효성 확인
│
├─ 유효? → Refresh Token으로 새 토큰 발급
│ │
│ ├─ 성공 → 동일 API 재시도 (1회만)
│ │ │
│ │ ├─ 성공 → 정상 응답 반환
│ │ └─ 실패 → 에러 응답 반환
│ │
│ └─ 실패 → 자동 로그아웃 + 로그인 페이지 리다이렉트
│
└─ 만료? → 자동 로그아웃 + 로그인 페이지 리다이렉트
자동 처리:
enhanceApiResponse()가autoLogout: true설정getRedirectUrl()이 User/Admin 타입별 로그인 페이지 반환- 토큰 제거 후 자동 리다이렉트
사용자 메시지:
- "인증이 필요합니다. 다시 로그인해주세요."
11.1.3 권한 오류 (403)
권한 오류 처리:
if (data.errorCode === ErrorCode.ACCESS_DENIED) {
return {
...response,
errorMessage: '접근 권한이 없습니다.',
redirectTo: '/', // 홈으로 리다이렉트
};
}
사용자 메시지:
- "접근 권한이 없습니다."
처리 방식:
- 에러 메시지 표시 후 홈으로 리다이렉트
11.1.4 서버 오류 (500)
서버 오류 처리:
if (res.status >= 500) {
return enhanceApiResponse({
success: false,
errorMessage: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.',
errorCode: ErrorCode.INTERNAL_SERVER_ERROR
});
}
사용자 메시지:
- "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요."
처리 방식:
- 에러 메시지 표시
- 재시도 없음 (사용자가 수동으로 재시도)
11.2 사용자 친화적 에러 메시지
11.2.1 ErrorCode 기반 메시지 생성 (createUserFriendlyMessage)
파일: src/api/api.ts
function createUserFriendlyMessage(data: any): string {
if (data?.errorMessage) {
return data.errorMessage; // Backend에서 제공한 메시지 우선
}
if (data?.errorCode) {
switch (data.errorCode) {
case ErrorCode.UNAUTHORIZED:
return '인증이 필요합니다. 다시 로그인해주세요.';
case ErrorCode.TOKEN_EXPIRED:
return '로그인 세션이 만료되었습니다. 다시 로그인해주세요.';
case ErrorCode.INVALID_TOKEN:
return '유효하지 않은 인증 정보입니다. 다시 로그인해주세요.';
case ErrorCode.ACCESS_DENIED:
return '접근 권한이 없습니다.';
case ErrorCode.USER_NOT_FOUND:
return '사용자를 찾을 수 없습니다.';
case ErrorCode.LOGIN_FAILED:
return '로그인에 실패했습니다. 아이디와 비밀번호를 확인해주세요.';
case ErrorCode.NETWORK_ERROR:
return '네트워크 오류가 발생했습니다.';
case ErrorCode.REQUEST_TIMEOUT:
return '요청 시간이 초과되었습니다.';
default:
return '오류가 발생했습니다. 다시 시도해주세요.';
}
}
return '알 수 없는 오류가 발생했습니다.';
}
메시지 우선순위:
- Backend
errorMessage(있으면 우선 사용) - ErrorCode 기반 Frontend 메시지
- 기본 메시지
11.2.2 Toast/Alert를 통한 에러 표시
ErrorAlert 컴포넌트:
// src/components/ErrorAlert.tsx
<Alert severity="error" onClose={onClose}>
{error}
</Alert>
CommonToast 사용:
// src/components/ToastProvider.tsx
const { showToast } = useToast();
// 성공 메시지
showToast('FAQ가 성공적으로 생성되었습니다.', 'success');
// 에러 메시지
showToast('FAQ 생성에 실패했습니다.', 'error');
표시 방식:
- Alert: 페이지 상단에 고정 표시 (닫기 버튼 포함)
- Toast: 화면 하단에 자동 사라지는 알림 (3-5초)
11.3 토큰 갱신 실패 시 처리
11.3.1 자동 로그아웃 및 로그인 페이지 리다이렉트
처리 Flow:
토큰 갱신 실패
│
▼
removeTokens() 호출 (현재 타입의 토큰 제거)
│
▼
clearLoginInfo() 호출 (현재 타입의 사용자 정보 제거)
│
▼
리다이렉트 URL 생성
│
├─ Admin 타입? → /admin/login
└─ User 타입? → /login
│
▼
window.location.href = redirectTo (페이지 이동)
파일: src/utils/apiResponseHandler.ts
export function handleApiResponse<T>(response: ApiResponse<T>, onSuccess, onError) {
if (!response.success) {
// 자동 로그아웃 처리
if (response.autoLogout) {
const userType = getUserType();
removeTokensByType(userType === 'A' ? 'A' : 'U');
}
// 리다이렉트 처리
if (response.redirectTo) {
setTimeout(() => {
window.location.href = response.redirectTo;
}, 100); // 에러 메시지 표시 후 이동
}
}
}
자동 로그아웃 발생 ErrorCode:
TOKEN_EXPIRED(40102)INVALID_TOKEN(40103)UNAUTHORIZED(40101)TOKEN_REQUIRED(40104)
11.4 권한 부족 시 UI 처리
11.4.1 버튼 비활성화, 메뉴 숨김, 경고 메시지
버튼 비활성화 예시:
const canEdit = hasContentEditPermission(adminRole);
<Button
disabled={!canEdit}
onClick={handleCreate}
>
생성
</Button>
{!canEdit && (
<Tooltip title="편집 권한이 없습니다.">
<InfoIcon />
</Tooltip>
)}
메뉴 숨김 예시:
// src/components/admin/SideNav.tsx
const adminRole = getAdminRole();
{hasAccountManagementPermission(adminRole) && (
<MenuItem onClick={() => navigate('/admin/operators')}>
운영자 관리
</MenuItem>
)}
경고 메시지 예시:
if (!hasContentEditPermission(adminRole)) {
showToast('편집 권한이 없습니다.', 'error');
return;
}
권한 체크 시점:
- 라우트 가드:
AdminProtectedRoute(인증 체크) - 메뉴 렌더링: 역할별 메뉴 표시/숨김
- 버튼 클릭: 액션 실행 전 권한 확인
- API 호출 전: 클라이언트 측 사전 검증
12. 성능 최적화
12.1 코드 스플리팅 (Lazy Loading)
12.1.1 관리자 페이지 Lazy Loading (React.lazy() + Suspense)
파일: src/App.tsx
import { Suspense, lazy } from 'react';
import LoadingSpinner from './components/LoadingSpinner';
// 관리자 페이지 Lazy Loading
const AdminProfile = lazy(() => import('./pages/admin/AdminProfile'));
const AdminFaqList = lazy(() => import('./pages/admin/FaqList'));
const AdminQnaList = lazy(() => import('./pages/admin/QnaManage'));
const AdminNoticeList = lazy(() => import('./pages/admin/NoticeManage'));
const AdminOpenApiClients = lazy(() => import('./pages/admin/OpenApiManage'));
const AdminOpenApiRequests = lazy(() => import('./pages/admin/OpenApiRequests'));
// ... 추가 관리자 페이지
// Suspense로 래핑
<Route
path="/admin/faqs"
element={
<AdminProtectedRoute>
<Suspense fallback={<LoadingSpinner loading={true} />}>
<AdminFaqList />
</Suspense>
</AdminProtectedRoute>
}
/>
12.1.2 구현 위치: App.tsx
적용 페이지:
- 모든 관리자 페이지 (
/admin/*) - 공개 페이지 (초기 로드 필요)
- 사용자 페이지 (초기 로드 필요)
12.2 Vite 빌드 최적화
12.2.1 Tree Shaking (기본 제공)
Vite는 ES Module 기반으로 사용하지 않는 코드를 자동 제거합니다.
12.2.2 번들 크기 최적화
Chunk 분리:
dist/assets/ index-[hash].js # Main Bundle vendor-[hash].js # Vendor Bundle (React, MUI 등) admin-[hash].js # Admin 페이지 (Lazy Loading)
12.3 API 요청 최적화
12.3.1 토큰 자동 갱신 (중복 요청 방지)
중복 갱신 방지 메커니즘:
- 여러 API가 동시에 호출되어도 토큰 갱신은 1회만 발생
ensureValidToken()함수가 갱신 중 여부 체크- 불필요한 중복 요청 방지
효과:
- 서버 부하 감소
- 토큰 갱신 속도 향상
12.3.2 API 타임아웃 설정 (10초)
기본 타임아웃: 10초 (API_TIMEOUT)
커스텀 타임아웃:
// 대용량 데이터 조회 시 30초
await apiFetch('/api/admin/logs', { timeoutMs: 30000 });
13. 보안
13.1 토큰 보안
13.1.1 LocalStorage 사용 (XSS 주의 필요)
저장 위치: LocalStorage
보안 고려사항:
- XSS 공격에 취약: LocalStorage는 JavaScript로 접근 가능
- HttpOnly Cookie 대안: 현재는 LocalStorage 사용 (향후 개선 검토)
- HTTPS 필수: 프로덕션 환경에서는 HTTPS 사용 필수
완화 방안:
- 토큰 형식 검증 (
isValidTokenFormat) - 토큰 만료 체크 (자동 정리)
- Content Security Policy (CSP) 설정 권장
13.1.2 토큰 형식 검증 (isValidTokenFormat)
// src/utils/jwt.ts
export function isValidTokenFormat(token: string): boolean {
if (!token || typeof token !== 'string') return false;
const parts = token.split('.');
return parts.length === 3; // header.payload.signature
}
검증 시점:
- 토큰 저장 시 (
saveTokens) - 토큰 조회 시 (
getAccessToken,getRefreshToken) - 유효하지 않은 형식 → 자동 제거
13.1.3 토큰 만료 체크 (isTokenExpired)
// src/utils/jwt.ts
export function isTokenExpired(token: string): boolean {
try {
const decoded = jwtDecode(token) as any;
if (!decoded || !decoded.exp) return true;
const currentTime = Math.floor(Date.now() / 1000);
return decoded.exp < currentTime;
} catch {
return true;
}
}
검증 시점:
- 인증 상태 확인 시 (
isUserAuthenticated,isAdminAuthenticated) - API 요청 전 (
ensureValidToken) - 라우트 가드 실행 시 (
PrivateRoute,AdminProtectedRoute)
만료된 토큰 처리:
- 자동 제거 (
validateAndCleanTokens) - Refresh Token으로 갱신 시도
- 갱신 실패 시 로그인 페이지 리다이렉트
13.2 권한 체크
13.2.1 Frontend 권한 체크 (UI 제어 목적)
파일: src/utils/auth.ts
목적: 사용자 경험(UX) 개선
- 권한 없는 버튼 비활성화
- 권한 없는 메뉴 숨김
- 불필요한 API 호출 방지
13.2.2 Backend 권한 체크 (실제 보안 담당)
실제 보안: Backend의 authMiddleware, adminAuthMiddleware에서 담당
이중 검증 필요:
Frontend 권한 체크 (UX)
│
▼
API 요청
│
▼
Backend 권한 체크 (보안) 실제 보안
│
├─ 권한 있음 → 처리
└─ 권한 없음 → 403 Forbidden
13.2.3 이중 검증 필요성 (Frontend는 UX, Backend는 보안)
Frontend 권한 체크:
- UX 개선: 권한 없는 버튼 숨김
- 사전 검증: 불필요한 API 호출 방지
- 보안 역할 아님: 우회 가능
Backend 권한 체크:
- 실제 보안: 서버에서 강제 검증
- 우회 불가능: 클라이언트에서 조작 불가
- 최종 검증: 모든 요청에 대해 검증
권장 사항:
- Frontend: UX 개선 목적으로만 사용
- Backend: 실제 보안 검증 (필수)
- 두 검증 모두 구현하여 UX와 보안 모두 확보
14. 부록
14.1 주요 npm 패키지 설명 (테이블 형태)
| 패키지 | 버전 | 카테고리 | 설명 |
|---|---|---|---|
react |
18.2.0 | Core | UI 라이브러리 |
react-dom |
18.2.0 | Core | React DOM 렌더링 |
react-router-dom |
6.20.1 | Routing | SPA 라우팅 |
typescript |
5.x | Dev | 정적 타입 체킹 |
@mui/material |
5.15.0 | UI | Material-UI 컴포넌트 |
@mui/icons-material |
5.15.0 | UI | Material-UI 아이콘 |
@emotion/react |
11.11.1 | UI | CSS-in-JS (MUI 의존성) |
@emotion/styled |
11.11.0 | UI | Styled Components |
jwt-decode |
4.0.0 | Auth | JWT 토큰 디코딩 |
axios |
1.11.0 | HTTP | HTTP 클라이언트 (실제로는 fetch 사용) |
vite |
5.0.8 | Build | 빌드 도구 및 개발 서버 |
@vitejs/plugin-react |
4.2.1 | Build | Vite React 플러그인 |
@iitp-dabt/common |
file:../packages/common | Shared | BE/FE 공유 코드 (검증, ErrorCode 등) |
rimraf |
- | Dev | 디렉토리 삭제 (빌드 전 정리) |
14.2 공통 타입 정의
파일 목록:
| 파일 | 설명 |
|---|---|
types/api.ts |
API 요청/응답 타입, ApiResponse<T> 인터페이스 |
types/errorCodes.ts |
ErrorCode 타입 (Common 패키지 재정의) |
주요 타입:
ApiResponse<T>:
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
errorCode?: number;
errorMessage?: string;
showPopup?: boolean;
redirectTo?: string;
autoLogout?: boolean;
details?: any;
}
DataState<T>:
export type DataState<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'empty' }
| { status: 'error'; error: string };
14.3 프로젝트 아키텍처 가이드 참조
관련 섹션:
- 3. 권한 체계 및 접근 제어: User/Admin 권한 구조 전체 개요
- 4. 시스템 연동 Flow: Client → FE → BE → DB Flow
- Appendix B: Common 패키지 구조: 공유 코드 상세
Common 패키지 활용 방법:
- 검증 함수:
isValidEmail,isValidPassword,isValidName - ErrorCode 체계: 11xxx-22xxx 범위
- API URL 상수:
FULL_API_URLS - 관리자 역할 코드:
CODE_SYS_ADMIN_ROLES
전체 시스템 연동 Flow (프로젝트 아키텍처 가이드 참조):
Client (Browser)
│
▼
Frontend (React SPA)
├─ 토큰 검증 (JWT)
├─ 권한 체크 (UI 제어)
└─ API 호출
│
▼
Backend (Express API)
├─ 인증 체크 (authMiddleware)
├─ 권한 체크 (adminAuthMiddleware, 역할 검증)
└─ 비즈니스 로직
│
▼
Database (PostgreSQL)
└─ 데이터 CRUD
14.4 Backend API 규격서 참조
각 페이지에서 사용하는 API 엔드포인트:
| 페이지 | 사용 API | 상세 규격서 참조 |
|---|---|---|
| 로그인 | POST /api/user/login |
API 규격서 - 인증 섹션 |
| 회원가입 | POST /api/user/register |
API 규격서 - 인증 섹션 |
| FAQ 관리 | GET/POST/PUT/DELETE /api/admin/faq |
API 규격서 - FAQ 섹션 |
| QnA 관리 | GET/POST/PUT/DELETE /api/admin/qna |
API 규격서 - QnA 섹션 |
| 공지사항 관리 | GET/POST/PUT/DELETE /api/admin/notice |
API 규격서 - 공지사항 섹션 |
| 사용자 관리 | GET/POST/PUT/DELETE /api/admin/user-accounts |
API 규격서 - 사용자 관리 섹션 |
| 운영자 관리 | GET/POST/PUT/DELETE /api/admin/account |
API 규격서 - 운영자 관리 섹션 |
| OpenAPI 키 관리 | GET/POST/PUT/DELETE /api/admin/open-api |
API 규격서 - OpenAPI 섹션 |
| 코드 관리 | GET/POST/PUT/DELETE /api/admin/common-code |
API 규격서 - 코드 관리 섹션 |
API 요청/응답 형식:
- 모든 API는
ApiResponse<T>형식 사용 - ErrorCode 기반 에러 처리
- 상세 내용은 API 규격서 참조
본 문서는 IITP DABT Admin Frontend 시스템의 상세 설계를 다루고 있습니다.
추가 정보:
- 프로젝트 아키텍처 가이드: 전체 시스템 구조 및 Common 패키지
- Backend 상세 설계서: Backend API 구현 및 데이터베이스 설계
- API 규격서: 모든 API 엔드포인트 상세 명세
- 배포 및 서버 설치 가이드: 빌드 및 배포 상세 절차