@vibexnpm/talkx 4.0.0 → 4.0.1

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibexnpm/talkx",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "description": "TalkFlow SDK - Chat & WebRTC unified client",
6
6
  "main": "dist/talkflow-sdk.umd.js",
@@ -289,7 +289,15 @@ class TalkFlowClient extends EventEmitter {
289
289
  }
290
290
 
291
291
  /**
292
- * 사용자 정보 수정
292
+ * 사용자 정보 수정.
293
+ *
294
+ * 서버는 갱신된 프로필이 담긴 **새 JWT** 를 응답에 함께 실어 준다(`/auth` 와 같은 계약).
295
+ * 이걸 채택하지 않으면 옛 토큰의 name/profileUrl 클레임이 계속 나가, 서버가 그 클레임을 읽는
296
+ * 경로에서 낡은 값이 최대 7일(토큰 TTL) 동안 보인다.
297
+ *
298
+ * 같은 사용자의 토큰 교체라 `setToken` 은 재연결을 일으키지 않는다 — userId 가 바뀔 때만
299
+ * 재초기화하고, 여기서는 STOMP connectHeaders 만 갱신된다.
300
+ *
293
301
  * @param {Object} userData
294
302
  * @param {string} [userData.nickname] - 닉네임 (max 100자)
295
303
  * @param {string} [userData.profileImageUrl] - 프로필 이미지 URL (max 500자)
@@ -299,7 +307,20 @@ class TalkFlowClient extends EventEmitter {
299
307
  */
300
308
  async updateMyInfo(userData) {
301
309
  this._checkToken();
302
- return this.apiClient.put('/api/v1/users/update', userData);
310
+ const response = await this.apiClient.put('/api/v1/users/update', userData);
311
+
312
+ if (response?.data?.accessToken) {
313
+ // 토큰 교체 실패로 수정 자체를 실패시키지 않는다 — 서버 반영은 이미 끝났고,
314
+ // 옛 토큰도 유효(낡았을 뿐)하므로 여기서 throw 하면 호출자가 이미 적용된 수정을
315
+ // 다시 시도하게 된다. authenticate() 는 토큰이 없으면 아무것도 못 하므로 계약이 다르다.
316
+ try {
317
+ await this.setToken(response.data.accessToken);
318
+ } catch (error) {
319
+ this.logger.warn('Profile updated but token refresh failed; keeping the old token', error);
320
+ }
321
+ }
322
+
323
+ return response;
303
324
  }
304
325
 
