feat(web, server): 실시간 회의 구현 - #16
Open
hyesngy wants to merge 6 commits into
Open
Conversation
LiveKit SFU를 P2P WebRTC mesh로 교체한다. 서버에 이미 있는 offer/answer/ice 중계(MeetingSocketHandler.forwardSignal)를 그대로 쓰고, 회의 WebSocket 하나로 roster·자막·시그널링을 함께 나른다. - useWebRtcMesh: 오디오 전용 mesh. memberId가 작은 쪽이 offer를 걸어 glare를 막고, 마이크 확보 전 도착한 offer/ice는 버퍼링했다가 처리한다. - 발화 표시: 로컬·원격 스트림 RMS를 100ms로 샘플링해 참가자 타일에 표시 - LiveKit 제거: useLiveKitRoom, livekit-client, getRtcToken, RTC_TOKEN - hasRtcToken -> hasLocalMedia. 토큰이 없어져 상태 문구도 마이크 기준으로 변경 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
web의 WebRTC 전환(98ade11)으로 LiveKit Egress 녹음이 사라졌다. 오디오 대신 실시간 자막을 회의록 원본으로 삼도록 서버를 맞춘다. 자막 영속화 - MeetingDialogueCommandService: SPEECH 수신마다 Dialogue INSERT. 연관 엔티티는 프록시로만 잡아 발화당 쿼리 1회. 저장 실패는 회의를 끊지 않는다. - 지금까지 자막은 메모리에만 있어 서버 재시작에 회의록이 통째로 사라졌다. 분석 분기 - audioKey는 회의 생성 시점에 항상 채워져 분기 기준이 못 된다. MeetingAudioKeyResolver로 S3 실제 존재 여부를 보고 경로를 고른다. - 오디오 없으면 저장된 Dialogue를 전사 세그먼트로 만들어 meeting-analysis/extract 호출. extract 응답의 analysis_result는 전사 run과 스키마가 같아 저장 로직을 그대로 쓴다. - 오디오 준비 재시도(20회x3초)를 제거했다. 녹음이 없어 매번 60초를 버리던 대기다. - persistAnalysis에 replaceDialogues 플래그 추가. 자막 경로에서 false로 넘기지 않으면 orphanRemoval로 이미 저장한 회의록이 전부 삭제된다. LiveKit 제거 - LiveKitTokenService, LiveKitEgressClient, MeetingRtcService, rtc-token 엔드포인트, admin의 LiveKit room 관리 API, Egress 정지 로직, livekit 설정 블록 - Meeting.audioEgressId 필드와 @Setter 제거 + V2 마이그레이션으로 컬럼 DROP - 오디오 다시듣기 경로(S3/audio_key//audio)는 유지한다 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
서버에서 LiveKit Egress를 제거해 새 회의는 오디오가 생기지 않는다. 플레이어를 무조건 렌더하면 완료된 회의마다 동작하지 않는 바가 남는다. 오디오가 있는 과거 회의에서만 보이게 한다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
whylog-dev
reviewed
Aug 21, 2026
Contributor
WhyLog AI 리뷰결과: ✅ 차단 항목 없음 · 모델: Google 실시간 회의 방식을 LiveKit SFU에서 P2P WebRTC mesh로 전환하고, 오디오 녹음 없이 실시간 자막(Dialogue)으로 회의를 분석하는 파이프라인을 구현했습니다. 관련 LiveKit 코드 정리 및 DB 마이그레이션(V2), mock 데이터 수정이 충실히 포함되었으며 차단 항목은 없습니다. PR 리뷰 문서: 차단없음 제안없음 이 코멘트는 새 실행 때 갱신됩니다. 차단 항목은 사람이 타당성을 확인한 뒤 수정하세요. |
appendSpeech가 meetingId와 memberId를 Long 두 개로 나란히 받고 있었다. 호출부에서 뒤바꿔 넣어도 컴파일이 통과하고 아무도 잡지 못한다. 실제로 AI 리뷰가 이 시그니처를 인자 순서 오류로 오독했다. 핸들러가 메모리 버퍼용으로 이미 만들던 LiveMessageEntry를 그대로 넘긴다. 새 타입을 만들지 않았고(MeetingLiveMessageBundleService가 같은 타입을 쓴다), 메모리와 회의록이 같은 객체를 공유해 타임스탬프도 어긋나지 않는다. 동작 변경 없음. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
작업내용
실시간 회의를 LiveKit SFU에서 P2P WebRTC mesh로 직접 구현하고, 그에 따라 사라진 녹음을 자막 기반 분석으로 대체합니다. 2026-08-09 회의 결정(
docs/decisions/2026-08.md)의 "실시간 회의는 LiveKit 대신 RTC를 직접 구현한다" 항목입니다.LiveKit은 미디어 전송뿐 아니라 녹음(Egress) 도 담당하고 있었고, 그 녹음 파일이 AI 요약 → 결정사항 파이프라인의 입력이었습니다. 그래서 RTC 교체만으로 끝나지 않고 서버 분석 경로까지 함께 바꿉니다.
포함된 영역
V2__drop_meeting_audio_egress_id.sql,mock.sql동반 수정)예측 (작업 초기 PR 생성 시 작성)
결과 (머지 전 작성)
MeetingMessageType에OFFER/ANSWER/ICE와forwardSignal이 구현돼 있어서 예상보다 RTC 작업이 가벼웠음audioKey로 분기하려다 실패. 회의 생성 시점에buildRecordingKey로 무조건 채워져서 녹음 여부와 무관하게 항상 non-null. 실제 판정자는MeetingAudioKeyResolver.resolvePlayableAudioKey(S3 존재 확인)였음persistMeetingAnalysis를 그냥 재사용했으면 자막이 전멸했음. 세그먼트가 비면getDialogues().clear()만 실행되고orphanRemoval=true라 저장해둔 회의록이 전부 DELETE됨 →replaceDialogues플래그로 분리/admin/livekit/rooms계열 3개 엔드포인트와 DTO 5개. 계획에 없던 범위였고 함께 정리함TranscribeSegment의speaker·is_final이 FastAPI 필수 필드인데 서버 DTO엔speaker가 없어서, Dialogue에서 재구성할 때 채워 넣어야 했음 (누락 시 조용히 422)ratchetFrom origin/main이라 건드린 파일이 통째로 재포맷됨.MeetingSocketHandler의 기존 와일드카드 import를 spotless가 못 펴서 수동으로 확장함 → diff가 실제 변경보다 크게 보입니다audioKey건이 딱 그 사례orphanRemoval이 걸린 컬렉션에clear()가 있는 메서드는 재사용 전에 반드시 확인해야 함리뷰 요청
준용 (server 가디언) — AGENTS.md "경계" 항목을 3개 건드렸습니다. 설계 확인 부탁드립니다.
MeetingAnalysisService.analyzeMeetingAudio를 오디오 없는 경로로 분기한 것 — 결정사항 대시보드에 영향이 갑니다global/external/fast/dto/request/TranscriptSegmentPayload추가 (기존 DTO 수정 없이 추가만)application.yaml의livekit블록 제거 /Meeting.audioEgressId필드 + 컬럼 DROP인덱스 검토 근거 (체크리스트 항목):
dialogue를meeting_id로 조회하는 쿼리를 새로 추가했습니다. V1에fk_dialogue_meetingFK가 걸려 있고 InnoDB가 FK 컬럼에 인덱스를 자동 생성하므로 별도 인덱스는 추가하지 않았습니다.ORDER BY speech_datetime, dialogue_id는 회의 하나 범위 내 정렬이라 회의당 발화 수 규모에서 filesort 비용이 문제되지 않는다고 판단했습니다.메모
아직 검증 안 된 것 (머지 전 필요)
dialogue테이블에 행이 실시간으로 쌓이는지dialogue행이 지워지지 않았는지 ← 이번 변경에서 가장 깨지기 쉬운 지점docs/decisions/에 아직 안 썼습니다. 회의에서 확정된 것만 기록하는 규칙이라, 다음 회의에서 확정 후 추가하겠습니다범위 밖 — 별도 이슈 제안
analysis-test엔드포인트가 프로덕션 컨트롤러에 노출돼 회의 참여자 누구나 임의 JSON으로 Decision/Application/Dialogue를 덮어쓸 수 있습니다 (MeetingController) — 보안 이슈replaceApplications가 삭제 후 재삽입이라 재분석마다applicationId가 바뀌어 ChromaDB 임베딩·커밋 매칭과 어긋납니다MeetingLiveMessageRepository에 상한·TTL이 없어 메모리가 계속 쌓입니다