# Web Platform (학습센터) 유튜브 링크와 PowerPoint(`.pptx`) 강의를 등록하고, 목록에서 클릭해 바로 시청할 수 있는 사내 학습센터 예제입니다. **소스 저장소(Git):** [https://git.xavis.co.kr/AI_Innovation_Team/ai_platform](https://git.xavis.co.kr/AI_Innovation_Team/ai_platform) --- ## 접속 방법 **브라우저에서 열기 전에 반드시 터미널에서 서버를 실행해야 합니다.** (`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` 리다이렉트), WM(`/wm` 리다이렉트), AI(`/ai-explore`), **프롬프트**(`/ai-explore/prompts`, 좌측 전역 메뉴), **AI 활용 사례**(`/ai-cases`), **대시보드**(`/dashboard`, AI 활용 사례 아래 메뉴), 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`를 사용할 수 있습니다. ### 배포 서버: `git pull`이 `data/` 런타임 파일 때문에 막힐 때 원격이 `data/`(예: `data/ai-success-stories.json`)를 Git 추적에서 제외하는 쪽으로 바뀌는 동안, 서버에 **수정해 둔** 같은 경로가 있으면 *Your local changes to the following files would be overwritten by merge* 메시지와 함께 `git pull`이 중단될 수 있습니다. - **권장:** 프로젝트 루트에서 `bash scripts/safe-git-pull.sh`를 실행합니다. 백업 → 로컬 변경만 Git 관점에서 정리 → `git pull` → 백업 복원 순서로, 서비스에 쓰는 JSON 내용을 유지합니다. `main`이 반영된 뒤에는 해당 파일이 **무시(ignore)** 대상이 되어, 이후에는 보통 `git pull`만으로 갱신됩니다. - **수동:** 파일을 임시로 복사한 뒤 `git restore data/ai-success-stories.json`(구버전은 `git checkout -- data/ai-success-stories.json`) → `git pull` → 필요 시 다시 복사합니다. --- ## 핵심 기능 - 학습센터 UI (좌측 메뉴 + 상단 헤더 + 강의 카드 레이아웃) - **AI 탐색** (`/ai-explore`): 전체 너비 레이아웃, AI 서비스 카드(회의록·체크리스트 등; **프롬프트 라이브러리**는 좌측 **프롬프트** 메뉴에서 진입) - **회의록 AI** (`/ai-explore/meeting-minutes`): 텍스트 입력 탭에서 **회의 원문** 옆 **`복사` 버튼**(생성 결과의 회의록·전사와 동일한 연한 녹색 스타일)으로 원문을 클립보드에 복사할 수 있습니다. 회의록 생성 시스템 프롬프트는 `lib/meeting-minutes.js`의 `buildMeetingMinutesSystemPrompt`에서 구성하며, 기본 구조 중 「2) 참석·언급 인원」은 **표 없이 한 줄 쉼표 목록**(원문 근거 인명·직함·조직명)으로 안내합니다. 저장된 사용자 **추가 지시**(비어 두면 해당 블록 없음)만 시스템 프롬프트 우선 순위 블록에 합류합니다. 화면 **음성 업로드** 생성 시 처리 상태를 **1 업로드 / 2 전사 / 3 번역**(전사 결과를 회의록 형식으로 LLM이 정리하는 단계)으로 나누어 진행 표시 및 SSE 패딩·하트비트로 장시간 작업 안내를 제공합니다. 음성 UI는 브라우저에서 두 단계로 동작합니다. **`POST /api/meeting-minutes/prepare-audio`**(multipart 필드: `audio`, `title`, `meetingDate`, `model`, `whisperModel` · `whisperModel` 생략 시 서버 기본 전사는 `gpt-4o-transcribe`, 필요 시 **`OPENAI_WHISPER_MODEL`**) 요청 시 **XHR `upload.onprogress`**로 업로드 바이트를 표시하고, 수신 즉시 **`{ jobId }`** JSON을 돌려줍니다. 이어 같은 세션으로 **`GET /api/meeting-minutes/stream-audio/:jobId`**를 `fetch`(ReadableStream)로 열어 `text/event-stream` 전사·회의록 SSE를 **증분 수신**합니다. **업로드와 SSE가 한 HTTP 요청으로 묶이면**(레거시 `POST …/generate-audio`) 브라우저·역프록시가 응답 본문을 버퍼링해 진행이 멈춘 것처럼 보일 수 있어, 위 분리 플로를 권장합니다. 작업 메타는 `data/meeting-audio-job-meta/`(실행 시 생성, 저장소에서는 `data/`가 무시)에 두며 TTL은 **`MEETING_AUDIO_JOB_TTL_MS`**(미설정 시 약 1시간, 상한·하한은 코드 참고). **로드밸런스 뒤에 Node 인스턴스가 여러 대**이면 이 `data/` 디렉터리(또는 job 메타 저장소)와 업로드 디렉터리가 **인스턴스 간 공유**되어야 같은 `jobId`에 대해 prepare와 stream 요청이 같은 파일·메타를 읽을 수 있습니다. 클라이언트에서는 `consumeSseFromFetch`가 이벤트마다 **`mmAudioStreamHandlers.onSseEvent`**를 호출합니다(`views/meeting-minutes.ejs`). 디버깅 시 같은 페이지 콘솔에서 `getMeetingMinutesAudioDiag()`를 호출하면 `uploadPhase`, `lastSseEvent`, 세그먼트 진행 등을 확인할 수 있습니다. 회의 결과 저장 시 `lib/parse-checklist-from-minutes.js`가 **액션 아이템 마크다운 표**(GFM 권장)와 번호 목록을 규칙으로 읽은 뒤 `extractChecklistStructured`(LLM) 결과와 서버에서 병합하여 업무 체크리스트로 반영합니다(`MEETING_AUTO_CHECKLIST=0` 등으로 끌 수 있음). 동일 처리 경로에서 `prepareMeetingMinutesForApi`가 **헤더·본문 열 수 불일치**(헤더 3열 대 데이터 4열 등)로 깨진 액션 표를 교정하고, 「담당」열에 들어 간 **발언자·저희·우리 팀** 등 통칭·역할 명칭 단독은 규칙 후처리 시 **미정**으로 바꿀 수 있습니다. DB `meeting_ai_prompts.include_checklist`가 `true`일 때만 회의 체크리스트 강제 블록을 넣고, 기본값은 `false`입니다. `include_checklist`가 `false`이면 생성 직후 `prepareMeetingMinutesForApi`에서 `## 회의 체크리스트` 등 블록을 **후처리로 제거**합니다(모델이 습관적으로 넣은 경우 대비). 기존 DB에 `include_checklist = true`가 남아 있으면 `UPDATE meeting_ai_prompts SET include_checklist = false`로 끄거나, 화면에서 **프롬프트 저장**으로 덮어씁니다. - **임직원 인명 정규화**: `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`으로 동기화합니다. 리버스 프록시로 **Apache2**를 쓰는 경우 업로드 실패·413이면 VirtualHost **`LimitRequestBody`**(예: 바이트 단위로 `deploy/apache-ai.ncue.net-ssl.conf.example`와 맞출 것)와 **`/api/`·`/mgmt-perf/` → Node** `ProxyPass` 전달 여부를 확인. (Nginx를 쓰는 환경이면 **`client_max_body_size`** 등을 해당 서버 설정에 맞춥니다.) 엑셀 집계 치환은 `npm install`로 `xlsx` 설치 후 서버 재시작. - **경영성과 데이터 확인**: 브라우저에서 `GET /api/mgmt-perf/status`(JSON)로 최근 스냅샷의 `payloadKeys`, `_uploadMeta`(행 수 등)를 확인할 수 있습니다. **현재 구현**은 엑셀에서 **매출일보 행 수·시트명만** `payload._uploadMeta`에 넣고, **차트 수치는 기본 시드 JSON**(`config/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`): 좌측 **프롬프트** 메뉴로 진입 · 업무별 기본 프롬프트 카드 선택·미리보기·클립보드 복사 (`config/company-prompts.json`) · **공유하기** 탭의 본문은 **원문 보기** / **마크다운 보기** 토글(클라이언트 `marked` + `DOMPurify`) · **워크플로** 탭(①~④ 입력·초안 합치기·AI로 다듬기). **좌측 메뉴(채팅·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) │ ├─ pg-backup.sh # PostgreSQL 논리 백업 (cron·npm run db:backup) │ ├─ pg-restore.sh # 백업 복원 (npm run db:restore -- …) │ └─ lib/load-env.sh # 셸 스크립트용 .env 로더 ├─ public/ │ └─ styles.css # 전역 스타일(회의록 마크다운 뷰 `.mm-minutes-rendered` 표 셀 테두리 등) ├─ views/ │ ├─ partials/ │ │ ├─ nav.ejs # 좌측 공통 네비게이션(하단 관리자·토큰 모달 연동) │ │ └─ admin-token-modal.ejs # 관리자 토큰 입력 모달(`openAdminTokenModal`) │ ├─ learning-viewer.ejs # 학습센터 뷰어 (일반 사용자) │ ├─ learning-admin.ejs # 학습센터 관리 (업로드·삭제·썸네일) │ ├─ chat.ejs # OpenAI 기반 인앱 채팅 UI │ ├─ 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 활용 사례 목록(카드·`data/ai-success-stories.json` + PG `ai_use_case_submissions` 병합) │ ├─ ai-use-case-submission-detail.ejs # 일반 제출 상세(`GET /ai-cases/submit/:uuid`) │ ├─ ai-case-detail.ejs # AI 활용 사례 상세(마크다운 또는 PDF·페이지 이미지, 1·2·3단 보기) │ ├─ ai-cases-compose.ejs # AI 활용 사례 일반 글쓰기(TOAST UI WYSIWYG 한 칸, 1~4번 STAR는 편집기 안 템플릿, 제출 시 4필드로 분리·`sanitize-use-case-body` 정제) │ ├─ ai-cases-write.ejs # AI 활용 사례 관리자 등록·편집 │ ├─ ax-apply.ejs # AX 과제 신청 │ ├─ lecture-youtube.ejs # 유튜브 강의 상세 (iframe 임베드) │ ├─ lecture-ppt.ejs # PPT 강의 상세 (슬라이드 이미지) │ └─ admin-thumbnail-events.ejs # 썸네일 이벤트 로그 관리자 페이지 ├─ config/ │ ├─ company-prompts.json # AI 프롬프트 라이브러리 템플릿 (Git 추적) │ └─ mgmt-perf-default-payload.json # 경영성과 대시보드 기본 차트 시드 (Git 추적) ├─ data/ # 런타임 JSON·업로드 메타 (Git 제외, `.gitkeep`만 추적) │ ├─ lectures.json # 강의 DB (PG 미사용 시) │ ├─ ai-success-stories.json / ai-success-stories/ │ ├─ 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` 사용 | | **Git과 데이터** | `data/`, `public/resources/ai-success/`, `public/files/`는 **.gitignore**로 제외(런타임·업로드 전용). `config/`의 `company-prompts.json`·`mgmt-perf-default-payload.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`을 열지 않고 **Apache2**(또는 동급 리버스 프록시)로 TLS·역방향 프록시하는 구성이 일반적입니다. - **Apache2** 예시(모듈·VirtualHost·`LimitRequestBody`·`ProxyTimeout`)는 [docs/DEPLOYMENT-xavis.ncue.net.md](docs/DEPLOYMENT-xavis.ncue.net.md) 및 `deploy/apache-ai.ncue.net-ssl.conf.example`를 참고하세요. - `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회**(또는 스키마 변경 시); **일일 DB 백업**은 아래 「PostgreSQL 백업 및 복원」의 cron 절을 따릅니다. --- ## 실행 방법 **로컬에서 빠르게 띄우기**는 아래 순서면 됩니다. **새 서버에 맞춰 처음부터 배포**할 때는 위 「서버 배포 (새 머신·처음부터)」의 **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` 미지정 시 기본값: `ncue-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` --- ## PostgreSQL 백업 및 복원 운영 서버에서 **매일 cron**으로 PostgreSQL **서버의 연결 가능 DB 전체**를 백업하고, 장애·실수 삭제 시 복원하는 절차입니다. 기본(`PG_BACKUP_SCOPE=all`)은 템플릿 DB를 제외한 **모든 데이터베이스**를 DB마다 `{dbname}.dump` 파일로 저장합니다. 역할(계정)은 `PG_BACKUP_GLOBALS=1`과 슈퍼유저 비밀번호가 있으면 `00_globals.sql`로 함께 백업합니다. ### 개요 | 항목 | 내용 | |------|------| | 백업 범위(기본) | PostgreSQL 서버의 **모든 DB** (`template0`/`template1` 제외) | | 백업 방식 | DB별 `pg_dump -Fc` + (선택) `pg_dumpall --globals-only` | | 스크립트 | `scripts/pg-backup.sh`, `scripts/pg-restore.sh` | | npm | `npm run db:backup`, `npm run db:restore -- <옵션> <파일 또는 디렉터리>` | | 설정 | 프로젝트 루트 `.env`의 `DB_*`, `PG_BACKUP_*` | | 저장 위치(기본) | `/home/xavis/workspace/backup/ai_platform/YYYYMMDD/` | | 최신 심볼릭 링크 | `/home/xavis/workspace/backup/ai_platform/latest` → 가장 최근 백업 디렉터리 | 백업 파일에는 **각 DB의 테이블·데이터·인덱스·시퀀스**와(옵션) **역할·권한**이 포함됩니다. Git·공개 저장소에 올리지 마세요. ### 사전 요구사항 1. **PostgreSQL 클라이언트 도구** (`pg_dump`, `pg_restore`, `psql`)가 서버 PATH에 있어야 합니다. - Ubuntu/Debian: `sudo apt install -y postgresql-client` - macOS: `brew install libpq` 후 PATH에 `pg_dump` 추가 2. **`.env`**에 `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`가 앱과 동일하게 설정되어 있어야 합니다. 3. **전체 DB 백업**을 위해서는 `.env`에 `PG_BACKUP_SUPERUSER_PASSWORD`(postgres 등 슈퍼유저)를 설정하는 것을 권장합니다. 없으면 `DB_USERNAME`이 접근 가능한 DB만 백업됩니다. 4. **디스크 여유**: (모든 DB 크기 합) × 보관 일수(30) × 1.2 이상 권장. ### 환경 변수 (`.env`) | 변수 | 필수 | 기본값 | 설명 | |------|------|--------|------| | `DB_HOST` | O | — | PostgreSQL 호스트 | | `DB_PORT` | — | `5432` | 포트 | | `DB_DATABASE` | O | — | 앱 DB 이름(복원·단일 백업 시 참고). `PG_BACKUP_SCOPE=single`일 때만 백업 대상 | | `DB_USERNAME` | O | — | 앱 DB 계정 | | `DB_PASSWORD` | O | — | 앱 DB 비밀번호 | | `PG_BACKUP_DIR` | — | `/home/xavis/workspace/backup/ai_platform` | 백업 루트 디렉터리 | | `PG_BACKUP_RETENTION_DAYS` | — | `30` | 이 일수보다 **오래된 날짜 폴더**(YYYYMMDD) 삭제. `0`이면 삭제 안 함 | | `PG_BACKUP_SCOPE` | — | `all` | `all`=서버의 모든 DB, `single`=`DB_DATABASE`만 | | `PG_BACKUP_GLOBALS` | — | `1` | `1`이면 역할(계정)·권한 SQL(`00_globals.sql`)도 함께 백업 | | `PG_BACKUP_SUPERUSER` | — | `postgres` | globals 백업·전체 DB dump 시 사용 | | `PG_BACKUP_SUPERUSER_PASSWORD` | 전체 백업 권장 | — | 슈퍼유저 비밀번호 | **운영 서버 `.env` 예시 (백업 관련만):** ```env PG_BACKUP_DIR=/home/xavis/workspace/backup/ai_platform PG_BACKUP_RETENTION_DAYS=30 PG_BACKUP_SCOPE=all PG_BACKUP_GLOBALS=1 PG_BACKUP_SUPERUSER=postgres PG_BACKUP_SUPERUSER_PASSWORD=실제_슈퍼유저_비밀번호 ``` `PG_BACKUP_SCOPE=single`로 바꾸면 예전처럼 `DB_DATABASE` 하나만 백업합니다. ### 수동 백업 프로젝트 루트에서: ```bash npm run db:backup # 또는 bash scripts/pg-backup.sh ``` 성공 시 예시 출력: ```text [2026-05-25T02:00:01+09:00] pg-backup start → /home/xavis/workspace/backup/ai_platform/20260525 (scope=all, retention 30 days) [2026-05-25T02:00:02+09:00] globals saved: .../20260525/00_globals.sql [2026-05-25T02:00:03+09:00] dumping database: ai_web_platform [2026-05-25T02:00:15+09:00] dump saved: .../20260525/ai_web_platform.dump (12345678 bytes) [2026-05-25T02:00:16+09:00] dumping database: postgres [2026-05-25T02:00:17+09:00] dump saved: .../20260525/postgres.dump (456789 bytes) [2026-05-25T02:00:17+09:00] retention: pruned 1 dir(s); keeping backups from 20260425 onward (30 days) [2026-05-25T02:00:17+09:00] pg-backup done: 2 database(s), latest → .../latest ``` 생성 파일: ```text /home/xavis/workspace/backup/ai_platform/ ├─ 20260525/ │ ├─ 00_globals.sql # 역할·권한 (PG_BACKUP_GLOBALS=1) │ ├─ 00_manifest.txt # 백업 DB 목록 │ ├─ ai_web_platform.dump │ ├─ postgres.dump │ └─ … # 서버의 다른 DB마다 *.dump └─ latest -> 20260525/ # 심볼릭 링크 (최대 30일치 폴더 유지) ``` ### 매일 cron 등록 (운영 서버) 1. 백업 디렉터리 생성 및 권한: ```bash sudo mkdir -p /home/xavis/workspace/backup/ai_platform sudo chown "$(whoami):$(whoami)" /home/xavis/workspace/backup/ai_platform # PM2/앱과 같은 사용자로 cron을 돌릴 경우 그 사용자로 chown ``` 2. `.env`에 `PG_BACKUP_DIR=/home/xavis/workspace/backup/ai_platform` 설정. 3. crontab 편집: ```bash crontab -e ``` 4. **매일 새벽 2시** (프로젝트 경로는 배포 위치에 맞게 수정): ```cron 0 2 * * * cd /home/xavis/workspace/ai_platform && /usr/bin/bash scripts/pg-backup.sh >> /var/log/pg-backup.log 2>&1 ``` 5. 로그 로테이션(선택): `/var/log/pg-backup.log`가 커지지 않도록 `logrotate` 또는 주기적 truncate. **cron 점검** ```bash # 다음 실행 예약 확인 crontab -l # 수동 1회 실행 후 로그 확인 cd /home/xavis/workspace/ai_platform && bash scripts/pg-backup.sh tail -20 /var/log/pg-backup.log # 최신 dump 목록 확인 ls -lh /home/xavis/workspace/backup/ai_platform/latest/ cat /home/xavis/workspace/backup/ai_platform/latest/00_manifest.txt ``` ### 복원 `.dump` 파일은 **custom format** 전용입니다. plain SQL(`.sql`)이 아니면 `psql -f`가 아니라 `pg_restore`를 사용합니다. #### 1) 검증용 복원 — DB 하나 ```bash npm run db:restore -- --test --confirm /home/xavis/workspace/backup/ai_platform/latest/ai_web_platform.dump ``` #### 1-2) 검증용 복원 — 서버 DB 전체 ```bash npm run db:restore -- --all --globals --test --confirm /home/xavis/workspace/backup/ai_platform/latest/ ``` 각 DB는 `{dbname}_restore_test` 이름으로 복원됩니다(예: `ai_web_platform_restore_test`). 확인: ```bash psql -h "$DB_HOST" -U "$DB_USERNAME" -d ai_web_platform_restore_test -c "\dt" psql -h "$DB_HOST" -U "$DB_USERNAME" -d ai_web_platform_restore_test \ -c "SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 10;" ``` 검증 후 테스트 DB 삭제: ```bash psql -h "$DB_HOST" -U postgres -c 'DROP DATABASE ai_web_platform_restore_test;' ``` (`DROP`은 슈퍼유저 또는 DB owner 권한 필요) #### 2) 운영 DB 복원 — DB 하나 (주의) ```bash pm2 stop webplatform # 앱 이름에 맞게 bash scripts/pg-restore.sh --clean --confirm /home/xavis/workspace/backup/ai_platform/latest/ai_web_platform.dump pm2 start webplatform ``` #### 2-2) 운영 DB 복원 — 서버 DB 전체 (주의) ```bash pm2 stop webplatform bash scripts/pg-restore.sh --all --globals --clean --confirm /home/xavis/workspace/backup/ai_platform/latest/ pm2 start webplatform ``` - `--clean`: 기존 객체를 삭제한 뒤 dump 내용으로 재생성 (`--if-exists` 포함). - 운영 DB에 `--clean` 없이 복원하면 중복 객체 오류가 날 수 있습니다. - `--confirm` 없이 실행하면 대화형 확인 프롬프트가 뜹니다. 역할(계정)까지 함께 백업해 둔 경우(`00_globals.sql`): ```bash bash scripts/pg-restore.sh --globals --clean --confirm /home/xavis/workspace/backup/ai_platform/.../ai_web_platform.dump ``` `.env`에 `PG_BACKUP_SUPERUSER`, `PG_BACKUP_SUPERUSER_PASSWORD` 필요. #### 3) 완전히 새 DB로 복원 (드물게) DB 자체를 drop/create한 뒤 복원할 때는 DBA·슈퍼유저로: ```bash psql -h "$DB_HOST" -U postgres -c "DROP DATABASE IF EXISTS ai_web_platform;" psql -h "$DB_HOST" -U postgres -c "CREATE DATABASE ai_web_platform OWNER xavis;" bash scripts/pg-restore.sh --confirm /home/xavis/workspace/backup/ai_platform/.../ai_web_platform.dump ``` ### 백업·복원 체크리스트 (월 1회 권장) | Check | 방법 | |-------|------| | cron 정상 실행 | `/var/log/pg-backup.log`에 전일 `pg-backup done` 기록 | | dump 파일 크기 | `00_manifest.txt`·각 `.dump`가 0 byte가 아닌지 확인 | | 복원 테스트 | `--all --test` 또는 단일 `--test`로 `_restore_test` DB 확인 | | 디스크 | `df -h $PG_BACKUP_DIR` | | 보관 정책 | `PG_BACKUP_RETENTION_DAYS`에 맞게 오래된 폴더 삭제되는지 | ### 보안 - dump 파일은 **개인정보·업무 데이터 전체**를 담습니다. 권한 `600`/`700` 디렉터리, Git·Slack·메일 첨부 금지. - 가능하면 백업 저장소를 **앱 서버와 분리**(NAS, 객체 스토리지)하고 주기적으로 오프사이트 복사. - DB는 공인 IP 직접 노출 대신 **방화벽·VPN·SSH 터널** 사용(별도 보안 가이드 참고). ### 문제 해결 | 증상 | 조치 | |------|------| | `pg_dump: command not found` | `postgresql-client` 설치 | | `password authentication failed` | `.env`의 `DB_USERNAME`/`DB_PASSWORD` 확인 | | `could not connect to server` | `DB_HOST`·방화벽·PostgreSQL `listen_addresses` | | cron은 도는데 파일 없음 | cron의 `cd` 경로, `.env` 존재, 로그 파일 stderr 확인 | | `pg_restore: error: role "xxx" does not exist` | `--no-owner --role=DB_USERNAME`(스크립트 기본) 또는 `--globals`로 역할 먼저 복원 | | 복원 후 앱 오류 | `pm2 restart`, `npm run db:schema`는 **복원 dump에 스키마 포함** 시 보통 불필요 | ### `data/` JSON과의 관계 `ENABLE_POSTGRES=1`이면 **강의·회의록·경영성과 등 핵심 데이터는 PostgreSQL**이 단일 소스입니다. `data/ai-success-stories.json`, 업로드 파일(`uploads/`, `public/resources/`) 등 **파일 기반 데이터는 DB 백업에 포함되지 않습니다**. 운영 백업 정책에 파일 디렉터리 rsync/tar 백업을 **별도** 두는 것을 권장합니다. --- - 인앱 채팅 화면은 `/ai-explore/chat`에서 제공하며, 백엔드는 `POST /api/chat/stream` SSE로 응답을 생성합니다. - 기본 제공 모델 별칭은 `gpt-5.4`, `gpt-5-mini`이며, 실제 API 모델 ID는 `.env`의 `OPENAI_MODEL_DEFAULT`, `OPENAI_MODEL_MINI`로 매핑합니다. - `OPENAI_WEB_SEARCH=1`(기본)일 때 OpenAI Responses API의 웹 검색 도구를 사용해 출처 링크를 함께 내려줍니다. - 필수 환경 변수는 `OPENAI_API_KEY`입니다. ### 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=ncue-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 # OPS 이메일(@ncue.net) 로그인 세션: `OPS_SESSION_TTL_DAYS` 미설정·0·never = 만기 없음(이전 기본 15일). N(양의 정수)이면 로그인일+N일(서울 달력) 후 만료. # OPS_SESSION_TTL_DAYS=0 ``` --- ## 사용 방법 1. 메인 페이지에서 강의 등록 - **유튜브 강의 등록**: 제목 + 유튜브 링크 (+설명) - **PowerPoint 강의 등록**: 제목 + `.pptx` 파일 (+설명) - **동영상 파일 등록**: 제목 + 영상 파일 (`ffmpeg`로 카드 썸네일 생성) - 두 등록 폼 모두 **태그(쉼표 구분)** 입력 가능 2. 하단 **등록된 강의** 카드에서 항목 클릭 3. 강의 상세 화면에서 시청 - 유튜브: 동영상 재생 - PPT: 슬라이드 이미지 확인(미생성 장은 이미지 없이 카드만 표시) ### 검색/필터 - 검색어, 타입, 태그를 조합해서 목록을 조회할 수 있습니다. - 결과 목록은 페이지 단위로 분할되어 이동 가능합니다. ### 관리자 삭제 1. 화면의 관리자 모드에서 토큰 입력 후 활성화 2. 강의 카드의 `삭제` 버튼으로 즉시 삭제 3. PPT 강의 삭제 시 업로드 파일도 함께 제거 시도 ### 썸네일 재생성(관리자) - 관리자 모드에서 PPT·업로드 동영상 카드의 `썸네일 재생성` 버튼으로 수동 재생성 가능 - `실패 썸네일 일괄 재시도` 버튼으로 실패 건을 큐에 일괄 재등록 가능 - `이벤트 로그 페이지`에서 기간/유형/강의ID/사유 필터 조회 가능 - 이벤트 로그는 CSV 다운로드 지원 --- ## 주요 라우트 ### 메뉴별 화면 | 경로 | 설명 | |------|------| | `GET /` | 학습센터 뷰어 (강의 목록) | | `GET /learning` | 학습센터 뷰어 (검색/필터/페이지네이션) | | `GET /admin` | 관리자 대시보드 (강의·썸네일·AI 등) | | `GET /chat` | 회사규정 NotebookLM 페이지로 리다이렉트 | | `GET /wm` | WM NotebookLM 페이지로 리다이렉트 | | `GET /ai-explore/chat` | OpenAI 연동 채팅 화면 | | `GET /ai-explore/fscan` | AI 플랫폼 내 FSCAN 조사각 선정도우미 화면(내재화 iframe) | | `GET /ai-explore` | AI 탐색 (회의록·체크리스트 등 카드; 프롬프트 라이브러리는 `/ai-explore/prompts` 또는 좌측 **프롬프트** 메뉴) | | `GET /ai-explore/prompts` | 프롬프트 라이브러리 — 공식 템플릿(`config/company-prompts.json`)+팀 공유·좋아요(PostgreSQL `prompt_community_entries`·`prompt_likes`) · `?id=` 딥링크 | | `GET /ai-cases` | AI 활용 사례 목록(검색·태그 필터) — **관리자 등록**(`ai-success-stories` 메타) + **일반 제출**(`ai_use_case_submissions`) 병합 | | `GET /ai-cases/submit/:id` | 일반 제출 사례 상세(UUID) — **본인·관리자**에게 상단 **수정하기** 링크(`GET /ai-cases/compose?edit=`) | | `GET /ai-cases/:slug` | AI 활용 사례 상세(메타·슬러그) | | `GET /ai-cases/write` | AI 활용 사례 관리(관리자 토큰 필요) | | `POST/PUT/DELETE /api/ai-success-stories` | 사례 CRUD(관리자) · 본문은 `data/ai-success-stories/*.md` | | `GET /ai-cases/compose?edit=:uuid` | 일반 제출 **수정** 화면(제목·4섹션·태그·첨부·썸네일) — 권한 없으면 403 | | `POST /api/ai-use-case-submissions` | AI 활용 사례 **일반 제출(글쓰기)** — OPS 이메일 로그인·PostgreSQL, `ai_use_case_submissions` — 본문 4필드는 WYSIWYG HTML이며 `sanitize-html`로 안전한 태그만 저장, 글자 수는 **보이는 텍스트(태그 제외)** 합산 | | `PUT /api/ai-use-case-submissions/:id` | 일반 제출 **갱신**(제출자 이메일과 로그인 이메일 일치 또는 관리자 모드). **썸네일**은 새 파일이면 교체, `removeThumbnail=1`이면 삭제(새 파일·삭제 둘 다 없으면 유지). **첨부**는 기존을 `removeAttachmentPaths`(JSON 경로 배열)로 뺀 뒤, 새로 업로드한 파일이 뒤에 **추가**(합계 최대 10개) | | `GET /ax-apply` | AX 과제 신청 | | `GET /lectures/:id` | 강의 상세 뷰어 (유튜브/PPT) | **AI 활용 사례 목록이 줄어든 경우:** 카드 목록은 **`data/ai-success-stories.json`**만 본다. `git pull` 등으로 JSON만 예전 버전이 되면 `.md`·PDF는 서버에 남아 있어도 **1건만 보일 수 있다.** 서버에서 `data/ai-success-stories/*.md`는 있는데 JSON에 없으면 `node scripts/merge-orphan-ai-success-stories.js`로 고아 `.md`를 메타에 다시 붙인 뒤, 관리자 화면에서 **부서·저자·PDF 경로** 등을 점검한다. JSON·본문·`public/resources/ai-success/` PDF는 **배포 서버에서 백업**하거나 저장소에 포함하는 정책을 권장한다. ### API - `POST /api/prompts/likes/toggle` (OPS·DEV 이메일) JSON `{ "kind": "official" | "community", "id": "" }` — 좋아요 토글. `{ ok, liked, likeCount }` - `POST /api/prompts/community` (OPS·DEV 이메일) `multipart/form-data` — `title`, `body`, `description?`, `tag?`, `promptFiles`(0~5), `resultFiles`(0~5) · **작성자**는 요청한 OPS 로그인 이메일이 `prompt_community_entries.author_email`에 저장됨 · DB `prompt_attachments` / `result_sample_attachments` JSON · 파일당 20MB → `{ ok, id, createdAt, promptFiles, resultFiles }` · 라이브러리 카드에는 이메일의 `@` 앞부분(예: `spark_ai@ncue.net` → `spark_ai`)이 표시됨 - `DELETE /api/prompts/community/:id` (OPS·DEV 이메일) — 본인 글만 소프트 삭제 - `POST /api/prompts/polish-workflow` (OPS·DEV 이메일) JSON `{ draft }` — `OPENAI_API_KEY`로 워크플로 초안을 다듬은 프롬프트 문자열 `{ text }` 반환 - `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 다운로드 --- ## 최근 업데이트 (요약) - **PostgreSQL 백업·복원**: `scripts/pg-backup.sh` / `scripts/pg-restore.sh`, `npm run db:backup` · README 「PostgreSQL 백업 및 복원」에 cron·복원 절차 문서화 - **채팅** (`/ai-explore/chat`): OpenAI SSE 기반 인앱 채팅으로 응답/출처를 표시 - **AI 탐색 필터** (`/ai-explore`): 검색창 아래 타입 라디오 필터(일반/XScan/FScan)로 카드 목록 필터링 - **FSCAN 도구 내재화**: `public/resources/fscan/fscan-selector-v1.html` 정적 페이지를 `/ai-explore/fscan`에서 iframe으로 감싸 AI 플랫폼 레이아웃(좌측 메뉴/뒤로 이동) 안에서 사용 - **AI 탐색** (`/ai-explore`): 메인 콘텐츠를 뷰포트 전체 너비로 사용(회의록·체크리스트 등). **프롬프트 라이브러리**는 AI 목록이 아닌 좌측 **프롬프트** 메뉴(`/ai-explore/prompts`)로 진입. **좌측 전체 메뉴는 관리자 여부와 관계없이 동일하게 표시·접근 가능** - **프롬프트 라이브러리** (`/ai-explore/prompts`): 시나리오 카드·미리보기·복사 UI, 템플릿 데이터는 `config/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 변환 파이프라인) 연동이 필요합니다. - 이벤트 로그 페이지의 실시간 그래프는 클라이언트 폴링 기반이며, 다수 접속 시 폴링 주기 조정이 필요할 수 있습니다.