Files
comrelay/화상회의_화면명세.md
T
ttipo 3c7aec86a2 feat: 화상회의 단계 3 — 화면 구성(임시 레이아웃)
진행자/승격 참여자 큰 화면 + 썸네일 + 페이지네이션 + 화면 승격을 구현한다.
(레이아웃은 임시 — UI 는 추후 개편 예정. 데이터/로직만 확정.)

- useLiveKitRoom: featuredIdentity 상태 + promote(identity) 추가.
  진행자가 데이터 채널로 승격을 브로드캐스트 → RoomEvent.DataReceived 로 전원 동기화.
- MeetingPage:
  · stage(featured: 승격>진행자>첫 참여자) 큰 화면 + 나머지 썸네일
  · 진행자에게만 썸네일 "크게"(승격) 버튼
  · 썸네일 페이지네이션(페이지당 12명) — 현재 페이지만 렌더 →
    adaptiveStream 이 안 보이는 참여자 미디어 자동 미구독(대역폭 절약)
- 화상회의_화면명세.md: 단계 2~3 반영(범위/화면 구성/데이터 계약 갱신)

백엔드 변경 없음(승격은 클라이언트 데이터 채널).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:06:23 +09:00

11 KiB

화상회의 화면 명세 (단계 1: 인프라 토대)

이 문서는 현재 구현된 화상회의 화면의 기능·구조·데이터 계약을 정리한 것이다. 새 UI를 직접 설계/구현할 때, 아래 "데이터 계약"과 "상태 흐름"만 그대로 연결하면 시각 디자인은 자유롭게 바꿔도 동작이 보장된다. (미디어 전송은 LiveKit SDK가 담당하고, 우리는 토큰 발급 + 화면 구성만 한다.)


1. 범위

  • 포함(단계 1~3 구현됨)
    • 단계 1: 방 입장 → LiveKit 연결 → 카메라/마이크 발행 → 나가기, 마이크/카메라 on·off
    • 단계 2: 권한 분기 — 방 생성자=진행자(host), 이후 입장자=일반. 역할별 토큰 grant 차등
    • 단계 3: 진행자/승격된 참여자 큰 화면(stage) + 나머지 썸네일, 진행자의 화면 승격(데이터 채널 브로드캐스트), 썸네일 페이지네이션
  • 제외(후속 단계): 단계 4(방 목록/예약/참여 제한 UI), 5(부하 테스트), 6(셀프호스팅 배포)
  • ⚠️ 현재 화면 레이아웃은 임시(대충)이며 UI 는 새로 개편 예정. 아래 "데이터 계약"만 그대로 연결하면 동작이 유지된다.

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 로 이동)
  • 큰 화면(stage): featured(승격된 참여자 > 진행자 > 첫 참여자) 1명을 크게 렌더.
  • 썸네일(thumbs): 나머지 참여자를 작은 타일로 렌더. 진행자에게만 각 썸네일에 "크게"(승격) 버튼 표시 → promote(identity) 호출(데이터 채널로 전원 동기화).
  • 페이지네이션(pager): 썸네일이 페이지당 12명을 넘으면 이전/다음. 현재 페이지 타일만 렌더되므로 adaptiveStream 이 안 보이는 참여자의 미디어를 자동으로 구독 해제(대역폭 절약).
  • 본인 식별: participants[0] 이 항상 본인. 타일 is-localp.identity === participants[0].identity 로 판정.

현재 컨트롤은 이모지(🎙/📹) 버튼·상단 바 등 임시 배치다 → 새 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> 연결 이후 미디어 오류 메시지
featuredIdentity Ref<string | null> 승격되어 크게 볼 참여자 identity(없으면 진행자/첫 참여자가 기본)
connect(token) (token: string) => Promise<void> 입장(연결 + 카메라/마이크 발행)
disconnect() () => Promise<void> 나가기(연결 해제, 상태 초기화)
toggleMic() () => Promise<void> 마이크 on/off
toggleCam() () => Promise<void> 카메라 on/off
promote(identity) (identity: string) => Promise<void> 해당 참여자를 큰 화면으로 승격(진행자만 호출, 전원 동기화)

역할 식별: useMeeting().getToken() 응답의 role('host'|'participant')로 본인 역할을 안다. 다른 참여자의 진행자 여부는 participant.metadata(JSON {"role":"host"})로 판정한다.

  • 접속 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.developmentLIVEKIT_*). 운영 전환은 env 값 교체만으로 가능.