ZANI

Fullstack
2026.07 ~ 2026.08Fullstack Developer

학생이 수업에 얼마나 집중하고 있는지 브라우저 안의 AI로 판정해 강사에게 실시간 코칭 팁을 주고, 수업이 끝나면 녹화·전사·참여도를 같은 시간축에 정리한 리포트와 복습 클립을 만들어 주는 강의 플랫폼입니다. 삼성 청년 SW·AI 아카데미(SSAFY)에서 5인 팀으로 개발했고 풀스택을 맡았습니다.

대외비 정책에 따라 저장소와 상세 코드는 공개하지 않습니다.

ZANI 이미지

사용 기술

Next.jsTypeScriptSpring BootJava 21MySQLRedisLiveKitWhisper · LLM

LiveKit강사 카메라·마이크·화면공유와 학생 마이크를 트랙 단위로 따로 받아 녹화(Egress)까지 해야 했는데, 이걸 모두 지원해서 선택했습니다.

MediaPipe · ONNX (온디바이스 AI)학생 얼굴 분석 데이터는 서버로 전송하지 않았습니다. 참여도 판정을 브라우저 안에서 처리하여 프라이버시 걱정과 서버 연산 비용이 함께 줄었습니다.

Whisper · LLM그 시간대 수업 내용을 근거로 답하는 기능이 서비스의 핵심이라, Whisper로 화자를 구분해 전사하고 그 결과를 LLM에 근거로 넘겨주는 파이프라인을 구축했습니다.

주요 기능

  • MR을 올리면 Git 컨벤션과 DDD 경계를 자동으로 검사하는, 리뷰가 자동화된 환경에서 협업했습니다.
  • 학생 카메라 영상은 서버로 보내지 않고 브라우저에서 MediaPipe와 ONNX로 참여도를 판정합니다. 집중이 떨어진 학생 비율이 익명 집계 기준 30%를 넘으면, 그 시점 강사 발화를 전사해 어떤 내용을 다시 설명하면 좋을지 실시간 팁으로 전달합니다.
  • 강사의 카메라·마이크·화면공유와 학생 마이크를 트랙별로 나눠 녹화하고, 누가 말했는지 구분한 전사를 만듭니다. 수업이 끝나면 강사는 구간별 참여도와 개선 피드백을, 학생은 놓친 구간의 복습 클립과 이해도 퀴즈를 받아볼 수 있습니다.
  • 리포트를 읽다가 궁금한 문장을 드래그하면, 그 시간대 수업 내용을 근거로 답해 주는 챗봇에 바로 질문할 수 있습니다.

담당 역할

  • 팀 협업 자동화를 직접 만들었습니다. 브랜치명을 읽어 커밋에 티켓 번호를 자동으로 넣고 컨벤션을 검사하는 훅을 구축했고, 지라 이슈 상태 자동화와 JQL로 매일 아침 작업자별 할 일을 공지하는 봇도 만들었습니다.
  • 강사 음성을 LiveKit Egress WebSocket으로 받아 세션마다 최근 300초를 링버퍼에 쌓아 두고, 코칭이 트리거되면 그 구간만 16kHz로 다운샘플해 MP3로 인코딩한 뒤 Whisper 전사로 넘기는 실시간 오디오 파이프라인을 구현했습니다. 음소거로 생기는 시간축 오차를 무음 패딩으로 최대 0.5초 이내로 개선하고, 인코딩을 락 밖으로 분리해 락 점유 시간을 약 800ms에서 3ms로 단축했습니다.
  • 리포트 질의응답 챗봇을 만들었습니다. 드래그 위치를 전사 시간창으로 바꿔 답변 근거를 주입하는 API에 익명화와 빈도 제한, 토큰 사용량 로깅을 더했고, 드래그 후 질문할 수 있는 채팅 드로어와 인용 구간으로 바로 이동하는 UI까지 구현했습니다. 근거를 전사 전체 대신 드래그 시간대 주변 3개 구간으로 좁혀 주입량을 90% 이상 절감했고, 프롬프트를 LLM 본문 예산의 약 27% 수준까지 낮춰 토큰 효율을 개선했습니다.
  • 학생·강사 리포트 API와 리포트 플레이어(화자별 전사 타임라인, 복습 클립, 구간 이동)를 만들었습니다. 녹화 영상은 권한을 확인한 뒤 짧게 유효한 URL로 서빙해 무단 접근을 막았습니다.
  • 프론트엔드 초기 세팅을 맡아 Next.js에 DDD 4계층 구조를 잡고 Vitest, TanStack Query·Zustand, OpenAPI 타입 자동 생성을 구성했습니다. 12개 전체 화면 퍼블리싱과 공통 UI 컴포넌트도 담당했습니다.

