Files
ai_platform/README.md
dsyoon 7bee72f287 feat(learning): 업로드 동영상 카드 썸네일(ffmpeg 프레임 추출)
- 동영상 업로드 후 PPT와 동일한 썸네일 큐로 PNG 생성
- ENABLE_VIDEO_THUMBNAIL, VIDEO_THUMB_SEEK_SEC 환경 변수 지원
- 관리자: 동영상도 썸네일 재생성·삭제 시 썸네일 파일 정리
- 카드/스타일: 썸네일 이미지 표시 시 불투명도 1

Made-with: Cursor
2026-04-21 17:02:21 +09:00

593 lines
32 KiB
Markdown

# 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) 시 프롬프트 라이브러리로 이동
- **회의록 AI** (`/ai-explore/meeting-minutes`): 회의록 생성 시스템 프롬프트는 `lib/meeting-minutes.js``buildMeetingMinutesSystemPrompt`에서 구성하며, **추가 지시**가 형식·섹션(체크리스트 포함 여부, 액션 표기 등)에 우선합니다. DB `meeting_ai_prompts.include_checklist``true`일 때만 회의 체크리스트 강제 블록을 넣고, 기본값은 `false`입니다. `include_checklist``false`이면 생성 직후 `prepareMeetingMinutesForApi`에서 `## 회의 체크리스트` 등 블록을 **후처리로 제거**합니다(모델이 습관적으로 넣은 경우 대비). 기존 DB에 `include_checklist = true`가 남아 있으면 `UPDATE meeting_ai_prompts SET include_checklist = false`로 끄거나, 화면에서 **프롬프트 저장**으로 덮어씁니다. 기본 **추가 지시**는 `views/meeting-minutes.ejs``mmDefaultCustomInstructions`입니다.
- **임직원 인명 정규화**: `data/meeting-employee-names.txt`(또는 `MEETING_EMPLOYEE_NAMES_FILE`)에 성명을 두고, `lib/meeting-employee-names.js`가 전사·원문에서 **이름으로 보이는 토큰만** 명단과 퍼지 매칭해, 회의록 LLM 요청 **사용자 메시지 상단**에 짧은「이번 원문/전사 한정 · 임직원 표기 통일」블록만 붙입니다. 전 직원 명단을 시스템 프롬프트에 넣지 않습니다. 끄려면 `MEETING_NAME_NORMALIZATION=0`.
- **대시보드** (`/dashboard`): AI 탐색과 유사한 카드 그리드·검색으로 대시보드를 모아 표시. 첫 카드 **경영성과 대시보드**는 `/dashboard/business-performance`로 연결
- **경영성과 대시보드** (`/dashboard/business-performance`): **위쪽 대시보드 조회**(Chart.js 인라인), **아래 엑셀 업로드**(`.xlsx`, 매출일보 시트) 순서. 본문은 AI 프롬프트 페이지와 동일하게 **`main.container.container-ai-full`**(전체 너비·좌우 24px)로 맞춤. 대시보드 상단 **연도·분기**로 `mgmt_perf_uploads`에 저장된 해당 기간 **최신 스냅샷**을 불러오며, 쿼리 **`?year=2026&quarter=1`** 또는 폼 조회와 동일. 해당 기간 업로드가 없으면 기본 JSON 샘플을 쓰고 안내 문구를 표시합니다. `public/mgmt-perf/dashboard.css`에 있던 **범용 `.container { background: white }`** 는 앱 페이지에서 `main.container`까지 적용되어 회색 본문이 가려졌으므로 제거하고, 흰 카드는 **`.mgmt-perf-embed .container`** 만 사용합니다. 업로드는 DB(`mgmt_perf_uploads` / `mgmt_perf_snapshots`) 또는 DB 미연결 시 `data/mgmt-perf-last-state.json`에 스냅샷 저장. 최근 업로드 행 **`DELETE /api/mgmt-perf/upload/:id`** 로 삭제(PG는 CASCADE, 파일 전용 모드는 `id=file`). 단독 임베드 페이지는 `/dashboard/business-performance/embed`(본문에 `body.mgmt-perf-standalone`으로 어두운 배경). Express에서 **`/mgmt-perf/*``public/mgmt-perf/`** 정적 제공이 등록되어 있어 `dashboard-app.js`·`chart.umd.min.js`(CDN 대신 동봉)가 항상 같은 오리진에서 로드됩니다. 업로드 시 **한글 파일명**은 multer 기본(`defParamCharset` 생략 시 latin1)으로 온 `originalname`을 **`lib/decode-upload-filename.js`**의 `decodeUploadFilename`으로 보정합니다(`decodeURIComponent(escape(...))` 우선, 이어서 `Buffer` latin1→utf8). Busboy에 `defParamCharset: 'utf8'`를 켜면 이중 디코딩으로 깨질 수 있어 두지 않습니다. 탭 전환·차트 렌더는 **ASCII 섹션 id**(`mgmt-sec-sales` 등)와 `state.currentSection`으로 동기화합니다. 리버스 프록시 사용 시 업로드 실패하면 **`client_max_body_size`**(예: 64m)와 **`/api/`·`/mgmt-perf/` → Node** 전달 여부를 확인. 엑셀 집계 치환은 `npm install``xlsx` 설치 후 서버 재시작.
- **경영성과 데이터 확인**: 브라우저에서 `GET /api/mgmt-perf/status`(JSON)로 최근 스냅샷의 `payloadKeys`, `_uploadMeta`(행 수 등)를 확인할 수 있습니다. **현재 구현**은 엑셀에서 **매출일보 행 수·시트명만** `payload._uploadMeta`에 넣고, **차트 수치는 기본 시드 JSON**(`data/mgmt-perf-default-payload.json`)을 씁니다. 5,000행이어도 차트가 엑셀 집계와 일치하려면 **별도 집계·매핑 로직**이 필요합니다.
- **대시보드 메뉴 접근**: `.env``DASHBOARD_MENU_ALLOWED_EMAILS`**쉼표로 구분한 OPS 로그인 이메일**만 좌측 **대시보드** 메뉴·`/dashboard`·경영성과 API가 보입니다. 목록이 비어 있으면 누구에게도 표시되지 않습니다. 로컬(DEV)에서 관리자 토큰만 쓰는 경우 `DASHBOARD_MENU_DEV_USE_MEETING_EMAIL=1``MEETING_DEV_EMAIL`을 허용 목록과 맞추면 대조됩니다.
- **프롬프트 라이브러리** (`/ai-explore/prompts`): 업무별 기본 프롬프트 카드 선택·미리보기·클립보드 복사 (`data/company-prompts.json`). **좌측 메뉴(채팅·AI·AI 성공 사례·학습센터·AX 과제 신청)는 관리자 여부와 관계없이 접근 가능**(강의 삭제·관리자 대시보드 등 일부 기능은 관리자 모드에서만)
- 검색/필터/페이지네이션
- 검색어(`q`) 기반 제목/설명/태그 필터
- 타입(`YouTube`, `PPT`) 필터
- 태그 필터 + 페이지네이션
- 유튜브 강의 등록/시청
- 유튜브 URL 입력 후 목록에 추가
- 강의 상세에서 iframe 임베드로 재생
- PPT 강의 등록/시청
- `.pptx` 파일 업로드
- 상세에서 슬라이드 이미지(PNG)만 표시(XML 텍스트 추출 목록은 표시하지 않음)
- PPT/PDF(`.pdf`) 상세: **1단·2단·3단** 보기 전환(그리드), 선택값은 브라우저 `localStorage`에 저장
- 목록 카드에 PPT 프리뷰(첫 슬라이드 제목 + 장수) 표시
- macOS 환경에서는 `qlmanage` 기반 실제 썸네일(첫 장 이미지) 자동 생성
- **업로드 동영상**(mp4/webm/mov 등): 목록 카드에 **ffmpeg**로 뽑은 대표 프레임 썸네일 표시(기본 약 0.5초 지점). 서버에 `ffmpeg`가 있어야 하며, PPT 썸네일과 동일한 백그라운드 큐·재시도 정책을 사용합니다.
- 썸네일 백그라운드 큐
- 썸네일 생성은 비동기 큐에서 처리
- 상태값: `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 # 경영성과(업로드 + 인라인 Chart.js 조회)
│ ├─ mgmt_perf_embed.ejs # 경영성과 차트 단독 페이지(직접 열람·임베드용)
│ ├─ partials/mgmt_perf_dashboard_container.ejs
│ ├─ ai-prompts.ejs # 프롬프트 라이브러리 (카드·미리보기·복사)
│ ├─ ai-cases.ejs # AI 성공 사례 목록(카드)
│ ├─ ai-case-detail.ejs # AI 성공 사례 상세(마크다운 또는 PDF·페이지 이미지, 1·2·3단 보기)
│ ├─ 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`
- **PPT 슬라이드 이미지 한글(□·토푸)**: 변환은 서버에서 이루어지므로 **한글 글꼴**이 없으면 PNG에만 한글이 깨집니다(HTML UI 한글은 정상일 수 있음). 예: `sudo apt install -y fonts-nanum fonts-noto-cjk`, 이후 `sudo fc-cache -fv` 로 fontconfig 갱신 → 관리자 화면에서 해당 강의 **슬라이드 이미지 재생성**.
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 패키지)
- **한글 깨짐(이미지 안만 □)**: 서버에 PPT가 쓰는 글꼴·한글 폰트가 없을 때 발생. Linux: `fonts-nanum`, `fonts-noto-cjk` 등 설치 후 `fc-cache -fv`, 슬라이드 이미지 재생성.
### 업로드 동영상 카드 썸네일(선택)
- 기본값: `ENABLE_VIDEO_THUMBNAIL`이 `0`이 아니면 활성(미설정 시 켜짐).
- 서버 **PATH**에 `ffmpeg`가 있어야 합니다. macOS: `brew install ffmpeg`, Ubuntu: `sudo apt install -y ffmpeg`.
- 추출 시각은 `VIDEO_THUMB_SEEK_SEC`(초, 기본 `0.5`)로 조정할 수 있습니다. 영상 앞부분이 검은 화면이면 값을 키워 보세요.
- `ENABLE_VIDEO_THUMBNAIL=0`이면 업로드 직후 썸네일은 생성하지 않으며, 카드는 기존처럼 텍스트 폴백만 표시합니다.
### `.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
ENABLE_VIDEO_THUMBNAIL=1
# VIDEO_THUMB_SEEK_SEC=0.5
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` 파일 (+설명)
- **동영상 파일 등록**: 제목 + 영상 파일 (`ffmpeg`로 카드 썸네일 생성)
- 두 등록 폼 모두 **태그(쉼표 구분)** 입력 가능
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`
관리자 토큰 기반 PDF/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·동영상 썸네일은 시스템 도구(`qlmanage`/LibreOffice/`ffmpeg` 등) 상태에 따라 생성 실패할 수 있으며, 이 경우 이미지 없이 텍스트 프리뷰만 표시됩니다.
- 썸네일 큐는 단일 프로세스 워커 기준으로 동작합니다(다중 인스턴스 분산 락은 미구현).
- 폰트/도형/애니메이션까지 완전 동일 렌더링이 필요하면 별도 문서 렌더러(예: LibreOffice/PDF 변환 파이프라인) 연동이 필요합니다.
- 이벤트 로그 페이지의 실시간 그래프는 클라이언트 폴링 기반이며, 다수 접속 시 폴링 주기 조정이 필요할 수 있습니다.