# 웹 기반 원격 지원 화면 뷰어 — 기술 소개서
> 고객이 브라우저만으로 화면을 공유하고, 관리자가 실시간으로 확인하는 WebRTC 기반 원격 지원 시스템
---
# # 1. 개요
## # 1.1 목표
고객이 별도 프로그램 설치 없이 웹 브라우저에서 화면을 공유하면, 관리자가 PIN 번호만으로 해당 화면을 실시간 확인할 수 있는 기능을 WordPress 웹사이트에 추가한다.
## # 1.2 핵심 원칙
| 원칙 | 내용 |
|------|------|
| **설치 없음** | 고객은 브라우저만 열면 됨 |
| **간단한 연결** | 4자리 PIN만으로 연결 |
| **보기만** | 원격 조작 없이 화면 관찰만 |
| **관리자 인증** | 관리자 로그인 시에만 시청 가능 |
## # 1.3 대상 사용자
| 역할 | 행동 | 필요 조건 |
|------|------|-----------|
| **고객** (화면 공유자) | 사이트 접속 → 화면 공유 → PIN 관리자에게 전달 | Chrome/Edge/Firefox 브라우저 |
| **관리자** (화면 시청자) | 사이트 접속 → PIN 입력 → 고객 화면 실시간 확인 | WordPress 관리자 권한 |
---
# # 2. 시스템 아키텍처
## # 2.1 전체 구조
```mermaid
graph TB
subgraph "고객 브라우저"
C1["🌐 사이트/remote-support/"]
C2["📤 getDisplayMedia()"]
C3["🎥 WebRTC Video Track"]
C1 -->|"화면 공유 클릭"| C2
C2 -->|"MediaStream"| C3
end
subgraph "서버 (Docker)"
subgraph "WordPress 컨테이너"
WP["WordPress + wellcoms-remote 플러그인"]
end
subgraph "Signal 컨테이너"
WS["Node.js Signaling Server<br/>WebSocket"]
RM["Room Manager<br/>PIN ↔ Room 매칭"]
end
end
subgraph "관리자 브라우저"
A1["🌐 사이트/remote-support/"]
A2["📥 RTCPeerConnection"]
A3["🖥️ 화면 실시간 표시"]
A1 -->|"PIN 입력"| A2
A2 -->|"Video Track"| A3
end
C3 -->|"P2P WebRTC"| A2
C1 -->|"1. PIN 생성"| WS
A1 -->|"2. PIN 연결 요청"| WS
WS -->|"3. SDP/ICE 교환"| WS
WS -->|"4. WebRTC 연결 설정"| C3
```
## # 2.2 Docker 구성
```mermaid
graph LR
subgraph "docker-compose.yml"
DB["DB<br/>MariaDB"]
WP["WordPress<br/>Apache+PHP"]
SIG["Signal<br/>Node.js"]
TURN["TURN<br/>coturn"]
end
WP ---|"DB"| DB
WP -.->|"WebSocket"| SIG
CLIENT["고객/관리자<br/>브라우저"] -->|"HTTPS"| WP
CLIENT -->|"WSS"| SIG
CLIENT -->|"TURN"| TURN
```
## # 2.3 기술 스택
| 계층 | 기술 | 이유 |
|------|------|------|
| **프론트엔드** | Vanilla JS + WebRTC API | 의존성 최소화 |
| **WordPress 플러그인** | PHP (shortcode) | 기존 아키텍처 유지, 재사용성 |
| **Signaling 서버** | Node.js + `ws` | 경량, WebSocket 지원 |
| **P2P 연결** | WebRTC (Built-in browser API) | 설치 없이 실시간 영상 전송 |
| **NAT 통과** | Google STUN + coturn TURN (자체 서버) | 모든 네트워크 환경에서 연결 보장 |
| **컨테이너** | Docker + docker-compose | 인프라 관리 |
---
# # 3. 연결 시퀀스
## # 3.1 전체 흐름
```mermaid
sequenceDiagram
actor Customer as 고객
participant PageC as 고객 페이지
participant Signal as Signaling 서버
participant PageA as 관리자 페이지
actor Admin as 관리자
Note over Customer,Admin: Phase 1 — PIN 생성
Customer->>PageC: /remote-support/ 접속
PageC->>Customer: "화면 공유 시작" 버튼 표시
Customer->>PageC: 버튼 클릭
PageC->>Customer: 브라우저 화면 공유 선택 팝업
Customer->>PageC: "전체 화면" 선택
PageC->>Signal: WebSocket 연결 + create_room
Signal-->>PageC: PIN "4827" 생성
PageC->>Customer: PIN 4827 표시
Note over Customer,Admin: Phase 2 — 관리자 연결
Admin->>PageA: /remote-support/ 접속 (로그인 상태)
PageA->>Admin: PIN 입력 폼 표시
Admin->>PageA: PIN 4827 입력 + "연결" 클릭
PageA->>Signal: WebSocket 연결 + join_room {pin: "4827"}
Note over Customer,Admin: Phase 3 — WebRTC 연결
Signal-->>PageC: peer_joined 이벤트
PageC->>Signal: SDP Offer 전송
Signal->>PageA: SDP Offer 전달
PageA->>Signal: SDP Answer 전송
Signal->>PageC: SDP Answer 전달
loop ICE Candidate 교환
PageC->>Signal: ICE Candidates
Signal->>PageA: ICE Candidates
PageA->>Signal: ICE Candidates
Signal->>PageC: ICE Candidates
end
Note over Customer,Admin: Phase 4 — 실시간 화면 시청
Customer-->>Admin: WebRTC P2P Video Stream
Admin->>Admin: 고객 화면 실시간 표시
```
## # 3.2 WebSocket 프로토콜
**클라이언트 → 서버**
| 타입 | 설명 |
|------|------|
| `create_room` | 방 생성 요청 |
| `join_room {pin}` | PIN으로 방 참여 |
| `sdp_offer {sdp}` | WebRTC SDP Offer |
| `sdp_answer {sdp}` | WebRTC SDP Answer |
| `ice_candidate {candidate}` | ICE Candidate |
| `leave_room` | 방 나가기 |
**서버 → 클라이언트**
| 타입 | 설명 |
|------|------|
| `room_created {pin}` | PIN 발급 |
| `peer_joined` | 상대방 입장 알림 |
| `sdp_offer {sdp}` | SDP Offer 전달 |
| `sdp_answer {sdp}` | SDP Answer 전달 |
| `ice_candidate {candidate}` | ICE Candidate 전달 |
| `peer_left` | 상대방 퇴장 알림 |
| `error {message}` | 에러 메시지 |
## # 3.3 메시지 포맷
```json
// 방 생성
{ "type": "create_room" }
→ { "type": "room_created", "pin": "4827" }
// 방 참여
{ "type": "join_room", "pin": "4827" }
→ { "type": "peer_joined" }
// SDP Offer
{ "type": "sdp_offer", "sdp": { "type": "offer", "sdp": "..." } }
// SDP Answer
{ "type": "sdp_answer", "sdp": { "type": "answer", "sdp": "..." } }
// ICE Candidate
{ "type": "ice_candidate", "candidate": { "candidate": "...", "sdpMid": "...", "sdpMLineIndex": 0 } }
```
---
# # 4. 컴포넌트 설계
## # 4.1 WordPress 플러그인 (v2.0.0 — Shortcode 아키텍처)
```mermaid
graph TD
subgraph "wellcoms-remote/"
MAIN["wellcoms-remote.php<br/>플러그인 메인 v2.0.0"]
PAGE["templates/page-remote.php<br/>독립형 원격지원 페이지"]
SIG["signal/<br/>Signaling 서버"]
ASSETS["assets/<br/>css/ js/ (예약)"]
end
subgraph "Shortcode 모드 (권장)"
THEME["테마 page 템플릿"]
SC["[wellcoms_screen_share]<br/>WebRTC 전체 렌더링"]
THEME -->|"do_shortcode()"| SC
MAIN -->|"add_shortcode()"| SC
end
subgraph "독립형 모드 (다른 사이트용)"
MAIN -->|"template_include"| PAGE
end
```
### # 4.1.1 wellcoms-remote.php (메인)
```php
<?php
/**
* Plugin Name: Wellcoms Remote
* Description: 웹 기반 원격 지원 화면 뷰어 — shortcode [wellcoms_screen_share]
* Version: 2.0.0
*/
define("WR_PATH", plugin_dir_path(__FILE__));
define("WR_URL", plugin_dir_url(__FILE__));
// 관리자 판별 (필터로 확장 가능)
function wr_is_admin() {
$is = current_user_can("manage_options");
return apply_filters("wr_is_admin", $is);
}
// Shortcode: [wellcoms_screen_share] → WebRTC HTML+CSS+JS 반환
add_shortcode("wellcoms_screen_share", function($atts) { /* ... */ });
// 독립형: 커스텀 템플릿 없는 페이지만 template_include로 대체
add_filter("template_include", function($template) {
if (is_page("remote-support")) {
$assigned = get_page_template_slug(get_queried_object_id());
if ($assigned) return $template;
return WR_PATH . "templates/page-remote.php";
}
return $template;
}, 1);
```
### # 4.1.2 라우팅 전략 (이중 모드)
```mermaid
graph LR
REQ["/remote-support/ 요청"]
REQ -->|"WordPress 페이지"| CHECK{"커스텀 템플릿<br/>지정됨?"}
CHECK -->|"Yes"| THEME["테마 page 템플릿"]
CHECK -->|"No"| STANDALONE["독립형 템플릿<br/>(templates/page-remote.php)"]
THEME -->|"do_shortcode()"| SC["[wellcoms_screen_share]<br/>WebRTC 렌더링"]
subgraph "shortcode 내부 분기"
LOGGED_IN{"관리자<br/>로그인?"}
LOGGED_IN -->|"Yes"| ADMIN_VIEW["관리자 화면<br/>PIN 입력 + 시청"]
LOGGED_IN -->|"No"| CUSTOMER_VIEW["고객 화면<br/>화면 공유 + PIN 표시"]
end
```
### # 4.1.3 다른 사이트에서 사용법
1. 플러그인 설치 + 활성화
2. 페이지에 `[wellcoms_screen_share]` shortcode 삽입
3. (또는 slug가 `remote-support`인 페이지 생성하면 독립형 모드 자동 적용)
4. 관리자 권한 커스터마이징: `add_filter('wr_is_admin', fn() => my_check());`
## # 4.2 Signaling 서버
```mermaid
graph TD
subgraph "signal/server.js"
WSS["WebSocket Server"]
RM["RoomManager"]
STORE["rooms Map<br/>Map<pin, room>"]
WSS -->|"connection"| CONN["연결 핸들러"]
CONN -->|"create_room"| RM
CONN -->|"join_room"| RM
CONN -->|"sdp_offer"| RELAY["메시지 릴레이"]
CONN -->|"sdp_answer"| RELAY
CONN -->|"ice_candidate"| RELAY
RM --> STORE
RM -->|"4자리 PIN 생성"| GEN["PIN Generator<br/>1000-9999 랜덤<br/>중복 체크"]
end
```
### # Room 데이터 구조
```javascript
// 서버 메모리에 저장 (DB 불필요)
rooms = new Map(); // pin → room
room = {
pin: "4827",
host: WebSocket, // 고객 연결
viewer: WebSocket | null, // 관리자 연결
createdAt: Date.now(), // 30분 후 자동 삭제
}
```
### # 서버 로직
```mermaid
stateDiagram-v2
[*] --> Waiting: create_room
Waiting --> Connected: join_room (관리자 입장)
Connected --> Signaling: WebRTC 시그널링 시작
Signaling --> Streaming: P2P 연결 완료
Streaming --> Waiting: 관리자 퇴장
Streaming --> Ended: 고객 퇴장
Waiting --> Ended: 고객 퇴장
Ended --> [*]
Waiting --> Ended: 30분 타임아웃
```
## # 4.3 프론트엔드 (WebRTC)
```mermaid
graph TD
subgraph "WebRTC 로직"
UI["UI Controller"]
WS["WebSocket Client"]
RTC["WebRTC Manager"]
UI -->|"공유 시작"| SCREEN["getDisplayMedia()"]
UI -->|"PIN 입력"| CONNECT["연결 시도"]
SCREEN -->|"MediaStream"| RTC
CONNECT -->|"join_room"| WS
WS -->|"sdp_offer/answer"| RTC
WS -->|"ice_candidate"| RTC
RTC -->|"Video Track"| VIDEO["<video> 엘리먼트"]
end
```
### # 고객 측 (화면 공유자)
```mermaid
sequenceDiagram
participant C as 고객 JS
participant WS as WebSocket
participant PC as RTCPeerConnection
C->>C: getDisplayMedia({video: true})
C->>WS: create_room
WS-->>C: room_created {pin: "4827"}
C->>C: PIN 화면에 표시
Note over C: 관리자 대기 중...
WS-->>C: peer_joined
C->>PC: new RTCPeerConnection(config)
C->>PC: addTrack(screenStream)
C->>PC: createOffer()
PC-->>C: SDP Offer
C->>WS: sdp_offer {sdp}
WS-->>C: sdp_answer {sdp}
C->>PC: setRemoteDescription(answer)
loop ICE
PC-->>C: onicecandidate
C->>WS: ice_candidate
WS-->>C: ice_candidate
C->>PC: addIceCandidate
end
Note over C: 스트리밍 중...
```
### # 관리자 측 (화면 시청자)
```mermaid
sequenceDiagram
participant A as 관리자 JS
participant WS as WebSocket
participant PC as RTCPeerConnection
A->>WS: join_room {pin: "4827"}
WS-->>A: sdp_offer {sdp}
A->>PC: new RTCPeerConnection(config)
A->>PC: setRemoteDescription(offer)
A->>PC: createAnswer()
PC-->>A: SDP Answer
A->>WS: sdp_answer {sdp}
loop ICE
PC-->>A: onicecandidate
A->>WS: ice_candidate
WS-->>A: ice_candidate
A->>PC: addIceCandidate
end
PC-->>A: ontrack (video)
A->>A: video.srcObject = stream
Note over A: 고객 화면 실시간 표시
```
---
# # 5. UI/UX 설계
## # 5.1 고객 화면 (비로그인)
```mermaid
graph TD
START["고객 접속<br/>/remote-support/"] --> VIEW["안내 문구 +<br/>🔵 화면 공유 시작 버튼"]
VIEW -->|"클릭"| PICK["브라우저 화면 선택 팝업"]
PICK -->|"화면 선택"| LIVE["📺 화면 공유 중<br/>PIN: 4827 표시"]
PICK -->|"취소"| VIEW
LIVE -->|"공유 중지"| VIEW
```
**화면 공유 중 UI:**
| 요소 | 내용 |
|------|------|
| 상태 | 📺 화면 공유 중 |
| PIN | 4자리 숫자 크게 표시 (예: `4 8 2 7`) |
| 안내 | "이 번호를 상담원에게 알려주세요" |
| 버튼 | [공유 중지] |
| 하단 안내 | ℹ️ 상담원은 화면을 볼 수만 있으며 원격 조작은 하지 않습니다 |
## # 5.2 관리자 화면 (로그인)
```mermaid
graph TD
ADMIN["관리자 접속<br/>/remote-support/"] --> FORM["PIN 입력 폼"]
FORM -->|"PIN 입력 + 연결"| CONNECT["WebRTC 연결"]
CONNECT -->|"연결 성공"| WATCH["🖥️ 고객 화면 실시간 표시"]
CONNECT -->|"연결 실패"| ERROR["에러 토스트 표시"]
WATCH -->|"연결 종료"| FORM
```
---
# # 6. 보안 설계
## # 6.1 위협 모델과 대응
```mermaid
graph LR
subgraph "위협"
T1["무단 시청"]
T2["PIN 브루트포스"]
T3["중간자 공격"]
T4["방 해킹"]
end
subgraph "대응"
D1["WordPress 관리자 인증"]
D2["PIN 시도 횟수 제한"]
D3["WSS (TLS) 암호화"]
D4["PIN 1회용 + 30분 만료"]
D5["1방 = 1관리자만"]
end
T1 --> D1
T2 --> D2
T3 --> D3
T4 --> D4
T4 --> D5
```
## # 6.2 보안 체크리스트
| 항목 | 구현 |
|------|------|
| Signaling 서버 | WSS (TLS) 필수 |
| 관리자 인증 | WordPress 권한 확인 (필터로 커스터마이징 가능) |
| PIN 만료 | 30분 후 자동 삭제 |
| PIN 시도 제한 | 5회 실패 시 5분 차단 |
| 방 인원 제한 | 1방 = 고객 1명 + 관리자 1명 |
| 고객 화면 선택 | `getDisplayMedia()` — 고객이 공유할 화면 직접 선택 |
| WebRTC 암호화 | SRTP — WebRTC 기본 암호화 |
| 연결 종료 | 고객이 언제든 "공유 중지" 가능 |
---
# # 7. Docker 구성
## # 7.1 docker-compose.yml
```yaml
services:
wordpress:
image: wordpress:latest
volumes:
- ./data:/var/www/html
networks:
- backend
- web
signal:
image: wellcoms-signal:latest
environment:
SIGNAL_PORT: "3000"
networks:
- web
turn:
image: coturn/coturn:latest
network_mode: host
volumes:
- ./turn/turnserver.conf:/etc/coturn/turnserver.conf:ro
networks:
web:
external: true
backend:
external: true
```
## # 7.2 TURN 서버 설정 (`turnserver.conf`)
```
listening-port=3478
tls-listening-port=5349
external-ip=<YOUR_PUBLIC_IP>
realm=your-domain.com
user=<TURN_USERNAME>:<TURN_PASSWORD>
log-file=stdout
fingerprint
min-port=49152
max-port=65535
no-cli
```
## # 7.3 리버스 프록시 설정 (Caddy 예시)
```
# Signaling WebSocket 프록시
handle /signal/* {
reverse_proxy signal:3000 {
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
}
}
# TURN 서버는 network_mode: host로 직접 포트 3478 노출
# 리버스 프록시 불필요
```
---
# # 8. 파일 구조
```
wp-wellcoms-remote/ ← GitLab 리포
├── wellcoms-remote.php ← 플러그인 메인 v2.0.0 (shortcode + template_include)
├── templates/
│ └── page-remote.php ← 독립형 원격지원 페이지 (템플릿 없는 사이트용)
├── assets/
│ ├── js/ ← (예약)
│ └── css/ ← (예약)
├── signal/ ← Signaling 서버
│ ├── server.js ← Node.js WebSocket 서버
│ ├── package.json
│ └── Dockerfile
└── README.md
```
**테마 연동 예시:**
```
themes/your-theme/
└── page-remote-support.php ← 원격지원 페이지 템플릿
├── 페이지 레이아웃, 안내 문구, 다운로드 섹션 등
└── <?php echo do_shortcode('[wellcoms_screen_share]'); ?> ← WebRTC 섹션
```
---
# # 9. 구현 단계
## # Phase 1: 기반 구축 ✅
| 작업 | 상태 | 비고 |
|------|------|------|
| Node.js + ws Signaling 서버 | ✅ | Docker 컨테이너 |
| Room Manager (PIN 생성/매칭) | ✅ | 4자리 PIN, 30분 타임아웃 |
| 메시지 릴레이 | ✅ | SDP/ICE 교환 |
| WordPress 페이지 템플릿 | ✅ | shortcode + 독립형 |
| Docker Compose 구성 | ✅ | signal 컨테이너 |
| 리버스 프록시 WSS 설정 | ✅ | `/signal/*` → signal:3000 |
## # Phase 2: WebRTC 연결 ✅
| 작업 | 상태 | 비고 |
|------|------|------|
| getDisplayMedia 화면 캡처 | ✅ | 고객 측 |
| RTCPeerConnection (Offer/Answer) | ✅ | SDP 교환 |
| ICE Candidate 교환 | ✅ | Signaling 서버 경유 |
| PIN 입력 UI | ✅ | 4칸 입력 + 자동 포커스 |
| Video 재생 | ✅ | `<video>` 엘리먼트 |
| ICE disconnect 처리 | ✅ | 1.5초 지연 후 재연결 시도 |
## # Phase 2A: TURN 서버 ✅
| 작업 | 상태 | 비고 |
|------|------|------|
| coturn Docker 컨테이너 | ✅ | network_mode: host |
| TURN 설정 (turnserver.conf) | ✅ | 포트 3478 |
| WebRTC iceServers 업데이트 | ✅ | TURN UDP + TCP 추가 |
## # Phase 3: UI/UX + 안정화 ✅
| 작업 | 상태 | 비고 |
|------|------|------|
| 고객/관리자 통합 페이지 | ✅ | 권한으로 관리자 구분 |
| 2컬럼 레이아웃 (좌:WebRTC / 우:프로그램 정보) | ✅ | 카카오톡 상담 버튼 포함 |
| 반응형 (모바일) | ✅ | 모바일에서도 동작 |
| 관리자 인증 | ✅ | WordPress 권한 + 필터 확장 |
| 에러 핸들링 | ✅ | toast 알림 |
| 연결 끊김 처리 | ✅ | intentionalClose 플래그 |
| 연결 유형 표시 | ✅ | P2P / TURN 릴레이 배지 |
| 모바일 전체화면 | ✅ | 핀치투줌 + 가로 회전 |
## # Phase 4: 배포 ✅
| 작업 | 상태 | 비고 |
|------|------|------|
| Docker 배포 | ✅ | signal, turn 컨테이너 |
| 리버스 프록시 설정 | ✅ | WSS 프록시 적용 |
| SSL 인증서 | ✅ | 자동 관리 |
| 테스트 | ✅ | 외부망 연결 테스트 완료 |
## # Phase 5: 플러그인 분리 리팩토링 ✅
| 작업 | 상태 | 비고 |
|------|------|------|
| 테마 템플릿에서 WebRTC 코드 분리 | ✅ | CSS/HTML/JS 약 500줄 이전 |
| 플러그인 shortcode 등록 | ✅ | `[wellcoms_screen_share]` — WebRTC 전체 렌더링 |
| 관리자 판별 함수 `wr_is_admin()` | ✅ | 권한 체크 + 필터 확장 |
| template_include 이중 모드 | ✅ | 커스텀 템플릿 있으면 shortcode 모드, 없으면 독립형 |
| 테마 템플릿 `do_shortcode()` 교체 | ✅ | 745줄 → 230줄 |
| 버전 업 | ✅ | v1.0.0 → v2.0.0 |
---
# # 10. WebRTC 설정
## # 10.1 RTCPeerConnection Config
```javascript
var rtcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' },
{ urls: 'turn:your-domain.com:3478', username: '<USER>', credential: '<PASS>' },
{ urls: 'turn:your-domain.com:3478?transport=tcp', username: '<USER>', credential: '<PASS>' }
]
};
```
## # 10.2 getDisplayMedia 옵션
```javascript
const screenStream = await navigator.mediaDevices.getDisplayMedia({
video: {
displaySurface: 'monitor', // 전체 화면 권장
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 15 } // 원격지원은 15fps면 충분
},
audio: false // 화면만 공유
});
```
## # 10.3 NAT 통과 시나리오
```mermaid
graph TD
A["WebRTC 연결 시도"] --> B{"STUN으로<br/>P2P 연결?"}
B -->|"성공 (80~90%)"| C["✅ 직접 P2P 연결"]
B -->|"실패 (10~20%)"| D["coturn TURN 서버<br/>릴레이 연결"]
D --> E["✅ TURN 릴레이로<br/>연결 성공"]
style C fill:#4ade80
style E fill:#4ade80
```
---
# # 11. 에러 처리
## # 11.1 시나리오별 대응
| 상황 | 감지 | 대응 |
|------|------|------|
| **고객이 공유 중지** | `stream.onended` 이벤트 | 관리자에게 "고객이 공유를 중지했습니다" 알림 |
| **관리자가 연결 종료** | `WebSocket.onclose` | 고객에게 "상담이 종료되었습니다" 알림 |
| **네트워크 끊김** | `ICE connectionState = 'disconnected'` | 1.5초 대기 후 "연결이 불안정합니다" 알림 |
| **PIN 만료** | 30분 타임아웃 | 고객에게 "PIN이 만료되었습니다. 다시 시도해주세요" 안내 |
| **잘못된 PIN** | 서버에서 room not found | "PIN이 올바르지 않습니다" |
| **브라우저 미지원** | `!navigator.mediaDevices.getDisplayMedia` | "이 브라우저는 화면 공유를 지원하지 않습니다" |
| **고객이 화면 선택 취소** | `getDisplayMedia()` reject | 아무 일도 안 함 (대기 상태 유지) |
---
# # 12. 추후 확장
```mermaid
graph TD
P1["Phase 1<br/>화면 보기만<br/>(구현 완료)"]
P2A["Phase 2A<br/>TURN 서버 추가<br/>(구현 완료)"]
P2B["Phase 2B<br/>음성 채팅 추가<br/>(WebRTC Audio)"]
P3["Phase 3<br/>원격 조작<br/>(포터블 .exe 필요)"]
P1 --> P2A
P1 --> P2B
P2A --> P3
P2B --> P3
```
---
# # 13. 브라우저 호환성
| 브라우저 | getDisplayMedia | WebRTC | 비고 |
|----------|----------------|--------|------|
| Chrome 94+ | ✅ | ✅ | 권장 |
| Edge 94+ | ✅ | ✅ | 권장 |
| Firefox 66+ | ✅ | ✅ | 지원 |
| Safari 15+ | ✅ | ✅ | 제한적 (모바일은 미지원 가능) |
| 모바일 Chrome | ❌ | ✅ | 화면 공유 미지원 (관리자 시청만 가능) |
| 모바일 Safari | ❌ | ✅ | 화면 공유 미지원 |
> 고객(공유자)은 **데스크톱 브라우저** 필요. 관리자(시청자)는 모바일에서도 가능.
| 이전 | | 김재석 | 2026-05-28 |
|---|---|---|---|
| 다음 | | 김재석 | 2026-05-24 |