@vibexnpm/talkx 8.13.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@vibexnpm/talkx",
3
- "version": "8.13.0",
3
+ "version": "9.1.0",
4
4
  "type": "module",
5
- "description": "TalkFlow SDK - Chat & WebRTC unified client",
5
+ "description": "TalkFlow SDK - Chat & video call (LiveKit SFU) unified client",
6
6
  "main": "dist/talkflow-sdk.umd.js",
7
7
  "module": "dist/talkflow-sdk.esm.js",
8
8
  "browser": "dist/talkflow-sdk.standalone.js",
@@ -28,6 +28,7 @@
28
28
  "talkflow",
29
29
  "chat",
30
30
  "webrtc",
31
+ "livekit",
31
32
  "websocket",
32
33
  "stomp",
33
34
  "realtime"
@@ -37,6 +38,7 @@
37
38
  "dependencies": {
38
39
  "@stomp/stompjs": "^7.0.0",
39
40
  "firebase": "^11.8.1",
41
+ "livekit-client": "2.22.4",
40
42
  "sockjs-client": "^1.6.0"
41
43
  },
42
44
  "devDependencies": {
@@ -31,7 +31,7 @@ class TalkFlowClient extends EventEmitter {
31
31
  * @param {boolean} [options.useSockJS=true] - SockJS 사용 여부
32
32
  * @param {number} [options.reconnectDelay=5000] - 재연결 지연 (ms)
33
33
  * @param {number} [options.maxReconnectAttempts=10] - 최대 재연결 시도
34
- * @param {Object[]} [options.iceServers] - ICE 서버 설정
34
+ * @param {number} [options.callRingTimeoutMs=65000] - 받는 쪽 울림을 스스로 끄는 시각(서버 «응답함»·«취소» 가 먼저 오면 그때 끈다)
35
35
  * @param {boolean} [options.autoSubscribeRoomList=true] - connect() 시 채팅방 리스트 자동 구독 여부 (카톡 스타일)
36
36
  * @param {number} [options.logLevel=LogLevel.WARN] - 로그 레벨
37
37
  *
@@ -89,7 +89,7 @@ class TalkFlowClient extends EventEmitter {
89
89
  : DefaultConfig.rewindMillis,
90
90
  maxSyncPages: options.maxSyncPages || DefaultConfig.maxSyncPages,
91
91
  syncPageSize: options.syncPageSize || DefaultConfig.syncPageSize,
92
- iceServers: options.iceServers || DefaultConfig.iceServers,
92
+ callRingTimeoutMs: options.callRingTimeoutMs || DefaultConfig.callRingTimeoutMs,
93
93
  // connect() 시 채팅방 리스트 자동 구독 여부 (카톡 스타일).
94
94
  // 기본 true — 앱 어디서든 리스트 이벤트를 놓치지 않음 (배지/알림 등).
95
95
  // false 로 설정 시 수동으로 client.chat.subscribeRoomList() 호출 필요.
@@ -241,6 +241,11 @@ class TalkFlowClient extends EventEmitter {
241
241
  }
242
242
 
243
243
  async _acceptAuthSession(data) {
244
+ // 로그인 세션은 클라이언트에 알리기 전에 새 신원의 토큰으로 바꾼다(AuthSession._accept) — 사용자·회사가 바뀌었으면 옛 신원의 통화
245
+ // 요청(나가기·재시도 등)은 새 신원 이름으로 간다. 이 기기에서만 끝내고 옛 신원의 요청을 멈춘다(서버는 미디어 끊김 뒤 참가를 닫는다).
246
+ if (this.options.projectId !== data.projectId || this.userId !== data.user.userId) {
247
+ await this._webrtc?._resetIdentity();
248
+ }
244
249
  if (this.options.projectId && this.options.projectId !== data.projectId && this.connectionManager) {
245
250
  await this._clearAuthSession();
246
251
  }
@@ -252,6 +257,10 @@ class TalkFlowClient extends EventEmitter {
252
257
  }
253
258
 
254
259
  async _clearAuthSession() {
260
+ // 신원이 지워졌거나(로그아웃·다른 탭·자격 만료) 다른 회사로 바뀌었다 — 통화 나가기를 보낼 자격이 없다. 이 기기에서만 끝내고 옛
261
+ // 신원의 요청을 멈춘다(보내면 갱신 토큰 없는 갱신 → 로그아웃 표식이 다른 탭 로그인까지 끊는다). 이 탭의 logout() 은 자격이 살아
262
+ // 있을 때 먼저 나갔다.
263
+ await this._webrtc?._resetIdentity();
255
264
  this._authConnectionRequested = false;
256
265
  this.options.jwtToken = null;
257
266
  this._userId = null;
@@ -682,9 +691,8 @@ class TalkFlowClient extends EventEmitter {
682
691
  } : null,
683
692
  webrtc: this.webrtc ? {
684
693
  isInCall: this.webrtc.isInCall(),
685
- currentRoom: this.webrtc.getCurrentRoom(),
686
- participants: this.webrtc.getParticipants(),
687
- mediaState: this.webrtc.getMediaState()
694
+ currentCall: this.webrtc.getCurrentCall(),
695
+ ringingCalls: this.webrtc.getRingingCalls()
688
696
  } : null
689
697
  };
690
698
  }
@@ -1488,6 +1488,161 @@ class ChatClient extends EventEmitter {
1488
1488
  return this.apiClient.get(`/api/v1/pm-reports/${encodeURIComponent(reportId)}`);
1489
1489
  }
1490
1490
 
1491
+ // ── 예약 보고 ──────────────────────────────────────
1492
+ //
1493
+ // 정해 둔 시각마다 PM 이 정해 둔 일을 해서 예약의 전용 방에 올린다 — 가이드는 docs/scheduled-reports.md.
1494
+ // 요청한 사람이 만든 사람이고, 남의 예약은 없는 예약과 같은 404 다. 결과는 전용 방의 보통 글이라 방 목록·
1495
+ // 메시지·푸시는 기존 경로 그대로고, 여기에는 예약을 다루는 메서드만 있다.
1496
+ //
1497
+ // 커서 페이지가 없는 응답이라 투표처럼 데이터만 돌려준다. 실패 코드는 {@code error.response.code}.
1498
+
1499
+ /**
1500
+ * 내가 만든 예약 목록 (만든 순).
1501
+ *
1502
+ * @returns {Promise<Array<Object>>} 예약 목록
1503
+ */
1504
+ async getScheduledReports() {
1505
+ const response = await this.apiClient.get('/api/v1/scheduled-reports');
1506
+ return this._unwrapSuccessResponse(response);
1507
+ }
1508
+
1509
+ /**
1510
+ * 예약 하나와 최근 회차 (예정 늦은 순, 최대 10개).
1511
+ *
1512
+ * @param {string} reportId - 예약 id
1513
+ * @returns {Promise<{report: Object, recentRuns: Array<Object>}>}
1514
+ * @throws {Error} 없거나 남의 예약이면 404 {@code SCHEDULED_REPORT_NOT_FOUND}
1515
+ */
1516
+ async getScheduledReport(reportId) {
1517
+ if (!reportId) {
1518
+ throw new Error('reportId is required');
1519
+ }
1520
+ const response = await this.apiClient.get(`/api/v1/scheduled-reports/${encodeURIComponent(reportId)}`);
1521
+ return this._unwrapSuccessResponse(response);
1522
+ }
1523
+
1524
+ /**
1525
+ * 지금 새로 만들 수 있는 작업 종류.
1526
+ *
1527
+ * <p>키에 PM 권한이 없거나 일시정지 중인 종류는 빠진다. 빈 배열이면 만들기를 막는다.</p>
1528
+ *
1529
+ * @returns {Promise<Array<'RESEARCH'|'ROOM_DIGEST'>>}
1530
+ */
1531
+ async getScheduledReportKinds() {
1532
+ const response = await this.apiClient.get('/api/v1/scheduled-reports/kinds');
1533
+ return this._unwrapSuccessResponse(response);
1534
+ }
1535
+
1536
+ /**
1537
+ * 방 정리의 원천 방 고르기 — 내가 지금 참여 중이고 메시지 보관 기간이 없는 방.
1538
+ *
1539
+ * @returns {Promise<Array<{roomId: string, name?: string, roomType?: string}>>}
1540
+ */
1541
+ async getScheduledReportSourceRooms() {
1542
+ const response = await this.apiClient.get('/api/v1/scheduled-reports/source-rooms');
1543
+ return this._unwrapSuccessResponse(response);
1544
+ }
1545
+
1546
+ /**
1547
+ * 보낼 방 고르기 — 만들기·고치기와 같은 판정으로 거른 방이다.
1548
+ *
1549
+ * <p>만들기 화면은 {@code kind}(방 정리는 {@code sourceRoomId} 도), 고치기 화면은 {@code reportId} 를 넘긴다.
1550
+ * {@code reportId} 를 주면 나머지는 보지 않는다.</p>
1551
+ *
1552
+ * @param {Object} params
1553
+ * @param {'RESEARCH'|'ROOM_DIGEST'} [params.kind] - 만들기 화면의 작업 종류
1554
+ * @param {string} [params.sourceRoomId] - 방 정리일 때 필수 — 방 정리는 원천 방으로만 보낸다
1555
+ * @param {string} [params.reportId] - 고치기 화면의 예약 id
1556
+ * @returns {Promise<Array<{roomId: string, name?: string, roomType?: string}>>}
1557
+ * @throws {Error} 고를 기준이 모자라면 400 {@code SCHEDULED_REPORT_SPEC_INVALID}
1558
+ */
1559
+ async getScheduledReportDeliveryRooms(params = {}) {
1560
+ const { kind, sourceRoomId, reportId } = params;
1561
+ const response = await this.apiClient.get('/api/v1/scheduled-reports/delivery-rooms', { kind, sourceRoomId, reportId });
1562
+ return this._unwrapSuccessResponse(response);
1563
+ }
1564
+
1565
+ /**
1566
+ * 예약 만들기 — 전용 방을 만들고 예약을 저장한다. 응답 {@code homeRoomId} 가 결과가 올라갈 방이다.
1567
+ *
1568
+ * <p>🔴 <b>{@code idempotencyKey} 는 필수다.</b> SDK 가 대신 만들지 않는다 — 만들면 그 값이 호출자에게 남지 않아,
1569
+ * 응답만 유실된 타임아웃에서 같은 키로 다시 보낼 수 없다(새 키는 예약과 방을 하나 더 만든다). 폼마다 한 번 만들고,
1570
+ * 재시도에는 <b>같은 키</b>를 넘긴다 — 서버가 처음 만든 예약을 돌려준다. 503
1571
+ * {@code SCHEDULED_REPORT_CREATE_RESULT_UNKNOWN}·{@code SCHEDULED_REPORT_RETRY_LATER} 도 같은 키로 다시 보낸다.</p>
1572
+ *
1573
+ * @param {Object} report - 예약 폼
1574
+ * @param {string} report.title - 제목 (앞뒤 공백을 뺀 1~50자). 전용 방 이름으로도 쓴다
1575
+ * @param {Object} report.spec - 작업 칸, {@code kind} 로 가른다. {@code RESEARCH} 는 {@code instruction}(1~2,000자),
1576
+ * {@code ROOM_DIGEST} 는 {@code sourceRoomId}·{@code period}·{@code format}·{@code focus}(선택, 0~500자)
1577
+ * @param {Object} report.cadence - 주기. {@code type}·{@code days}({@code WEEKLY} 만)·{@code time}({@code HH:mm})·
1578
+ * {@code zoneId}(비우면 내 프로필 시간대)
1579
+ * @param {string[]} [report.deliveryRoomIds] - 결과를 복사할 방 (0~3개)
1580
+ * @param {Object} options
1581
+ * @param {string} options.idempotencyKey - <b>필수.</b> 공백 없는 인쇄 가능 ASCII 1~200자. 재시도에는 반드시 같은 값
1582
+ * @returns {Promise<Object>} 만든 예약
1583
+ * @throws {Error} {@code idempotencyKey} 가 없으면 서버를 호출하지 않고 즉시 실패한다
1584
+ * @example
1585
+ * const key = crypto.randomUUID();
1586
+ * const report = await chat.createScheduledReport({
1587
+ * title: '개발팀 주간 정리',
1588
+ * spec: { kind: 'ROOM_DIGEST', sourceRoomId, period: 'LAST_7_DAYS', format: 'MINUTES' },
1589
+ * cadence: { type: 'WEEKLY', days: ['MONDAY'], time: '09:00' }
1590
+ * }, { idempotencyKey: key });
1591
+ */
1592
+ async createScheduledReport(report, options = {}) {
1593
+ const { idempotencyKey } = options;
1594
+ if (!idempotencyKey) {
1595
+ // SDK 가 만들어 주면 호출자는 그 값을 볼 수 없다 — 타임아웃으로 응답만 유실됐을 때
1596
+ // 같은 키로 재시도할 방법이 사라지고, 다음 호출이 예약과 방을 하나 더 만든다.
1597
+ throw new Error(
1598
+ 'createScheduledReport: options.idempotencyKey is required — '
1599
+ + 'generate it once per form and reuse the same value on retry '
1600
+ + '(재시도에 같은 키를 넘겨야 예약과 방이 중복 생성되지 않는다).'
1601
+ );
1602
+ }
1603
+ const response = await this.apiClient.post(
1604
+ '/api/v1/scheduled-reports',
1605
+ report,
1606
+ undefined,
1607
+ { 'X-Idempotency-Key': idempotencyKey }
1608
+ );
1609
+ return this._unwrapSuccessResponse(response);
1610
+ }
1611
+
1612
+ /**
1613
+ * 예약 고치기·멈추기·다시 켜기 — 고칠 칸만 보낸다.
1614
+ *
1615
+ * <p>{@code spec} 은 보내면 칸 전체이고 작업 종류·원천 방은 지금 값과 같아야 한다. {@code cadence} 는 보내면 주기
1616
+ * 전체이고 {@code zoneId} 를 비우면 지금 시간대를 쓴다. {@code deliveryRoomIds} 는 보내면 목록 전체다(빈 배열 =
1617
+ * 보내지 않음). 멈추기는 {@code status: 'PAUSED'}, 다시 켜기는 {@code status: 'ACTIVE'} 다.</p>
1618
+ *
1619
+ * @param {string} reportId - 예약 id
1620
+ * @param {Object} changes - 고칠 칸 ({@code title}·{@code spec}·{@code cadence}·{@code deliveryRoomIds}·{@code status})
1621
+ * @returns {Promise<Object>} 바꾼 예약
1622
+ * @throws {Error} 없거나 남의 예약이면 404 {@code SCHEDULED_REPORT_NOT_FOUND}
1623
+ */
1624
+ async updateScheduledReport(reportId, changes) {
1625
+ if (!reportId) {
1626
+ throw new Error('reportId is required');
1627
+ }
1628
+ const response = await this.apiClient.patch(`/api/v1/scheduled-reports/${encodeURIComponent(reportId)}`, changes);
1629
+ return this._unwrapSuccessResponse(response);
1630
+ }
1631
+
1632
+ /**
1633
+ * 예약 지우기 — 예약만 지우고 전용 방은 남긴다. 방은 {@link #leaveRoom}·{@link #deleteRoom} 으로 정리한다.
1634
+ *
1635
+ * @param {string} reportId - 예약 id
1636
+ * @returns {Promise<void>}
1637
+ * @throws {Error} 없거나 남의 예약이면 404 {@code SCHEDULED_REPORT_NOT_FOUND}
1638
+ */
1639
+ async deleteScheduledReport(reportId) {
1640
+ if (!reportId) {
1641
+ throw new Error('reportId is required');
1642
+ }
1643
+ await this.apiClient.delete(`/api/v1/scheduled-reports/${encodeURIComponent(reportId)}`);
1644
+ }
1645
+
1491
1646
  /**
1492
1647
  * 메시지 목록 조회
1493
1648
  * @param {string} roomId - 채팅방 ID
package/src/constants.js CHANGED
@@ -40,15 +40,15 @@ export const ErrorTypes = {
40
40
  CHAT_MESSAGE_FAILED: 'CHAT_MESSAGE_FAILED',
41
41
  CHAT_SUBSCRIPTION_FAILED: 'CHAT_SUBSCRIPTION_FAILED',
42
42
 
43
- // WebRTC errors
43
+ // 통화 errors
44
44
  MEDIA_ACCESS_DENIED: 'MEDIA_ACCESS_DENIED',
45
45
  SCREEN_SHARE_DENIED: 'SCREEN_SHARE_DENIED',
46
- PEER_CONNECTION_FAILED: 'PEER_CONNECTION_FAILED',
47
- ICE_CONNECTION_FAILED: 'ICE_CONNECTION_FAILED',
48
- SIGNALING_FAILED: 'SIGNALING_FAILED',
46
+ MEDIA_CONNECTION_FAILED: 'MEDIA_CONNECTION_FAILED',
49
47
  DEVICE_SWITCH_FAILED: 'DEVICE_SWITCH_FAILED',
50
48
  ENUMERATE_DEVICES_FAILED: 'ENUMERATE_DEVICES_FAILED',
51
49
  CALL_ROOM_NOT_FOUND: 'CALL_ROOM_NOT_FOUND',
50
+ CALL_ALREADY_ACTIVE: 'CALL_ALREADY_ACTIVE',
51
+ NO_ACTIVE_CALL: 'NO_ACTIVE_CALL',
52
52
 
53
53
  // General errors
54
54
  INVALID_STATE: 'INVALID_STATE',
@@ -56,36 +56,38 @@ export const ErrorTypes = {
56
56
  };
57
57
 
58
58
  /**
59
- * WebRTC 시그널 타입 (서버 WebRTCMessageType과 일치)
59
+ * 서버 → 클라이언트 통화 이벤트 종류(서버 WebRTCMessageType 의 값). 클라이언트는 통화 신호를 보내지 않는다 — 통화 제어는 REST,
60
+ * 미디어는 LiveKit 이 맡는다(화상통화 SFU 설계 v0.7 I7).
60
61
  */
61
- export const SignalTypes = {
62
- // Offer/Answer (WebRTC SDP)
63
- CALL_OFFER: 'call_offer',
64
- CALL_ANSWER: 'call_answer',
65
-
66
- // ICE
67
- ICE_CANDIDATE: 'ice_candidate',
68
-
69
- // Room events
70
- JOIN_ROOM: 'join_room',
71
- LEAVE_ROOM: 'leave_room',
72
- PEER_JOINED: 'peer_joined',
73
- PEER_LEFT: 'peer_left',
74
-
75
- // Media state
76
- VIDEO_STATE_CHANGED: 'video_state_changed',
77
- AUDIO_STATE_CHANGED: 'audio_state_changed',
78
- SCREEN_SHARE_STARTED: 'screen_share_started',
79
- SCREEN_SHARE_ENDED: 'screen_share_ended',
80
-
81
- // Call events (UI 레벨)
82
- CALL_REQUEST: 'call_request',
83
- CALL_ACCEPT: 'call_accept',
84
- CALL_REJECT: 'call_reject',
85
- CALL_CANCEL: 'call_cancel',
86
- CALL_END: 'call_end',
87
- CALL_INVITATION: 'call_invitation',
88
- CALL_BUSY: 'call_busy'
62
+ export const CallEventTypes = {
63
+ /** 통화 초대 — 초대 대상의 모든 기기. */
64
+ INVITATION: 'call_invitation',
65
+ /** 그 사용자가 어느 기기에서 응답했다 — 그 사람의 다른 기기는 울림을 끈다. */
66
+ ANSWERED: 'call_answered',
67
+ /** 거절·무응답 — 방장에게. */
68
+ REJECT: 'call_reject',
69
+ /** 울림을 멈춘다 — 취소·종료·무응답 때 참가 기록 없는 초대 대상에게. */
70
+ CANCEL: 'call_cancel',
71
+ /** 통화 종료 — 통화방 토픽. */
72
+ END: 'call_end'
73
+ };
74
+
75
+ /** 통화 종류(서버 CallKind). */
76
+ export const CallKind = {
77
+ DIRECT: 'DIRECT',
78
+ GROUP: 'GROUP'
79
+ };
80
+
81
+ /**
82
+ * 통화 상태(`callStateChanged` 의 state). ringing = 받는 쪽 울림, connecting = 입장·미디어 접속 중, connected = 미디어 연결,
83
+ * reconnecting = LiveKit 재접속 또는 «제거됨» 뒤 다시 입장 중, ended = 끝(사유는 reason).
84
+ */
85
+ export const CallState = {
86
+ RINGING: 'ringing',
87
+ CONNECTING: 'connecting',
88
+ CONNECTED: 'connected',
89
+ RECONNECTING: 'reconnecting',
90
+ ENDED: 'ended'
89
91
  };
90
92
 
91
93
  /**
@@ -397,15 +399,14 @@ export const WebSocketPaths = {
397
399
  // 서버가 convertAndSendToUser(userId, "/queue/mail", MAIL_CHANGED) 로 전송 — 새 메일이면 messages[] 미리보기 포함
398
400
  MAIL_USER_DESTINATION: '/user/queue/mail',
399
401
 
400
- // WebRTC destinations
402
+ // 통화 이벤트 — 서버만 보낸다(통화방 토픽은 종료, 개인 큐는 초대·응답·거절·취소)
401
403
  getWebRTCDestination: (roomId) => `/topic/webrtc/${roomId}`,
402
404
  getWebRTCUserDestination: () => `/user/queue/webrtc`,
403
405
 
404
406
  // Send destinations
405
407
  CHAT_SEND: '/app/chat/send',
406
408
  CHAT_READ: '/app/chat/read',
407
- CHAT_TYPING: '/app/chat/typing',
408
- WEBRTC_SIGNAL: '/app/webrtc/signal'
409
+ CHAT_TYPING: '/app/chat/typing'
409
410
  };
410
411
 
411
412
  /**
@@ -465,11 +466,8 @@ export const DefaultConfig = {
465
466
  // API
466
467
  apiTimeout: 30000,
467
468
 
468
- // WebRTC
469
- iceServers: [
470
- { urls: 'stun:stun.l.google.com:19302' },
471
- { urls: 'stun:stun1.l.google.com:19302' }
472
- ],
469
+ // 통화 — 받는 쪽 울림을 끄는 시각(서버 울림 상한 60초 + 여유). 서버 «응답함»·«취소» 가 먼저 오면 그때 끈다.
470
+ callRingTimeoutMs: 65000,
473
471
 
474
472
  // Logging
475
473
  logLevel: 2 // WARN
@@ -781,9 +781,19 @@ class ConnectionManager extends EventEmitter {
781
781
  /**
782
782
  * 구독 해제
783
783
  * @param {string} destination - 구독 경로
784
+ * @param {Object} [subscription] - 이 구독만 푼다(subscribe 가 돌려준 객체). 끊긴 사이 같은 목적지 구독이 대기 줄에 여럿 섰다가 함께
785
+ * 붙으면 맵에는 마지막 것만 남는다 — 맵 항목은 그것이 이 구독일 때만 지운다. 생략하면 그 목적지의 지금(맵의) 구독을 푼다.
784
786
  */
785
- unsubscribe(destination) {
787
+ unsubscribe(destination, subscription) {
786
788
  const entry = this.subscriptions.get(destination);
789
+ if (subscription) {
790
+ subscription.unsubscribe();
791
+ if (entry?.subscription === subscription) {
792
+ this.subscriptions.delete(destination);
793
+ }
794
+ this.logger.debug(`Unsubscribed from: ${destination}`);
795
+ return;
796
+ }
787
797
  if (entry) {
788
798
  entry.subscription.unsubscribe();
789
799
  this.subscriptions.delete(destination);
package/src/index.js CHANGED
@@ -41,7 +41,9 @@ import TalkFlowClient from './TalkFlowClient.js';
41
41
  export {
42
42
  ConnectionState,
43
43
  ErrorTypes,
44
- SignalTypes,
44
+ CallEventTypes,
45
+ CallKind,
46
+ CallState,
45
47
  ChatMessageType,
46
48
  DecisionCardLimits,
47
49
  ChatRoomType,
@@ -81,10 +83,8 @@ export { default as ChatClient } from './chat/ChatClient.js';
81
83
  // 방 미독 계산기 — ROOM_UNREAD_UPDATED / MESSAGE_RECEIVED 를 순서 역전에 안전하게 합산(READ_SYNC_DESIGN §6).
82
84
  export { RoomUnreadReconciler } from './chat/RoomUnreadReconciler.js';
83
85
 
84
- // WebRTC
86
+ // 화상통화 (LiveKit SFU)
85
87
  export { default as WebRTCClient } from './webrtc/WebRTCClient.js';
86
- export { default as MediaStreamManager } from './webrtc/MediaStreamManager.js';
87
- export { default as PeerConnectionManager } from './webrtc/PeerConnectionManager.js';
88
88
 
89
89
  // Push — PushError/PushErrorCode 는 호출자가 instanceof / code 분기에 사용 가능하도록 noamed export.
90
90
  export { default as PushManager, PushError, PushErrorCode } from './push/PushManager.js';
@@ -25,6 +25,15 @@ const CHAT_DELEGATE_METHODS = [
25
25
  // PM 보고서 — 참여 중인 방들에 PM 이 올린 작업 결과(지금은 자료조사) 목록·상세.
26
26
  'getPmReports',
27
27
  'getPmReport',
28
+ // 예약 보고 — 정해 둔 시각마다 PM 이 전용 방에 올리는 보고의 만들기·고치기·지우기와 고르기 목록.
29
+ 'getScheduledReports',
30
+ 'getScheduledReport',
31
+ 'getScheduledReportKinds',
32
+ 'getScheduledReportSourceRooms',
33
+ 'getScheduledReportDeliveryRooms',
34
+ 'createScheduledReport',
35
+ 'updateScheduledReport',
36
+ 'deleteScheduledReport',
28
37
  'fetchLinkPreview',
29
38
  'sendMessage',
30
39
  'sendMessageOptimistic',
@@ -99,41 +108,28 @@ const CHAT_DELEGATE_METHODS = [
99
108
  ];
100
109
 
101
110
  const WEBRTC_DELEGATE_METHODS = [
102
- 'initializeIceServers',
103
- 'getTurnCredentials',
104
- 'createCallRoom',
105
- 'getCallRoom',
106
- 'joinCallRoomApi',
107
- 'leaveCallRoomApi',
108
111
  'enableIncomingCalls',
109
112
  'disableIncomingCalls',
110
113
  'isIncomingCallsEnabled',
111
- 'startCall',
112
114
  'callUser',
115
+ 'createGroupCall',
113
116
  'acceptCall',
117
+ 'joinCall',
114
118
  'rejectCall',
115
119
  'cancelCall',
116
- 'endCall',
117
- 'toggleVideo',
118
- 'toggleAudio',
119
- 'setVideoEnabled',
120
- 'setAudioEnabled',
120
+ 'leaveCall',
121
+ 'endCallForAll',
122
+ 'setCameraEnabled',
123
+ 'setMicrophoneEnabled',
121
124
  'startScreenShare',
122
125
  'stopScreenShare',
123
- 'getLocalStream',
124
126
  'getDevices',
125
127
  'switchDevice',
126
- 'startDeviceChangeDetection',
127
- 'stopDeviceChangeDetection',
128
- 'applyVideoConstraints',
129
- 'getVideoSettings',
130
- 'getAudioSettings',
128
+ 'getCallRoom',
129
+ 'getParticipation',
131
130
  'isInCall',
132
- 'getCurrentRoom',
133
- 'getParticipants',
134
- 'getMediaState',
135
- 'getConnectionSummary',
136
- 'getStats'
131
+ 'getCurrentCall',
132
+ 'getRingingCalls'
137
133
  ];
138
134
 
139
135
  function defineDelegates(prototype, targetKey, methods) {
@@ -63,32 +63,20 @@ const CHAT_EVENT_MAP = [
63
63
  ];
64
64
 
65
65
  const WEBRTC_EVENT_MAP = [
66
- ['localStreamStarted', 'localStreamStarted'],
67
- ['localStreamStopped', 'localStreamStopped'],
68
- ['remoteTrack', 'remoteTrack'],
69
- ['screenShareStarted', 'screenShareStarted'],
70
- ['screenShareEnded', 'screenShareEnded'],
71
- ['deviceChange', 'deviceChange'],
72
- ['mediaStateChanged', 'mediaStateChanged'],
73
- ['callStarted', 'callStarted'],
66
+ ['incomingCall', 'incomingCall'],
67
+ ['ringStopped', 'ringStopped'],
68
+ ['callStateChanged', 'callStateChanged'],
74
69
  ['callEnded', 'callEnded'],
75
- ['callRecoveryFailed', 'callRecoveryFailed'],
76
- ['callRequested', 'callRequested'],
77
- ['callAccepted', 'callAccepted'],
78
70
  ['callRejected', 'callRejected'],
79
- ['callCancelled', 'callCancelled'],
80
- ['callBusy', 'callBusy'],
81
- ['incomingCall', 'incomingCall'],
82
- ['incomingCallWhileBusy', 'incomingCallWhileBusy'],
83
- ['callInvitation', 'callInvitation'],
84
- ['userJoined', 'userJoined'],
85
- ['userLeft', 'userLeft'],
71
+ ['participantJoined', 'participantJoined'],
86
72
  ['participantLeft', 'participantLeft'],
87
- ['participantMediaState', 'participantMediaState'],
88
- ['peerConnected', 'peerConnected'],
89
- ['peerDisconnected', 'peerDisconnected'],
90
- ['peerClosed', 'peerClosed'],
91
- ['error', 'webrtcError']
73
+ ['trackSubscribed', 'trackSubscribed'],
74
+ ['trackUnsubscribed', 'trackUnsubscribed'],
75
+ ['localTrackPublished', 'localTrackPublished'],
76
+ ['localTrackUnpublished', 'localTrackUnpublished'],
77
+ ['participantMediaChanged', 'participantMediaChanged'],
78
+ ['callRecoveryFailed', 'callRecoveryFailed'],
79
+ ['callError', 'callError']
92
80
  ];
93
81
 
94
82
  function forwardMappedEvents(source, target, eventMap) {
@@ -5,6 +5,23 @@ import WebRTCClient from '../webrtc/WebRTCClient.js';
5
5
  import PushManager from '../push/PushManager.js';
6
6
  import { handleMailPush } from './mailNotifications.js';
7
7
 
8
+ /** 로그아웃·사용자 전환·연결 끊기 전에 통화에서 나가기를 기다리는 상한 — 망이 매달려도 오래 막지 않는다(미디어는 바로 끊긴다). */
9
+ export const LEAVE_CALL_ON_EXIT_TIMEOUT_MS = 3000;
10
+
11
+ /**
12
+ * 통화 중이면 나간다 — 자격(토큰·로그인)이 바뀌기 «전» 에 보내야 서버에 닿는다(바꾼 뒤면 다른 사용자 이름으로 가거나 로그아웃 표식
13
+ * 때문에 보내지 못한다). 실패·지연은 기다리지 않는다 — 서버는 끊김 유예 뒤 닫는다.
14
+ */
15
+ async function leaveCallBeforeExit(client) {
16
+ if (!client.webrtc?.isInCall()) {
17
+ return;
18
+ }
19
+ let timer;
20
+ const leave = client.webrtc.leaveCall().catch(error => client.logger.warn('Failed to leave the call before exit:', error));
21
+ await Promise.race([leave, new Promise((resolve) => { timer = setTimeout(resolve, LEAVE_CALL_ON_EXIT_TIMEOUT_MS); })]);
22
+ clearTimeout(timer);
23
+ }
24
+
8
25
  export function initializeSubClients(client) {
9
26
  if (client.connectionManager) {
10
27
  return;
@@ -52,7 +69,7 @@ export function initializeSubClients(client) {
52
69
  connectionManager: client.connectionManager,
53
70
  apiClient: client.apiClient,
54
71
  userId: client._userId,
55
- iceServers: client.options.iceServers,
72
+ ringTimeoutMs: client.options.callRingTimeoutMs,
56
73
  logLevel: client.options.logLevel
57
74
  });
58
75
 
@@ -120,9 +137,7 @@ export function applySessionMethods(TalkFlowClient) {
120
137
  return;
121
138
  }
122
139
 
123
- if (this.webrtc && this.webrtc.isInCall()) {
124
- this.webrtc.endCall();
125
- }
140
+ await leaveCallBeforeExit(this);
126
141
 
127
142
  if (this.webrtc) {
128
143
  this.webrtc.disableIncomingCalls();
@@ -139,6 +154,10 @@ export function applySessionMethods(TalkFlowClient) {
139
154
  };
140
155
 
141
156
  TalkFlowClient.prototype.logout = async function logout() {
157
+ await leaveCallBeforeExit(this);
158
+ // 이 신원의 통화는 여기서 끝 — 늦게 끝나는 요청(만드는 중이던 통화의 취소·재시도 등)이 자격이 바뀐 뒤에 나가지 않게 한다. 토큰
159
+ // 직접 방식은 WebRTCClient 를 다음 setToken 에 그대로 쓴다.
160
+ await this._webrtc?._resetIdentity();
142
161
  if (this.authSession) return this.authSession.logout();
143
162
  try {
144
163
  await this.apiClient.post('/v1/auth/signout');
@@ -362,6 +381,15 @@ export function applySessionMethods(TalkFlowClient) {
362
381
  const { userId } = validateAndParseJWT(token, { validateExpiry: false });
363
382
  const needReinitialize = this.userId && this.userId !== userId;
364
383
 
384
+ if (needReinitialize && this.connectionManager) {
385
+ // 다른 사용자로 바꾸기 전에 지금 사용자의 통화에서 나간다 — 토큰을 바꾼 뒤면 나가기가 새 사용자 이름으로 간다. 나간 뒤 화상통화를
386
+ // 바로 닫아 늦게 끝나는 요청(만드는 중이던 통화의 취소 등)도 새 사용자 이름으로 나가지 않게 한다. 로그인 세션 방식은 토큰이
387
+ // 이미 바뀌어 있어 _acceptAuthSession 이 먼저 이 기기에서만 끝냈다.
388
+ await leaveCallBeforeExit(this);
389
+ await this._webrtc?._resetIdentity();
390
+ this._webrtc?.destroy();
391
+ }
392
+
365
393
  this.options.jwtToken = token;
366
394
  this._userId = userId;
367
395
  this.apiClient.setJwtToken(token);
@@ -386,6 +414,11 @@ export function applySessionMethods(TalkFlowClient) {
386
414
  this._initializeSubClients();
387
415
  } else if (this.connectionManager) {
388
416
  this.connectionManager.updateToken(token);
417
+ // 토큰 직접 방식의 로그아웃 뒤 다른 사용자로 setToken 하면 재초기화 없이 여기로 온다 — «다른 기기에서 받음» 판정이 새 사용자로
418
+ // 하게 한다(옛 신원은 logout 이 _resetIdentity 로 끝냈다).
419
+ if (this._webrtc) {
420
+ this._webrtc.userId = userId;
421
+ }
389
422
  } else {
390
423
  this._initializeSubClients();
391
424
  }