305
326
  /**
@@ -1344,6 +1344,9 @@ class ChatClient extends EventEmitter {
1344
1344
  * @param {Object[]} [data.fileInfos] - 파일 메타데이터 배열. 각 원소는 <b>UUID {@code fileId} 필수</b>
1345
1345
  * ({@link #uploadFile} 반환값은 이미 포함). 외부 스토리지 직접 업로드 연동은 직접 채워야 한다.
1346
1346
  * @param {string} [data.replyToMessageId] - 답글 대상 원본 메시지 ID (옵션)
1347
+ * @param {Object} [data.decisionResponse] - 의사결정 카드의 선택 — {@code { optionId }} (옵션).
1348
+ * 카드 버튼을 누르면 <b>평범한 답글</b>로 보낸다: {@code replyToMessageId} = 카드의 messageId,
1349
+ * {@code message} = 고른 선택지의 {@code label}, 여기에 {@code optionId}. 전용 endpoint 는 없다.
1347
1350
  * @returns {Promise<{duplicate: boolean, messages: Object[]}>}
1348
1351
  * - {@code duplicate}: 멱등 재시도로 기존 결과를 반환한 경우 {@code true}
1349
1352
  * - {@code messages}: 저장된 메시지 배열. {@code messageId} 는 서버가 부여한 ID.
@@ -1361,6 +1364,15 @@ class ChatClient extends EventEmitter {
1361
1364
  * });
1362
1365
  *
1363
1366
  * @example
1367
+ * // 의사결정 카드의 선택 — 답글 하나로 나간다 (새 API 없음)
1368
+ * const option = card.decisionRequest.options[0];
1369
+ * await chat.sendMessage('room-id', {
1370
+ * message: option.label, // 폴백 가독성 — 카드를 못 그리는 화면에서도 대화가 읽힌다
1371
+ * replyToMessageId: card.messageId,
1372
+ * decisionResponse: { optionId: option.id }
1373
+ * });
1374
+ *
1375
+ * @example
1364
1376
  * // 첨부 3장 + 캡션 — 메시지 1건
1365
1377
  * const { messages } = await chat.sendMessage('room-id', {
1366
1378
  * message: '사진 보냅니다',
@@ -1439,7 +1451,10 @@ class ChatClient extends EventEmitter {
1439
1451
  messageId,
1440
1452
  message: data.message,
1441
1453
  fileInfos: data.fileInfos,
1442
- replyToMessageId: data.replyToMessageId
1454
+ replyToMessageId: data.replyToMessageId,
1455
+ // 의사결정 카드의 선택. 🔴 이 줄이 없으면 호출자가 넘긴 값이 <b>조용히 유실</b>된다 —
1456
+ // 이 payload 는 화이트리스트라 data 에 있어도 전달되지 않는다.
1457
+ decisionResponse: data.decisionResponse
1443
1458
  });
1444
1459
 
1445
1460
  const result = this._unwrapSuccessResponse(response);
@@ -1735,6 +1750,44 @@ class ChatClient extends EventEmitter {
1735
1750
  return this.sendMessageOptimistic(roomId, { message, replyToMessageId });
1736
1751
  }
1737
1752
 
1753
+ /**
1754
+ * 의사결정 카드의 선택 전송 (간편 버전).
1755
+ *
1756
+ * <p>세 가지를 <b>한 번에 맞춰</b> 보낸다 — 셋 중 하나만 빠져도 계약이 깨지므로 직접 조립하기보다
1757
+ * 이 메서드를 쓰는 편이 안전하다:</p>
1758
+ * <ul>
1759
+ * <li>{@code replyToMessageId} = 카드 messageId — 어느 질문에 답한 것인지</li>
1760
+ * <li>{@code message} = 고른 선택지의 {@code label} — <b>가독 폴백</b>. 빈 본문이면 대화에
1761
+ * 빈 칸이 남아 나중에 읽을 때 무엇을 골랐는지 알 수 없다</li>
1762
+ * <li>{@code decisionResponse.optionId} — 서버가 보는 <b>권위</b> 값</li>
1763
+ * </ul>
1764
+ *
1765
+ * <p>전용 endpoint 가 아니라 <b>평범한 답글</b>이다. 그래서 선택이 대화에 메시지로 남고, 카드를
1766
+ * 못 그리는 화면에서도 흐름이 읽힌다.</p>
1767
+ *
1768
+ * @param {string} roomId - 채팅방 ID
1769
+ * @param {Object} card - 카드 메시지 ({@code decisionRequest} 를 가진 그 메시지)
1770
+ * @param {Object} option - 사용자가 고른 {@code card.decisionRequest.options[i]}
1771
+ * @returns {Promise<{duplicate: boolean, messages: Object[]}>}
1772
+ *
1773
+ * @example
1774
+ * const card = messages.find(m => m.decisionRequest);
1775
+ * await chat.sendDecisionChoice('room-id', card, card.decisionRequest.options[1]);
1776
+ */
1777
+ async sendDecisionChoice(roomId, card, option) {
1778
+ if (!card || !card.messageId) {
1779
+ throw new Error('sendDecisionChoice: card with messageId is required');
1780
+ }
1781
+ if (!option || !option.id) {
1782
+ throw new Error('sendDecisionChoice: option with id is required');
1783
+ }
1784
+ return this.sendMessage(roomId, {
1785
+ message: option.label,
1786
+ replyToMessageId: card.messageId,
1787
+ decisionResponse: { optionId: option.id }
1788
+ });
1789
+ }
1790
+
1738
1791
  /**
1739
1792
  * 메시지 삭제.
1740
1793
  *
package/src/constants.js CHANGED
@@ -110,6 +110,30 @@ export const ChatMessageType = {
110
110
  SYSTEM: 'SYSTEM'
111
111
  };
112
112
 
113
+ /**
114
+ * AI 의사결정 카드({@code message.decisionRequest}) 렌더 상한 — 서버가 강제하는 값의 사본.
115
+ *
116
+ * <p>🔴 <b>카드는 새 {@code chatMessageType} 이 아니다.</b> 카드 메시지의 타입은 그냥 {@code TEXT} 이고,
117
+ * 선택 요청은 {@code decisionRequest} <b>필드</b>로 온다. 그래서 필드를 모르는 렌더러는 <b>평범한 텍스트
118
+ * 메시지로 그리면 되고 그게 정상 동작</b>이다 — {@code content} 에 질문과 후보가 사람이 읽는 형태로
119
+ * 들어 있다.</p>
120
+ *
121
+ * <p>🔴 반대로 카드를 그린다면 {@code content} 를 <b>다시 그리지 마라</b> — 버튼과 본문에 같은 질문·후보가
122
+ * 두 번 보인다. 둘 중 하나만 그린다.</p>
123
+ *
124
+ * <p>클라가 이 값으로 <b>검증할 필요는 없다</b>(서버가 이미 강제한다). 레이아웃 계산용 참고값이다.</p>
125
+ */
126
+ export const DecisionCardLimits = {
127
+ /** 선택지 최소 개수 — 1개면 물을 이유가 없어 서버가 카드를 만들지 않는다. */
128
+ MIN_OPTIONS: 2,
129
+ /** 선택지 최대 개수 — 그 이상은 선택이 아니라 목록이다. */
130
+ MAX_OPTIONS: 5,
131
+ /** 라벨 표시 길이(초과분은 서버가 말줄임 처리해서 보낸다). */
132
+ LABEL_MAX_LENGTH: 80,
133
+ /** 질문 표시 길이. */
134
+ QUESTION_MAX_LENGTH: 200
135
+ };
136
+
113
137
  /**
114
138
  * 메시지 발신자 타입 — 서버 {@code SenderType} 과 일치.
115
139
  *
package/src/index.js CHANGED
@@ -50,6 +50,7 @@ export {
50
50
  ErrorTypes,
51
51
  SignalTypes,
52
52
  ChatMessageType,
53
+ DecisionCardLimits,
53
54
  ChatRoomType,
54
55
  AssistantMode,
55
56
  RoomAiType,
@@ -24,6 +24,8 @@ const CHAT_DELEGATE_METHODS = [
24
24
  'sendTextMessageOptimistic',
25
25
  'sendReply',
26
26
  'sendReplyOptimistic',
27
+ // AI 의사결정 카드의 선택 — 답글·라벨 본문·optionId 를 한 번에 맞춰 보낸다.
28
+ 'sendDecisionChoice',
27
29
  'uploadFile',
28
30
  'sendFileMessage',
29
31
  'sendFileMessageOptimistic',
package/types/index.d.ts CHANGED
@@ -91,6 +91,20 @@ export const ChatMessageType: {
91
91
  readonly SYSTEM: 'SYSTEM';
92
92
  };
93
93
 
94
+ /**
95
+ * AI 의사결정 카드 렌더 상한 — 서버가 강제하는 값의 사본(참고용, 클라 검증 불필요).
96
+ *
97
+ * <p>🔴 카드는 <b>새 {@code chatMessageType} 이 아니다</b> — 타입은 {@code TEXT} 이고 선택 요청은
98
+ * {@link ChatMessage#decisionRequest} 필드로 온다. 모르는 렌더러는 텍스트로 그리면 되고 그게 정상이다.
99
+ * 카드를 그린다면 {@code content} 를 다시 그리지 마라(같은 내용이 두 번 보인다).</p>
100
+ */
101
+ export const DecisionCardLimits: {
102
+ readonly MIN_OPTIONS: 2;
103
+ readonly MAX_OPTIONS: 5;
104
+ readonly LABEL_MAX_LENGTH: 80;
105
+ readonly QUESTION_MAX_LENGTH: 200;
106
+ };
107
+
94
108
  /**
95
109
  * 서버가 보낼 수 있는 메시지 타입 값 전체 — {@link ChatMessageType} 상수와 같은 집합.
96
110
  * <p>수신 {@link ChatMessage#chatMessageType} 비교 시 이 union 을 사용한다.</p>
@@ -728,6 +742,42 @@ export interface ChatMessage {
728
742
  translations?: Record<string, string> | null;
729
743
  /** 번역 당시 content 해시 — stale 판별용 (내부). */
