# Admin Dashboard 표준 배포 방식 전환 및 구조 리팩토링

**날짜**: 2025-11-17
**작성자**: admin
**관련 파일**: 
- `admin-dashboard/docker-compose.yml`
- `admin-dashboard/backend/main.py`
- `admin-dashboard/frontend/`
- `robeing-gateway/app/main.py`
- `nginx-infra/server-nginx-default`

---

## 문제 상황

### 1. 비표준 배포 방식
- FastAPI 백엔드가 정적 HTML 파일을 FileResponse로 서빙
- 표준 방식(nginx 직접 서빙)과 불일치
- frontend-customer는 이미 표준 방식으로 전환 완료

### 2. 폴더 구조 혼란
- `frontend-base` 폴더명이 역할을 명확히 표현하지 않음
- `admin-ui` 폴더명이 `frontend`보다 덜 직관적

### 3. Gateway JWT 검증 문제
- `/admin` 라우팅에서 JWT 검증을 필수로 하면, JWT가 없는 사용자는 401로 HTML을 받지 못해 로그인 페이지를 볼 수 없음

### 4. 토큰 불일치
- frontend-base가 `adminToken`을 사용하나, Gateway는 표준 JWT(`auth_token`)를 기대

## 해결 방안

### 1. 구조 변경

**폴더 리네임**:
- `frontend-base` → `admin-dashboard`
- `admin-ui` → `frontend`

**최종 구조**:
```
admin-dashboard/
├── backend/          # FastAPI 서버 (API만 처리)
├── frontend/         # React + Tailwind + shadcn + Vite
└── docker-compose.yml
```

### 2. 표준 배포 방식 채택

**nginx 설정** (`nginx-infra/server-nginx-default`):
```nginx
location /admin {
    alias /home/admin/admin-dashboard/frontend/;
    try_files $uri $uri/ /admin/index.html;
    index index.html;
}
```

**권한 설정**:
```bash
sudo chmod o+x /home
sudo chmod o+x /home/admin
```

**빌드 및 배포**:
- Vite로 `npm run build` 실행 → `dist/` 폴더 생성
- nginx가 `/home/admin/admin-dashboard/frontend/dist/` 직접 서빙
- FastAPI는 API만 처리 (`/admin/api/*`)

### 3. Gateway JWT 검증 수정

**robeing-gateway/app/main.py** - JWT 검증을 선택적으로 처리:
```python
@app.api_route("/admin/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def admin_proxy(
    path: str,
    request: Request,
    user_uuid: Optional[str] = Depends(get_verified_user_optional)  # 선택적
):
    # JWT가 없어도 HTML 반환, frontend JavaScript가 로그인 페이지 표시
```

### 4. 토큰 통일

**frontend/index.html** - `adminToken` → `auth_token`으로 변경:
```javascript
// Before: localStorage.setItem('adminToken', token)
// After:
localStorage.setItem('auth_token', token)
```

## 구현 완료

**커밋**: 920672a
**일시**: 2025-11-17
**브랜치**: main

**변경 사항**:
- `admin-dashboard/docker-compose.yml`: 컨테이너명, 볼륨 경로 수정
- `admin-dashboard/backend/main.py`: frontend 경로 수정
- `admin-dashboard/README.md`: 구조 설명 업데이트
- Git 원격 저장소: `admin-dashboard.git`로 변경
- `admin-dashboard/.gitea/workflows/deploy.yml`: 배포 스크립트 경로 수정 및 포트 충돌 방지
- `nginx-infra/server-nginx-default`: `/admin` 404 해결 - `root` → `alias` 변경 및 `try_files` 경로 수정

## 교훈

### 표준 방식의 중요성
- nginx가 정적 파일을 직접 서빙하는 것이 성능과 보안 측면에서 최적
- FastAPI는 API 처리에 집중, 정적 파일 서빙은 웹서버가 담당
- 참고: `250717_happybell80_auth서버구축및정적빌드배포전환.md`

### 폴더 구조 명확성
- 프로젝트명은 역할을 명확히 표현해야 함 (`admin-dashboard`)
- `frontend`가 `admin-ui`보다 직관적이고 표준적

### JWT 검증 전략
- HTML은 공개되어도 되고, 실제 데이터 API는 별도로 JWT 검증
- Gateway는 HTML 요청은 통과시키고, API 요청만 검증하는 선택적 검증 필요

### 토큰 통일
- 프로젝트 전체에서 표준 JWT 키(`auth_token`) 사용으로 일관성 확보
- Gateway와 frontend 간 토큰 키 불일치 방지

### CI/CD 배포 스크립트 수정

**문제**: 배포 스크립트가 이전 폴더명(`frontend-base`)을 참조하여 배포 실패

**해결**:
1. **배포 경로 수정** (`admin-dashboard/.gitea/workflows/deploy.yml`):
   - `/home/admin/frontend-base` → `/home/admin/admin-dashboard`
   - 백업 경로: `frontend-base.backup.*` → `admin-dashboard.backup.*`

