- OPS+관리자: 사용자 현황관리·구분선·관리자 off·구분선·로그아웃 - 비OPS+관리자: 관리자 버튼 제거, 사용자 현황관리·구분선·관리자 off - README 하단 메뉴 설명 갱신 Made-with: Cursor
569 lines
26 KiB
Markdown
569 lines
26 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`), 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-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 lectures 테이블 스키마 (서버 기동 시 자동 적용)
|
|
├─ 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 탐색 (전체 너비, 프롬프트 카드·검색)
|
|
│ ├─ 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 변환 파이프라인) 연동이 필요합니다.
|
|
- 이벤트 로그 페이지의 실시간 그래프는 클라이언트 폴링 기반이며, 다수 접속 시 폴링 주기 조정이 필요할 수 있습니다.
|
|
|