bf2321f2d5
LiveKit(SFU)에 미디어 전송을 위임하고, 앱은 입장 토큰 발급 + 회의 화면만 담당하는 구조로 단계 1(기본 연결)을 구현한다. 백엔드 - meeting 모듈: POST /api/meetings/token (JWT 인증) livekit-server-sdk 로 입장 JWT 발급(identity=userId, name, grants: roomJoin/publish/subscribe/publishData, TTL 1h). 단계 2에서 역할별 분기 예정. 프론트 - useMeeting(토큰 발급), useLiveKitRoom(연결/참여자/컨트롤 상태) - ParticipantTile(카메라/마이크 트랙 attach, 본인 거울·음소거, 이니셜 플레이스홀더) - MeetingPage(로비 + 영상 그리드), 라우트 /meeting/:room?, 사이드바 네비 추가 - VITE_LIVEKIT_URL 주입(build arg + dev 런타임 env) 인프라/설정 - 개발 미디어 서버는 LiveKit Cloud 사용(로컬 self-host 는 WSL2/Docker Desktop WebRTC 네트워킹 제약으로 제외). env 만으로 운영 전환 가능. - livekit/livekit.yaml 은 단계 6 셀프호스팅 참고용으로 보관. - 화상회의_화면명세.md: 현재 화면 기능/구조/데이터 계약 문서(UI 개편 참고). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.6 KiB
9.6 KiB
화상회의 화면 명세 (단계 1: 인프라 토대)
이 문서는 현재 구현된 화상회의 화면의 기능·구조·데이터 계약을 정리한 것이다. 새 UI를 직접 설계/구현할 때, 아래 "데이터 계약"과 "상태 흐름"만 그대로 연결하면 시각 디자인은 자유롭게 바꿔도 동작이 보장된다. (미디어 전송은 LiveKit SDK가 담당하고, 우리는 토큰 발급 + 화면 구성만 한다.)
1. 범위
- 포함(단계 1): 방 입장 → LiveKit 연결 → 카메라/마이크 발행 → 참여자 영상 그리드 표시 → 나가기. 마이크/카메라 on·off.
- 제외(후속 단계, 이번 UI에서 구현 안 함)
- 단계 2: 관리자(진행자)/일반 사용자 권한 분기 (토큰 grant 차등)
- 단계 3: 진행자 고해상도 큰 화면 + 참여자 썸네일, 특정 참여자 화면 승격, 그리드 페이지네이션(동시 표시 ~50명)
- 단계 4: 방 목록/예약/참여 제한
- 즉 지금은 "같은 방 이름으로 들어온 사람끼리 동등하게 연결되는 기본 회의실"이다.
2. 진입 / 라우트
| 항목 | 값 |
|---|---|
| 라우트 | /meeting/:room? (room 파라미터는 선택) |
| 라우트 name | meeting |
| 인증 | requiresAuth: true (로그인 필요) |
| 페이지 컴포넌트 | frontend/src/pages/relay/MeetingPage.vue |
| 사이드바 네비 | AppShell.vue 의 "화상회의" 항목 (/meeting) |
/meeting으로 들어오면 방 이름 미입력 상태의 로비./meeting/<방이름>으로 들어오면 그 방 이름이 입력란에 채워진 로비(공유 링크 용도). 입장 버튼을 눌러야 실제 연결된다(카메라 권한 타이밍 확보).- 입장 성공 시 주소창을
/meeting/<방이름>으로replace한다(공유 가능 링크).
3. 화면 상태 (4가지)
| 상태 | 조건 | 화면 |
|---|---|---|
| 로비(미연결) | connected === false |
방 이름 입력 폼 |
| 연결 중 | connecting === true |
입장 버튼이 "입장 중…" + 비활성 |
| 회의실(연결됨) | connected === true |
컨트롤 바 + 영상 그리드 |
| 에러 | joinError / errorMsg 존재 |
해당 영역에 빨간/주황 문구 |
joinError: 입장 시도 단계의 오류(방 이름 형식 오류, 토큰 발급 실패 등) — 로비에 표시.errorMsg: 연결 이후 미디어 단계의 오류(카메라/마이크 권한 거부 등) — 회의실에 표시. 권한 거부여도 연결 자체는 유지된다.
4. 로비 화면 (현재 구성)
- 페이지 제목
화상회의+ 안내 문구(lede) - 카드 폼:
- 라벨
* 방 이름(필수 표기) - 힌트: "영문·숫자·하이픈(-)·밑줄(_)만 사용할 수 있습니다."
- 입력란:
maxlength=64, placeholder예: team-standup - 오류 문구(
joinError) - 푸터:
입장하기버튼(제출 시onJoin)
- 라벨
- 방 이름 규칙:
^[A-Za-z0-9_-]+$, 1~64자. (백엔드 DTO와 동일하게 검증 — 한글/공백 불가)
5. 회의실 화면 (현재 구성)
- 상단 바(room-bar)
- 방 이름(
roomName) - 참여자 수(
참여자 N명=participants.length) - 컨트롤 버튼 3개:
- 마이크 토글 →
toggleMic()(상태micEnabled) - 카메라 토글 →
toggleCam()(상태camEnabled) - 나가기 →
onLeave()(연결 해제 후/meeting로 이동)
- 마이크 토글 →
- 방 이름(
- 영상 그리드(grid):
participants를 순회하며ParticipantTile렌더. 첫 번째(index === 0)가 본인(localParticipant).
현재 컨트롤은 이모지(🎙/📹) 버튼이고 상단 우측에 있다 → 새 UI에서 하단 중앙 컨트롤 바 + 아이콘 버튼 등으로 자유롭게 재배치 가능. 연결되는 함수/상태만 동일하게 쓰면 된다.
6. 참여자 타일 (ParticipantTile.vue)
- Props
participant: Participant(livekit-client) — 필수isLocal?: boolean— 본인 타일 여부
- 동작
- 카메라 트랙을
<video>에 attach. 본인 타일은 좌우 반전(거울) +muted(에코 방지). - 원격 참여자의 마이크 트랙을
<audio>에 attach(본인 오디오는 재생 안 함). - 영상이 없으면(카메라 off/미발행) 이름 첫 글자 이니셜 플레이스홀더 표시.
- 하단에 마이크 상태 아이콘 + 표시 이름(본인은
(나)접미).
- 카메라 트랙을
- 표시 이름:
participant.name || participant.identityname= 토큰에 담은 사용자 이름(한글 이름),identity= 사용자 ID(UUID).
- 마이크 상태:
participant.isMicrophoneEnabled - 트랙 변경 반영:
ParticipantEvent의 TrackSubscribed/Unsubscribed/LocalTrackPublished/LocalTrackUnpublished/TrackMuted/TrackUnmuted 구독 → 재attach.
새 디자인에서 "말하는 사람 하이라이트(speaking)"가 필요하면
participant.isSpeaking+ParticipantEvent.IsSpeakingChanged를 추가로 쓰면 된다.
7. 상태 흐름 (입장 → 표시 → 나가기)
[로비] 방 이름 입력 + 입장
│ onJoin(): 형식 검증
▼
useMeeting().getToken(roomName) ← 백엔드 POST /api/meetings/token
│ { token, roomName, identity }
▼
useLiveKitRoom().connect(token)
│ ├─ Room.connect(VITE_LIVEKIT_URL, token) (시그널링/ICE)
│ ├─ enableCameraAndMicrophone() (카메라/마이크 발행, 실패해도 연결 유지)
│ └─ 이벤트 구독 → participants 갱신
▼
[회의실] participants 그리드 표시 (본인 + 원격)
│ toggleMic / toggleCam 로 발행 제어
▼
onLeave() → disconnect() → /meeting
(화면 이탈 시 onBeforeUnmount 에서도 disconnect 자동 호출)
8. 데이터 계약 (새 UI가 그대로 연결할 부분)
8.1 useLiveKitRoom() — frontend/src/composables/useLiveKitRoom.ts
LiveKit 방 연결/생명주기를 캡슐화. 반환 객체:
| 이름 | 타입 | 의미 |
|---|---|---|
participants |
Ref<Participant[]> |
본인(0번) + 원격 참여자. 배열 통째 교체로 갱신(shallowRef) |
connected |
Ref<boolean> |
방 연결 여부(회의실 표시 기준) |
connecting |
Ref<boolean> |
연결 진행 중 |
micEnabled |
Ref<boolean> |
본인 마이크 on/off |
camEnabled |
Ref<boolean> |
본인 카메라 on/off |
errorMsg |
Ref<string | null> |
연결 이후 미디어 오류 메시지 |
connect(token) |
(token: string) => Promise<void> |
입장(연결 + 카메라/마이크 발행) |
disconnect() |
() => Promise<void> |
나가기(연결 해제, 상태 초기화) |
toggleMic() |
() => Promise<void> |
마이크 on/off |
toggleCam() |
() => Promise<void> |
카메라 on/off |
- 접속 URL은
import.meta.env.VITE_LIVEKIT_URL(없으면ws://localhost:7880). 개발은 LiveKit Cloud(wss://<project>.livekit.cloud). Room옵션:adaptiveStream: true,dynacast: true.
8.2 useMeeting() — frontend/src/composables/useMeeting.ts
| 메서드 | 반환 | 설명 |
|---|---|---|
getToken(roomName) |
Promise<{ token, roomName, identity }> |
백엔드에서 LiveKit 입장 토큰(JWT) 발급 |
9. 백엔드 (참고 — 변경 불필요)
- 엔드포인트:
POST /api/meetings/token(JWT 인증 필요) - 요청 바디:
{ "roomName": "team-standup" }(DTO 검증:^[A-Za-z0-9_-]+$, 1~64자) - 응답:
{ "token": "<LiveKit JWT>", "roomName": "...", "identity": "<userId>" } - 권한(현재 단계 1): 모든 입장자에게
roomJoin + canPublish + canSubscribe + canPublishData부여. identity=사용자 ID, name=사용자 이름. 토큰 TTL 1시간. - 파일:
backend/src/modules/meeting/{meeting.controller.ts, meeting.service.ts, dto/create-meeting-token.dto.ts}
10. 파일 맵
| 파일 | 역할 |
|---|---|
frontend/src/pages/relay/MeetingPage.vue |
화면(로비 + 회의실) — 새로 디자인할 대상 |
frontend/src/components/meeting/ParticipantTile.vue |
참여자 1명 영상 타일 — 새 디자인 시 함께 손봄 |
frontend/src/composables/useLiveKitRoom.ts |
연결/참여자/컨트롤 상태 (재사용, 그대로 연결) |
frontend/src/composables/useMeeting.ts |
토큰 발급 API |
frontend/src/router/index.ts |
/meeting/:room? 라우트 |
frontend/src/layouts/AppShell.vue |
사이드바 "화상회의" 네비 |
11. 디자인 규약 (새 화면이 사이트와 어울리게)
- rem 단위 사용 (px 금지). 단, 1px 보더/그림자는 px 유지. 앱 기준 폰트는 14px(루트), rem은 16px 기준으로 작성해도 14px 스케일로 렌더된다.
- 인라인 스타일 금지,
<style scoped>사용. - 색상은 CSS 변수 사용(예시):
- 강조:
--accent(#4f46e5),--accent-hover,--accent-weak - 텍스트:
--text,--text-2,--text-3 - 경계/패널:
--border,--border-strong,--panel - 상태색:
--green/-weak/-border,--amber/-weak/-border,--red/-weak/-border
- 강조:
- 공용 컴포넌트 재사용 가능:
BaseModal,SegmentedTabs,EmptyState,UserAvatar(아바타 플레이스홀더로 타일에 활용 가능) 등frontend/src/components/common/참고. - 버튼 높이/폼 규격은 기존 페이지(
ProjectCreatePage.vue등)의.btn,.tinput패턴 참고.
12. 동작 확인 방법
- dev 스택 기동 후
http://localhost:5273/meeting접속 - 방 이름 입력 → 입장 → 카메라/마이크 권한 허용 → 본인 영상 표시
- 두 번째 탭(또는 다른 브라우저) 에서 같은 방으로 입장 → 서로의 영상/음성 확인
- 미디어 서버는 LiveKit Cloud 사용(
.env.development의LIVEKIT_*). 운영 전환은 env 값 교체만으로 가능.