feat: xavis ai_platform 기능 이전 및 ncue 환경 전환
xavis 소스·DB 스키마·활용사례/F-Scan/프롬프트 라이브러리 등 기능 반영. @xavis.co.kr → @ncue.net, 관리자 토큰 ncue-admin, 런타임 data/ Git 추적 제외. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
368
README.md
368
README.md
@@ -2,6 +2,8 @@
|
||||
|
||||
유튜브 링크와 PowerPoint(`.pptx`) 강의를 등록하고, 목록에서 클릭해 바로 시청할 수 있는 사내 학습센터 예제입니다.
|
||||
|
||||
**소스 저장소(Git):** [https://git.xavis.co.kr/AI_Innovation_Team/ai_platform](https://git.xavis.co.kr/AI_Innovation_Team/ai_platform)
|
||||
|
||||
---
|
||||
|
||||
## 접속 방법
|
||||
@@ -19,7 +21,7 @@
|
||||
- **학습센터 뷰어** (`/learning`): 강의 검색/필터 + 카드 목록 (일반 사용자)
|
||||
- **관리자** (`/admin`): 강의 등록(YouTube/PPT/웹 링크 등), 썸네일·AI 추가 등 통합 관리
|
||||
- **강의 상세**: 카드 클릭 시 유튜브 재생 또는 PPT 뷰어
|
||||
- **기타 메뉴**: 채팅(`/chat`), AI(`/ai-explore`), **프롬프트 라이브러리**(`/ai-explore/prompts`), **AI 성공 사례**(`/ai-cases`), **대시보드**(`/dashboard`, 성공 사례 아래 메뉴), AX 과제 신청(`/ax-apply`)
|
||||
- **기타 메뉴**: 회사규정(`/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}`
|
||||
|
||||
### 접속이 안 될 때 (트러블슈팅)
|
||||
@@ -35,19 +37,26 @@
|
||||
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 서비스 카드(프롬프트·회의록 등). 검색창에 **「프롬프트」**가 포함된 채 검색(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`입니다.
|
||||
- **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`으로 동기화합니다. 리버스 프록시 사용 시 업로드 실패하면 **`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행이어도 차트가 엑셀 집계와 일치하려면 **별도 집계·매핑 로직**이 필요합니다.
|
||||
- **경영성과 대시보드** (`/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`): 업무별 기본 프롬프트 카드 선택·미리보기·클립보드 복사 (`data/company-prompts.json`). **좌측 메뉴(채팅·AI·AI 성공 사례·학습센터·AX 과제 신청)는 관리자 여부와 관계없이 접근 가능**(강의 삭제·관리자 대시보드 등 일부 기능은 관리자 모드에서만)
|
||||
- **프롬프트 라이브러리** (`/ai-explore/prompts`): 좌측 **프롬프트** 메뉴로 진입 · 업무별 기본 프롬프트 카드 선택·미리보기·클립보드 복사 (`config/company-prompts.json`) · **공유하기** 탭의 본문은 **원문 보기** / **마크다운 보기** 토글(클라이언트 `marked` + `DOMPurify`) · **워크플로** 탭(①~④ 입력·초안 합치기·AI로 다듬기). **좌측 메뉴(채팅·AI·프롬프트·AI 활용 사례·학습센터·AX 과제 신청)는 관리자 여부와 관계없이 접근 가능**(강의 삭제·관리자 대시보드 등 일부 기능은 관리자 모드에서만)
|
||||
- 검색/필터/페이지네이션
|
||||
- 검색어(`q`) 기반 제목/설명/태그 필터
|
||||
- 타입(`YouTube`, `PPT`) 필터
|
||||
@@ -75,7 +84,7 @@
|
||||
- **관리자 인증·바로가기(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 로그인 중에도 표시).
|
||||
- **AI 활용 사례** (`/ai-cases`): 관리자일 때 상단 **사례 등록·관리**로 편집 화면(`/ai-cases/write`)에 진입합니다(동일하게 OPS 로그인 중에도 표시).
|
||||
|
||||
---
|
||||
|
||||
@@ -88,36 +97,44 @@ ai_platform/
|
||||
├─ .env # 환경 변수 (실제 값, .gitignore 대상)
|
||||
├─ .env.example # 환경 변수 예시 템플릿
|
||||
├─ db/
|
||||
│ └─ schema.sql # PostgreSQL 스키마 (강의·회의록·경영성과 업로드 등, 기동 시 자동 적용)
|
||||
│ └─ schema.sql # PostgreSQL 스키마 (강의·회의록·경영성과·프롬프트 라이브러리 좋아요/공유 등, 기동 시 자동 적용)
|
||||
├─ scripts/
|
||||
│ └─ apply-schema.js # 수동 스키마 적용 스크립트 (npm run db:schema)
|
||||
│ ├─ 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 # 전역 스타일
|
||||
│ └─ styles.css # 전역 스타일(회의록 마크다운 뷰 `.mm-minutes-rendered` 표 셀 테두리 등)
|
||||
├─ views/
|
||||
│ ├─ partials/
|
||||
│ │ ├─ nav.ejs # 좌측 공통 네비게이션(하단 관리자·토큰 모달 연동)
|
||||
│ │ └─ admin-token-modal.ejs # 관리자 토큰 입력 모달(`openAdminTokenModal`)
|
||||
│ ├─ learning-viewer.ejs # 학습센터 뷰어 (일반 사용자)
|
||||
│ ├─ learning-admin.ejs # 학습센터 관리 (업로드·삭제·썸네일)
|
||||
│ ├─ chat.ejs # 채팅
|
||||
│ ├─ ai-explore.ejs # AI 탐색 (전체 너비, 프롬프트 카드·검색)
|
||||
│ ├─ 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 성공 사례 목록(카드)
|
||||
│ ├─ ai-case-detail.ejs # AI 성공 사례 상세(마크다운 또는 PDF·페이지 이미지, 1·2·3단 보기)
|
||||
│ ├─ ai-cases-write.ejs # AI 성공 사례 관리자 등록·편집
|
||||
│ ├─ 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 # 썸네일 이벤트 로그 관리자 페이지
|
||||
├─ data/
|
||||
│ ├─ lectures.json # 강의 메타데이터 (ENABLE_POSTGRES=0 또는 DB 폴백 시)
|
||||
│ ├─ company-prompts.json # AI 프롬프트 라이브러리 템플릿 (제목·설명·본문)
|
||||
│ ├─ thumbnail-jobs.json # 썸네일 큐 스냅샷 (재시작 후 복구용)
|
||||
│ └─ thumbnail-events.json # 썸네일 작업 이벤트 로그
|
||||
├─ 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/
|
||||
@@ -130,6 +147,7 @@ ai_platform/
|
||||
|------|------|
|
||||
| **서버** | `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/관리자 페이지 렌더링 |
|
||||
|
||||
@@ -235,8 +253,8 @@ PPT 썸네일·슬라이드 이미지는 **macOS**에서는 `qlmanage`가 우선
|
||||
```
|
||||
|
||||
6. **방화벽·리버스 프록시**
|
||||
- 외부에 직접 `8030`을 열지 않고 **Nginx/Apache**로 TLS·프록시하는 구성이 일반적입니다.
|
||||
- **Apache2** 예시(모듈·VirtualHost)는 [docs/DEPLOYMENT-xavis.ncue.net.md](docs/DEPLOYMENT-xavis.ncue.net.md)를 참고하세요.
|
||||
- 외부에 직접 `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. **데이터 디렉터리 권한**
|
||||
@@ -248,7 +266,7 @@ PPT 썸네일·슬라이드 이미지는 **macOS**에서는 `qlmanage`가 우선
|
||||
|
||||
- 터미널 또는 `pm2 logs ai_platform`에서 **에러 없이** `Server started` 로그 확인
|
||||
- 브라우저로 메인·`/learning`·관리자(토큰)까지 동작 확인
|
||||
- PostgreSQL 사용 시 `npm run db:schema`는 **최초 1회**(또는 스키마 변경 시); 백업·마이그레이션 정책은 운영 환경에 맞게 별도 수립
|
||||
- PostgreSQL 사용 시 `npm run db:schema`는 **최초 1회**(또는 스키마 변경 시); **일일 DB 백업**은 아래 「PostgreSQL 백업 및 복원」의 cron 절을 따릅니다.
|
||||
|
||||
---
|
||||
|
||||
@@ -394,7 +412,7 @@ Windows에서는 작업 관리자에서 `Node.js` 프로세스를 종료하거
|
||||
ADMIN_TOKEN=my-secret PAGE_SIZE=12 npm start
|
||||
```
|
||||
|
||||
- `ADMIN_TOKEN` 미지정 시 기본값: `xavis-admin`
|
||||
- `ADMIN_TOKEN` 미지정 시 기본값: `ncue-admin`
|
||||
- `PAGE_SIZE` 미지정 시 기본값: `8`
|
||||
- `.env` 파일이 있으면 `dotenv`로 자동 로드
|
||||
- 브라우저에서는 좌측 메뉴 하단 **관리자** → 모달에 위 토큰을 입력해 세션 쿠키를 발급받는 방식으로 `/admin`에 진입할 수 있습니다(임직원 이메일 로그인 여부와 동일한 흐름).
|
||||
@@ -406,16 +424,256 @@ ADMIN_TOKEN=my-secret PAGE_SIZE=12 npm start
|
||||
- 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을 반환하고, 채팅 화면 상단에 안내 배너가 표시됩니다.
|
||||
## 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 썸네일 및 슬라이드 이미지 생성 제어(선택)
|
||||
|
||||
@@ -445,7 +703,7 @@ ENABLE_PPT_THUMBNAIL=1 npm start
|
||||
```env
|
||||
PORT=8030
|
||||
HOST=0.0.0.0
|
||||
ADMIN_TOKEN=xavis-admin
|
||||
ADMIN_TOKEN=ncue-admin
|
||||
PAGE_SIZE=8
|
||||
ENABLE_POSTGRES=1
|
||||
DB_HOST=your-db-host
|
||||
@@ -466,6 +724,8 @@ THUMBNAIL_EVENT_PAGE_SIZE=50
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
@@ -511,18 +771,38 @@ OPENAI_API_KEY=
|
||||
| `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 성공 사례 관리(관리자 토큰 필요) |
|
||||
| `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=<uuid>`) |
|
||||
| `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": "<json id or uuid>" }` — 좋아요 토글. `{ 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 }` (키 문자열은 노출하지 않음)
|
||||
|
||||
@@ -563,8 +843,12 @@ OPENAI_API_KEY=
|
||||
|
||||
## 최근 업데이트 (요약)
|
||||
|
||||
- **AI 탐색** (`/ai-explore`): 메인 콘텐츠를 뷰포트 전체 너비로 사용, **프롬프트** 서비스 카드를 첫 번째에 배치, 검색어에 **「프롬프트」**가 포함된 채 검색(Enter) 시 프롬프트 라이브러리로 이동. **좌측 전체 메뉴는 관리자 여부와 관계없이 동일하게 표시·접근 가능**
|
||||
- **프롬프트 라이브러리** (`/ai-explore/prompts`): 시나리오 카드·미리보기·복사 UI, 템플릿 데이터는 `data/company-prompts.json`에서 로드
|
||||
- **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` 접속 불가 시 확인할 항목을 아래 「접속이 안 될 때 (트러블슈팅)」 절에 정리
|
||||
|
||||
|
||||
Reference in New Issue
Block a user