2. **포트 충돌 방지**:
   - 기존 `frontend-base` 컨테이너 강제 제거
   - 포트 8000을 사용하는 모든 컨테이너 중지 및 제거
   ```bash
   # 강제로 컨테이너 제거 (포트 충돌 방지)
   sudo docker ps -a --filter "name=admin-dashboard" --format "{{.ID}}" | xargs -r sudo docker rm -f || true
   # frontend-base 컨테이너도 정리 (이전 이름)
   sudo docker ps -a --filter "name=frontend-base" --format "{{.ID}}" | xargs -r sudo docker rm -f || true
   # 포트 8000을 사용하는 컨테이너 확인 및 제거
   sudo docker ps --filter "publish=8000" --format "{{.ID}}" | xargs -r sudo docker stop || true
   sudo docker ps --filter "publish=8000" --format "{{.ID}}" | xargs -r sudo docker rm -f || true
   ```

**교훈**:
- 폴더명 변경 시 CI/CD 스크립트도 함께 업데이트 필요
- 배포 전 기존 컨테이너 완전 정리로 포트 충돌 방지
- 이전 이름의 컨테이너도 정리하여 혼란 방지

### nginx 설정 적용 및 404 해결

**문제**: `/admin` 경로에서 404 에러 발생

**원인**:
- nginx 설정 파일(`server-nginx-default`)은 수정했으나 실제 적용된 설정(`/etc/nginx/sites-enabled/default`)은 이전 설정 유지
- `root` 방식과 `alias` 방식의 차이로 인한 경로 불일치

**해결**:
1. **설정 파일 동기화**:
   ```bash
   sudo cp /home/admin/nginx-infra/server-nginx-default /etc/nginx/sites-enabled/default
   sudo nginx -t
   sudo systemctl reload nginx
   ```

2. **최종 nginx 설정**:
   ```nginx
   location /admin {
       alias /home/admin/admin-dashboard/frontend/;
       try_files $uri $uri/ /admin/index.html;
       index index.html;
   }
   ```

**검증**:
```bash
curl -I http://localhost/admin/  # HTTP/1.1 200 OK
```

**교훈**:
- 설정 파일 수정 후 반드시 실제 nginx 설정에 적용 필요
- `root` vs `alias` 차이 이해: `root`는 경로를 합치고, `alias`는 경로를 대체
- `try_files`의 fallback 경로도 `alias` 사용 시 `/admin/index.html`로 명시 필요

### 터미널 bash 문제 해결

**문제**: Cursor IDE 터미널에서 명령어 실행 시 오류 발생

**증상**:
- `cd` 명령어 실행 실패
- Git 명령어 실행 시 오류

**해결**:
- **Cursor IDE 재시작**: 터미널 설정이 다시 로드되어 문제 해결됨
- 또는 `Ctrl+Shift+P` → "Developer: Reload Window"

**교훈**:
- IDE 터미널 문제 발생 시 가장 먼저 IDE 재시작 시도
- 재시작으로 해결되지 않으면 shell 경로 확인 (`/bin/bash`)

### Admin 로그인 API 라우팅 문제 해결

**문제**: `/admin/api/login` POST 요청 시 404 오류 발생

**원인**:
1. **프론트엔드 JavaScript 실행 문제**: 폼 제출 시 POST 요청이 발생하지 않고 GET 요청으로 처리됨
2. **Gateway 라우팅 문제**: Gateway 컨테이너에서 `localhost:8000`으로 접근 시 자기 자신을 가리킴
3. **Docker 네트워크 문제**: 컨테이너 간 통신은 컨테이너 이름 또는 Docker 네트워크 IP 사용 필요

**해결**:

1. **프론트엔드 JavaScript 수정** (`admin-dashboard/frontend/index.html`):
   - form 태그에 `method="POST"` 및 `action="javascript:void(0);"` 추가
   - JavaScript 이벤트 리스너를 IIFE로 감싸서 확실히 실행되도록 수정
   - 에러 처리 개선 (showError 함수 의존성 제거)

2. **Gateway 라우팅 수정** (`robeing-gateway/app/routers/admin.py`):
   ```python
   # Before: ADMIN_BACKEND_URL = "http://localhost:8000"
   # After:
   ADMIN_BACKEND_URL = "http://admin-dashboard-backend:8000"
   ```
   - Docker 컨테이너 내부에서는 `localhost` 대신 컨테이너 이름 사용
   - 같은 Docker 네트워크(`appnet`)에 있는 컨테이너는 이름으로 접근 가능

3. **Gateway 라우터 등록 확인** (`robeing-gateway/app/main.py`):
   ```python
   app.include_router(admin.router, prefix="")
   ```
   - admin router에 prefix가 없으므로 main.py에서도 prefix="" 명시

**검증**:
```bash
curl -X POST http://localhost:8100/admin/api/login \
  -H "Content-Type: application/json" \
  -d '{"password":"19800508"}'
# {"access_token":"...", "token_type":"bearer"}
```

