Skip to main content
이미 있는 교육 웹앱을 Classroom Hub에 올릴 때 쓰는 재사용 플레이북입니다. 새 앱을 처음부터 만들 때는 빠른 시작과 에이전트 스펙을 먼저 보세요. 첫 적용: Reading Passport. 다음 적용: Image Studio.
이 페이지는 제품 화면을 바꾸라는 문서가 아닙니다. 로그인 UI, API 키, 과금 ID만 Daven 쪽으로 옮기고, 학습 흐름과 브랜드·카피는 앱이 그대로 가집니다.

한 줄

앱이 학습 UX와 결과물을 소유한다. Hub가 사람과 교실을 소유한다. Daven이 AI 실행과 학교 지갑을 소유한다. 브라우저는 키와 payer를 모른다.

세 층

@davenai/sdk/edu(브라우저)와 @davenai/sdk/server(BFF)가 두 층을 연결합니다. 앱 코드에 login()을 만들지 마세요. 공개 이름은 session입니다.

불변 조건

아래를 깨면 다른 앱으로 복제하지 마세요.
  1. Hub 시작은 {hostedUrl}?session=&proxy=&locale=를 새 탭으로 연다. locale은 Hub 경로와 같다 (en | kr). 앱 라우트가 ko이면 kr을 ko로 받는다. 교실 기본 실행을 대시보드 iframe으로 두지 않는다.
  2. 학생·교사는 Hub에서만 로그인한다. 앱에 Daven / Google / 자체 학생 코드 로그인을 Edu 기본 흐름으로 두지 않는다.
  3. 브라우저와 공개 번들에 Daven 키, MCP 키, OPENAI_API_KEY, 학교 payer ID를 넣지 않는다.
  4. 클라이언트가 studentId, schoolId, classroomId, payer를 고르지 않는다. GET /api/session/context만 믿는다.
  5. Edu 런타임의 AI는 proxy만 탄다. 세션이 없거나 만료되면 EDU_RELAUNCH_REQUIRED로 닫고, 운영자 provider 키로 fallback하지 않는다.
  6. 사용자 동작마다 안정적인 operationId를 한 번 만든다. 재시도는 그 ID에서 결정적으로 파생한다. 본문이 다른 요청에 같은 멱등키를 쓰지 않는다.
  7. 앱 안 “저장 / 공개 / 다운로드”와 Hub 최종 제출은 다른 버튼이다. 제출 externalArtifactId는 앱 작품 ID다. 같은 ID 재제출만 기존 Hub 행을 갱신한다.
  8. shouldRelaunchEducationApp(error)가 true면 작업은 유지하고 Hub에서 다시 열라고 안내한다. 자동 로그인 갱신은 기본값이 아니다.
  9. 18세 미만 학생 경로에 Gemini 등 약관상 막힌 provider를 쓰지 않는다.
  10. 도메인 프롬프트·검증·화면은 앱에 남긴다. SDK로 옮기는 것은 transport뿐이다.
  11. 탭 파비콘은 Classroom Hub 마크 /brand/classroom-logo.png다. 탭 제목은 앱 이름이다. Next / Vercel 기본 아이콘을 남기지 않는다.
계약 상세: 세션과 proxy · MCP API · 학생 제출 · 에이전트 스펙.

실행 모드

이미 운영 중인 앱은 한 플래그로 경로만 갈라야 합니다. 기본값을 운영 중에 바꾸지 마세요. 모드가 하나뿐이어도 코드에는 이 이름을 쓰세요. 나중에 다른 앱이 같은 플래그를 찾습니다. 로컬·Playground는 개발자 계정이 냅니다. 교실은 학교 관리형 지갑이 냅니다. 두 경로를 한 요청에서 동시에 차감하지 마세요.

책임 경계

  • 제품 이름, 카피, 학습 루프
  • 도메인 프롬프트와 결과 검증
  • 초안·작품·진행 상태 (앱 DB 또는 브라우저 저장)
  • 앱 고유 교사 도구 (콘텐츠 검수, 진행 보드)
  • 내보내기 포맷 (PDF, PPTX, ZIP)
학생별 Daven 계정을 만들지 않습니다. edu_user_id를 기존 user_id / daven_id 컬럼에 넣지 않습니다.

최소 코드

브라우저:
앱 서버가 프롬프트를 조합하면 브라우저로 키를 내리지 말고, 검증된 session + proxy로 @davenai/sdk/server를 만듭니다. SDK에 payer ID를 넘기지 않습니다. 도메인 route는 gateway만 봅니다.
npm에 SDK가 없으면 같은 계약의 내부 transport를 쓰고, publish 뒤 파일만 갈아끼웁니다. route와 프롬프트는 그대로 둡니다.

인증 브리지

앱에 이미 사용자 테이블과 RLS가 있으면 Reading Passport처럼 합니다.
1

Launch

Hub가 {hostedUrl}?session=&proxy=를 새 탭으로 엽니다. SDK가 query를 헤더로 옮기고 주소에서 지웁니다.
2

Bootstrap