730
744
  translationsOf?: string | null;
745
+ /**
746
+ * AI 의 구조화 선택 요청(카드). 어시스턴트 메시지에만 존재하고, 일반 메시지에는 부재.
747
+ *
748
+ * <p><b>모르면 그냥 텍스트로 그려라 — 그게 정상 동작이다.</b> {@code content} 에 질문과 후보가
749
+ * 사람이 읽는 형태로 들어 있어 카드를 못 그려도 대화가 그대로 읽힌다.</p>
750
+ *
751
+ * <p>🔴 반대로 카드를 그린다면 {@code content} 를 <b>다시 그리지 마라</b> — 질문과 후보가 두 번 보인다.</p>
752
+ */
753
+ decisionRequest?: DecisionRequest | null;
754
+ /** 사용자의 선택. 어느 카드인지는 {@code replyTo} 가 특정한다. */
755
+ decisionResponse?: DecisionResponse | null;
756
+ }
757
+
758
+ /**
759
+ * AI 가 낸 선택 요청 — 질문 + 후보 2~5개.
760
+ *
761
+ * <p>후보는 <b>서버가 방의 실제 첨부에서</b> 만든다(클라가 만들거나 편집하지 않는다).</p>
762
+ */
763
+ export interface DecisionRequest {
764
+ /** 사용자에게 보일 질문 — 최대 200자. */
765
+ question: string;
766
+ /** 선택지 2~5개. 라벨은 이 카드 안에서 유일하다. */
767
+ options: DecisionOption[];
768
+ }
769
+
770
+ export interface DecisionOption {
771
+ /** 불투명 식별자 — 선택 전송 시 {@code decisionResponse.optionId} 로 <b>그대로</b> 돌려보낸다. */
772
+ id: string;
773
+ /** 버튼에 보일 이름 — 최대 80자(초과분은 서버가 말줄임). */
774
+ label: string;
775
+ /** 보조 설명(업로드 시각 등). 없을 수 있다. */
776
+ hint?: string | null;
777
+ }
778
+
779
+ export interface DecisionResponse {
780
+ optionId: string;
731
781
  }
