Songstark Logo

2026년 4월 25일 · 장대철 · Lead Engineer

AI-네이티브 LMS — pgvector RAG · 비디오 시청률 · 평가 자동화

lms-ct · gangwon-gender-edu · smit-practicum 3개 LMS 운영에서 발견한 패턴. RAG 챗봇 비용 최적화, 비디오 시청률의 함정, NABCD 31항목 자동 평가, Vercel sin1 1-connection 회피.

  • LMS
  • pgvector
  • RAG
  • Vimeo
  • Function Calling
  • Drizzle ORM

결론 한 단락

Songstark은 lms-ct(B2B 기업 교육), gangwon-gender-edu(지자체 성평등 교육), smit-practicum-platform(대학 캡스톤 평가) 3개 LMS를 운영 중이다. AI 챗봇 RAG, 비디오 시청률 추적, 평가 자동화 — LMS 도메인의 까다로운 요구를 어떻게 풀었는지 4개 패턴으로 정리한다.

1. AI 챗봇 — Gemini 3 Flash + pgvector(3072d) + Function Calling 10개

lms-ct 사례

기업 교육 LMS의 AI 챗봇은 학습자 질문에 답할 때 다음을 동시에 한다.

  1. 강의 자료(PDF, DOCX, VTT 자막) 검색 → RAG
  2. 학습자 본인의 진도·과제·성적 조회 → Function Calling
  3. 회사별 정책 문서 검색 → RAG (다른 인덱스)

아키텍처

사용자 질문
  ↓
Gemini 3 Flash (intent 판단 + tool selection)
  ↓
[case A: RAG] pgvector retrieve top-5 → context 주입 → Gemini 답변
[case B: tool] get_progress / get_assignments / get_grades → Gemini 답변
[case C: 혼합] retrieve + tool 동시 호출 → context 모두 주입 → Gemini 답변

pgvector는 3072차원(text-embedding-3-large) 사용. PDF·DOCX는 어절 기준 청킹, VTT 자막은 문장 기준. 한국어 인덱싱 효율은 어절 청킹이 약 30% 좋았다.

-- pgvector 인덱스
CREATE TABLE doc_chunks (
  id uuid PRIMARY KEY,
  course_id uuid REFERENCES courses(id),
  source_type text,  -- 'pdf' | 'docx' | 'vtt'
  content text,
  embedding vector(3072)
);

CREATE INDEX ON doc_chunks USING ivfflat (embedding vector_cosine_ops);

-- 검색
SELECT id, content, 1 - (embedding <=> $1) AS similarity
FROM doc_chunks
WHERE course_id = $2
ORDER BY embedding <=> $1
LIMIT 5;

비용 (월 100명 + 5000 챗봇 호출 기준)

  • Gemini 3 Flash 토큰: ~$30
  • pgvector 스토리지(Supabase): $0 (포함)
  • Vercel 호스팅: $20
  • 합계: ~$50–$150/월 (피크 시)

월 1000명 규모로 갈 때는 모델 mix(짧은 답은 Flash, 정밀 reasoning은 Sonnet)와 응답 캐싱으로 곡선이 완만해진다.

2. 비디오 시청률 추적의 함정

문제

LMS의 핵심 데이터 중 하나가 "이 학습자가 정말 봤는가"다. 단순 currentTime 추적은 시크바로 우회된다.

// ❌ 우회 가능
video.addEventListener('timeupdate', () => {
  saveProgress(video.currentTime);
});

학습자가 시크바를 끝까지 끌면 currentTime이 영상 길이가 되어 100% 완료로 기록된다. 실제로 본 적 없다.

4-Layer 패턴

  1. 시크바 제한 — 처음 보는 구간으로는 점프 불가

    video.addEventListener('seeking', () => {
      if (video.currentTime > maxWatchedSeconds) {
        video.currentTime = maxWatchedSeconds;
      }
    });
    
  2. 새로고침 복구 — sessionStorage에 시청 구간 저장, 복원 시 재생 가능 위치를 기억

    const watchedRanges: Array<[number, number]> = JSON.parse(
      sessionStorage.getItem(`video:${videoId}`) ?? '[]'
    );
    
  3. 실제 시청 구간 누적play ~ pause 사이의 시간만 누적

    let lastPlayTime: number | null = null;
    video.addEventListener('play', () => { lastPlayTime = Date.now(); });
    video.addEventListener('pause', () => {
      if (lastPlayTime) {
        watchedSeconds += (Date.now() - lastPlayTime) / 1000;
      }
    });
    
  4. 서버 검증 — 클라이언트 데이터를 신뢰하지 않고, 서버에서 "현재 시청률 vs 비디오 길이" 검증

    if (clientReportedSeconds > videoDuration * 1.1) {
      // 클라이언트가 거짓말 → 무시
      return;
    }
    

이 4-layer 패턴으로 lms-ct의 시청 신뢰도가 95%+다. 단순 currentTime만 보면 50% 이하로 떨어진다.

