Skip to content

Commit 98a6a5e

Browse files
authored
Merge pull request #8 from f-lab-edu/feature/whiteboard-sync
Feature/whiteboard sync
2 parents f715aa7 + 3a7dc48 commit 98a6a5e

File tree

22 files changed

+1240
-381
lines changed

22 files changed

+1240
-381
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,5 +250,7 @@ $RECYCLE.BIN/
250250
# additional
251251
.claude
252252
/documents/**
253+
claude.md
253254

254255
.kiro
256+
claude.md

.vscode/settings.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,13 @@
4747
"files.trimTrailingWhitespace": true,
4848

4949
// 파일 끝에 빈 줄 추가
50-
"files.insertFinalNewline": true
50+
"files.insertFinalNewline": true,
51+
"cSpell.words": [
52+
"Autoplay",
53+
"canplay",
54+
"canplaythrough",
55+
"loadedmetadata",
56+
"unmuted",
57+
"webrtc"
58+
]
5159
}

AUDIO_DEBUG_GUIDE.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# 오디오 문제 디버깅 가이드
2+
3+
## 수정 사항
4+
5+
### 1. VideoPlayer.jsx
6+
7+
- 원격 비디오의 `volume` 속성을 명시적으로 1.0으로 설정
8+
- 스트림 연결 시 오디오/비디오 트랙 정보를 콘솔에 출력
9+
- 원격 스트림의 오디오 트랙 활성화 상태 확인
10+
11+
### 2. MediaContext.js
12+
13+
- 로컬 스트림 초기화 시 오디오 트랙 정보 로깅
14+
- `autoGainControl: true` 옵션 추가
15+
16+
### 3. WebRTCService.js
17+
18+
- WebRTC 초기화 시 로컬 스트림의 트랙 정보 로깅
19+
- 원격 스트림 수신 시 트랙 정보 로깅
20+
21+
## 디버깅 체크리스트
22+
23+
브라우저 콘솔(F12)에서 다음 로그를 확인하세요:
24+
25+
### 1단계: 로컬 스트림 확인
26+
27+
```
28+
미디어 스트림 획득 성공
29+
로컬 스트림 트랙 정보:
30+
- 비디오 트랙: 1개 ["카메라 이름 (enabled: true)"]
31+
- 오디오 트랙: 1개 ["마이크 이름 (enabled: true)"]
32+
```
33+
34+
**문제 발견 시:**
35+
36+
- 오디오 트랙이 0개: 마이크 권한 또는 장치 문제
37+
- enabled: false: toggleAudio()가 호출되었거나 초기화 문제
38+
39+
### 2단계: WebRTC 초기화 확인
40+
41+
```
42+
WebRTC 초기화 - 로컬 스트림 트랙:
43+
- 비디오: 1개 ["카메라 이름 (enabled: true)"]
44+
- 오디오: 1개 ["마이크 이름 (enabled: true)"]
45+
```
46+
47+
**문제 발견 시:**
48+
49+
- 오디오 트랙이 없다면 로컬 스트림이 WebRTC에 제대로 전달되지 않음
50+
51+
### 3단계: 원격 스트림 수신 확인
52+
53+
```
54+
원격 스트림 수신
55+
원격 스트림 트랙 정보:
56+
- 비디오: 1개 ["카메라 이름 (enabled: true)"]
57+
- 오디오: 1개 ["마이크 이름 (enabled: true)"]
58+
```
59+
60+
**문제 발견 시:**
61+
62+
- 오디오 트랙이 0개: 상대방의 로컬 스트림에 오디오가 없거나 WebRTC 연결 문제
63+
- enabled: false: 상대방이 오디오를 끈 상태
64+
65+
### 4단계: Video 엘리먼트 확인
66+
67+
```
68+
비디오 스트림 연결: 상대방
69+
- 비디오 트랙: 1개 ["카메라 이름 (enabled: true)"]
70+
- 오디오 트랙: 1개 ["마이크 이름 (enabled: true)"]
71+
비디오 엘리먼트 volume: 1, muted: false
72+
```
73+
74+
**문제 발견 시:**
75+
76+
- muted: true인 경우 VideoPlayer 컴포넌트의 isLocal 값 확인
77+
- volume: 0인 경우 브라우저 설정 확인
78+
79+
## 추가 확인 사항
80+
81+
### 브라우저 콘솔에서 직접 확인
82+
83+
```javascript
84+
// 원격 비디오 엘리먼트 찾기
85+
const videos = document.querySelectorAll('video');
86+
videos.forEach((v, i) => {
87+
console.log(`Video ${i}:`, {
88+
muted: v.muted,
89+
volume: v.volume,
90+
srcObject: v.srcObject,
91+
audioTracks: v.srcObject?.getAudioTracks().map(t => ({
92+
label: t.label,
93+
enabled: t.enabled,
94+
muted: t.muted,
95+
readyState: t.readyState
96+
}))
97+
});
98+
});
99+
```
100+
101+
### 일반적인 문제 해결
102+
103+
1. **마이크 권한 문제**
104+
- 브라우저 주소창의 자물쇠 아이콘 클릭
105+
- 마이크 권한이 "허용"으로 설정되어 있는지 확인
106+
107+
2. **브라우저 음소거 상태**
108+
- 브라우저 탭이 음소거되어 있지 않은지 확인
109+
- 시스템 볼륨이 0이 아닌지 확인
110+
111+
3. **HTTPS 필요**
112+
- localhost가 아닌 경우 HTTPS 필수
113+
- getUserMedia는 보안 컨텍스트에서만 작동
114+
115+
4. **브라우저 호환성**
116+
- Chrome, Edge, Firefox 최신 버전 사용 권장
117+
- Safari는 일부 제한 사항 있음
118+
119+
5. **방화벽/네트워크**
120+
- STUN/TURN 서버 연결 확인
121+
- ICE candidate 교환이 정상적으로 이루어지는지 확인
122+
123+
## 테스트 방법
124+
125+
1. 두 개의 브라우저 탭 또는 다른 기기에서 접속
126+
2. 각 탭의 콘솔을 열어 로그 확인
127+
3. 위의 체크리스트 순서대로 확인
128+
4. 문제가 발견된 단계를 기록하고 해당 부분 집중 디버깅

app/layout.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const metadata = {
99

1010
export default function RootLayout({ children }) {
1111
return (
12-
<html lang="ko">
12+
<html lang="ko" suppressHydrationWarning>
1313
<body suppressHydrationWarning>
1414
<RoomProvider>
1515
<MediaProvider>{children}</MediaProvider>

app/page.js

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,60 @@
11
"use client";
22

3+
import { useState } from "react";
34
import { useRouter } from "next/navigation";
45
import { Button } from "@/components/ui/button";
5-
import { useRoomContext } from "@/contexts/RoomContext";
66

77
/**
88
* 홈 페이지 컴포넌트
9-
* 방 생성 버튼을 제공하고 생성된 방으로 리다이렉트
9+
* 방 이름 입력을 통해 방 입장 (방이 없으면 자동 생성)
1010
*/
1111
export default function Home() {
1212
const router = useRouter();
13-
const { createRoom } = useRoomContext();
13+
const [roomName, setRoomName] = useState("");
14+
const [error, setError] = useState("");
1415

1516
/**
16-
* 방 생성 및 리다이렉트 핸들러
17+
* 방 이름을 URL-safe 형식으로 변환
18+
* - 공백 → 하이픈
19+
* - 대문자 → 소문자
20+
* - 특수문자 제거 (영문 소문자, 숫자, 하이픈만 허용)
21+
* - 연속 하이픈 → 단일 하이픈
22+
* - 앞뒤 하이픈 제거
1723
*/
18-
const handleCreateRoom = () => {
19-
// 고유한 방 ID 생성
20-
const roomId = createRoom();
21-
console.log("방 생성 완료, 리다이렉트:", roomId);
24+
const sanitizeRoomName = (name) => {
25+
return name
26+
.trim()
27+
.toLowerCase()
28+
.replace(/\s+/g, "-") // 공백 → 하이픈
29+
.replace(/[^a-z0-9-]/g, "") // 영문 소문자, 숫자, 하이픈만
30+
.replace(/^-+|-+$/g, "") // 앞뒤 하이픈 제거
31+
.replace(/-+/g, "-") // 연속 하이픈을 하나로
32+
.substring(0, 50); // 최대 50자
33+
};
34+
35+
/**
36+
* 방 입장 핸들러
37+
*/
38+
const handleJoinRoom = () => {
39+
setError("");
40+
const sanitized = sanitizeRoomName(roomName);
41+
42+
if (sanitized.length < 3) {
43+
setError("방 이름은 최소 3자 이상이어야 합니다 (영문 소문자, 숫자, 하이픈만 가능)");
44+
return;
45+
}
46+
47+
console.log(`방 입장: ${sanitized}`);
48+
router.push(`/room/${sanitized}`);
49+
};
2250

23-
// 생성된 방 페이지로 이동
24-
router.push(`/room/${roomId}`);
51+
/**
52+
* Enter 키 입력 핸들러
53+
*/
54+
const handleKeyDown = (e) => {
55+
if (e.key === "Enter") {
56+
handleJoinRoom();
57+
}
2558
};
2659

2760
return (
@@ -35,10 +68,31 @@ export default function Home() {
3568
</p>
3669
</div>
3770

38-
{/* 방 생성 버튼 */}
39-
<Button onClick={handleCreateRoom} size="lg" className="text-lg px-8 py-6">
40-
새 방 만들기
41-
</Button>
71+
{/* 방 입장 폼 */}
72+
<div className="w-full max-w-md space-y-4">
73+
<div className="space-y-2">
74+
<label htmlFor="roomName" className="text-sm font-medium text-muted-foreground">
75+
방 이름 입력
76+
</label>
77+
<input
78+
id="roomName"
79+
type="text"
80+
value={roomName}
81+
onChange={(e) => setRoomName(e.target.value)}
82+
onKeyDown={handleKeyDown}
83+
placeholder="my-meeting"
84+
className="w-full px-4 py-3 rounded-lg border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
85+
/>
86+
{error && <p className="text-sm text-destructive">{error}</p>}
87+
<p className="text-xs text-muted-foreground">
88+
💡 영문 소문자, 숫자, 하이픈만 사용 가능합니다
89+
</p>
90+
</div>
91+
92+
<Button onClick={handleJoinRoom} size="lg" className="w-full text-lg py-6">
93+
방 입장하기
94+
</Button>
95+
</div>
4296

4397
{/* 기능 설명 */}
4498
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12 max-w-4xl">

0 commit comments

Comments
 (0)