732
782
 
733
783
  /** 채팅방 참가자. */
@@ -1145,6 +1195,11 @@ export interface SendMessageData {
1145
1195
  */
1146
1196
  separateFiles?: boolean;
1147
1197
  replyToMessageId?: string;
1198
+ /**
1199
+ * 의사결정 카드의 선택 — 카드 버튼을 눌렀을 때만. {@link ChatClient#sendDecisionChoice} 가
1200
+ * {@code replyToMessageId}/{@code message}/{@code optionId} 셋을 한 번에 맞춰 준다.
1201
+ */
1202
+ decisionResponse?: DecisionResponse;
1148
1203
  }
1149
1204
 
1150
1205
  export interface SendMessageResult {
@@ -1637,6 +1692,11 @@ export class ChatClient implements SdkEventEmitter<ChatClientEvents> {
1637
1692
  sendTextMessageOptimistic(roomId: string, message: string): OptimisticSendMessageHandle;
1638
1693
  sendReply(roomId: string, message: string, replyToMessageId: string): Promise<SendMessageResult>;
1639
1694
  sendReplyOptimistic(roomId: string, message: string, replyToMessageId: string): OptimisticSendMessageHandle;
1695
+ /**
1696
+ * 의사결정 카드의 선택 전송 — 답글 + 라벨 본문 + optionId 를 한 번에 맞춰 보낸다.
1697
+ * 직접 조립하지 말고 이 메서드를 쓰는 편이 안전하다(셋 중 하나만 빠져도 계약이 깨진다).
1698
+ */
1699
+ sendDecisionChoice(roomId: string, card: ChatMessage, option: DecisionOption): Promise<SendMessageResult>;
1640
1700
 
1641
1701
  uploadFile(roomId: string, file: File, options?: FileUploadOptions): Promise<SendFileInfo>;
1642
1702
  sendFileMessage(roomId: string, files: File | File[], options?: SendFileMessageOptions): Promise<SendMessageResult>;
@@ -2000,6 +2060,11 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
2000
2060
  sendTextMessageOptimistic(roomId: string, message: string): OptimisticSendMessageHandle;
2001
2061
  sendReply(roomId: string, message: string, replyToMessageId: string): Promise<SendMessageResult>;
2002
2062
  sendReplyOptimistic(roomId: string, message: string, replyToMessageId: string): OptimisticSendMessageHandle;
2063
+ /**
2064
+ * 의사결정 카드의 선택 전송 — 답글 + 라벨 본문 + optionId 를 한 번에 맞춰 보낸다.
2065
+ * 직접 조립하지 말고 이 메서드를 쓰는 편이 안전하다(셋 중 하나만 빠져도 계약이 깨진다).
2066
+ */
2067
+ sendDecisionChoice(roomId: string, card: ChatMessage, option: DecisionOption): Promise<SendMessageResult>;
2003
2068
 
2004
2069
  uploadFile(roomId: string, file: File, options?: FileUploadOptions): Promise<SendFileInfo>;
2005
2070
  sendFileMessage(roomId: string, files: File | File[], options?: SendFileMessageOptions): Promise<SendMessageResult>;