앱 서버가 GET {proxy}/api/session/context로 서명 세션을 재검증합니다. JWT를 클라이언트가 디코드해 믿지 않습니다.
3

로컬 principal

edu_actor_id → 결정적 앱 내부 ID. 기존 FK/RLS를 깨지 않으려면 로그인 불가 로컬 principal만 발급합니다. 이것은 별도 계정이 아닙니다.
4

쿠키

session과 proxy는 HttpOnly. 현재 요청의 Edu actorId와 서버가 가진 edu_actor_id는 항상 같습니다.
앱에 사용자 DB가 없으면 principal을 만들지 마세요. context의 actorId를 런타임 키로만 쓰고, 초안은 브라우저에 둬도 됩니다. Hub 제출 URL만 영구 HTTPS여야 합니다. 이름·학생 코드·이메일로 Edu 사용자를 합치지 마세요. 매칭 키는 actorId뿐입니다. teacher_manage는 학생으로 impersonation하지 않습니다. 교사 전용 화면이 있을 때만 카탈로그에 supportsTeacherManage를 켭니다.

AI 어댑터

1

경계 하나

모든 생성은 gateway를 통과합니다. route에서 OpenAI/Gemini SDK를 직접 부르지 않습니다.
2

런타임 확정

요청 앞에서 Edu인지 legacy인지 정합니다. after() / worker에도 그 런타임을 명시적으로 넘깁니다. 백그라운드에서 쿠키가 사라져도 Edu 작업이 운영자 키로 내려가면 안 됩니다.
3

견적 → 생성 → 폴링

이미지는 quote → media/create → media/get. 텍스트는 text/create. 고정 크레딧과 다른 숫자를 UI에 지어내지 않습니다.
4

실패

provider가 명시적으로 실패하면 예약을 해제합니다. 세션 만료는 저장본을 유지하고 재실행을 안내합니다.
프롬프트, 레퍼런스 타일, 안전 규칙, 화면 문구는 앱 파일에 남깁니다.

제출

같은 작품을 다시 내면 그 행만 갱신합니다. 다른 ID는 새 행입니다. 생략하면 __default__ 한 줄이 됩니다. 제출 전에 생성 이미지를 만료되지 않는 저장소로 복사하세요. 7일 cleanup 버킷 URL을 primaryUrl에 넣지 마세요.

호스트와 탭 아이콘

다음 앱에도 그대로 복사합니다.
  • 공개 URL은 https://{slug}.edu-app.daven.ai입니다. DNS는 *.edu-app.daven.ai → cname.vercel-dns.com 한 장입니다. 앱을 *.edu.daven.ai에 두지 마세요. 그 존은 Hub / api / dev입니다.
  • Vercel 프로젝트에 {slug}.edu-app.daven.ai만 추가합니다. 와일드카드 DNS는 이미 있습니다.
  • 탭 파비콘은 Classroom Hub 마크입니다. Hub 파일 dv-edu/apps/web/public/brand/classroom-logo.png를 public/brand/classroom-logo.png로 복사하고, metadata icons.icon / icons.apple에 넣습니다.
  • 탭 제목은 앱 이름입니다. 예: Image Studio. 파비콘은 Edu입니다. 앱 자체 로고, Vibe, Next/Vercel 기본 아이콘을 탭에 쓰지 마세요.
  • 운영 Vercel에는 DAVEN_API_KEY를 넣지 않습니다. 교실은 Hub session만 씁니다. 키는 로컬 next dev와 Playground 개발 모드 전용입니다.
  • Playground Submit → Master Approve. appSlug, appName, hostedUrl을 맞춥니다. 교사 전용 화면이 없으면 supportsTeacherManage를 끕니다.
  • 카탈로그 Approve 전에 학생 세션으로 텍스트 1번, 이미지 1번이 실제로 나와야 합니다. 래핑·배포·등록을 한 번에 끝내면 아래 오류가 교실에서 처음 드러납니다.

Image Studio에서 깨진 것

