@vibexnpm/talkx 5.6.0 → 7.0.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": "5.6.0",
3
+ "version": "7.0.0",
4
4
  "type": "module",
5
5
  "description": "TalkFlow SDK - Chat & WebRTC unified client",
6
6
  "main": "dist/talkflow-sdk.umd.js",
@@ -35,15 +35,11 @@ class TalkFlowClient extends EventEmitter {
35
35
  * @param {number} [options.logLevel=LogLevel.WARN] - 로그 레벨
36
36
  *
37
37
  * @example
38
- * // 1단계: 고객사 backend 가 talkflow JWT 를 발급받음 (Server API Key 사용)
39
- * // POST https://chat.apiorbit.net/api/v1/users/auth
40
- * // Headers: X-API-KEY: <SERVER_KEY>, X-PROJECT-ID: <project-id>
41
- * // Body: { userId: 'user-123', nickname: '홍길동' }
42
- * // Response: { accessToken, refreshToken }
38
+ * // 1단계: 로그인 — 클라이언트 생성 «전» 에 부른다. ck/projectId/JWT 를 한 번에 받는다.
39
+ * // const { clientKey, projectId, accessToken } = await TalkFlowClient.loginUser({
40
+ * // serverUrl, email, password });
43
41
  *
44
- * // 2단계: 고객사 backend 가 최종 사용자 브라우저에 JWT 전달 (자사 API 응답 등)
45
- *
46
- * // 3단계: 브라우저에서 SDK 초기화
42
+ * // 2단계: 받은 값으로 SDK 초기화
47
43
  * const client = new TalkFlowClient({
48
44
  * apiKey: 'CLIENT_KEY', // 브라우저에 노출되는 Client Key
49
45
  * projectId: 'your-project-id',
@@ -174,24 +170,13 @@ class TalkFlowClient extends EventEmitter {
174
170
  throw new Error('projectId is required');
175
171
  }
176
172
 
177
- // Server Key (sk-) 브라우저 사용 경고
178
- // - development: 경고만 (registerUser 빠른 테스트 허용)
179
- // - staging/production: 경고 + 강력 안내
173
+ // Server Key (sk-) 는 SDK 가 쓸 일이 없다 — 유일한 용도였던 registerUser(/users/auth) 가
174
+ // 사라졌다. 그래도 들어오면 잘못된 키를 넣은 것이므로 알린다.
180
175
  if (options.apiKey.startsWith('sk-')) {
181
- const env = options.env || 'production';
182
- if (env === 'development') {
183
- console.warn(
184
- '⚠️ [TalkFlow] Server Key (sk-) 를 개발 모드에서 사용 중입니다. ' +
185
- '프로덕션 배포 전에 반드시 Client Key (ck-) 로 교체하세요.'
186
- );
187
- } else {
188
- console.error(
189
- '🚨 [TalkFlow] Server Key (sk-) 가 비개발 환경 (' + env + ') 에서 감지되었습니다. ' +
190
- '보안 위험: Server Key 를 브라우저에 포함하면 공격자가 임의 사용자로 JWT 를 발급받을 수 있습니다. ' +
191
- '반드시 Client Key (ck-) 로 교체하세요. ' +
192
- '서버에서도 프로덕션 모드에서 Server Key 의 브라우저 호출을 차단합니다.'
193
- );
194
- }
176
+ console.error(
177
+ '🚨 [TalkFlow] Server Key (sk-) 로 SDK 를 초기화했습니다. ' +
178
+ 'SDK 는 Client Key (ck-) 만 사용합니다 — loginUser() 응답의 clientKey 를 그대로 쓰세요.'
179
+ );
195
180
  }
196
181
  }
197
182
 
@@ -240,7 +225,7 @@ class TalkFlowClient extends EventEmitter {
240
225
  /**
241
226
  * 회원 로그인 — 이메일/비밀번호로 채팅 접속 일체를 받는다. **클라이언트 생성 전에 호출한다.**
242
227
  *
243
- * <p>{@link #registerUser} 와 달리 Server Key 도 고객사 BFF 도 필요 없다. 응답의
228
+ * <p>Server Key 도 고객사 BFF 도 필요 없다. 응답의
244
229
  * {@code clientKey}/{@code projectId}/{@code accessToken} 이 그대로 SDK 초기화 입력이다.</p>
245
230
  *
246
231
  * <p><b>회사 선택 단계가 없다</b>: 로그인 키가 전역 유일이라 이메일 하나로 회원과 회사가
@@ -302,7 +287,16 @@ class TalkFlowClient extends EventEmitter {
302
287
  /**
303
288
  * 내 정보 조회.
304
289
  *
305
- * @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, ...}`
306
300
  */
307
301
  async getMyProfile() {
308
302
  this._checkToken();
@@ -319,10 +313,24 @@ class TalkFlowClient extends EventEmitter {
319
313
  * <p>이메일·조직·역할은 바꿀 수 없다 — 이메일은 로그인 키, 조직은 파트너가 정하고,
320
314
  * 역할은 본인이 승급할 수 없어야 한다.</p>
321
315
  *
322
- * @param {Object} profile
323
- * @param {string} [profile.name]
324
- * @param {string} [profile.phone]
325
- * @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` 값을 그대로 보내면 된다.
326
334
  * @returns {Promise<Object>} 갱신된 내 정보 (accessToken 포함)
327
335
  */
328
336
  async updateMyProfile(profile) {
@@ -357,106 +365,13 @@ class TalkFlowClient extends EventEmitter {
357
365
  return this.apiClient.put('/api/v1/me/password', { currentPassword, newPassword });
358
366
  }
359
367
 
360
- /**
361
- * 사용자 인증 (등록/로그인).
362
- * 인증 성공 시 반환된 JWT 토큰을 자동으로 설정합니다.
363
- *
364
- * <h3>⚠️ DEPRECATED — {@link TalkFlowClient.loginUser} 로 대체</h3>
365
- * <p>고객사 앱이 자기 회원 ID 를 들고 오던 경로다. 서버에서 {@code POST /api/v1/users/auth} 가
366
- * 제거될 예정이며 그때 이 메서드도 함께 사라진다. 신규 코드는 {@code loginUser} 를 쓴다.</p>
367
- *
368
- * <h3>⚠️ Server API Key 전용</h3>
369
- * <p>이 메서드는 JWT 를 발급하는 엔드포인트 ({@code POST /api/v1/users/auth}) 를 호출합니다.
370
- * <strong>프로덕션 환경에서는 Client API Key 로 이 메서드를 호출하면 서버가 403 을 반환합니다.</strong></p>
371
- *
372
- * <ul>
373
- * <li><strong>개발/프로토타입</strong>: Server Key 로 SDK 를 초기화한 뒤 이 메서드를 사용하면 빠르게 테스트 가능</li>
374
- * <li><strong>프로덕션</strong>: 고객사 backend 에서 Server Key 로 직접 {@code POST /api/v1/users/auth} 를 호출 →
375
- * JWT 를 받아 SDK 에 {@code setToken(jwt)} 으로 전달. 이 메서드를 브라우저에서 쓰지 마세요.</li>
376
- * </ul>
377
- *
378
- * <p>자세한 내용은 README 의 "프로덕션 인증 플로우" 섹션 참고.</p>
379
- *
380
- * @param {Object} userData
381
- * @param {string} userData.userId - 사용자 ID (필수, 고객사 회원 ID)
382
- * @param {string} userData.nickname - 닉네임 (필수, max 100자)
383
- * @param {string} [userData.profileImageUrl] - 프로필 이미지 URL (max 500자)
384
- * @param {Object} [userData.metadata] - 추가 메타데이터
385
- * @returns {Promise<Object>} 인증 결과 (토큰 포함)
386
- *
387
- * @example
388
- * // 개발 환경 — Server Key 로 빠른 테스트
389
- * const client = new TalkFlowClient({
390
- * apiKey: 'SERVER_KEY',
391
- * projectId: 'your-project-id',
392
- * env: 'development'
393
- * });
394
- * await client.registerUser({ userId: 'user-123', nickname: '홍길동' });
395
- * await client.connect();
396
- */
397
- async registerUser(userData) {
398
- // 다국어: navigator.language 자동 시드 — 호출자가 preferredLanguage 를 명시하지 않은 경우에만.
399
- // 서버 /users/auth 는 seed-if-absent (기존 값 비클로버) 라 매 인증마다 덮어쓰지 않아 안전.
400
- const payload = { ...userData };
401
- if (payload.preferredLanguage === undefined) {
402
- const detected = TalkFlowClient.detectBrowserLanguage();
403
- if (detected) {
404
- payload.preferredLanguage = detected;
405
- }
406
- }
407
-
408
- const response = await this.apiClient.post('/api/v1/users/auth', payload);
409
-
410
- if (response.data && response.data.accessToken) {
411
- await this.setToken(response.data.accessToken);
412
- this.logger.info('User authenticated, token auto-set');
413
- }
414
-
415
- return response;
416
- }
417
-
418
- /**
419
- * 사용자 정보 수정.
420
- *
421
- * 서버는 갱신된 프로필이 담긴 **새 JWT** 를 응답에 함께 실어 준다(`/auth` 와 같은 계약).
422
- * 이걸 채택하지 않으면 옛 토큰의 name/profileUrl 클레임이 계속 나가, 서버가 그 클레임을 읽는
423
- * 경로에서 낡은 값이 최대 7일(토큰 TTL) 동안 보인다.
424
- *
425
- * 같은 사용자의 토큰 교체라 `setToken` 은 재연결을 일으키지 않는다 — userId 가 바뀔 때만
426
- * 재초기화하고, 여기서는 STOMP connectHeaders 만 갱신된다.
427
- *
428
- * @param {Object} userData
429
- * @param {string} [userData.nickname] - 닉네임 (max 100자)
430
- * @param {string} [userData.profileImageUrl] - 프로필 이미지 URL (max 500자)
431
- * @param {Object} [userData.metadata] - 메타데이터 (전체 교체)
432
- * @param {string|null} [userData.preferredLanguage] - 기본 선호 언어 (BCP-47 / 'OFF'(원문) / null). 다국어.
433
- * @returns {Promise<Object>}
434
- */
435
- async updateMyInfo(userData) {
436
- this._checkToken();
437
- const response = await this.apiClient.put('/api/v1/users/update', userData);
438
-
439
- if (response?.data?.accessToken) {
440
- // 토큰 교체 실패로 수정 자체를 실패시키지 않는다 — 서버 반영은 이미 끝났고,
441
- // 옛 토큰도 유효(낡았을 뿐)하므로 여기서 throw 하면 호출자가 이미 적용된 수정을
442
- // 다시 시도하게 된다. authenticate() 는 토큰이 없으면 아무것도 못 하므로 계약이 다르다.
443
- try {
444
- await this.setToken(response.data.accessToken);
445
- } catch (error) {
446
- this.logger.warn('Profile updated but token refresh failed; keeping the old token', error);
447
- }
448
- }
449
-
450
- return response;
451
- }
452
-
453
368
  /**
454
369
  * 내 기본 선호 언어 설정 (전 방 공통, 다국어). 명시 변경 — 방별 오버라이드는 {@link #setMyRoomLanguage}.
455
370
  * @param {string|null} language - BCP-47 코드(예 'ko','en') / 'OFF'(원문) / null·''(미설정)
456
371
  * @returns {Promise<Object>}
457
372
  */
458
373
  async setPreferredLanguage(language) {
459
- return this.updateMyInfo({ preferredLanguage: language });
374
+ return this.updateMyProfile({ preferredLanguage: language });
460
375
  }
461
376
 
462
377
  /**
@@ -468,7 +383,10 @@ class TalkFlowClient extends EventEmitter {
468
383
  * 지역 변형을 그대로 보내면 명시 요청({@code lang: 'en'})과 키가 분열되어 같은 언어 이중 생성·조회 miss 가
469
384
  * 발생한다. 서버 {@code LanguageCode} 도 동일 규칙으로 축약하므로 어느 쪽으로 들어와도 같은 값이 된다.</p>
470
385
  *
471
- * <p>{@link #registerUser} 자동 시드에 사용되며, 고객사 BFF 의 {@code /users/auth} 시드에도 쓸 수 있다.</p>
386
+ * <p>예전에는 {@code registerUser} 가 이 값을 자동으로 실어 보냈는데 그 경로가 사라졌다.
387
+ * 지금은 호출자가 직접 감지해 {@link #setPreferredLanguage} (또는 {@link #updateMyProfile} 의
388
+ * {@code preferredLanguage}) 로 넣어야 한다 — 넣지 않으면 사용자 기본 언어가 미설정으로 남아
389
+ * 자동 번역이 서버 기본값을 쓴다.</p>
472
390
  * @returns {string|null}
473
391
  */
474
392
  static detectBrowserLanguage() {
@@ -522,16 +440,37 @@ class TalkFlowClient extends EventEmitter {
522
440
  }
523
441
 
524
442
  /**
525
- * 사용자 존재 여부 확인
526
- * @param {string} projectUserId - 프로젝트 사용자 ID
527
- * @returns {Promise<Object>}
443
+ * 회사 조직도 + 인원 — <b>방 만들 때 «조직으로 초대»</b> 가 쓴다.
444
+ *
445
+ * <p>조직을 고르면 <b>하위 조직 인원까지</b> 초대된다. 그래서 각 노드의 {@code memberCount} 는
446
+ * 자손을 포함한 누적이고, {@code members} 는 그 노드 <b>직속</b>만이다 — 누적 목록이 필요하면
447
+ * {@code children} 을 따라 내려가며 모은다(상위마다 중복해 실으면 응답이 제곱으로 커진다).</p>
448
+ *
449
+ * <p>부서가 없는 회원은 {@code unassignedMembers} 에 있다. 퇴사·차단 회원은 어디에도 없다.</p>
450
+ *
451
+ * @returns {Promise<Object>} `{tree, unassignedMembers, totalMemberCount, membersOmitted}`
452
+ *
453
+ * @example
454
+ * const dir = await client.getCompanyDirectory();
455
+ * // 조직 하나를 고르면 그 자손까지 전부 모은다
456
+ * const collect = (node) => [...node.members, ...node.children.flatMap(collect)];
528
457
  */
529
- async checkUserExists(projectUserId) {
530
- return this.apiClient.get(`/api/v1/users/${projectUserId}/exists`);
458
+ async getCompanyDirectory() {
459
+ this._checkToken();
460
+ return this.apiClient.get('/api/v1/company/directory');
531
461
  }
532
462
 
533
463
  /**
534
464
  * 사용자 목록 조회
465
+ *
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>
473
+ *
535
474
  * @param {Object} [params]
536
475
  * @param {number} [params.size=50] - 페이지 크기 (1-100)
537
476
  * @param {string} [params.lastId] - 마지막 사용자 ID
@@ -565,9 +504,9 @@ class TalkFlowClient extends EventEmitter {
565
504
  _checkToken() {
566
505
  if (!this.options.jwtToken) {
567
506
  throw new Error(
568
- 'JWT token is required for this operation. Obtain it from your backend ' +
569
- '(POST /api/v1/users/auth with Server API Key) and pass it via setToken(). ' +
570
- 'See README "프로덕션 인증 플로우".'
507
+ 'JWT token is required for this operation. Sign in with ' +
508
+ 'TalkFlowClient.loginUser({ serverUrl, email, password }) and pass the ' +
509
+ 'returned accessToken via setToken(). See README "로그인".'
571
510
  );
572
511
  }
573
512
  }
@@ -376,7 +376,7 @@ class ChatClient extends EventEmitter {
376
376
 
377
377
  /**
378
378
  * 1:1 채팅방 생성/조회
379
- * @param {string} friendId - 상대방 사용자 ID (projectUserId)
379
+ * @param {string} friendId - 상대방 사용자 ID (서버 내부 {@code users._id} — 참가자 목록의 userId)
380
380
  * @returns {Promise<Object>}
381
381
  */
382
382
  async createOneToOneRoom(friendId) {
package/src/index.js CHANGED
@@ -10,35 +10,27 @@
10
10
  * 자세한 내용은 README 의 "프로덕션 인증 플로우" 섹션 참고.
11
11
  *
12
12
  * @example
13
- * // === 빠른 시작 (개발/프로토타입) — Server Key 사용 ===
13
+ * // === 시작하기 — 이메일/비밀번호 로그인 ===
14
14
  * import TalkFlowClient from '@talkflow/sdk';
15
15
  *
16
- * const client = new TalkFlowClient({
17
- * apiKey: 'SERVER_KEY', // 개발용 Server Key (프로덕션에서는 쓰지 마세요)
18
- * projectId: 'your-project-id',
19
- * env: 'development'
16
+ * // 1단계: 로그인. 클라이언트 생성 «전» 에 부른다 — ck/projectId/JWT 를 한 번에 받는다.
17
+ * const { clientKey, projectId, accessToken } = await TalkFlowClient.loginUser({
18
+ * serverUrl: 'https://chat.apiorbit.net',
19
+ * email: 'me@example.com',
20
+ * password: '••••••••'
20
21
  * });
21
22
  *
22
- * await client.registerUser({ userId: 'user-123', nickname: '홍길동' });
23
- * await client.connect();
24
- * await client.chat.sendTextMessage('room-id', '안녕하세요!');
25
- *
26
- * @example
27
- * // === 프로덕션 — Client Key + Backend JWT 발급 ===
28
- * // 1단계: 고객사 backend 에서 Server Key 로 JWT 발급
29
- * // POST /api/v1/users/auth (X-API-KEY: SERVER_KEY)
30
- * // → { accessToken }
31
- *
32
- * // 2단계: 브라우저 SDK 는 Client Key + JWT 로 초기화
23
+ * // 2단계: 받은 값 그대로 초기화
33
24
  * const client = new TalkFlowClient({
34
- * apiKey: 'CLIENT_KEY', // 브라우저 노출 안전 (registerUser 서버 차단)
35
- * projectId: 'your-project-id',
36
- * jwtToken: receivedJwt, // 백엔드로부터 받은 JWT
25
+ * apiKey: clientKey, // ck- (브라우저 노출 안전)
26
+ * projectId,
27
+ * jwtToken: accessToken,
37
28
  * env: 'production'
38
29
  * });
39
30
  *
40
31
  * await client.connect();
41
32
  * await client.chat.sendTextMessage('room-id', '안녕하세요!');
33
+ * await client.chat.sendTextMessage('room-id', '안녕하세요!');
42
34
  */
43
35
 
44
36
  // 메인 클라이언트
@@ -62,9 +62,9 @@ export function applySessionMethods(TalkFlowClient) {
62
62
 
63
63
  if (!this.options.jwtToken) {
64
64
  throw new Error(
65
- 'JWT token is required. Obtain it from your backend (which calls ' +
66
- 'POST /api/v1/users/auth with a Server API Key) and pass it to the SDK ' +
67
- 'via the jwtToken option, setToken(), or connect(jwt). See README "프로덕션 인증 플로우".'
65
+ 'JWT token is required. Sign in with TalkFlowClient.loginUser({ serverUrl, ' +
66
+ 'email, password }) and pass the returned accessToken to the SDK via the ' +
67
+ 'jwtToken option, setToken(), or connect(jwt). See README "로그인".'
68
68
  );
69
69
  }
70
70
 
package/types/index.d.ts CHANGED
@@ -725,8 +725,6 @@ export interface ChatMessage {
725
725
  nickName?: string;
726
726
  /** 발신자 프로필 이미지 URL. */
727
727
  profileUrl?: string | null;
728
- /** 발신자의 프로젝트 스코프 사용자 ID. */
729
- projectUserId?: string;
730
728
  content: string | null;
731
729
  chatMessageType: ChatMessageTypeValue;
732
730
  chatType: string;
@@ -968,7 +966,16 @@ export interface LoginMember {
968
966
  email?: string | null;
969
967
  nickname?: string | null;
970
968
  profileUrl?: string | null;
969
+ /** 회사 역할 — 클라이언트가 파트너 화면 노출을 결정한다. 인가 근거는 아니다(서버가 재확인). */
970
+ companyRole: 'PARTNER' | 'MEMBER';
971
971
  preferredLanguage?: string | null;
972
+ /**
973
+ * 초기 비밀번호를 아직 안 바꿨는가 — `true` 면 **비밀번호 변경 화면으로 보내라**.
974
+ *
975
+ * 회원은 파트너가 정한 임시 비밀번호로 처음 들어오고, 파트너가 초기화하면 다시 이 상태가 된다.
976
+ * 로그인은 **성공** 한 것이다 — 실패로 다루면 사용자가 「비밀번호가 틀렸다」로 오해한다.
977
+ */
978
+ mustChangePassword: boolean;
972
979
  }
973
980
 
974
981
  /**
@@ -986,6 +993,39 @@ export interface LoginResult {
986
993
  user: LoginMember;
987
994
  }
988
995
 
996
+ /** 회사 조직도의 회원 — 초대 UI 에 필요한 것만. */
997
+ export interface DirectoryMember {
998
+ userId: string;
999
+ name?: string | null;
1000
+ profileUrl?: string | null;
1001
+ }
1002
+
1003
+ /** 조직도 노드. `parentId` 가 null 이면 최상위. */
1004
+ export interface DirectoryNode {
1005
+ orgNodeId: string;
1006
+ name: string;
1007
+ parentId?: string | null;
1008
+ sortOrder: number;
1009
+ /** 자손 포함 누적 인원 — 화면의 「개발팀 (2)」가 이 값. */
1010
+ memberCount: number;
1011
+ /** 이 노드 직속 인원 수. */
1012
+ directMemberCount: number;
1013
+ /** 이 노드 **직속** 회원. 누적 목록은 `children` 을 따라 모은다. */
1014
+ members: DirectoryMember[];
1015
+ children: DirectoryNode[];
1016
+ }
1017
+
1018
+ /** `getCompanyDirectory()` 응답. */
1019
+ export interface CompanyDirectory {
1020
+ tree: DirectoryNode[];
1021
+ /** 부서가 없는 회원 — 빠지면 초대 화면에서 그 사람을 찾을 수 없다. */
1022
+ unassignedMembers: DirectoryMember[];
1023
+ /** 활성 회원 총수(트리 + 미지정). */
1024
+ totalMemberCount: number;
1025
+ /** true 면 인원 **수** 는 정확하지만 **목록** 이 비어 있다 — 검색 기반 선택으로 넘어가야 한다. */
1026
+ membersOmitted: boolean;
1027
+ }
1028
+
989
1029
  /** 내 정보. 수정 응답에는 `accessToken` 이 함께 온다(name/profileUrl 이 토큰 claim). */
990
1030
  export interface MyProfile {
991
1031
  userId: string;
@@ -998,8 +1038,24 @@ export interface MyProfile {
998
1038
  orgNodeId?: string | null;
999
1039
  orgName?: string | null;
1000
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;
1001
1050
  projectId: string;
1002
1051
  joinedAt?: string | null;
1052
+ /**
1053
+ * 초기 비밀번호를 아직 안 바꿨는가 — `true` 면 비밀번호 변경을 요구해야 한다.
1054
+ *
1055
+ * 로그인 응답에도 같은 값이 있다. 여기에도 있는 이유: 새로고침·딥링크로 로그인 응답을 거치지
1056
+ * 않고 들어오는 화면에서 그 요구가 조용히 사라지지 않게 하려는 것이다.
1057
+ */
1058
+ mustChangePassword: boolean;
1003
1059
  accessToken?: string;
1004
1060
  expiresIn?: number;
1005
1061
  }
@@ -1009,7 +1065,6 @@ export interface ParticipantResponse {
1009
1065
  userId: string;
1010
1066
  nickName?: string;
1011
1067
  profileUrl?: string | null;
1012
- projectUserId?: string;
1013
1068
  /**
1014
1069
  * 참가자 역할 — 서버 {@code ParticipantRole} enum 과 일치.
1015
1070
  * <ul>
@@ -2312,7 +2367,17 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
2312
2367
  getMyProfile(): Promise<MyProfile>;
2313
2368
 
2314
2369
  /** 내 정보 수정 — 응답의 새 토큰을 자동 채택한다(name/profileUrl 이 토큰 claim). */
2315
- updateMyProfile(profile: { name?: string; phone?: string; profileUrl?: string }): Promise<MyProfile>;
2370
+ updateMyProfile(profile: {
2371
+ name?: string;
2372
+ phone?: string;
2373
+ profileUrl?: string;
2374
+ /** **전체 교체** — 부분 수정은 `getMyProfile()` 로 읽어 병합한 뒤 통째로 보낸다. */
2375
+ metadata?: Record<string, unknown> | null;
2376
+ /** BCP-47 코드 / 'OFF'(원문) / null */
2377
+ preferredLanguage?: string | null;
2378
+ /** IANA zone id — `Intl.DateTimeFormat().resolvedOptions().timeZone` */
2379
+ timeZone?: string;
2380
+ }): Promise<MyProfile>;
2316
2381
 
2317
2382
  /** 비밀번호 변경 — 현재 비밀번호 재확인. 토큰은 그대로 유효해 재로그인이 필요 없다. */
2318
2383
  changeMyPassword(params: { currentPassword: string; newPassword: string }): Promise<unknown>;