@vibexnpm/talkx 6.1.0 → 7.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibexnpm/talkx",
3
- "version": "6.1.0",
3
+ "version": "7.1.0",
4
4
  "type": "module",
5
5
  "description": "TalkFlow SDK - Chat & WebRTC unified client",
6
6
  "main": "dist/talkflow-sdk.umd.js",
@@ -287,7 +287,16 @@ class TalkFlowClient extends EventEmitter {
287
287
  /**
288
288
  * 내 정보 조회.
289
289
  *
290
- * @returns {Promise<Object>} `{userId, email, name, phone, profileUrl, companyRole, orgNodeId, orgName, ...}`
290
+ * `metadata` 를 볼 수 있는 **유일한** 곳이다. 고치는 문은 {@link #updateMyProfile} 하나뿐이고
291
+ * 그쪽은 **전체 교체**라, 부분 수정하려면 여기서 먼저 읽어 병합한 뒤 통째로 보내야 한다:
292
+ *
293
+ * ```js
294
+ * const me = await client.getMyProfile();
295
+ * await client.updateMyProfile({ metadata: { ...me.metadata, statusMessage: '회의 중' } });
296
+ * ```
297
+ *
298
+ * @returns {Promise<Object>} `{userId, email, name, phone, profileUrl, companyRole,
299
+ * orgNodeId, orgName, preferredLanguage, metadata, mustChangePassword, ...}`
291
300
  */
292
301
  async getMyProfile() {
293
302
  this._checkToken();
@@ -304,10 +313,24 @@ class TalkFlowClient extends EventEmitter {
304
313
  * <p>이메일·조직·역할은 바꿀 수 없다 — 이메일은 로그인 키, 조직은 파트너가 정하고,
305
314
  * 역할은 본인이 승급할 수 없어야 한다.</p>
306
315
  *
307
- * @param {Object} profile
308
- * @param {string} [profile.name]
309
- * @param {string} [profile.phone]
310
- * @param {string} [profile.profileUrl]
316
+ * <p><b>내 정보를 고치는 유일한 메서드다.</b> {@code updateMyInfo} 와
317
+ * {@code PUT /api/v1/users/update} 는 v7.0.0 에서 걷어냈다 — 같은 칸(이름·프로필 이미지)을
318
+ * 고치는 문이 둘이라 한 화면에서 순서 없이 두 번 부르면 나중 호출이 앞을 덮었고, 어느 쪽도
319
+ * 혼자서는 부족했다(그쪽엔 {@code phone} 이, 이쪽엔 나머지 셋이 없었다).</p>
320
+ *
321
+ * <p>서버는 갱신된 프로필이 담긴 <b>새 JWT</b> 를 함께 준다. 채택하지 않으면 옛 토큰의
322
+ * name/profileUrl 클레임이 최대 7일(토큰 TTL) 동안 계속 나간다. 같은 사용자의 토큰 교체라
323
+ * {@code setToken} 은 재연결을 일으키지 않는다 — STOMP connectHeaders 만 갱신된다.</p>
324
+ *
325
+ * @param {Object} profile — 넘기지 않은 필드는 «변경 안 함»
326
+ * @param {string} [profile.name] - 이름 (max 100자)
327
+ * @param {string} [profile.phone] - 전화번호 (max 30자)
328
+ * @param {string} [profile.profileUrl] - 프로필 이미지 URL (max 500자)
329
+ * @param {Object} [profile.metadata] - 메타데이터 (**전체 교체** — 부분 수정하려면
330
+ * {@link #getMyProfile} 로 현재 값을 읽어 병합한 뒤 통째로 보내라. 서버는 병합하지 않는다)
331
+ * @param {string|null} [profile.preferredLanguage] - 기본 선호 언어 (BCP-47 / 'OFF'(원문) / null)
332
+ * @param {string} [profile.timeZone] - IANA zone id. 어시가 대화 시각을 이 기준으로 렌더한다.
333
+ * `Intl.DateTimeFormat().resolvedOptions().timeZone` 값을 그대로 보내면 된다.
311
334
  * @returns {Promise<Object>} 갱신된 내 정보 (accessToken 포함)
312
335
  */
313
336
  async updateMyProfile(profile) {
@@ -342,48 +365,13 @@ class TalkFlowClient extends EventEmitter {
342
365
  return this.apiClient.put('/api/v1/me/password', { currentPassword, newPassword });
343
366
  }
344
367
 
345
- /**
346
- * 사용자 정보 수정.
347
- *
348
- * 서버는 갱신된 프로필이 담긴 **새 JWT** 를 응답에 함께 실어 준다(`/auth` 와 같은 계약).
349
- * 이걸 채택하지 않으면 옛 토큰의 name/profileUrl 클레임이 계속 나가, 서버가 그 클레임을 읽는
350
- * 경로에서 낡은 값이 최대 7일(토큰 TTL) 동안 보인다.
351
- *
352
- * 같은 사용자의 토큰 교체라 `setToken` 은 재연결을 일으키지 않는다 — userId 가 바뀔 때만
353
- * 재초기화하고, 여기서는 STOMP connectHeaders 만 갱신된다.
354
- *
355
- * @param {Object} userData
356
- * @param {string} [userData.nickname] - 닉네임 (max 100자)
357
- * @param {string} [userData.profileImageUrl] - 프로필 이미지 URL (max 500자)
358
- * @param {Object} [userData.metadata] - 메타데이터 (전체 교체)
359
- * @param {string|null} [userData.preferredLanguage] - 기본 선호 언어 (BCP-47 / 'OFF'(원문) / null). 다국어.
360
- * @returns {Promise<Object>}
361
- */
362
- async updateMyInfo(userData) {
363
- this._checkToken();
364
- const response = await this.apiClient.put('/api/v1/users/update', userData);
365
-
366
- if (response?.data?.accessToken) {
367
- // 토큰 교체 실패로 수정 자체를 실패시키지 않는다 — 서버 반영은 이미 끝났고,
368
- // 옛 토큰도 유효(낡았을 뿐)하므로 여기서 throw 하면 호출자가 이미 적용된 수정을
369
- // 다시 시도하게 된다. authenticate() 는 토큰이 없으면 아무것도 못 하므로 계약이 다르다.
370
- try {
371
- await this.setToken(response.data.accessToken);
372
- } catch (error) {
373
- this.logger.warn('Profile updated but token refresh failed; keeping the old token', error);
374
- }
375
- }
376
-
377
- return response;
378
- }
379
-
380
368
  /**
381
369
  * 내 기본 선호 언어 설정 (전 방 공통, 다국어). 명시 변경 — 방별 오버라이드는 {@link #setMyRoomLanguage}.
382
370
  * @param {string|null} language - BCP-47 코드(예 'ko','en') / 'OFF'(원문) / null·''(미설정)
383
371
  * @returns {Promise<Object>}
384
372
  */
385
373
  async setPreferredLanguage(language) {
386
- return this.updateMyInfo({ preferredLanguage: language });
374
+ return this.updateMyProfile({ preferredLanguage: language });
387
375
  }
388
376
 
389
377
  /**
@@ -396,8 +384,9 @@ class TalkFlowClient extends EventEmitter {
396
384
  * 발생한다. 서버 {@code LanguageCode} 도 동일 규칙으로 축약하므로 어느 쪽으로 들어와도 같은 값이 된다.</p>
397
385
  *
398
386
  * <p>예전에는 {@code registerUser} 가 이 값을 자동으로 실어 보냈는데 그 경로가 사라졌다.
399
- * 지금은 호출자가 직접 감지해 {@code PUT /api/v1/users/update} 의 {@code preferredLanguage}
400
- * 로 넣어야 한다 — 넣지 않으면 사용자 기본 언어가 미설정으로 남아 자동 번역이 서버 기본값을 쓴다.</p>
387
+ * 지금은 호출자가 직접 감지해 {@link #setPreferredLanguage} (또는 {@link #updateMyProfile} 의
388
+ * {@code preferredLanguage}) 로 넣어야 한다 — 넣지 않으면 사용자 기본 언어가 미설정으로 남아
389
+ * 자동 번역이 서버 기본값을 쓴다.</p>
401
390
  * @returns {string|null}
402
391
  */
403
392
  static detectBrowserLanguage() {
@@ -474,8 +463,13 @@ class TalkFlowClient extends EventEmitter {
474
463
  /**
475
464
  * 사용자 목록 조회
476
465
  *
477
- * <p>응답 행에 {@code phone}/{@code orgNodeId}/{@code orgName} 이 포함된다 — 부서명은
478
- * 저장값이 아니라 조회 시점에 조직 마스터에서 해소한 값이다.</p>
466
+ * <p>응답 행에 {@code email}/{@code phone}/{@code orgNodeId}/{@code orgName} 이 포함된다 —
467
+ * 부서명은 저장값이 아니라 조회 시점에 조직 마스터에서 해소한 값이다.</p>
468
+ *
469
+ * <p>{@code email} 은 <b>연락용</b>이다. 로그인 키인 {@code loginEmail} 은 어떤 목록에도
470
+ * 실리지 않는다 — 둘은 다른 값이고, 퇴사 시 {@code loginEmail} 만 비워진다.</p>
471
+ *
472
+ * <p>퇴사·차단된 회원은 목록·검색 어디에도 나오지 않는다.</p>
479
473
  *
480
474
  * @param {Object} [params]
481
475
  * @param {number} [params.size=50] - 페이지 크기 (1-100)
@@ -142,6 +142,7 @@ class ChatClient extends EventEmitter {
142
142
  THINKING: '생각 중…',
143
143
  SEARCHING: '검색 중…',
144
144
  PLANNING: '기획 중…',
145
+ DOCUMENT_ANALYSIS: '문서 분석 중…',
145
146
  WRITING: '작성 중…'
146
147
  };
147
148
  // PHASE 의 expiresAt 누락/이상 시 fallback TTL (ms) — 서버 ttl(최소 30s) 보다 약간 길게.
@@ -3078,12 +3079,19 @@ class ChatClient extends EventEmitter {
3078
3079
  return;
3079
3080
  }
3080
3081
  const phase = event.phase || null;
3082
+ const samePhase = Boolean(prevSlot && prevSlot.phase === phase);
3083
+ // 같은 phase의 일반 heartbeat에는 progress가 없다. 그 틱이 직전 실측 숫자를 지우지 않게
3084
+ // 보존하고, phase가 바뀔 때만 이전 작업의 숫자를 폐기한다.
3085
+ const progress = event.progress
3086
+ ? this._normalizeAssistantProgress(event.progress)
3087
+ : (samePhase && prevSlot ? prevSlot.progress : null);
3081
3088
  // 같은 stream 의 PHASE 재발행(B-3 synthesis heartbeat 등)이면 누적 상태를 보존한다.
3082
3089
  // ⚠️ 새 stream 의 커서는 <b>-1</b> 로 연다. DeltaCoalescer 의 첫 청크가 seq=0 이라 0 으로 열면
3083
3090
  // 첫 글자가 "중복"으로 버려진다 (설계 §5.5.0).
3084
3091
  const prev = prevSlot;
3085
3092
  const slot = this._setAssistantSlot(roomId, streamId, event, {
3086
3093
  phase,
3094
+ progress,
3087
3095
  phaseUpdatedAt: createdAt !== null ? createdAt : (prev ? prev.phaseUpdatedAt : 0),
3088
3096
  text: prev ? (prev.text || '') : '',
3089
3097
  lastSeq: prev ? this._seqOf(prev.lastSeq) : -1,
@@ -3132,7 +3140,9 @@ class ChatClient extends EventEmitter {
3132
3140
  lastSeq = seq;
3133
3141
  }
3134
3142
 
3135
- const slot = this._setAssistantSlot(roomId, streamId, event, { phase, text, lastSeq, textAvailable });
3143
+ const slot = this._setAssistantSlot(roomId, streamId, event, {
3144
+ phase, progress: prev ? (prev.progress || null) : null, text, lastSeq, textAvailable
3145
+ });
3136
3146
  this._emitAssistantStream(roomId, streamId, slot, {
3137
3147
  active: true,
3138
3148
  text: slot.textAvailable ? slot.text : null, // 누적 전체(버블 렌더용)
@@ -3153,6 +3163,7 @@ class ChatClient extends EventEmitter {
3153
3163
  active: false,
3154
3164
  personaId: event.personaId || (slot && slot.personaId) || null,
3155
3165
  phase: null,
3166
+ progress: null,
3156
3167
  label: null,
3157
3168
  status: event.status || null,
3158
3169
  messageId: event.messageId || null
@@ -3242,7 +3253,7 @@ class ChatClient extends EventEmitter {
3242
3253
  // 유예 구간에 걸린 종료분. 진행 표시를 켜지 않고 최종 메시지로 바로 확정시킨다.
3243
3254
  this._tombstoneAssistantStream(roomId, streamId);
3244
3255
  this._emitAssistantStream(roomId, streamId, null, {
3245
- active: false, phase: null, label: null, personaId: s.personaId || null,
3256
+ active: false, phase: null, progress: null, label: null, personaId: s.personaId || null,
3246
3257
  status: s.doneStatus || null, messageId: s.messageId || null, resumed: true
3247
3258
  });
3248
3259
  return;
@@ -3253,6 +3264,7 @@ class ChatClient extends EventEmitter {
3253
3264
  personaId: s.personaId, expiresAt: undefined
3254
3265
  }, {
3255
3266
  phase: s.phase || null,
3267
+ progress: this._normalizeAssistantProgress(s.progress),
3256
3268
  phaseUpdatedAt: typeof s.phaseUpdatedAt === 'number' ? s.phaseUpdatedAt : 0,
3257
3269
  text: textAvailable ? (s.text || '') : '',
3258
3270
  lastSeq: this._seqOf(s.lastSeq),
@@ -3452,6 +3464,7 @@ class ChatClient extends EventEmitter {
3452
3464
  const slot = {
3453
3465
  personaId: event.personaId || (prev && prev.personaId) || null,
3454
3466
  phase: fields.phase,
3467
+ progress: fields.progress || null,
3455
3468
  // 마지막으로 <b>적용된</b> PHASE 의 발행 시각 — pending PHASE 역행 판정 커서(§6.2).
3456
3469
  phaseUpdatedAt: fields.phaseUpdatedAt !== undefined
3457
3470
  ? fields.phaseUpdatedAt
@@ -3476,7 +3489,7 @@ class ChatClient extends EventEmitter {
3476
3489
  this._deleteAssistantSlot(roomId, streamId);
3477
3490
  this.logger.debug(`Assistant progress auto-cleared (timeout): room=${roomId}, stream=${streamId}`);
3478
3491
  this._emitAssistantStream(roomId, streamId, slot, {
3479
- active: false, phase: null, label: null, reason: 'timeout'
3492
+ active: false, phase: null, progress: null, label: null, reason: 'timeout'
3480
3493
  });
3481
3494
  }
3482
3495
 
@@ -3491,12 +3504,14 @@ class ChatClient extends EventEmitter {
3491
3504
  */
3492
3505
  _emitAssistantStream(roomId, streamId, slot, overrides) {
3493
3506
  const phase = 'phase' in overrides ? overrides.phase : (slot ? slot.phase : null);
3507
+ const progress = 'progress' in overrides ? overrides.progress : (slot ? slot.progress : null);
3494
3508
  this.emit('assistantProgress', {
3495
3509
  roomId,
3496
3510
  streamId,
3497
3511
  personaId: slot ? slot.personaId : null,
3498
3512
  phase,
3499
- label: phase ? (this._assistantPhaseLabels[phase] || null) : null,
3513
+ progress,
3514
+ label: this._assistantPhaseLabel(phase, progress),
3500
3515
  textAvailable: slot ? slot.textAvailable : true,
3501
3516
  lastSeq: slot ? slot.lastSeq : -1,
3502
3517
  ...overrides
@@ -3528,6 +3543,7 @@ class ChatClient extends EventEmitter {
3528
3543
  return {
3529
3544
  roomId, active: false, streamCount: 0,
3530
3545
  streamId: null, personaId: null, phase: null, label: null, text: null, textAvailable: true,
3546
+ progress: null,
3531
3547
  streams: []
3532
3548
  };
3533
3549
  }
@@ -3539,7 +3555,8 @@ class ChatClient extends EventEmitter {
3539
3555
  streamId,
3540
3556
  personaId: slot.personaId,
3541
3557
  phase: slot.phase,
3542
- label: slot.phase ? (this._assistantPhaseLabels[slot.phase] || null) : null,
3558
+ progress: slot.progress || null,
3559
+ label: this._assistantPhaseLabel(slot.phase, slot.progress),
3543
3560
  text: slot.textAvailable ? (slot.text || null) : null,
3544
3561
  textAvailable: slot.textAvailable,
3545
3562
  // stream 단위 UI(권장 (B))가 마운트 즉시 버블을 복원할 수 있도록 전부 싣는다.
@@ -3550,7 +3567,8 @@ class ChatClient extends EventEmitter {
3550
3567
  streamId: id,
3551
3568
  personaId: s.personaId,
3552
3569
  phase: s.phase,
3553
- label: s.phase ? (this._assistantPhaseLabels[s.phase] || null) : null,
3570
+ progress: s.progress || null,
3571
+ label: this._assistantPhaseLabel(s.phase, s.progress),
3554
3572
  text: s.textAvailable ? (s.text || null) : null,
3555
3573
  textAvailable: s.textAvailable,
3556
3574
  resumed: s.resumed === true
@@ -3558,6 +3576,32 @@ class ChatClient extends EventEmitter {
3558
3576
  };
3559
3577
  }
3560
3578
 
3579
+ /** @private */
3580
+ _normalizeAssistantProgress(progress) {
3581
+ if (!progress || !['ANALYZING', 'SUMMARIZING'].includes(progress.stage)) return null;
3582
+ if (progress.stage === 'SUMMARIZING') {
3583
+ return { stage: 'SUMMARIZING', current: null, total: null, unit: null };
3584
+ }
3585
+ const current = Number.isInteger(progress.current) ? progress.current : null;
3586
+ const total = Number.isInteger(progress.total) ? progress.total : null;
3587
+ const unit = ['DOCUMENT', 'PAGE', 'TEXT_CHUNK'].includes(progress.unit) ? progress.unit : null;
3588
+ if (current === null || total === null || !unit || current < 0 || total < 1 || current > total) {
3589
+ return null;
3590
+ }
3591
+ return { stage: 'ANALYZING', current, total, unit };
3592
+ }
3593
+
3594
+ /** @private */
3595
+ _assistantPhaseLabel(phase, progress) {
3596
+ if (!phase) return null;
3597
+ if (phase !== 'DOCUMENT_ANALYSIS') return this._assistantPhaseLabels[phase] || null;
3598
+ if (progress && progress.stage === 'SUMMARIZING') return '분석 결과 종합 중…';
3599
+ if (!progress || progress.stage !== 'ANALYZING') return this._assistantPhaseLabels[phase];
3600
+ const suffix = progress.unit === 'PAGE' ? '페이지'
3601
+ : progress.unit === 'TEXT_CHUNK' ? '구간' : '개';
3602
+ return `문서 분석 중… (${progress.current}/${progress.total}${suffix})`;
3603
+ }
3604
+
3561
3605
  /**
3562
3606
  * 방의 <b>현재</b> AI 진행 상태를 즉시 반환 (네트워크 호출 없음).
3563
3607
  *
package/types/index.d.ts CHANGED
@@ -1038,6 +1038,15 @@ export interface MyProfile {
1038
1038
  orgNodeId?: string | null;
1039
1039
  orgName?: string | null;
1040
1040
  preferredLanguage?: string | null;
1041
+ /**
1042
+ * 확장 메타데이터 — 내 값을 볼 수 있는 **유일한** 곳이다.
1043
+ *
1044
+ * 고치는 문은 `updateMyProfile({ metadata })` 하나뿐이고 **전체 교체**다 —
1045
+ * 서버는 병합하지 않는다. 부분 수정하려면 이 값을 먼저 읽어 병합한 뒤 통째로 보내라.
1046
+ */
1047
+ metadata?: Record<string, unknown> | null;
1048
+ /** IANA zone id — 어시가 대화 시각을 이 기준으로 렌더한다. 읽을 수 없으면 설정 화면이 현재 값을 못 보여준다. */
1049
+ timeZone?: string | null;
1041
1050
  projectId: string;
1042
1051
  joinedAt?: string | null;
1043
1052
  /**
@@ -1255,6 +1264,18 @@ export interface TypingEvent {
1255
1264
  senderType: 'USER' | 'ASSISTANT';
1256
1265
  }
1257
1266
 
1267
+ /** 문서 분석의 재입장 복원 가능한 실측 진행률. */
1268
+ export interface DocumentAnalysisProgress {
1269
+ /** ANALYZING=페이지/텍스트 구간 처리, SUMMARIZING=확보한 근거 종합. */
1270
+ stage: 'ANALYZING' | 'SUMMARIZING';
1271
+ /** ANALYZING일 때 처리 완료 수. */
1272
+ current: number | null;
1273
+ /** ANALYZING일 때 전체 수. */
1274
+ total: number | null;
1275
+ /** 진행 단위. SUMMARIZING일 때 null. */
1276
+ unit: 'DOCUMENT' | 'PAGE' | 'TEXT_CHUNK' | null;
1277
+ }
1278
+
1258
1279
  /**
1259
1280
  * AI 진행 표시 이벤트 — `/topic/chat/{roomId}/assistant-stream` 의 PHASE/DONE.
1260
1281
  *
@@ -1272,6 +1293,7 @@ export interface TypingEvent {
1272
1293
  *
1273
1294
  * 현재는 PM_BACKSTAGE(방당 단일 PM) 만 phase 를 발행 — 멘션/버튼/14인 경로는 본 이벤트 없이 typing 만.
1274
1295
  */
1296
+
1275
1297
  export interface AssistantProgressEvent {
1276
1298
  /** 채팅방 ID. */
1277
1299
  roomId: string;
@@ -1285,7 +1307,9 @@ export interface AssistantProgressEvent {
1285
1307
  /** 진행 주체 페르소나 ID. */
1286
1308
  personaId: string | null;
1287
1309
  /** 진행 단계 — `active:true` 일 때. 종료 시 null. */
1288
- phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'WRITING' | null;
1310
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'WRITING' | null;
1311
+ /** DOCUMENT_ANALYSIS의 실측 진행률. 같은 phase heartbeat 사이와 재입장 후에도 보존된다. */
1312
+ progress?: DocumentAnalysisProgress | null;
1289
1313
  /** phase 의 기본 표시 라벨(예: "생각 중…"). SDK 매핑 — 커스텀하려면 `phase` 사용. 종료 시 null. */
1290
1314
  label: string | null;
1291
1315
  /** true=진행중(PHASE/DELTA) / false=종료(DONE·TTL). */
@@ -1351,7 +1375,8 @@ export interface AssistantRoomProgressEvent {
1351
1375
  /** 표시 기준이 된 stream (가장 최근 갱신). 없으면 null. */
1352
1376
  streamId: string | null;
1353
1377
  personaId: string | null;
1354
- phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'WRITING' | null;
1378
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'WRITING' | null;
1379
+ progress?: DocumentAnalysisProgress | null;
1355
1380
  label: string | null;
1356
1381
  /** 표시 기준 stream 의 누적 본문. `textAvailable:false` 거나 진행 중이 아니면 null. */
1357
1382
  text?: string | null;
@@ -1363,7 +1388,8 @@ export interface AssistantRoomProgressEvent {
1363
1388
  export interface AssistantProgressStreamSnapshot {
1364
1389
  streamId: string;
1365
1390
  personaId: string | null;
1366
- phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'WRITING' | null;
1391
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'DOCUMENT_ANALYSIS' | 'WRITING' | null;
1392
+ progress?: DocumentAnalysisProgress | null;
1367
1393
  label: string | null;
1368
1394
  text: string | null;
1369
1395
  textAvailable: boolean;
@@ -2358,7 +2384,17 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
2358
2384
  getMyProfile(): Promise<MyProfile>;
2359
2385
 
2360
2386
  /** 내 정보 수정 — 응답의 새 토큰을 자동 채택한다(name/profileUrl 이 토큰 claim). */
2361
- updateMyProfile(profile: { name?: string; phone?: string; profileUrl?: string }): Promise<MyProfile>;
2387
+ updateMyProfile(profile: {
2388
+ name?: string;
2389
+ phone?: string;
2390
+ profileUrl?: string;
2391
+ /** **전체 교체** — 부분 수정은 `getMyProfile()` 로 읽어 병합한 뒤 통째로 보낸다. */
2392
+ metadata?: Record<string, unknown> | null;
2393
+ /** BCP-47 코드 / 'OFF'(원문) / null */
2394
+ preferredLanguage?: string | null;
2395
+ /** IANA zone id — `Intl.DateTimeFormat().resolvedOptions().timeZone` */
2396
+ timeZone?: string;
2397
+ }): Promise<MyProfile>;
2362
2398
 
2363
2399
  /** 비밀번호 변경 — 현재 비밀번호 재확인. 토큰은 그대로 유효해 재로그인이 필요 없다. */
2364
2400
  changeMyPassword(params: { currentPassword: string; newPassword: string }): Promise<unknown>;