**교훈**:
- Docker 컨테이너 간 통신은 `localhost`가 아닌 컨테이너 이름 또는 네트워크 IP 사용
- 같은 Docker 네트워크에 있는 컨테이너는 이름으로 자동 DNS 해석됨
- 프론트엔드 JavaScript는 브라우저 환경에서 실행되므로 상대 경로(`/admin/api/login`) 사용 가능
- Gateway는 nginx를 통해 프록시되므로 nginx 설정(`/admin/api/` → Gateway) 확인 필요

### JavaScript 문법 오류 및 auth 리다이렉트 제거

**문제**: 
1. JavaScript "Illegal return statement" 오류 발생
2. JWT 토큰이 없을 때 auth.ro-being.com으로 리다이렉트됨 (admin dashboard는 자체 로그인 사용)

**원인**:
1. 스크립트 최상위 레벨에서 `return` 문 사용 (함수 밖에서 사용 불가)
2. admin dashboard는 자체 비밀번호 로그인을 사용해야 하는데 auth 서버로 리다이렉트하는 로직이 있음

**해결**:

1. **IIFE로 스크립트 감싸기** (`admin-dashboard/frontend/index.html`):
   ```javascript
   <script>
       (function() {
       // 모든 스크립트 코드
       // ...
       })();
   </script>
   ```
   - 전체 스크립트를 즉시 실행 함수로 감싸서 `return` 문 사용 가능하도록 수정

2. **auth 리다이렉트 로직 제거**:
   ```javascript
   // Before: JWT가 없으면 auth.ro-being.com으로 리다이렉트
   if (!jwtToken && !window.location.hostname.includes('auth.ro-being.com')) {
       window.location.href = 'https://auth.ro-being.com/login?redirect=...';
       return;
   }
   
   // After: JWT가 있으면 대시보드, 없으면 로그인 화면
   if (jwtToken) {
       showDashboard();
   } else {
       showLogin();
   }
   ```

3. **401 에러 처리 수정**:
   ```javascript
   // Before: 401 에러 시 auth 서버로 리다이렉트
   if (response.status === 401) {
       window.location.href = 'https://auth.ro-being.com/login?redirect=...';
   }
   
   // After: 401 에러 시 로그인 화면으로 전환
   if (response.status === 401) {
       localStorage.removeItem('auth_token');
       showLogin();
   }
   ```

**교훈**:
- JavaScript에서 최상위 레벨의 `return` 문은 함수 내부에서만 사용 가능
- IIFE를 사용하면 스크립트 전체를 함수로 감싸서 `return` 문 사용 가능
- Admin dashboard는 자체 인증 시스템을 사용하므로 auth 서버 리다이렉트 불필요

### API 경로 수정 및 JWT Secret Key 일치

**문제**: 
1. API 호출이 `/admin/*` 경로로 되어 있어 Gateway 라우팅과 불일치
2. JWT 토큰 검증 실패 (401 Unauthorized)

**원인**:
1. 프론트엔드에서 `/admin/system/overview` 같은 경로로 호출하지만 Gateway는 `/admin/api/*`만 처리
2. Gateway의 JWT_SECRET_KEY와 admin-dashboard backend의 SECRET_KEY가 불일치

**해결**:

1. **API 경로 자동 변환** (`admin-dashboard/frontend/index.html`):
   ```javascript
   async function apiCall(endpoint) {
       // endpoint가 /admin/로 시작하면 /admin/api/로 변경
       const apiEndpoint = endpoint.startsWith('/admin/') && !endpoint.startsWith('/admin/api/') 
           ? endpoint.replace('/admin/', '/admin/api/')
           : endpoint;
       
       const response = await fetch(apiEndpoint, {
           headers: {
               'Authorization': `Bearer ${token}`
           }
       });
       // ...
   }
   ```

2. **JWT Secret Key 일치** (`robeing-gateway/docker-compose.yml`):
   ```yaml
   services:
     robeing-gateway:
       environment:
         - JWT_SECRET_KEY=admin_secret_key_robeing_2025
   ```
   - admin-dashboard backend의 `SECRET_KEY = "admin_secret_key_robeing_2025"`와 동일하게 설정
   - Gateway 재빌드 필요: `docker compose down && docker compose up -d --build`

**검증**:
```bash
# JWT 토큰 발급
TOKEN=$(curl -s -X POST http://localhost:8000/admin/login \
  -H "Content-Type: application/json" \
  -d '{"password":"19800508"}' | jq -r '.access_token')

# API 호출 테스트
curl -X GET http://localhost:8100/admin/api/system/overview \
  -H "Authorization: Bearer $TOKEN"
# {"timestamp":"...","cpu":{"percent":...},...}
```

**교훈**:
- Gateway 라우팅 경로(`/admin/api/*`)와 프론트엔드 API 호출 경로 일치 필요
- JWT 검증을 위해서는 발급 서버와 검증 서버의 secret key가 반드시 일치해야 함
- Docker Compose의 `environment` 섹션으로 환경변수 오버라이드 가능

