# Web Platform (학습센터)

유튜브 링크와 PowerPoint(`.pptx`) 강의를 등록하고, 목록에서 클릭해 바로 시청할 수 있는 사내 학습센터 예제입니다.

---

## 접속 방법

**브라우저에서 열기 전에 반드시 터미널에서 서버를 실행해야 합니다.** (`npm start` 미실행 상태에서는 `http://localhost:8030`에 연결되지 않습니다.)

서버 실행 후 아래 주소로 접속합니다.

| 환경 | URL |
|------|-----|
| 로컬 기본 | [http://localhost:8030](http://localhost:8030) |
| 포트 변경 시 | `http://localhost:{PORT}` (예: `PORT=4000 npm start` → `http://localhost:4000`) |

- **메인 페이지** (`/`): 학습센터 뷰어 (강의 목록)
- **학습센터 뷰어** (`/learning`): 강의 검색/필터 + 카드 목록 (일반 사용자)
- **관리자** (`/admin`): 강의 등록(YouTube/PPT/웹 링크 등), 썸네일·AI 추가 등 통합 관리
- **강의 상세**: 카드 클릭 시 유튜브 재생 또는 PPT 뷰어
- **기타 메뉴**: 채팅(`/chat`), AI(`/ai-explore`), **프롬프트 라이브러리**(`/ai-explore/prompts`), **AI 성공 사례**(`/ai-cases`), **대시보드**(`/dashboard`, 성공 사례 아래 메뉴), AX 과제 신청(`/ax-apply`)
- **관리자 이벤트 로그**: `http://localhost:8030/admin/thumbnail-events?token={ADMIN_TOKEN}`

### 접속이 안 될 때 (트러블슈팅)

1. **서버가 떠 있는지 확인**  
   프로젝트 루트에서 `npm start`를 실행하고, 터미널에 `Server started: http://localhost:8030` 같은 로그가 나오는지 확인합니다.
2. **포트 번호 확인**  
   `.env`에 `PORT=...`가 있으면 **그 포트**로 접속합니다. (기본값은 `8030`)
3. **부팅 실패**  
   터미널에 `Failed to bootstrap:`이 나오면 프로세스가 종료되며 HTTP 서버가 뜨지 않습니다. 메시지를 확인한 뒤 `data/` 쓰기 권한, 디스크 여유, DB 설정 등을 점검합니다.
4. **포트 충돌**  
   다른 프로그램이 8030을 쓰는 경우 `PORT=8031 npm start`처럼 바꿔 실행하거나, **이미 떠 있는 Node 서버 프로세스를 종료**합니다. 구체적인 명령은 아래 **실행 방법** 절의 **기존 서버 프로세스 종료** 항목을 참고하세요.
5. **바인드 주소**  
   기본은 모든 네트워크 인터페이스(`0.0.0.0`)에서 수신합니다. 로컬 루프백만 쓰려면 `HOST=127.0.0.1 npm start`를 사용할 수 있습니다.

---

## 핵심 기능

- 학습센터 UI (좌측 메뉴 + 상단 헤더 + 강의 카드 레이아웃)
- **AI 탐색** (`/ai-explore`): 전체 너비 레이아웃, AI 서비스 카드(프롬프트·회의록 등). 검색창에 **「프롬프트」**가 포함된 채 검색(Enter) 시 프롬프트 라이브러리로 이동
- **대시보드** (`/dashboard`): AI 탐색과 유사한 카드 그리드·검색으로 대시보드를 모아 표시. 첫 카드 **경영성과 대시보드**는 `/dashboard/business-performance`로 연결
- **경영성과 대시보드** (`/dashboard/business-performance`): 상단 **엑셀 업로드**(`.xlsx`, 매출일보 시트) → DB(`mgmt_perf_uploads` / `mgmt_perf_snapshots`) 또는 DB 미연결 시 `data/mgmt-perf-last-state.json`에 스냅샷 저장. 하단 **대시보드 조회**는 기존 HTML 템플릿(Chart.js, 매출·수주·예상실적 탭)을 iframe(`/dashboard/business-performance/embed`)으로 표시. 엑셀에서 수치 집계를 치환하려면 `npm install`로 `xlsx` 패키지를 설치한 뒤 서버를 재시작하세요.
- **프롬프트 라이브러리** (`/ai-explore/prompts`): 업무별 기본 프롬프트 카드 선택·미리보기·클립보드 복사 (`data/company-prompts.json`). **좌측 메뉴(채팅·AI·AI 성공 사례·학습센터·AX 과제 신청)는 관리자 여부와 관계없이 접근 가능**(강의 삭제·관리자 대시보드 등 일부 기능은 관리자 모드에서만)
- 검색/필터/페이지네이션
  - 검색어(`q`) 기반 제목/설명/태그 필터
  - 타입(`YouTube`, `PPT`) 필터
  - 태그 필터 + 페이지네이션
- 유튜브 강의 등록/시청
  - 유튜브 URL 입력 후 목록에 추가
  - 강의 상세에서 iframe 임베드로 재생
- PPT 강의 등록/시청
  - `.pptx` 파일 업로드
  - 상세에서 슬라이드 텍스트 기반 뷰어 제공
  - 목록 카드에 PPT 프리뷰(첫 슬라이드 제목 + 장수) 표시
  - macOS 환경에서는 `qlmanage` 기반 실제 썸네일(첫 장 이미지) 자동 생성
- 썸네일 백그라운드 큐
  - 썸네일 생성은 비동기 큐에서 처리
  - 상태값: `pending` / `processing` / `ready` / `failed`
  - 실패 시 자동 재시도 정책 적용(최대 횟수 이후 `failed` 고정)
  - 큐 스냅샷을 `data/thumbnail-jobs.json`에 저장해 재시작 후 복구
  - 작업 이벤트를 `data/thumbnail-events.json`에 기록
- 관리자 삭제
  - 관리자 토큰으로 강의 삭제 가능
- 초기 샘플 데이터 시드
  - `resources/lecture`에 있는 `.pptx`를 최초 실행 시 자동 등록
- **관리자 인증·바로가기(UI)**
  - **좌측 메뉴 하단 `관리자` / `관리자 off`**: 운영(OPS)에서 **이메일 인증**으로 로그인한 임직원에게도 표시됩니다. **`관리자`**는 모달에서 `ADMIN_TOKEN`을 입력해 검증한 뒤 `/admin`으로 이동합니다(`POST /api/admin/validate-token`). **`관리자 off`**는 관리자 쿠키를 지우고 학습센터 목록(`/learning`)으로 돌아갑니다(`GET /admin/logout`). 이메일 미로그인 환경에서도 동일합니다. **이미 관리자 세션**이면 중복이므로 `관리자` 항목은 숨기고, **사용자 현황관리** → 구분선 → **관리자 off** →(OPS일 때 구분선)→ **로그아웃** 순으로 표시됩니다.
  - **학습센터** (`/learning`): 관리자 쿠키가 있을 때 상단 오른쪽 **학습 등록**으로 통합 관리 화면(`/admin`)에 들어갈 수 있습니다. 이메일(OPS) 로그인과 동시에 있어도 버튼이 숨겨지지 않습니다.
  - **AI 성공 사례** (`/ai-cases`): 관리자일 때 상단 **사례 등록·관리**로 편집 화면(`/ai-cases/write`)에 진입합니다(동일하게 OPS 로그인 중에도 표시).

---

## 프로젝트 구조

```text
ai_platform/
├─ server.js                  # Express 서버 진입점 (라우팅, 업로드, 썸네일 큐, DB 연동)
├─ package.json               # 의존성 및 npm 스크립트
├─ .env                       # 환경 변수 (실제 값, .gitignore 대상)
├─ .env.example               # 환경 변수 예시 템플릿
├─ db/
│  └─ schema.sql              # PostgreSQL 스키마 (강의·회의록·경영성과 업로드 등, 기동 시 자동 적용)
├─ scripts/
│  └─ apply-schema.js         # 수동 스키마 적용 스크립트 (npm run db:schema)
├─ public/
│  └─ styles.css              # 전역 스타일
├─ views/
│  ├─ partials/
│  │  ├─ nav.ejs              # 좌측 공통 네비게이션(하단 관리자·토큰 모달 연동)
│  │  └─ admin-token-modal.ejs # 관리자 토큰 입력 모달(`openAdminTokenModal`)
│  ├─ learning-viewer.ejs     # 학습센터 뷰어 (일반 사용자)
│  ├─ learning-admin.ejs      # 학습센터 관리 (업로드·삭제·썸네일)
│  ├─ chat.ejs                # 채팅
│  ├─ ai-explore.ejs          # AI 탐색 (전체 너비, 프롬프트 카드·검색)
│  ├─ dashboard.ejs           # 대시보드 목록(카드·검색)
│  ├─ dashboard-business-performance.ejs  # 경영성과(업로드 + iframe 조회)
│  ├─ mgmt_perf_embed.ejs       # 경영성과 차트 단독 페이지(iframe용)
│  ├─ partials/mgmt_perf_dashboard_container.ejs
│  ├─ ai-prompts.ejs          # 프롬프트 라이브러리 (카드·미리보기·복사)
│  ├─ ai-cases.ejs            # AI 성공 사례 목록(카드)
│  ├─ ai-case-detail.ejs       # AI 성공 사례 상세(마크다운)
│  ├─ ai-cases-write.ejs      # AI 성공 사례 관리자 등록·편집
│  ├─ ax-apply.ejs            # AX 과제 신청
│  ├─ lecture-youtube.ejs     # 유튜브 강의 상세 (iframe 임베드)
│  ├─ lecture-ppt.ejs         # PPT 강의 상세 (슬라이드 텍스트 뷰어)
│  └─ admin-thumbnail-events.ejs  # 썸네일 이벤트 로그 관리자 페이지
├─ data/
│  ├─ lectures.json           # 강의 메타데이터 (ENABLE_POSTGRES=0 또는 DB 폴백 시)
│  ├─ company-prompts.json    # AI 프롬프트 라이브러리 템플릿 (제목·설명·본문)
│  ├─ thumbnail-jobs.json     # 썸네일 큐 스냅샷 (재시작 후 복구용)
│  └─ thumbnail-events.json   # 썸네일 작업 이벤트 로그
├─ uploads/                   # 업로드된 .pptx 파일 저장
│  └─ thumbnails/             # 생성된 썸네일 이미지
└─ resources/
   └─ lecture/                # 초기 시드용 샘플 PPT (최초 실행 시 자동 등록)
```

### 구조 설명

| 구분 | 설명 |
|------|------|
| **서버** | `server.js`가 Express 앱, 라우트, Multer 업로드, 썸네일 백그라운드 워커, PostgreSQL 연동을 모두 담당 |
| **데이터 저장소** | `ENABLE_POSTGRES=1`이면 PostgreSQL `lectures` 테이블이 단일 소스, `0`이면 `data/lectures.json` 사용 |
| **썸네일** | 비동기 큐 처리, `data/thumbnail-jobs.json`으로 영속화, `data/thumbnail-events.json`에 이벤트 기록 |
| **뷰** | EJS 템플릿으로 메인/유튜브/PPT/관리자 페이지 렌더링 |

---

## 서버 배포 (새 머신·처음부터)

아래는 **저장소를 처음 받아 운영 서버에 올리는 경우**를 가정한 절차입니다. **macOS**(개발용·소규모 호스팅)와 **Linux**(일반적인 VPS·온프레미스 서버)를 구분했습니다.

### 공통 사전 요구사항

| 항목 | 설명 |
|------|------|
| **Node.js** | **v18 이상** 권장 (`node -v`로 확인) |
| **npm** | Node와 함께 설치 |
| **Git** | 저장소 클론용 |
| **PostgreSQL** | `ENABLE_POSTGRES=1` 사용 시 접속 가능한 DB(로컬 설치 또는 원격) |
| **환경 변수** | `.env.example`을 복사해 `.env` 작성(비밀번호·토큰은 반드시 변경) |

PPT 썸네일·슬라이드 이미지는 **macOS**에서는 `qlmanage`가 우선 사용되고, 그 외에는 **LibreOffice**·**poppler(`pdftoppm`)** 조합이 필요합니다. 도구가 없으면 텍스트 프리뷰로 동작합니다.

---

### macOS에서 배포 (새 머신)

1. **도구 설치**  
   - **Node.js**: [nodejs.org](https://nodejs.org/) LTS 설치, 또는 `brew install node@20` 등  
   - **Git**: Xcode Command Line Tools(`xcode-select --install`) 또는 `brew install git`  
   - **PostgreSQL**(로컬 DB를 쓸 때): `brew install postgresql@16` 후 서비스 기동, 또는 원격 DB만 사용  
   - **PPT 변환 강화(선택)**: `brew install --cask libreoffice`, `brew install poppler`

2. **코드 받기**  
   ```bash
   mkdir -p ~/workspace && cd ~/workspace
   git clone <저장소 URL> ai_platform
   cd ai_platform
   ```
   (이미 클론한 경우 예: `cd /Users/dsyoon/workspace/ai_platform`)

3. **의존성·환경**  
   ```bash
   npm install
   cp .env.example .env
   # 편집기로 .env 수정: PORT, ADMIN_TOKEN, DB_*, ENABLE_POSTGRES 등
   ```

4. **DB 스키마** (`ENABLE_POSTGRES=1`일 때)  
   ```bash
   npm run db:schema
   ```

5. **실행 방식 선택**  
   - **포그라운드(테스트)**: `npm start` → 터미널에 `Server started: http://localhost:8030` 확인 후 브라우저 접속  
   - **백그라운드(PM2)**: 아래 「[PM2로 실행](#pm2로-실행)」 절 참고 (`pm2 start … --name ai_platform`)

6. **접속**  
   같은 Mac에서: `http://127.0.0.1:8030` (또는 `.env`의 `PORT`). 다른 기기에서 접속하려면 `HOST=0.0.0.0`이 기본이므로, **macOS 방화벽**에서 Node 허용 여부를 확인합니다.

---

### Linux에서 배포 (새 서버)

배포 경로는 예시로 `/var/www/ai_platform`을 둡니다. 배포 사용자·그룹(`www-data` 등)은 배포 정책에 맞게 조정하세요.

1. **시스템 패키지** (Ubuntu/Debian 예시)  
   ```bash
   sudo apt update
   sudo apt install -y git build-essential
   ```  
   - **Node.js**: 배포판 기본 패키지가 오래된 경우가 많으므로 **[NodeSource](https://github.com/nodesource/distributions)** 또는 **nvm**으로 **v18+** 설치를 권장합니다.  
   - **PostgreSQL 클라이언트/서버**: 원격 DB만 쓰면 클라이언트 라이브러리만으로도 되고, 로컬 DB면 `postgresql` 패키지 설치 후 DB·사용자 생성.  
   - **PPT 변환(선택)**: `sudo apt install -y libreoffice poppler-utils`

2. **코드 배치**  
   ```bash
   sudo mkdir -p /var/www
   cd /var/www
   sudo git clone <저장소 URL> ai_platform
   cd ai_platform
   sudo chown -R "$USER:$USER" /var/www/ai_platform   # 이후 작업 사용자에 맞게 조정
   ```

3. **의존성·환경**  
   ```bash
   npm install --production
   cp .env.example .env
   nano .env   # 또는 vim — PORT, ADMIN_TOKEN, DB_*, ENABLE_POSTGRES 등
   ```

4. **DB 스키마** (`ENABLE_POSTGRES=1`일 때)  
   ```bash
   npm run db:schema
   ```

5. **프로세스 관리 (PM2 권장)**  
   ```bash
   sudo npm install -g pm2
   cd /var/www/ai_platform
   pm2 start server.js --name ai_platform
   pm2 save
   pm2 startup    # 부팅 시 자동 기동(출력되는 sudo 명령 실행)
   ```

6. **방화벽·리버스 프록시**  
   - 외부에 직접 `8030`을 열지 않고 **Nginx/Apache**로 TLS·프록시하는 구성이 일반적입니다.  
   - **Apache2** 예시(모듈·VirtualHost)는 [docs/DEPLOYMENT-xavis.ncue.net.md](docs/DEPLOYMENT-xavis.ncue.net.md)를 참고하세요.  
   - `ufw` 사용 시: `sudo ufw allow 80,443/tcp` 후 앱은 로컬에서만 듣게 하거나, 프록시 뒤에 둡니다.

7. **데이터 디렉터리 권한**  
   `data/`, `uploads/` 등에 앱 실행 사용자(예: `pm2`로 띄운 사용자, 또는 `www-data`)가 쓰기 가능해야 합니다. 실패 시 로그에 `Failed to bootstrap` 또는 권한 오류가 납니다.

---

### 배포 후 확인

- 터미널 또는 `pm2 logs ai_platform`에서 **에러 없이** `Server started` 로그 확인  
- 브라우저로 메인·`/learning`·관리자(토큰)까지 동작 확인  
- PostgreSQL 사용 시 `npm run db:schema`는 **최초 1회**(또는 스키마 변경 시); 백업·마이그레이션 정책은 운영 환경에 맞게 별도 수립  

---

## 실행 방법

**로컬에서 빠르게 띄우기**는 아래 순서면 됩니다. **새 서버에 맞춰 처음부터 배포**할 때는 위 「서버 배포 (새 머신·처음부터)」의 **macOS** 또는 **Linux** 절을 따르세요.

## 1) 의존성 설치

```bash
npm install
```

## 2) PostgreSQL 스키마 적용

```bash
npm run db:schema
```

## 3) 서버 실행

프로젝트 루트에서:

```bash
npm start
```

정상 기동 시 터미널에 `Server started: http://localhost:8030` (또는 설정한 `PORT`)가 출력됩니다. **이 상태에서만** 브라우저로 접속할 수 있습니다.

### PM2로 실행

프로젝트 루트에서 실행해야 `server.js`가 같은 디렉터리의 `.env`를 읽습니다( `dotenv` 기준).

**1) PM2 설치 (전역, 한 번)**

```bash
npm install -g pm2
```

**2) 프로젝트 디렉터리로 이동 후 의존성**

```bash
cd /Users/dsyoon/workspace/ai_platform
npm install
cp .env.example .env   # 최초 1회 — 값 편집
```

**3) 앱 기동**

```bash
pm2 start server.js --name ai_platform
```

`npm start`와 동일하게 `server.js`를 띄웁니다. 이름만 PM2에서 `ai_platform`으로 관리합니다.

**4) 재부팅 후에도 유지 (선택)**