gangwon-gender-edu의 추가 case

Vimeo Player를 임베드한 경우, 컨트롤 오버레이가 모바일에서 잘리는 문제가 있었다. 반응형 카브아웃(viewport-aware mask)으로 해결.

.vimeo-wrapper {
  position: relative;
}
.vimeo-wrapper::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: env(safe-area-inset-bottom, 0);
  background: black;  /* 모바일 하단 시스템 영역 보호 */
}

3. NABCD 31개 항목 평가 자동화 — smit-practicum

문제

대학 창업 캡스톤은 학생 팀이 NABCD 프레임워크(Need, Approach, Benefit, Competition, Delivery)로 사업 아이디어를 정리하고, 31개 하위 항목을 교수가 평가한다. 31개 × 30개 팀 × 학기 2회 = 1860회 채점이 학기마다 발생한다.

패턴: AI 1차 + 교수 검수

// AI 1차 채점
const aiScore = await llm.evaluate({
  rubric: NABCD_RUBRIC,
  submission: studentSubmission,
});

// "Path to Green" 단계별 수정 가이드 자동 생성
const pathToGreen = await llm.generate({
  prompt: `이 NABCD 답안을 어떻게 수정하면 점수가 오를지, 단계별 가이드를 제공`,
  context: aiScore.weakAreas,
});

// 교수 인터랙티브 검수
return {
  aiScore,
  pathToGreen,
  awaitingProfessorVerdict: true,
};

교수는 AI 점수 + Path to Green 가이드를 보고, verdict를 final로 확정한다. AI 단독 채점은 권장하지 않는다 — 검수 단계가 필수.

결과

학기마다 평가 시간이 60시간 → 12시간으로 줄었다. 가이드 품질은 교수 의견상 "혼자 짜는 것보다 더 일관적".

4. Vercel sin1 region에서 Drizzle 커넥션 풀 1개로 운영

문제

Vercel 서버리스의 한계 — 함수당 동시 DB 커넥션이 1개로 제한된다(개별 함수 invocation별). Drizzle ORM의 기본 풀 설정은 10개라, 그대로 쓰면 first invocation 후 모든 후속이 timeout.

해결

Drizzle 풀을 1개로 강제 + unstable_cache로 사이드바 60초 캐시.

// db.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

const client = postgres(process.env.DATABASE_URL!, {
  max: 1,  // 핵심: Vercel 서버리스에선 1개
  idle_timeout: 20,
  connect_timeout: 10,
});

export const db = drizzle(client);
// app/sidebar/data.ts
import { unstable_cache } from 'next/cache';

export const getSidebarData = unstable_cache(
  async (userId: string) => {
    // 6개 쿼리를 5개 parallel + 1개 sequential로 묶음
    const [user, courses, recentActivity, notifications, ai] = await Promise.all([
      db.query.users.findFirst({ where: eq(users.id, userId) }),
      db.query.courses.findMany(...),
      db.query.activity.findMany(...),
      db.query.notifications.findMany(...),
      db.query.aiChats.findFirst(...),
    ]);
    const progress = await computeProgress(courses);
    return { user, courses, recentActivity, notifications, ai, progress };
  },
  ['sidebar'],
  { revalidate: 60, tags: ['sidebar'] }
);

결과: 사이드바 응답시간 600ms → 80ms.

5. 다국어 LMS의 비대칭

smit-practicum-platform은 ko/en 다국어다. 강의 콘텐츠와 UI를 분리하는 게 핵심.

  • UI: next-intl JSON 파일 (public/locales/{ko,en}/lms.json)
  • 강의 콘텐츠: MDX 파일 (content/courses/{course_slug}/{ko,en}/{lesson}.mdx)

비대칭을 허용한다 — 어떤 강의는 ko만, 어떤 강의는 ko+en, 어떤 강의는 ko+en+fr. UI는 항상 ko/en 둘 다 있어야 하지만, 강의는 강사가 짠다.

// 강의 로드 시 fallback 체인
async function loadLesson(courseSlug: string, lessonSlug: string, locale: string) {
  const fallbackChain = [locale, 'en', 'ko'];
  for (const lang of fallbackChain) {
    const path = `content/courses/${courseSlug}/${lang}/${lessonSlug}.mdx`;
    if (await fileExists(path)) {
      return loadMdx(path);
    }
  }
  return null;
}

정리

LMS는 "코스 + 비디오 + 퀴즈"의 단순 조합이 아니다. RAG 챗봇 비용 최적화, 비디오 시청률 4-layer, AI 1차 + 인간 검수 평가, 서버리스 1-connection 회피, 다국어 비대칭 허용 — 5가지 패턴을 알고 가야 LMS가 실제로 운영된다.

여러분이 LMS를 짓고 계신다면 문의 보내기 · 교육/LMS 솔루션에서 시작할 수 있다.