feat: 화상회의 단계 1 — LiveKit 기반 인프라 토대
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>
This commit is contained in:
+193
@@ -0,0 +1,193 @@
|
||||
# 화상회의 화면 명세 (단계 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.identity`
|
||||
- `name` = 토큰에 담은 사용자 이름(한글 이름), `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. 동작 확인 방법
|
||||
|
||||
1. dev 스택 기동 후 `http://localhost:5273/meeting` 접속
|
||||
2. 방 이름 입력 → 입장 → 카메라/마이크 권한 허용 → 본인 영상 표시
|
||||
3. **두 번째 탭(또는 다른 브라우저)** 에서 같은 방으로 입장 → 서로의 영상/음성 확인
|
||||
4. 미디어 서버는 LiveKit Cloud 사용(`.env.development` 의 `LIVEKIT_*`). 운영 전환은 env 값 교체만으로 가능.
|
||||
Reference in New Issue
Block a user