```bash
pm2 save
pm2 startup
```

`pm2 startup`이 출력하는 `sudo env PATH=...` 한 줄을 그대로 실행한 뒤, 다시 `pm2 save`를 하면 부팅 시 자동 기동에 맞춰집니다.

**5) 자주 쓰는 명령**

| 목적 | 명령 |
|------|------|
| 상태 확인 | `pm2 list` |
| 로그(실시간) | `pm2 logs ai_platform` |
| 재시작 | `pm2 restart ai_platform` |
| 중지 | `pm2 stop ai_platform` |
| 목록에서 제거 | `pm2 delete ai_platform` |

**환경 변수**: 포트·DB 등은 프로젝트 루트의 `.env`에 두고, 변경 후 `pm2 restart ai_platform`으로 반영합니다. 별도 경로에 두었다면 해당 디렉터리에서 `pm2 start` 하거나, `ecosystem` 설정으로 `cwd`를 지정하세요.

운영/배포 환경에서 이미 PM2로 띄운 경우 재시작 예:

```bash
pm2 restart ai_platform
```

로컬에서 포그라운드로만 확인할 때:

```bash
node server.js
```

- 기본 포트: `8030`
- 접속 URL: [http://localhost:8030](http://localhost:8030)

### 포트·수신 주소 변경

```bash
PORT=8030 npm start
```

```bash
# 로컬 루프백만 (외부 네트워크 인터페이스에 바인딩하지 않음)
HOST=127.0.0.1 npm start
```

서버는 기본적으로 `HOST=0.0.0.0`으로 바인딩합니다(동일 기기의 `localhost` 접속에 사용).

### 기존 서버 프로세스 종료

터미널을 닫았는데도 이전에 실행한 `node server.js`가 남아 있거나, **포트가 이미 사용 중(`EADDRINUSE`)**이라 새로 `npm start`가 실패할 때, 기존 프로세스를 먼저 종료합니다.

**포트로 PID 확인 후 종료 (macOS / Linux, 기본 포트 8030 예시)**

```bash
lsof -i :8030
```

출력의 `PID` 열 값을 확인한 뒤:

```bash
kill PID
```

응답이 없으면 `kill -9 PID`로 강제 종료할 수 있으나, 다른 Node 작업이 같은 포트를 쓰는지 확인한 뒤 사용하세요.

**프로젝트 진입점만 대상으로 종료 (다른 `node` 작업에 영향을 줄 수 있으니 주의)**

```bash
pkill -f "node server.js"
```

**PM2로 띄운 경우**

```bash
pm2 list
pm2 stop ai_platform
# 완전히 제거하려면
pm2 delete ai_platform
```

Windows에서는 작업 관리자에서 `Node.js` 프로세스를 종료하거나, PowerShell에서 `Get-NetTCPConnection -LocalPort 8030` 등으로 점유 프로세스를 확인한 뒤 해당 PID를 종료합니다.

### 관리자 토큰/페이지 크기 설정(선택)

```bash
ADMIN_TOKEN=my-secret PAGE_SIZE=12 npm start
```

- `ADMIN_TOKEN` 미지정 시 기본값: `xavis-admin`
- `PAGE_SIZE` 미지정 시 기본값: `8`
- `.env` 파일이 있으면 `dotenv`로 자동 로드
- 브라우저에서는 좌측 메뉴 하단 **관리자** → 모달에 위 토큰을 입력해 세션 쿠키를 발급받는 방식으로 `/admin`에 진입할 수 있습니다(임직원 이메일 로그인 여부와 동일한 흐름).

### PostgreSQL 연결 설정

- `ENABLE_POSTGRES=1`일 때: PostgreSQL이 **단일 소스**로 사용됩니다. `data/lectures.json`은 사용하지 않습니다.
- `ENABLE_POSTGRES=0`일 때: `data/lectures.json`만 사용합니다.
- DB 연결 실패 시 자동으로 `data/lectures.json` 기반 파일 저장소로 폴백합니다.
- 필수 변수: `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`

### 채팅 기능 (OpenAI API)

- `.env`에 `OPENAI_API_KEY`를 넣은 뒤 **서버를 재시작**하면 채팅 메뉴(`/chat`)에서 OpenAI와 실제 대화가 이루어집니다.
- API 키 발급: [OpenAI Platform](https://platform.openai.com/api-keys)
- 키 값 앞뒤 공백은 자동으로 제거합니다.
- 화면의 모델 선택(`gpt-5.4`, `gpt-5-mini`)은 내부적으로 OpenAI에 전달할 **실제 모델 ID**로 매핑됩니다. 기본값은 각각 `gpt-4o`, `gpt-4o-mini`이며, 필요하면 `.env`에서 `OPENAI_MODEL_DEFAULT`, `OPENAI_MODEL_MINI`로 바꿀 수 있습니다.
- **웹 검색(기본 활성):** `OPENAI_WEB_SEARCH`가 `0`이 아니면 OpenAI **Responses API**와 내장 **`web_search`** 도구로 질의에 맞게 웹을 검색한 뒤 답변합니다. 끄려면 `OPENAI_WEB_SEARCH=0`으로 두면 이전과 같이 **Chat Completions**만 사용합니다. 선택적으로 `OPENAI_WEB_SEARCH_COUNTRY`(기본 `KR`), `OPENAI_WEB_SEARCH_CITY`, `OPENAI_WEB_SEARCH_REGION`, `OPENAI_WEB_SEARCH_TIMEZONE`(기본 `Asia/Seoul`)로 검색 맥락을 줄 수 있습니다.
- `GET /api/chat/config`는 `{ configured, webSearch }`를 반환합니다(`webSearch`는 위 플래그와 동일).
- 스트리밍(`POST /api/chat/stream`)은 SSE로 `data: {JSON}` 줄을 보냅니다. 타입 예: `delta`(본문 조각), `status`(예: `phase: "web_search"` — UI에서 «웹 검색 중…» 표시), `sources`(인용 URL·제목 목록), `done`, `error`. 비스트리밍 `POST /api/chat`은 필요 시 응답 JSON에 `sources` 배열을 포함할 수 있습니다.
- 키가 비어 있으면 `POST /api/chat`·`POST /api/chat/stream`은 503을 반환하고, 채팅 화면 상단에 안내 배너가 표시됩니다.

### PPT 썸네일 및 슬라이드 이미지 생성 제어(선택)

```bash
ENABLE_PPT_THUMBNAIL=1 npm start
```

- 기본값: `1` (활성)
- `ENABLE_PPT_THUMBNAIL=0`이면 썸네일·슬라이드 이미지 생성 비활성화
- 우선순위: macOS `qlmanage` → LibreOffice(`soffice/libreoffice`) + `pdftoppm`
- 도구 미설치 또는 변환 실패 시 텍스트 프리뷰로 자동 폴백

**PPT 슬라이드 이미지(뷰어용):** PPTX 파일은 LibreOffice로 PDF 변환 후 `pdftoppm`으로 이미지 생성.  
- **PPTX**: LibreOffice 필수. macOS: `brew install --cask libreoffice`  
- **PDF**: `pdftoppm`만 있으면 동작 (poppler 패키지)

### `.env` 예시

```env
PORT=8030
HOST=0.0.0.0
ADMIN_TOKEN=xavis-admin
PAGE_SIZE=8
ENABLE_POSTGRES=1
DB_HOST=your-db-host
DB_PORT=5432
DB_DATABASE=your_database
DB_USERNAME=your_user
DB_PASSWORD=your_password
ENABLE_PPT_THUMBNAIL=1
THUMBNAIL_WIDTH=1000
THUMBNAIL_MAX_RETRY=2
THUMBNAIL_RETRY_DELAY_MS=5000
THUMBNAIL_EVENT_KEEP=200
THUMBNAIL_EVENT_PAGE_SIZE=50

# 채팅 기능 (OpenAI API 키)
OPENAI_API_KEY=
# OPENAI_MODEL_DEFAULT=gpt-4o
# OPENAI_MODEL_MINI=gpt-4o-mini
```

---

## 사용 방법

1. 메인 페이지에서 강의 등록
   - **유튜브 강의 등록**: 제목 + 유튜브 링크 (+설명)
   - **PowerPoint 강의 등록**: 제목 + `.pptx` 파일 (+설명)
   - 두 등록 폼 모두 **태그(쉼표 구분)** 입력 가능
2. 하단 **등록된 강의** 카드에서 항목 클릭
3. 강의 상세 화면에서 시청
   - 유튜브: 동영상 재생
   - PPT: 슬라이드 텍스트 목록 확인

### 검색/필터

- 검색어, 타입, 태그를 조합해서 목록을 조회할 수 있습니다.
- 결과 목록은 페이지 단위로 분할되어 이동 가능합니다.

### 관리자 삭제

1. 화면의 관리자 모드에서 토큰 입력 후 활성화
2. 강의 카드의 `삭제` 버튼으로 즉시 삭제
3. PPT 강의 삭제 시 업로드 파일도 함께 제거 시도

### 썸네일 재생성(관리자)

- 관리자 모드에서 PPT 카드의 `썸네일 재생성` 버튼으로 수동 재생성 가능
- `실패 썸네일 일괄 재시도` 버튼으로 실패 건을 큐에 일괄 재등록 가능
- `이벤트 로그 페이지`에서 기간/유형/강의ID/사유 필터 조회 가능
- 이벤트 로그는 CSV 다운로드 지원

---

## 주요 라우트

### 메뉴별 화면

| 경로 | 설명 |
|------|------|
| `GET /` | 학습센터 뷰어 (강의 목록) |
| `GET /learning` | 학습센터 뷰어 (검색/필터/페이지네이션) |
| `GET /admin` | 관리자 대시보드 (강의·썸네일·AI 등) |
| `GET /chat` | 채팅 |
| `GET /ai-explore` | AI 탐색 (프롬프트·회의록 등 카드, 전체 너비 레이아웃) |
| `GET /ai-explore/prompts` | 프롬프트 라이브러리 (업무 시나리오별 기본 프롬프트) |
| `GET /ai-cases` | AI 성공 사례 목록(검색·태그 필터, 카드) |
| `GET /ai-cases/:slug` | AI 성공 사례 상세 |
| `GET /ai-cases/write` | AI 성공 사례 관리(관리자 토큰 필요) |
| `POST/PUT/DELETE /api/ai-success-stories` | 사례 CRUD(관리자) · 본문은 `data/ai-success-stories/*.md` |
| `GET /ax-apply` | AX 과제 신청 |
| `GET /lectures/:id` | 강의 상세 뷰어 (유튜브/PPT) |

### API

- `GET /api/chat/config`  
  채팅용 OpenAI 키 설정 여부 `{ configured: boolean }` (키 문자열은 노출하지 않음)

- `POST /api/chat`  
  JSON `{ model, messages }` — OpenAI Chat Completions 연동(비스트리밍). `OPENAI_API_KEY` 필수.

- `POST /api/chat/stream`  
  동일 본문으로 **SSE**(`text/event-stream`) 스트리밍 응답. 이벤트: `data: {"type":"delta","text":"..."}` 조각, 마지막 `{"type":"done"}`. 오류 시 `{"type":"error","error":"..."}`. 채팅 UI는 이 엔드포인트를 사용합니다.

- `POST /lectures/youtube`  
  유튜브 강의 등록

- `POST /lectures/ppt`  
  PPT 강의 등록 (`multipart/form-data`)

- `POST /lectures/:id/delete`  
  관리자 토큰 기반 강의 삭제

- `POST /lectures/:id/thumbnail/regenerate`  
  관리자 토큰 기반 PPT 썸네일 재생성

- `POST /thumbnails/retry-failed`  
  관리자 토큰 기반 실패 썸네일 일괄 재시도 큐 등록

- `GET /api/queue/metrics?token=...`  
  관리자 토큰 기반 큐/상태 메트릭 JSON 조회

- `GET /api/queue/events-summary?token=...&hours=24`  
  최근 시간대 요약 KPI/시간대별 처리량 JSON 조회

- `GET /admin/thumbnail-events?token=...`  
  관리자 이벤트 로그 페이지 (필터/페이지네이션)

- `GET /admin/thumbnail-events.csv?token=...`  
  필터 기준 이벤트 로그 CSV 다운로드

---

## 최근 업데이트 (요약)

- **AI 탐색** (`/ai-explore`): 메인 콘텐츠를 뷰포트 전체 너비로 사용, **프롬프트** 서비스 카드를 첫 번째에 배치, 검색어에 **「프롬프트」**가 포함된 채 검색(Enter) 시 프롬프트 라이브러리로 이동. **좌측 전체 메뉴는 관리자 여부와 관계없이 동일하게 표시·접근 가능**
- **프롬프트 라이브러리** (`/ai-explore/prompts`): 시나리오 카드·미리보기·복사 UI, 템플릿 데이터는 `data/company-prompts.json`에서 로드
- **서버 수신 주소**: 기본 `HOST=0.0.0.0`(로컬 `localhost` 접속에 사용). 필요 시 `HOST=127.0.0.1`로 제한 가능
- **문서**: `localhost` 접속 불가 시 확인할 항목을 아래 「접속이 안 될 때 (트러블슈팅)」 절에 정리

---

## 기술 스택

- Node.js
- Express
- EJS
- PostgreSQL (`pg`)
- Multer (파일 업로드)
- JSZip (`.pptx` 내부 XML 파싱)
- UUID

---

## 구현 참고/제약

- PPT 뷰어는 **원본 슬라이드 디자인 렌더링**이 아닌, `.pptx` 내부 텍스트를 추출해 보여주는 방식입니다.
- PPT 썸네일은 시스템 도구 상태에 따라 생성 실패할 수 있으며, 이 경우 이미지 없이 텍스트 프리뷰만 표시됩니다.
- 썸네일 큐는 단일 프로세스 워커 기준으로 동작합니다(다중 인스턴스 분산 락은 미구현).
- 폰트/도형/애니메이션까지 완전 동일 렌더링이 필요하면 별도 문서 렌더러(예: LibreOffice/PDF 변환 파이프라인) 연동이 필요합니다.
- 이벤트 로그 페이지의 실시간 그래프는 클라이언트 폴링 기반이며, 다수 접속 시 폴링 주기 조정이 필요할 수 있습니다.