한 번에 올리면 같은 순서로 다시 깨집니다. 다음 앱은 여기부터 막으세요.
Hub POST /api/quote body는 { "media_type": "image" | "text" }뿐입니다. 이걸 daven.ai/api/mcp/quote로 넘기면 PHP가 slug/code를 요구하고 VALIDATION_ERROR 422를 줍니다. 앱은 그걸 “Could not make the picture”로 보여 줍니다.Hub quote는 학교 풀 고정 크레딧을 로컬에서 돌려야 합니다. 이미지 10, 텍스트 1, billing: school-pool. Daven MCP 모델 견적이 아닙니다.견적은 숫자를 보여줄 뿐입니다. 지금 운영 차감은 text/create / media/create가 2xx일 때 학교 remaining_credits에서 됩니다. 견적 422로 생성을 멈추지 마세요. Reading Passport는 견적을 건너뜁니다.
media/create에 model을 안 넣으면 Daven /image/create 워크플로 기본값이 nano-banana-2 입니다. 문서의 gpt-image-2가 자동으로 오지 않습니다.교실 기본 모델은 gpt-image-2.5 입니다. 앱과 Hub 둘 다 이 값을 넣으세요. 학생 경로에 Gemini·nano-banana를 쓰지 마세요.gpt-image-2.5 quality는 fast | quality입니다. medium은 클램프되어 fast가 됩니다. Hub는 quality를 /image/create로 넘겨야 합니다.
text/create가 200인데 data.text가 ""이면 앱은 EMPTY_RESULT 502를 냅니다. gpt-5-mini + reasoning_effort: minimal + 작은 max_output_tokens(예: 900)면 추론에 토큰을 다 씁니다.교실 텍스트는 gpt-5.6-luna, 출력 토큰 1600 이상, reasoning을 기본으로 넣지 마세요. 빈 본문이면 파생 operationId로 한 번 더 치세요. 200 + 빈 텍스트는 실패로 취급하세요.
Daven 결과 PNG는 5MB를 넘는 경우가 많습니다. 생성·과금 뒤에 EDU_IMAGE_TOO_LARGE로 버리면 학생은 502만 봅니다. 다운로드 한도는 약 25MB로 두거나, 받은 뒤 압축하세요.
완성 페이지 PNG를 data URL로 모아 POST /api/studio/submit 하면 Vercel 함수 본문 한도(약 4.5MB)에 걸립니다. 학생은 413 FUNCTION_PAYLOAD_TOO_LARGE 또는 “Too Large”를 봅니다.페이지는 Hub upload-url로 한 장씩 올린 뒤, 제출 본문에는 HTTPS primaryUrl + assets[]만 넣으세요. 브라우저가 storage에 PUT하고, CORS가 막히면 한 장짜리 JPEG BFF로만 우회합니다. Hub 교사 제출물 탭이 그 URL을 엽니다.
운영 Vercel에 DAVEN_API_KEY / OPENAI_API_KEY를 넣지 마세요. 교실은 Hub session만 씁니다. 로컬 next dev + 개발자 키가 된다고 학생 그림이 되는 게 아닙니다.프로젝트 Framework는 Next.js여야 합니다. Other면 첫 배포가 404입니다. 도메인은 {slug}.edu-app.daven.ai만 붙입니다.motion.button에 Framer onDrag가 남으면 React 19 타입 때문에 프로덕션 빌드가 깨집니다. 드래그 props를 빼세요.session JWT를 쿠키에 그대로 넣으면 EDU_SESSION_TOO_LARGE가 납니다. 쿠키는 짧게, 본문은 서버에서만 읽으세요.

단계 (다른 앱에 그대로 복사)

1

0. 계약

expectedAppSlug, session v2 purpose, OpenAPI, 크레딧(text 1 / image 10)을 확인합니다.
2

1. 인증

/edu/bootstrap 또는 동등한 서버 진입. 로그인 화면을 Edu 모드에서 숨깁니다. 관리자 전용 로그인이 있으면 교실 역할과 분리해 유지해도 됩니다.
3

2. 비-AI 루프

launch → 저장 → 닫기 → 다시 열기. AI 없이 데이터가 남는지 먼저 확인합니다.
4

3. 텍스트 AI

낮은 위험 호출부터 gateway로 옮깁니다. 그다음 structured output.
5

4. 이미지 AI

model: gpt-image-2.5를 명시합니다. reference, 비율, job polling. 학생 경로에 Gemini / nano-banana 금지.
6

5. 학생 스모크

Hub Start로 텍스트 1번, 이미지 1번. VALIDATION_ERROR / EMPTY_RESULT / 5MB 폐기가 없는 뒤에만 카탈로그 Approve를 합니다.
7

6. 제출

영구 URL + 안정 externalArtifactId + 명시 버튼. 자동 제출 없음.
완료 기준:
  • 네트워크 탭에 daven.ai / app.daven.ai 브라우저 호출이 없다.
  • 키 없이 같은 generate를 치면 401 또는 relaunch이지, 운영자 키가 끼어들지 않는다.
  • 세션을 지워도 초안이 남고, AI만 막힌다.
  • 교사 preview 세션으로는 제출이 거절된다.

하지 말 것

  • Hub를 앱 안에 iframe으로 넣거나, 앱을 Hub 대시보드 iframe이 기본이 되게 하기
  • Daven 마케팅 화면, 프로필, 결제, auth.daven.ai를 학생에게 보여 주기
  • 앱을 SEED Classroom Hub처럼 다시 그리기 — Hub만 SEED다
  • Vibe 워드마크·Braid 그라디언트를 교육앱 브랜드로 쓰기
  • Next / Vercel 기본 파비콘을 탭에 남기기 — Edu classroom-logo.png를 씁니다
  • “플랫폼 / 솔루션 / AI 키”를 학생 버튼에 쓰기
화면 토큰과 컴포넌트 규칙은 앱 디자인 문서를 따르되, 대기·빈 칸·오류·i18n(EN 기본 / KR 보조) 은 Vibe foundations와 같은 층으로 맞추면 이후 앱이 같은 손을 씁니다. 제품 루프(채팅+프리뷰+Publish)는 복사하지 마세요.

참고 구현