트러블슈팅

음소거 중에도 최근 5분의 음성 구간을 정확하게 유지하도록 개선

문제 인식
실시간 코칭에 활용하기 위해 강사의 최근 5분 음성을 서버 메모리에 링버퍼 형태로 저장했습니다. 처음에는 버퍼에 쌓인 오디오 데이터의 크기를 기준으로 최근 5분을 관리했지만, LiveKit에서는 음소거 상태일 때 오디오 프레임 자체가 전달되지 않았습니다. 이 때문에 음소거한 시간만큼 버퍼의 시간도 함께 멈추는 문제가 발생했습니다. 예를 들어 2분 동안 음소거하면 최근 5분을 조회했을 때 실제로는 최대 7분 전의 음성까지 포함될 수 있었습니다.
해결 방안
버퍼가 실제 시간의 흐름과 맞도록 음소거로 오디오가 들어오지 않는 구간에는 서버에서 무음 데이터를 채우도록 변경했습니다. 다만 네트워크 지연으로 프레임이 잠시 늦게 도착한 경우까지 무음으로 처리하면 실제 음성 사이에 불필요한 공백이 생길 수 있었습니다. 이를 방지하기 위해 0.5초 미만의 지연은 그대로 기다리고, 그보다 긴 공백만 무음 데이터로 채우도록 기준을 정했습니다. 또한 최근 5분의 음성을 MP3로 변환하는 동안 버퍼 접근이 막히는 문제도 함께 개선했습니다. 기존에는 약 800ms가 걸리는 MP3 변환까지 버퍼를 잠근 상태에서 처리했지만, 필요한 음성 데이터만 먼저 복사한 뒤 바로 잠금을 해제하고 MP3 변환은 별도로 진행하도록 변경했습니다.
개선 성과
음소거 시간이 길어질수록 커지던 버퍼의 시간 오차를 최대 0.5초 이내로 줄였습니다. 또한 버퍼가 잠기는 시간을 MP3 변환까지 포함한 약 800ms에서 5분 분량의 음성 데이터 28.8MB를 복사하는 약 3ms로 줄여 99% 이상 단축했습니다. 이를 통해 MP3 변환 중에도 새로 들어오는 오디오를 지연 없이 계속 수신할 수 있도록 개선했습니다.

챗봇이 참고하는 전사 범위를 질문한 구간으로 줄여 요청 크기 개선

문제 인식
리포트의 문장을 드래그해 질문할 수 있는 챗봇을 구현하면서 처음에는 수업 전체 전사를 LLM에 함께 전달했습니다. 하지만 3시간 수업 기준 전사 데이터만 약 180KB까지 커졌고, 요청 크기 제한을 넘는 문제가 발생했습니다. 질문과 관련 없는 내용까지 매번 전달하면서 불필요하게 많은 토큰을 사용하는 문제도 있었습니다.
해결 방안
전체 전사를 전달하는 대신 사용자가 드래그한 문장이 수업의 어느 시점에 해당하는지 찾아, 해당 구간과 앞뒤 한 구간의 전사만 DB에서 조회하도록 변경했습니다. 한 구간만 사용하면 문장이 구간 사이에서 끊겼을 때 필요한 설명이 빠질 수 있어 앞뒤 구간까지 포함했고, 최대 3개 구간만 LLM에 전달했습니다. 또한 사용자가 입력한 대화 내용이 아닌 서버에서 조회한 전사만 답변에 참고하도록 해 대화 내용을 임의로 조작해 답변에 영향을 주기 어렵게 했습니다. 외부 LLM에 데이터를 전달할 때는 화자 이름을 별칭으로 변경했고, 드래그한 문장과 연결되는 구간을 찾지 못한 경우에는 수업 전체 요약을 기준으로 답변하도록 처리했습니다.
개선 성과
LLM에 전달하는 전사를 최대 40개 구간에서 3개 구간으로 줄여 데이터 전달량을 90% 이상 줄였습니다. 그 결과 프롬프트 크기가 LLM 요청 크기 한도의 약 27% 수준으로 줄면서 요청 크기 초과 문제를 해결했고, 질문과 관련된 내용만 전달해 토큰 사용량도 줄일 수 있었습니다. 또한 참여자 여부를 먼저 확인한 뒤 요청 횟수를 검사하도록 순서를 변경해 다른 사용자의 질문 횟수를 소모시키는 문제를 막았습니다. 호출별 토큰 사용량을 기록하고 짧은 시간에 요청이 반복되면 429 응답을 반환하도록 해 과도한 호출에도 대응했습니다.