JENNIFER × AI 발표 슬라이드와 데모를 모은 Next.js 서버다. 제니퍼 사용자정의 대시보드에 넣는 차트·위젯과 웹 슬라이드 덱을 한 앱에서 제공한다.
npm install
cp .env.example .env.local # 값은 아래 "환경변수" 참고. 비워 두면 데모 데이터로 동작한다
npm run dev # http://localhost:3000슬라이드
| 경로 | 용도 |
|---|---|
/slides |
발표 덱 목록 (날짜 최신순) |
/slides/<덱> |
웹 슬라이드. 예: /slides/japan-2026 |
/slides/<덱>/script |
모바일·태블릿 발표자 대본, 통역용 원고, 오프라인 저장·인쇄 |
제니퍼 사용자정의 대시보드 iframe 위젯용 (URL로 등록)
| 경로 | 용도 |
|---|---|
/charts/[id] |
레이아웃 없는 차트 단독 페이지 (예: /charts/tps) |
/widgets/incidents |
열린 PagerDuty 인시던트와 담당자. PAGERDUTY_API_KEY가 없으면 샘플 데이터 |
/widgets/deploy-timeline?hours=24 |
배포·변경 타임라인. 제니퍼 응답시간 차트 옆에 같은 시간 범위로 두면 "배포 직후 느려짐"을 바로 본다 |
데모 화면
| 경로 | 용도 |
|---|---|
/embed |
제니퍼 /popup/chartIframe과 AI 차트를 나란히 보여준다 |
API
| 경로 | 용도 |
|---|---|
/api/jennifer/* |
제니퍼 OpenAPI 프록시. 토큰은 서버에만 둔다 |
/api/incidents |
PagerDuty 열린 인시던트 |
/api/deploys |
배포 이벤트. GET ?hours=N 조회, POST는 CI가 배포 직후 호출 |
- 주소:
/slides/<덱>?jennifer=<제니퍼 대시보드 주소>.?jennifer=가 없으면JENNIFER_DASHBOARD_URL을 쓰고, 둘 다 없으면 라이브 화면에 "未設定"이 뜬다. 대시보드 주소 끝에layout=iframe을 붙이면 제니퍼 상단 메뉴가 빠진다. - 조작:
→/Space다음,←이전,O목록,F전체화면,I라이브 화면 조작(여백을 클릭하면 해제),T채팅 원문/번역 전환,Home/End처음/끝. 주소 끝#s3처럼 장 번호로 바로 연다. 슬라이드 화면에는 대본·노트를 띄우지 않는다(확대 화면에서 청중에게 그대로 보이기 때문). 발표 중 대본은 휴대폰·태블릿에서/slides/<덱>/script?view=stage로 본다. - 라이브 화면(iframe) 위에는 보호막이 덮여 있어 클릭·키보드는 슬라이드가 받는다. 화면을 직접 조작할 때만
I를 누르고, 끝나면 슬라이드 여백을 클릭해 돌아온다. - 제니퍼와 이 서버의 도메인이 다르면, 발표할 브라우저에서 제니퍼에 미리 로그인해 둔다(아래 "제니퍼 연동 주의").
- 발표자 노트의
[확인 필요]항목은 아직 확정 전이다.
/slides에서 대본 연습을 누른다. 일본 행사 대본은/slides/japan-2026/script다.- 기본 발표자 대본은 직접 말할 짧은 문장이다. 한 구간을 읽고 통역 대기에서 통역이 끝날 때까지 기다린다. 통역용 원고로 전환하면 준비용 상세 원고를 볼 수 있다. 현장에서는 발표자 대본의 구간에 맞춰 통역한다. 진행 메모는 별도로 켠다.
- 글자 크기와 장 선택으로 읽기 편하게 맞춘다.
#s17처럼 특정 장을 바로 열 수 있다.?mode=interpreter는 통역용 원고,?notes=1은 진행 메모를 처음부터 표시한다. - 비행 전 오프라인 저장을 눌러 대본 파일을 기기에 저장하고, 저장한 파일이 열리는지 확인한다. 파일에는 두 종류의 대본, 진행 메모, 화면 스타일과 조작 기능이 함께 들어 있어 인터넷 없이 사용할 수 있다. 웹 주소를 열어 두는 것만으로 오프라인 저장되는 것은 아니다.
- PDF가 편하면 인쇄 / PDF에서 저장한다. 현재 선택한 대본 종류와 메모 표시 상태로 인쇄된다. 기기의 파일 미리보기에서 HTML 조작이 지원되지 않으면 PDF를 이용한다.
대본 페이지는 슬라이드와 같은 방식의 독립 HTML 응답이다. CSS·JavaScript를 함께 넣고 외부 글꼴·영상·라이브 API에 의존하지 않는다. ?download=1은 같은 리더를 <덱>-script.html 첨부 파일로 반환한다.
공통 엔진은 한 번 만들고 모든 덱이 같이 쓴다. 엔진을 고치면 모든 덱에 반영된다.
slides/
_engine/ 공통 엔진. engine.css(무대·애니메이션·카드·타임라인·숫자 카드·라이브 iframe),
engine.js(이동·단계·노트·목록·전체화면), chrome.html(HUD)
_template/ 새 덱 시작 템플릿 (목록에 안 보이고, /slides/_template 로 미리보기)
japan-2026/ 덱 하나 = meta.json + slides.html (+ deck.css, deck.js)
/slides/<덱>은 lib/slides.ts가 엔진과 덱 파일을 합쳐 HTML 한 장으로 렌더링한다.
cp -r slides/_template slides/<덱이름>(덱이름은 소문자·숫자·하이픈)meta.json의title,theme(dark|light, 템플릿은light),lang,date,description을 고친다.?theme=light로 테마를 바꿔 미리볼 수 있다.slides.html에<section class="slide">를 한 장씩 쓴다.data-presenter는 짧은 발표자 대본(빈 줄은 통역 대기 구간),data-script는 통역용 상세 원고,data-notes는 진행 메모다.data-a는 등장 애니메이션(style="--d:n"으로 순서).- 실제 화면은 한 줄로 넣는다.
{{live:jennifer}}는 제니퍼 대시보드,{{live:/경로}}는 이 서버 페이지(예:{{live:/widgets/pagerduty}}). 움직이는 목업 차트는<canvas data-chart="line|bars|xview|heat" data-max="200" data-unit="ms">로 넣는다. line·bars·xview는 Y축(0·절반·최대)과 시간축을 같이 그린다. - 단계 연출이 필요하면 섹션에
data-steps="N"을 주고deck.js에hooks['<섹션 id>']=st=>{...}를 쓴다. - 여러 덱에 쓸 만한 스타일·컴포넌트가 생기면 덱 폴더가 아니라
_engine으로 올린다.
- 제니퍼 대시보드에 이 서버 차트를 넣을 때: 고객이
enable_csp를 켜면 기본값frame-src 'self'때문에 막힌다. 차트 도메인을csp_policy에 추가해야 한다. 이 서버는FRAME_ANCESTORS로 자신을 넣을 수 있는 origin을 제한한다. - sandbox: 다른 origin 위젯에는 sandbox가 자동으로 붙는다(view-server
DashboardIframePolicy). 차트가/api/jennifer를 부르려면 위젯 설정에서allow-scripts와allow-same-origin을 켠다. - 제니퍼 차트를 다른 도메인에서 불러올 때: HTTPS와 제니퍼의
x_frame_options,enable_csp,add_same_site_none_cookie설정이 필요하다. Safari·시크릿 창은 로그인 쿠키가 막힐 수 있다. - OpenAPI 인증: 프록시는
Authorization: Bearer <token>헤더를 쓴다. 제니퍼는?token=쿼리도 받지만(view-serverInterceptorUtil.getTokenKey) 쿼리 토큰은 로그에 남을 수 있다. - 허용 경로: 프록시는 차트용 조회 경로(
domain,instance,business,realtime/*,metrics,dbmetrics/*)만 허용한다. 필요한 경로는app/api/jennifer/[...path]/route.ts의ALLOWED_PATHS에 추가한다.
- 공개 데모는 가용성 보장이 없다. 응답이 없으면 차트에 "녹화 데이터"로 표시된다. fallback은 2026-10-11에 녹화한 실제 응답(15분)이다.
CI 배포 잡 마지막에 호출한다. DEPLOY_WEBHOOK_SECRET이 없으면 POST는 503으로 막힌다.
curl -X POST "$SHOWCASE_URL/api/deploys" -H "Authorization: Bearer $DEPLOY_WEBHOOK_SECRET" -H "Content-Type: application/json" -d '{"title":"v1.2.3","service":"order-api"}'- 데모용으로
DEPLOY_GITHUB_REPO(예:prometheus/prometheus)를 넣으면 그 저장소의 GitHub 릴리즈도 함께 찍힌다. 긴 범위(?hours=720)로 보면 된다. - POST로 받은 이벤트는 서버 메모리에만 있어 재시작하면 사라진다. 운영에는 외부 저장소(DB, Vercel KV, Upstash 등)가 필요하다.
.env.example에 같은 목록이 있다. 모두 비워도 데모 데이터로 동작한다.
| 이름 | 용도 | 필요할 때 |
|---|---|---|
JENNIFER_URL |
제니퍼 뷰서버 주소 (OpenAPI 프록시, chartIframe) | 제니퍼 연동 |
JENNIFER_API_TOKEN |
제니퍼 OPEN_API 토큰. 서버에서만 쓴다 | 제니퍼 연동 |
FRAME_ANCESTORS |
이 서버 페이지를 iframe으로 넣을 수 있는 origin(공백 구분). 제니퍼 도메인을 넣는다 | 대시보드 위젯 |
JENNIFER_DASHBOARD_URL |
슬라이드 라이브 화면의 제니퍼 대시보드 기본 주소 (?jennifer=로 덮어쓰기) |
선택 |
PAGERDUTY_API_KEY, PAGERDUTY_SERVICE_ID |
인시던트 위젯 (읽기 전용 키 권장) | 선택 |
DEPLOY_WEBHOOK_SECRET, DEPLOY_GITHUB_REPO, GITHUB_TOKEN |
배포 타임라인 | 선택 |
- GitHub 저장소를 Vercel 프로젝트에 연결하면
main푸시마다 배포된다. 환경변수는 Vercel 프로젝트 설정 → Environment Variables에 넣는다. - 커밋 작성자: Vercel은 배포하는 커밋의 작성자가 팀 멤버여야 배포한다. 이 저장소의 커밋은
alvin-hh/alvin@jennifersoft.com으로 한다.
git config user.name alvin-hh
git config user.email alvin@jennifersoft.com- Root Directory: 저장소 루트(
./). Framework는 Next.js(자동 감지). - 도메인: https://jennifer.alvin.ing. Cloudflare DNS에서 Vercel로 CNAME을 걸고, Cloudflare 프록시는 끈다(DNS only).
- Next.js 버전: 취약 버전이면 빌드가 성공해도 배포가 Error로 막힌다. 15.5 최신 패치(현재 15.5.27)를 유지한다.
- 슬라이드 파일:
/slides,/slides/<덱>은slides/폴더를 파일로 읽는다.next.config.ts의outputFileTracingIncludes로 배포물에 포함된다. - 서버리스 주의:
POST /api/deploys이벤트는 인스턴스가 바뀌면 사라진다. 데모는DEPLOY_GITHUB_REPO의 GitHub 릴리즈로 보여준다.
- 공통 토큰은
app/globals.css(기본 라이트). 위젯·차트는?theme=dark로 다크 테마가 된다. 제니퍼 대시보드 테마(classic/dark)에 맞춰 넘긴다. - 차트는
components/TimeSeriesChart.tsx하나를 쓴다. y축 눈금·격자, x축 시간, 현재 값, 마우스 위치 값이 있다. 패널은components/ChartWidget.tsx. - 슬라이드 테마는 엔진(
slides/_engine/engine.css)의:root[data-theme="light"]토큰으로 바뀐다. 덱 CSS에서는 색을 직접 쓰지 말고var(--fg),var(--panel)같은 토큰을 쓴다.
위젯·차트·대시보드는 한국어·일본어·영어를 모두 지원해야 한다. 첫 발표가 일본 고객 대상이라 이것을 기본 규칙으로 둔다.
- 화면 문구는 코드에 직접 쓰지 않는다.
lib/i18n.ts의DICT에 키를 추가하고ko·ja·en세 언어를 모두 채운다(하나라도 빠지면 타입 오류). - 언어는
?lang=ko|ja|en→ 브라우저Accept-Language→en순으로 정한다. 서버 페이지는requestLang(), 컴포넌트는t(lang, key)를 쓴다. - 대시보드는 받은
lang·theme를 하위 위젯 iframe 에 그대로 넘긴다(withParams). - 슬라이드의
{{live:/경로}}는 덱meta.json의lang을 자동으로 붙인다. 일본 덱이면 라이브 화면도 일본어로 뜬다. - 샘플 데이터 문구도 번역한다. 실제 외부 데이터(PagerDuty 제목 등)와 제품 용어(Triggered, TPS 등)는 그대로 둔다.