@things-factory/ai-assistant 10.1.6

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.
Files changed (164) hide show
  1. package/client/components/board-ai-chat.test.ts +149 -0
  2. package/client/components/board-ai-chat.ts +3228 -0
  3. package/client/components/chat-defaults.ts +27 -0
  4. package/client/components/chat-echo-dedup.test.ts +230 -0
  5. package/client/components/chat-echo-dedup.ts +155 -0
  6. package/client/components/chat-input-builder.ts +69 -0
  7. package/client/components/markdown.test.ts +518 -0
  8. package/client/components/markdown.ts +508 -0
  9. package/client/components/mention-popup-helpers.ts +433 -0
  10. package/client/components/mention-popup.test.ts +588 -0
  11. package/client/components/mention-popup.ts +238 -0
  12. package/client/index.ts +14 -0
  13. package/client/tsconfig.json +13 -0
  14. package/client/utils/board-edit-patch.ts +374 -0
  15. package/dist-client/components/board-ai-chat.d.ts +423 -0
  16. package/dist-client/components/board-ai-chat.js +3034 -0
  17. package/dist-client/components/board-ai-chat.js.map +1 -0
  18. package/dist-client/components/board-ai-chat.test.d.ts +1 -0
  19. package/dist-client/components/board-ai-chat.test.js +138 -0
  20. package/dist-client/components/board-ai-chat.test.js.map +1 -0
  21. package/dist-client/components/chat-defaults.d.ts +8 -0
  22. package/dist-client/components/chat-defaults.js +22 -0
  23. package/dist-client/components/chat-defaults.js.map +1 -0
  24. package/dist-client/components/chat-echo-dedup.d.ts +54 -0
  25. package/dist-client/components/chat-echo-dedup.js +115 -0
  26. package/dist-client/components/chat-echo-dedup.js.map +1 -0
  27. package/dist-client/components/chat-echo-dedup.test.d.ts +1 -0
  28. package/dist-client/components/chat-echo-dedup.test.js +203 -0
  29. package/dist-client/components/chat-echo-dedup.test.js.map +1 -0
  30. package/dist-client/components/chat-input-builder.d.ts +55 -0
  31. package/dist-client/components/chat-input-builder.js +32 -0
  32. package/dist-client/components/chat-input-builder.js.map +1 -0
  33. package/dist-client/components/markdown.d.ts +60 -0
  34. package/dist-client/components/markdown.js +422 -0
  35. package/dist-client/components/markdown.js.map +1 -0
  36. package/dist-client/components/markdown.test.d.ts +1 -0
  37. package/dist-client/components/markdown.test.js +418 -0
  38. package/dist-client/components/markdown.test.js.map +1 -0
  39. package/dist-client/components/mention-popup-helpers.d.ts +165 -0
  40. package/dist-client/components/mention-popup-helpers.js +330 -0
  41. package/dist-client/components/mention-popup-helpers.js.map +1 -0
  42. package/dist-client/components/mention-popup.d.ts +35 -0
  43. package/dist-client/components/mention-popup.js +223 -0
  44. package/dist-client/components/mention-popup.js.map +1 -0
  45. package/dist-client/components/mention-popup.test.d.ts +1 -0
  46. package/dist-client/components/mention-popup.test.js +474 -0
  47. package/dist-client/components/mention-popup.test.js.map +1 -0
  48. package/dist-client/index.d.ts +14 -0
  49. package/dist-client/index.js +15 -0
  50. package/dist-client/index.js.map +1 -0
  51. package/dist-client/tsconfig.tsbuildinfo +1 -0
  52. package/dist-client/utils/board-edit-patch.d.ts +157 -0
  53. package/dist-client/utils/board-edit-patch.js +284 -0
  54. package/dist-client/utils/board-edit-patch.js.map +1 -0
  55. package/dist-server/index.d.ts +28 -0
  56. package/dist-server/index.js +32 -0
  57. package/dist-server/index.js.map +1 -0
  58. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
  59. package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
  60. package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
  61. package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
  62. package/dist-server/service/chat-message/chat-message-publish.js +27 -0
  63. package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
  64. package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
  65. package/dist-server/service/chat-message/chat-message-subscription.js +57 -0
  66. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
  67. package/dist-server/service/chat-message/chat-message.d.ts +68 -0
  68. package/dist-server/service/chat-message/chat-message.js +172 -0
  69. package/dist-server/service/chat-message/chat-message.js.map +1 -0
  70. package/dist-server/service/chat-message/fold-history.d.ts +43 -0
  71. package/dist-server/service/chat-message/fold-history.js +33 -0
  72. package/dist-server/service/chat-message/fold-history.js.map +1 -0
  73. package/dist-server/service/chat-message/history-summary.d.ts +43 -0
  74. package/dist-server/service/chat-message/history-summary.js +77 -0
  75. package/dist-server/service/chat-message/history-summary.js.map +1 -0
  76. package/dist-server/service/chat-message/index.d.ts +5 -0
  77. package/dist-server/service/chat-message/index.js +12 -0
  78. package/dist-server/service/chat-message/index.js.map +1 -0
  79. package/dist-server/service/chat-message/llm-history.d.ts +46 -0
  80. package/dist-server/service/chat-message/llm-history.js +87 -0
  81. package/dist-server/service/chat-message/llm-history.js.map +1 -0
  82. package/dist-server/service/chat-session/chat-session.d.ts +60 -0
  83. package/dist-server/service/chat-session/chat-session.js +159 -0
  84. package/dist-server/service/chat-session/chat-session.js.map +1 -0
  85. package/dist-server/service/chat-session/index.d.ts +3 -0
  86. package/dist-server/service/chat-session/index.js +7 -0
  87. package/dist-server/service/chat-session/index.js.map +1 -0
  88. package/dist-server/service/chat-session/session-activity-broadcast.d.ts +30 -0
  89. package/dist-server/service/chat-session/session-activity-broadcast.js +34 -0
  90. package/dist-server/service/chat-session/session-activity-broadcast.js.map +1 -0
  91. package/dist-server/service/chat-session/session-activity-publish.d.ts +12 -0
  92. package/dist-server/service/chat-session/session-activity-publish.js +37 -0
  93. package/dist-server/service/chat-session/session-activity-publish.js.map +1 -0
  94. package/dist-server/service/chat-session/session-activity-subscription.d.ts +14 -0
  95. package/dist-server/service/chat-session/session-activity-subscription.js +87 -0
  96. package/dist-server/service/chat-session/session-activity-subscription.js.map +1 -0
  97. package/dist-server/service/chat-session/session-anchor.d.ts +26 -0
  98. package/dist-server/service/chat-session/session-anchor.js +100 -0
  99. package/dist-server/service/chat-session/session-anchor.js.map +1 -0
  100. package/dist-server/service/chat-session/session-inbox.d.ts +80 -0
  101. package/dist-server/service/chat-session/session-inbox.js +133 -0
  102. package/dist-server/service/chat-session/session-inbox.js.map +1 -0
  103. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +49 -0
  104. package/dist-server/service/chat-session-participant/chat-session-participant.js +141 -0
  105. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
  106. package/dist-server/service/chat-session-participant/index.d.ts +4 -0
  107. package/dist-server/service/chat-session-participant/index.js +10 -0
  108. package/dist-server/service/chat-session-participant/index.js.map +1 -0
  109. package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
  110. package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
  111. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
  112. package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
  113. package/dist-server/service/chat-session-participant/presence.js +22 -0
  114. package/dist-server/service/chat-session-participant/presence.js.map +1 -0
  115. package/dist-server/service/chat-session-resolver.d.ts +93 -0
  116. package/dist-server/service/chat-session-resolver.js +706 -0
  117. package/dist-server/service/chat-session-resolver.js.map +1 -0
  118. package/dist-server/service/index.d.ts +24 -0
  119. package/dist-server/service/index.js +39 -0
  120. package/dist-server/service/index.js.map +1 -0
  121. package/dist-server/service/subscribe-auth.d.ts +14 -0
  122. package/dist-server/service/subscribe-auth.js +33 -0
  123. package/dist-server/service/subscribe-auth.js.map +1 -0
  124. package/dist-server/service/types.d.ts +5 -0
  125. package/dist-server/service/types.js +12 -0
  126. package/dist-server/service/types.js.map +1 -0
  127. package/dist-server/tsconfig.tsbuildinfo +1 -0
  128. package/package.json +48 -0
  129. package/server/index.ts +28 -0
  130. package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
  131. package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
  132. package/server/service/chat-message/chat-message-broadcast.ts +27 -0
  133. package/server/service/chat-message/chat-message-publish.ts +22 -0
  134. package/server/service/chat-message/chat-message-subscription.ts +45 -0
  135. package/server/service/chat-message/chat-message.ts +195 -0
  136. package/server/service/chat-message/fold-history.test.ts +98 -0
  137. package/server/service/chat-message/fold-history.ts +77 -0
  138. package/server/service/chat-message/history-summary.test.ts +127 -0
  139. package/server/service/chat-message/history-summary.ts +100 -0
  140. package/server/service/chat-message/index.ts +12 -0
  141. package/server/service/chat-message/llm-history.test.ts +194 -0
  142. package/server/service/chat-message/llm-history.ts +163 -0
  143. package/server/service/chat-session/chat-session.ts +193 -0
  144. package/server/service/chat-session/index.ts +5 -0
  145. package/server/service/chat-session/session-activity-broadcast.ts +57 -0
  146. package/server/service/chat-session/session-activity-publish.ts +44 -0
  147. package/server/service/chat-session/session-activity-subscription.ts +67 -0
  148. package/server/service/chat-session/session-anchor.ts +106 -0
  149. package/server/service/chat-session/session-inbox.test.ts +185 -0
  150. package/server/service/chat-session/session-inbox.ts +161 -0
  151. package/server/service/chat-session-participant/chat-session-participant.ts +135 -0
  152. package/server/service/chat-session-participant/index.ts +6 -0
  153. package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
  154. package/server/service/chat-session-participant/invite-notification.ts +48 -0
  155. package/server/service/chat-session-participant/presence.test.ts +39 -0
  156. package/server/service/chat-session-participant/presence.ts +21 -0
  157. package/server/service/chat-session-resolver.ts +644 -0
  158. package/server/service/index.ts +37 -0
  159. package/server/service/session-anchor.test.ts +133 -0
  160. package/server/service/subscribe-auth.test.ts +57 -0
  161. package/server/service/subscribe-auth.ts +34 -0
  162. package/server/service/types.ts +15 -0
  163. package/things-factory.config.js +1 -0
  164. package/tsconfig.json +9 -0
@@ -0,0 +1,60 @@
1
+ import { Domain } from '@things-factory/shell';
2
+ import { User } from '@things-factory/auth-base';
3
+ import { ChatMessage } from '../chat-message/chat-message.js';
4
+ export declare class ChatSession {
5
+ readonly id?: string;
6
+ domain?: Domain;
7
+ domainId?: string;
8
+ /** Board.id 와 연결. Board 가 owning side 가 될 예정. 1:N — 한 보드에 여러 세션 가능.
9
+ *
10
+ * **앵커 일반화 이후에도 유지되는 하위호환 컬럼**이다(제거하지 말 것). 앵커가 board 인 세션은
11
+ * anchorId 와 함께 이 컬럼에도 같은 값을 쓴다(dual-write) — 기존 조회/인덱스/소비자가 그대로 동작한다. */
12
+ boardId?: string;
13
+ /**
14
+ * 대화가 매달린 대상의 **종류**. 세션은 보드에만 매달리지 않는다 —
15
+ * 저작 대화는 보드에, 운영 대화는 공간(사이트)에 매달린다. 앵커가 대화의 성격과 이력 경계를 정한다.
16
+ *
17
+ * 'board' | 'space' | 'instance' | 'none'. 미설정(기존 데이터)은 boardId 가 있으면 board 로 읽는다 —
18
+ * 백필 마이그레이션 없이 lazy 하위호환(컬럼 추가만).
19
+ */
20
+ anchorType?: string;
21
+ /** 앵커 대상의 id(보드 id · 공간 id · 인스턴스 id). anchorType 과 함께 의미를 갖는다. */
22
+ anchorId?: string;
23
+ /**
24
+ * 세션의 사용자 정의 이름 (탭 레이블 / 세션 식별 표시용).
25
+ *
26
+ * nullable — 미설정 시 UI 가 fallback 라벨 ("세션 N" 또는 createdAt 기반) 사용. 사용자가
27
+ * 다중 세션 운영 시 의미 부여 (예: "초기 설계", "운영 시뮬레이션", "사용자A 의 사적 세션").
28
+ */
29
+ name?: string;
30
+ creator?: User;
31
+ creatorId?: string;
32
+ updater?: User;
33
+ updaterId?: string;
34
+ /** 토큰 절감용 — 오래된 메시지를 LLM 으로 압축한 요약. Phase 2 에서 작성. */
35
+ /**
36
+ * 마지막 메시지 시각 — **비정규화**. 목록 정렬(최근 활동순)과 안 읽음 판정의 기준.
37
+ *
38
+ * 왜 비정규화인가: 세션마다 메시지를 집계하면(MAX/COUNT + group by) 목록 한 번에 집계 쿼리가 붙고,
39
+ * 이 프레임워크는 5개 DB 드라이버를 지원해야 한다. 세션 컬럼 하나로 두면 정렬·비교가 값싸게 끝나고
40
+ * 드라이버 차이가 없다. 메시지를 저장할 때 함께 갱신한다(activityPatch).
41
+ */
42
+ lastMessageAt?: Date;
43
+ /** 목록에 보여줄 마지막 메시지 미리보기 — 목록에서 메시지 테이블을 다시 읽지 않기 위해 짧게 저장. */
44
+ lastMessagePreview?: string;
45
+ lastSummary?: string;
46
+ /**
47
+ * `lastSummary` 가 **어디까지 덮는지** — 이 메시지까지 요약에 반영됐다.
48
+ *
49
+ * 이게 없으면 누적 요약을 할 수 없다: 매 턴 전체를 다시 요약하거나(비용) 이미 요약한 구간을
50
+ * 또 이어 붙인다(중복). 이력에서 이 id 를 못 찾으면(삭제·편집) 덮은 범위를 알 수 없으므로
51
+ * 전부 미반영으로 보고 다시 요약한다 — 빠뜨리는 쪽보다 안전하다.
52
+ */
53
+ summaryUpToMessageId?: string;
54
+ /** 사용 모델 식별 — 'anthropic:claude-sonnet-4-6' 등 */
55
+ aiClientId?: string;
56
+ createdAt?: Date;
57
+ updatedAt?: Date;
58
+ deletedAt?: Date;
59
+ messages?: ChatMessage[];
60
+ }
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChatSession = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /**
6
+ * ChatSession — Board 와 결합되는 AI 협력 세션.
7
+ *
8
+ * 한 보드에 여러 세션 가능 — DB 레벨 unique 제약 없음.
9
+ *
10
+ * 향후 폭증할 세션 분화 차원 (예: thread / 사용자별 / 컨텍스트별 design vs ops /
11
+ * archived / forked from clone / agent별) 을 미리 차단하지 않기 위해 구조 자체는 1:N
12
+ * 으로 열어둠. 단일 세션 정책 (현재 UX) 은 app 레벨 idempotent 로직 — `startBoardAISession`
13
+ * 이 boardId 매칭 시 기존 세션 반환, `chatSessionByBoard` 가 첫 매칭 반환.
14
+ *
15
+ * 미래에 다중 세션 활성화 시 discriminator 컬럼 (kind / name / threadId / creatorId 등)
16
+ * 을 그때 추가. 마이그레이션 부담 최소화 — 현재는 unique 만 풀어 자리 마련.
17
+ *
18
+ * board-service 의 Board 가 owning side 가 될 예정 (ChatSession FK).
19
+ */
20
+ const typeorm_1 = require("typeorm");
21
+ const type_graphql_1 = require("type-graphql");
22
+ const shell_1 = require("@things-factory/shell");
23
+ const auth_base_1 = require("@things-factory/auth-base");
24
+ const env_1 = require("@things-factory/env");
25
+ const chat_message_js_1 = require("../chat-message/chat-message.js");
26
+ const ORMCONFIG = env_1.config.get('ormconfig', {});
27
+ const DATABASE_TYPE = ORMCONFIG.type;
28
+ let ChatSession = class ChatSession {
29
+ };
30
+ exports.ChatSession = ChatSession;
31
+ tslib_1.__decorate([
32
+ (0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
33
+ (0, type_graphql_1.Field)(type => type_graphql_1.ID, { nullable: true }),
34
+ tslib_1.__metadata("design:type", String)
35
+ ], ChatSession.prototype, "id", void 0);
36
+ tslib_1.__decorate([
37
+ (0, typeorm_1.ManyToOne)(type => shell_1.Domain),
38
+ (0, type_graphql_1.Field)(type => shell_1.Domain, { nullable: true }),
39
+ tslib_1.__metadata("design:type", shell_1.Domain)
40
+ ], ChatSession.prototype, "domain", void 0);
41
+ tslib_1.__decorate([
42
+ (0, typeorm_1.RelationId)((session) => session.domain),
43
+ tslib_1.__metadata("design:type", String)
44
+ ], ChatSession.prototype, "domainId", void 0);
45
+ tslib_1.__decorate([
46
+ (0, typeorm_1.Column)({ nullable: true }),
47
+ (0, type_graphql_1.Field)({
48
+ nullable: true,
49
+ description: 'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts). Kept for backward compatibility — board-anchored sessions dual-write this alongside anchorId.'
50
+ }),
51
+ tslib_1.__metadata("design:type", String)
52
+ ], ChatSession.prototype, "boardId", void 0);
53
+ tslib_1.__decorate([
54
+ (0, typeorm_1.Column)({ nullable: true }),
55
+ (0, type_graphql_1.Field)({
56
+ nullable: true,
57
+ description: "What this conversation is anchored to: 'board' (authoring a board) | 'space' (a site and the twins running there) | 'instance' | 'none'. Absent on legacy rows — treated as 'board' when boardId is present."
58
+ }),
59
+ tslib_1.__metadata("design:type", String)
60
+ ], ChatSession.prototype, "anchorType", void 0);
61
+ tslib_1.__decorate([
62
+ (0, typeorm_1.Column)({ nullable: true }),
63
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Id of the anchored target (board id / space id / instance id).' }),
64
+ tslib_1.__metadata("design:type", String)
65
+ ], ChatSession.prototype, "anchorId", void 0);
66
+ tslib_1.__decorate([
67
+ (0, typeorm_1.Column)({ nullable: true, length: 200 }),
68
+ (0, type_graphql_1.Field)({ nullable: true, description: 'User-given name of this session (tab label / identification).' }),
69
+ tslib_1.__metadata("design:type", String)
70
+ ], ChatSession.prototype, "name", void 0);
71
+ tslib_1.__decorate([
72
+ (0, typeorm_1.ManyToOne)(type => auth_base_1.User),
73
+ (0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true, description: 'User who created this session.' }),
74
+ tslib_1.__metadata("design:type", auth_base_1.User)
75
+ ], ChatSession.prototype, "creator", void 0);
76
+ tslib_1.__decorate([
77
+ (0, typeorm_1.RelationId)((session) => session.creator),
78
+ tslib_1.__metadata("design:type", String)
79
+ ], ChatSession.prototype, "creatorId", void 0);
80
+ tslib_1.__decorate([
81
+ (0, typeorm_1.ManyToOne)(type => auth_base_1.User),
82
+ (0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true, description: 'User who last updated this session.' }),
83
+ tslib_1.__metadata("design:type", auth_base_1.User)
84
+ ], ChatSession.prototype, "updater", void 0);
85
+ tslib_1.__decorate([
86
+ (0, typeorm_1.RelationId)((session) => session.updater),
87
+ tslib_1.__metadata("design:type", String)
88
+ ], ChatSession.prototype, "updaterId", void 0);
89
+ tslib_1.__decorate([
90
+ (0, typeorm_1.Column)({ nullable: true }),
91
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Timestamp of the last message in this session (denormalized for list ordering and unread comparison — avoids per-session aggregation across the supported DB drivers).' }),
92
+ tslib_1.__metadata("design:type", Date
93
+ /** 목록에 보여줄 마지막 메시지 미리보기 — 목록에서 메시지 테이블을 다시 읽지 않기 위해 짧게 저장. */
94
+ )
95
+ ], ChatSession.prototype, "lastMessageAt", void 0);
96
+ tslib_1.__decorate([
97
+ (0, typeorm_1.Column)({ nullable: true, length: 200 }),
98
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Short preview of the last message, stored so the session list does not need to query messages.' }),
99
+ tslib_1.__metadata("design:type", String)
100
+ ], ChatSession.prototype, "lastMessagePreview", void 0);
101
+ tslib_1.__decorate([
102
+ (0, typeorm_1.Column)({
103
+ nullable: true,
104
+ type: DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
105
+ ? 'longtext'
106
+ : DATABASE_TYPE == 'oracle'
107
+ ? 'clob'
108
+ : DATABASE_TYPE == 'mssql'
109
+ ? 'nvarchar'
110
+ : 'text',
111
+ length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
112
+ }),
113
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Compressed summary of older messages (for token saving).' }),
114
+ tslib_1.__metadata("design:type", String)
115
+ ], ChatSession.prototype, "lastSummary", void 0);
116
+ tslib_1.__decorate([
117
+ (0, typeorm_1.Column)({ nullable: true }),
118
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Id of the last message folded into lastSummary — enables incremental summarization instead of re-reading the whole history.' }),
119
+ tslib_1.__metadata("design:type", String)
120
+ ], ChatSession.prototype, "summaryUpToMessageId", void 0);
121
+ tslib_1.__decorate([
122
+ (0, typeorm_1.Column)({ nullable: true }),
123
+ (0, type_graphql_1.Field)({ nullable: true }),
124
+ tslib_1.__metadata("design:type", String)
125
+ ], ChatSession.prototype, "aiClientId", void 0);
126
+ tslib_1.__decorate([
127
+ (0, typeorm_1.CreateDateColumn)(),
128
+ (0, type_graphql_1.Field)({ nullable: true }),
129
+ tslib_1.__metadata("design:type", Date)
130
+ ], ChatSession.prototype, "createdAt", void 0);
131
+ tslib_1.__decorate([
132
+ (0, typeorm_1.UpdateDateColumn)(),
133
+ (0, type_graphql_1.Field)({ nullable: true }),
134
+ tslib_1.__metadata("design:type", Date)
135
+ ], ChatSession.prototype, "updatedAt", void 0);
136
+ tslib_1.__decorate([
137
+ (0, typeorm_1.DeleteDateColumn)(),
138
+ tslib_1.__metadata("design:type", Date
139
+ // ── 관계 (TypeORM 만, GraphQL 노출은 별도 query 통해 페이징) ──
140
+ )
141
+ ], ChatSession.prototype, "deletedAt", void 0);
142
+ tslib_1.__decorate([
143
+ (0, typeorm_1.OneToMany)(type => chat_message_js_1.ChatMessage, message => message.session),
144
+ tslib_1.__metadata("design:type", Array)
145
+ ], ChatSession.prototype, "messages", void 0);
146
+ exports.ChatSession = ChatSession = tslib_1.__decorate([
147
+ (0, typeorm_1.Entity)()
148
+ // 인덱스는 조회 성능용으로 유지 (chatSessionByBoard 가 [domain, boardId] 로 자주 쿼리).
149
+ // unique 제약은 의도적으로 제거 — 한 보드에 여러 세션을 미래에 허용하기 위함.
150
+ ,
151
+ (0, typeorm_1.Index)('ix_chat_session_1', (session) => [session.domain, session.boardId])
152
+ // 앵커 조회용 — chatSessionsByAnchor 가 [domain, anchorType, anchorId] 로 질의.
153
+ ,
154
+ (0, typeorm_1.Index)('ix_chat_session_2', (session) => [session.domain, session.anchorType, session.anchorId]),
155
+ (0, type_graphql_1.ObjectType)({
156
+ description: 'AI 협력 세션 — Board 와 결합. 한 보드에 여러 세션 가능 (thread / 사용자별 / 컨텍스트별 등 미래 확장).'
157
+ })
158
+ ], ChatSession);
159
+ //# sourceMappingURL=chat-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-session.js","sourceRoot":"","sources":["../../../server/service/chat-session/chat-session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatSession — Board 와 결합되는 AI 협력 세션.\n *\n * 한 보드에 여러 세션 가능 — DB 레벨 unique 제약 없음.\n *\n * 향후 폭증할 세션 분화 차원 (예: thread / 사용자별 / 컨텍스트별 design vs ops /\n * archived / forked from clone / agent별) 을 미리 차단하지 않기 위해 구조 자체는 1:N\n * 으로 열어둠. 단일 세션 정책 (현재 UX) 은 app 레벨 idempotent 로직 — `startBoardAISession`\n * 이 boardId 매칭 시 기존 세션 반환, `chatSessionByBoard` 가 첫 매칭 반환.\n *\n * 미래에 다중 세션 활성화 시 discriminator 컬럼 (kind / name / threadId / creatorId 등)\n * 을 그때 추가. 마이그레이션 부담 최소화 — 현재는 unique 만 풀어 자리 마련.\n *\n * board-service 의 Board 가 owning side 가 될 예정 (ChatSession FK).\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n OneToMany,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\n\nimport { ChatMessage } from '../chat-message/chat-message.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n// 인덱스는 조회 성능용으로 유지 (chatSessionByBoard 가 [domain, boardId] 로 자주 쿼리).\n// unique 제약은 의도적으로 제거 — 한 보드에 여러 세션을 미래에 허용하기 위함.\n@Index('ix_chat_session_1', (session: ChatSession) => [session.domain, session.boardId])\n// 앵커 조회용 — chatSessionsByAnchor 가 [domain, anchorType, anchorId] 로 질의.\n@Index('ix_chat_session_2', (session: ChatSession) => [session.domain, session.anchorType, session.anchorId])\n@ObjectType({\n description: 'AI 협력 세션 — Board 와 결합. 한 보드에 여러 세션 가능 (thread / 사용자별 / 컨텍스트별 등 미래 확장).'\n})\nexport class ChatSession {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true })\n domain?: Domain\n\n @RelationId((session: ChatSession) => session.domain)\n domainId?: string\n\n /** Board.id 와 연결. Board 가 owning side 가 될 예정. 1:N — 한 보드에 여러 세션 가능.\n *\n * **앵커 일반화 이후에도 유지되는 하위호환 컬럼**이다(제거하지 말 것). 앵커가 board 인 세션은\n * anchorId 와 함께 이 컬럼에도 같은 값을 쓴다(dual-write) — 기존 조회/인덱스/소비자가 그대로 동작한다. */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n 'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts). Kept for backward compatibility — board-anchored sessions dual-write this alongside anchorId.'\n })\n boardId?: string\n\n /**\n * 대화가 매달린 대상의 **종류**. 세션은 보드에만 매달리지 않는다 —\n * 저작 대화는 보드에, 운영 대화는 공간(사이트)에 매달린다. 앵커가 대화의 성격과 이력 경계를 정한다.\n *\n * 'board' | 'space' | 'instance' | 'none'. 미설정(기존 데이터)은 boardId 가 있으면 board 로 읽는다 —\n * 백필 마이그레이션 없이 lazy 하위호환(컬럼 추가만).\n */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n \"What this conversation is anchored to: 'board' (authoring a board) | 'space' (a site and the twins running there) | 'instance' | 'none'. Absent on legacy rows — treated as 'board' when boardId is present.\"\n })\n anchorType?: string\n\n /** 앵커 대상의 id(보드 id · 공간 id · 인스턴스 id). anchorType 과 함께 의미를 갖는다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the anchored target (board id / space id / instance id).' })\n anchorId?: string\n\n /**\n * 세션의 사용자 정의 이름 (탭 레이블 / 세션 식별 표시용).\n *\n * nullable — 미설정 시 UI 가 fallback 라벨 (\"세션 N\" 또는 createdAt 기반) 사용. 사용자가\n * 다중 세션 운영 시 의미 부여 (예: \"초기 설계\", \"운영 시뮬레이션\", \"사용자A 의 사적 세션\").\n */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'User-given name of this session (tab label / identification).' })\n name?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this session.' })\n creator?: User\n\n @RelationId((session: ChatSession) => session.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who last updated this session.' })\n updater?: User\n\n @RelationId((session: ChatSession) => session.updater)\n updaterId?: string\n\n /** 토큰 절감용 — 오래된 메시지를 LLM 으로 압축한 요약. Phase 2 에서 작성. */\n /**\n * 마지막 메시지 시각 — **비정규화**. 목록 정렬(최근 활동순)과 안 읽음 판정의 기준.\n *\n * 왜 비정규화인가: 세션마다 메시지를 집계하면(MAX/COUNT + group by) 목록 한 번에 집계 쿼리가 붙고,\n * 이 프레임워크는 5개 DB 드라이버를 지원해야 한다. 세션 컬럼 하나로 두면 정렬·비교가 값싸게 끝나고\n * 드라이버 차이가 없다. 메시지를 저장할 때 함께 갱신한다(activityPatch).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Timestamp of the last message in this session (denormalized for list ordering and unread comparison — avoids per-session aggregation across the supported DB drivers).' })\n lastMessageAt?: Date\n\n /** 목록에 보여줄 마지막 메시지 미리보기 — 목록에서 메시지 테이블을 다시 읽지 않기 위해 짧게 저장. */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'Short preview of the last message, stored so the session list does not need to query messages.' })\n lastMessagePreview?: string\n\n @Column({\n nullable: true,\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n @Field({ nullable: true, description: 'Compressed summary of older messages (for token saving).' })\n lastSummary?: string\n\n /**\n * `lastSummary` 가 **어디까지 덮는지** — 이 메시지까지 요약에 반영됐다.\n *\n * 이게 없으면 누적 요약을 할 수 없다: 매 턴 전체를 다시 요약하거나(비용) 이미 요약한 구간을\n * 또 이어 붙인다(중복). 이력에서 이 id 를 못 찾으면(삭제·편집) 덮은 범위를 알 수 없으므로\n * 전부 미반영으로 보고 다시 요약한다 — 빠뜨리는 쪽보다 안전하다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the last message folded into lastSummary — enables incremental summarization instead of re-reading the whole history.' })\n summaryUpToMessageId?: string\n\n /** 사용 모델 식별 — 'anthropic:claude-sonnet-4-6' 등 */\n @Column({ nullable: true })\n @Field({ nullable: true })\n aiClientId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @DeleteDateColumn()\n deletedAt?: Date\n\n // ── 관계 (TypeORM 만, GraphQL 노출은 별도 query 통해 페이징) ──\n @OneToMany(type => ChatMessage, message => message.session)\n messages?: ChatMessage[]\n\n /*\n * ── 보드 패치 관계를 끊었다 (2026-09-10) ──────────────────────────────────\n * 여기 `@OneToMany(PatchEntry)` 가 있었다. 그래서 **대화 세션이 보드 편집기를 알아야** 했고,\n * 세션 계열을 중립 패키지로 옮기려는 순간 `ai-assistant → board-ai → ai-assistant` 순환이\n * 된다.\n *\n * 끊어도 잃는 것이 없다 — 이 관계를 **자료로 읽는 자리가 하나도 없었다**(전 저장소에서\n * `session.patches` 를 읽는 코드 0 건). 유일한 쓰임이 `PatchEntry` 쪽의 역방향 선언 한 줄이었고,\n * TypeORM 은 역방향 없이도 `@ManyToOne` 을 만든다. 패치는 `sessionId` 로 찾는다\n * (`chatPatches` 가 이미 그렇게 한다).\n *\n * 방향은 남는다: **보드 패치가 세션을 안다. 세션은 보드 패치를 모른다.** 종속이 한쪽이면\n * 중립 쪽이 옮겨질 수 있다.\n */\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import { ChatSession } from './chat-session.js';
2
+ export { ChatSession };
3
+ export declare const entities: (typeof ChatSession)[];
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.entities = exports.ChatSession = void 0;
4
+ const chat_session_js_1 = require("./chat-session.js");
5
+ Object.defineProperty(exports, "ChatSession", { enumerable: true, get: function () { return chat_session_js_1.ChatSession; } });
6
+ exports.entities = [chat_session_js_1.ChatSession];
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/chat-session/index.ts"],"names":[],"mappings":";;;AAAA,uDAA+C;AAEtC,4FAFA,6BAAW,OAEA;AAEP,QAAA,QAAQ,GAAG,CAAC,6BAAW,CAAC,CAAA","sourcesContent":["import { ChatSession } from './chat-session.js'\n\nexport { ChatSession }\n\nexport const entities = [ChatSession]\n"]}
@@ -0,0 +1,30 @@
1
+ /** 세션 활동 방송 토픽. */
2
+ export declare const SESSION_ACTIVITY_TOPIC = "board-ai-session-activity";
3
+ /** 방송 페이로드 — 필터용 메타(도메인·참여자) + 목록 갱신용 최소 정보. 본문은 싣지 않는다. */
4
+ /**
5
+ * 활동 종류.
6
+ * message — 새 메시지. **안 읽음 배지를 올린다.**
7
+ * meta — 이름 변경·참여자 변동 등 세션 자체의 변화. 목록은 갱신하지만 배지는 올리지 않는다.
8
+ * (이름이 바뀐 것을 "새 소식" 으로 알리면 소음이 된다.)
9
+ */
10
+ export type SessionActivityKind = 'message' | 'meta';
11
+ export interface SessionActivityPayload {
12
+ domainId: string;
13
+ sessionId: string;
14
+ kind: SessionActivityKind;
15
+ /** 이 세션 참여자 user id — 필터가 이 배열로 수신자를 정한다(필터에서 DB 를 읽지 않기 위함). */
16
+ participantIds: string[];
17
+ anchorType?: string;
18
+ anchorId?: string;
19
+ /** 활동 시각(ISO) — 클라가 목록 정렬·안 읽음 판정에 쓴다. */
20
+ at: string;
21
+ /** 한 줄 미리보기(짧게). 본문 전체가 아니다. */
22
+ preview?: string;
23
+ /** 이 활동을 만든 사용자 — 자기 발신은 배지를 올리지 않도록 클라가 쓴다. */
24
+ actorId?: string;
25
+ }
26
+ /**
27
+ * 같은 도메인 + **내가 참여자인 세션**의 활동만 통과. 도메인 격리가 보안 핵심이고,
28
+ * 참여자 검사가 "내 것만" 을 성립시킨다(참여하지 않은 대화의 활동을 받지 않는다).
29
+ */
30
+ export declare function sessionActivityFilter(domainId: string, userId: string): (payload: SessionActivityPayload) => boolean;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /*
3
+ * 세션 활동 방송 — 순수 로직(토픽·페이로드·필터). 데코레이터·pubsub import 없음 → 단위 테스트 가능.
4
+ *
5
+ * ── 왜 메시지 방송(chat-message)과 따로 두는가 ──────────────────────────────
6
+ * 기존 `chatMessageActivity` 는 **그 세션을 열어 둔** 구독자에게 메시지 본문을 보낸다. 그래서 창을 닫아
7
+ * 두면 새 메시지를 알 수 없다 — 초대 알림 한 번 받고 나면 이후 대화는 사용자가 스스로 들어와 봐야 안다.
8
+ *
9
+ * 이 토픽은 **내가 참여한 모든 세션**의 활동을 알린다. 목적이 배지·목록 갱신이므로 본문을 싣지 않고
10
+ * 최소 정보(세션·앵커·시각·미리보기)만 보낸다:
11
+ * · 열지도 않은 세션들의 본문이 한 채널로 쏟아지는 것을 피한다(대역·주의 낭비).
12
+ * · 본문이 필요한 시점은 그 세션을 열 때이고, 그때는 기존 메시지 구독이 담당한다.
13
+ *
14
+ * ── 구독자 판정을 필터에서 DB 로 하지 않는다 ────────────────────────────────
15
+ * 필터는 **구독자 × 메시지마다** 돈다. 여기서 DB 를 읽으면 방송 한 번이 구독자 수만큼의 질의가 된다.
16
+ * 그래서 발행 시점에 참여자 id 목록을 페이로드에 실어 보내고, 필터는 배열 포함 검사만 한다.
17
+ * 참여자 조회는 발행당 1회 — 사람 대화 빈도에서 감당 가능한 비용이다.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.SESSION_ACTIVITY_TOPIC = void 0;
21
+ exports.sessionActivityFilter = sessionActivityFilter;
22
+ /** 세션 활동 방송 토픽. */
23
+ exports.SESSION_ACTIVITY_TOPIC = 'board-ai-session-activity';
24
+ /**
25
+ * 같은 도메인 + **내가 참여자인 세션**의 활동만 통과. 도메인 격리가 보안 핵심이고,
26
+ * 참여자 검사가 "내 것만" 을 성립시킨다(참여하지 않은 대화의 활동을 받지 않는다).
27
+ */
28
+ function sessionActivityFilter(domainId, userId) {
29
+ return (payload) => !!payload &&
30
+ payload.domainId === domainId &&
31
+ Array.isArray(payload.participantIds) &&
32
+ payload.participantIds.includes(userId);
33
+ }
34
+ //# sourceMappingURL=session-activity-broadcast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-activity-broadcast.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-activity-broadcast.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;AAkCH,sDAMC;AAtCD,mBAAmB;AACN,QAAA,sBAAsB,GAAG,2BAA2B,CAAA;AA2BjE;;;GAGG;AACH,SAAgB,qBAAqB,CAAC,QAAgB,EAAE,MAAc;IACpE,OAAO,CAAC,OAA+B,EAAW,EAAE,CAClD,CAAC,CAAC,OAAO;QACT,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAC7B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC;QACrC,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;AAC3C,CAAC","sourcesContent":["/*\n * 세션 활동 방송 — 순수 로직(토픽·페이로드·필터). 데코레이터·pubsub import 없음 → 단위 테스트 가능.\n *\n * ── 왜 메시지 방송(chat-message)과 따로 두는가 ──────────────────────────────\n * 기존 `chatMessageActivity` 는 **그 세션을 열어 둔** 구독자에게 메시지 본문을 보낸다. 그래서 창을 닫아\n * 두면 새 메시지를 알 수 없다 — 초대 알림 한 번 받고 나면 이후 대화는 사용자가 스스로 들어와 봐야 안다.\n *\n * 이 토픽은 **내가 참여한 모든 세션**의 활동을 알린다. 목적이 배지·목록 갱신이므로 본문을 싣지 않고\n * 최소 정보(세션·앵커·시각·미리보기)만 보낸다:\n * · 열지도 않은 세션들의 본문이 한 채널로 쏟아지는 것을 피한다(대역·주의 낭비).\n * · 본문이 필요한 시점은 그 세션을 열 때이고, 그때는 기존 메시지 구독이 담당한다.\n *\n * ── 구독자 판정을 필터에서 DB 로 하지 않는다 ────────────────────────────────\n * 필터는 **구독자 × 메시지마다** 돈다. 여기서 DB 를 읽으면 방송 한 번이 구독자 수만큼의 질의가 된다.\n * 그래서 발행 시점에 참여자 id 목록을 페이로드에 실어 보내고, 필터는 배열 포함 검사만 한다.\n * 참여자 조회는 발행당 1회 — 사람 대화 빈도에서 감당 가능한 비용이다.\n */\n\n/** 세션 활동 방송 토픽. */\nexport const SESSION_ACTIVITY_TOPIC = 'board-ai-session-activity'\n\n/** 방송 페이로드 — 필터용 메타(도메인·참여자) + 목록 갱신용 최소 정보. 본문은 싣지 않는다. */\n/**\n * 활동 종류.\n * message — 새 메시지. **안 읽음 배지를 올린다.**\n * meta — 이름 변경·참여자 변동 등 세션 자체의 변화. 목록은 갱신하지만 배지는 올리지 않는다.\n * (이름이 바뀐 것을 \"새 소식\" 으로 알리면 소음이 된다.)\n */\nexport type SessionActivityKind = 'message' | 'meta'\n\nexport interface SessionActivityPayload {\n domainId: string\n sessionId: string\n kind: SessionActivityKind\n /** 이 세션 참여자 user id — 필터가 이 배열로 수신자를 정한다(필터에서 DB 를 읽지 않기 위함). */\n participantIds: string[]\n anchorType?: string\n anchorId?: string\n /** 활동 시각(ISO) — 클라가 목록 정렬·안 읽음 판정에 쓴다. */\n at: string\n /** 한 줄 미리보기(짧게). 본문 전체가 아니다. */\n preview?: string\n /** 이 활동을 만든 사용자 — 자기 발신은 배지를 올리지 않도록 클라가 쓴다. */\n actorId?: string\n}\n\n/**\n * 같은 도메인 + **내가 참여자인 세션**의 활동만 통과. 도메인 격리가 보안 핵심이고,\n * 참여자 검사가 \"내 것만\" 을 성립시킨다(참여하지 않은 대화의 활동을 받지 않는다).\n */\nexport function sessionActivityFilter(domainId: string, userId: string) {\n return (payload: SessionActivityPayload): boolean =>\n !!payload &&\n payload.domainId === domainId &&\n Array.isArray(payload.participantIds) &&\n payload.participantIds.includes(userId)\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import { type SessionActivityKind } from './session-activity-broadcast.js';
2
+ export declare function publishSessionActivity(input: {
3
+ domainId: string;
4
+ sessionId: string;
5
+ /** message = 새 메시지(배지 올림) · meta = 이름 변경 등(목록만 갱신). 기본 message. */
6
+ kind?: SessionActivityKind;
7
+ anchorType?: string;
8
+ anchorId?: string;
9
+ at?: Date;
10
+ preview?: string;
11
+ actorId?: string;
12
+ }): Promise<void>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.publishSessionActivity = publishSessionActivity;
4
+ /*
5
+ * 세션 활동 발행 — pubsub 의존만(데코레이터 없음). 순수 로직은 session-activity-broadcast.ts.
6
+ *
7
+ * 참여자 목록을 여기서 읽어 페이로드에 싣는다 — 필터는 구독자 × 메시지마다 돌기 때문에 거기서 DB 를
8
+ * 읽으면 방송 한 번이 구독자 수만큼의 질의가 된다. 발행당 1회로 끝낸다.
9
+ *
10
+ * best-effort: 실패해도 대화·저장을 막지 않는다(배지가 한 번 늦을 뿐).
11
+ */
12
+ const shell_1 = require("@things-factory/shell");
13
+ const chat_session_participant_js_1 = require("../chat-session-participant/chat-session-participant.js");
14
+ const session_activity_broadcast_js_1 = require("./session-activity-broadcast.js");
15
+ async function publishSessionActivity(input) {
16
+ try {
17
+ const participants = await (0, shell_1.getRepository)(chat_session_participant_js_1.ChatSessionParticipant).find({
18
+ where: { domain: { id: input.domainId }, session: { id: input.sessionId } }
19
+ });
20
+ const payload = {
21
+ domainId: input.domainId,
22
+ sessionId: input.sessionId,
23
+ kind: input.kind ?? 'message',
24
+ participantIds: participants.map(p => p.userId).filter(Boolean),
25
+ anchorType: input.anchorType,
26
+ anchorId: input.anchorId,
27
+ at: (input.at ?? new Date()).toISOString(),
28
+ preview: input.preview,
29
+ actorId: input.actorId
30
+ };
31
+ shell_1.pubsub.publish(session_activity_broadcast_js_1.SESSION_ACTIVITY_TOPIC, payload);
32
+ }
33
+ catch {
34
+ /* 방송 실패는 대화를 막지 않는다 */
35
+ }
36
+ }
37
+ //# sourceMappingURL=session-activity-publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-activity-publish.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-activity-publish.ts"],"names":[],"mappings":";;AAaA,wDA8BC;AA3CD;;;;;;;GAOG;AACH,iDAA6D;AAE7D,yGAAgG;AAChG,mFAA+H;AAExH,KAAK,UAAU,sBAAsB,CAAC,KAU5C;IACC,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,MAAM,IAAA,qBAAa,EAAC,oDAAsB,CAAC,CAAC,IAAI,CAAC;YACpE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAS,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,SAAS,EAAS,EAAE;SAC1F,CAAC,CAAA;QACF,MAAM,OAAO,GAA2B;YACtC,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,SAAS;YAC7B,cAAc,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAE,CAAS,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;YACxE,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE;YAC1C,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB,CAAA;QACD,cAAM,CAAC,OAAO,CAAC,sDAAsB,EAAE,OAAO,CAAC,CAAA;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,uBAAuB;IACzB,CAAC;AACH,CAAC","sourcesContent":["/*\n * 세션 활동 발행 — pubsub 의존만(데코레이터 없음). 순수 로직은 session-activity-broadcast.ts.\n *\n * 참여자 목록을 여기서 읽어 페이로드에 싣는다 — 필터는 구독자 × 메시지마다 돌기 때문에 거기서 DB 를\n * 읽으면 방송 한 번이 구독자 수만큼의 질의가 된다. 발행당 1회로 끝낸다.\n *\n * best-effort: 실패해도 대화·저장을 막지 않는다(배지가 한 번 늦을 뿐).\n */\nimport { pubsub, getRepository } from '@things-factory/shell'\n\nimport { ChatSessionParticipant } from '../chat-session-participant/chat-session-participant.js'\nimport { SESSION_ACTIVITY_TOPIC, type SessionActivityPayload, type SessionActivityKind } from './session-activity-broadcast.js'\n\nexport async function publishSessionActivity(input: {\n domainId: string\n sessionId: string\n /** message = 새 메시지(배지 올림) · meta = 이름 변경 등(목록만 갱신). 기본 message. */\n kind?: SessionActivityKind\n anchorType?: string\n anchorId?: string\n at?: Date\n preview?: string\n actorId?: string\n}): Promise<void> {\n try {\n const participants = await getRepository(ChatSessionParticipant).find({\n where: { domain: { id: input.domainId } as any, session: { id: input.sessionId } as any }\n })\n const payload: SessionActivityPayload = {\n domainId: input.domainId,\n sessionId: input.sessionId,\n kind: input.kind ?? 'message',\n participantIds: participants.map(p => (p as any).userId).filter(Boolean),\n anchorType: input.anchorType,\n anchorId: input.anchorId,\n at: (input.at ?? new Date()).toISOString(),\n preview: input.preview,\n actorId: input.actorId\n }\n pubsub.publish(SESSION_ACTIVITY_TOPIC, payload)\n } catch {\n /* 방송 실패는 대화를 막지 않는다 */\n }\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import { type SessionActivityPayload } from './session-activity-broadcast.js';
2
+ export { publishSessionActivity } from './session-activity-publish.js';
3
+ export declare class SessionActivity {
4
+ sessionId: string;
5
+ kind: string;
6
+ anchorType?: string;
7
+ anchorId?: string;
8
+ at: string;
9
+ preview?: string;
10
+ actorId?: string;
11
+ }
12
+ export declare class MySessionActivitySubscription {
13
+ mySessionActivity(payload: SessionActivityPayload): SessionActivity;
14
+ }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MySessionActivitySubscription = exports.SessionActivity = exports.publishSessionActivity = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /*
6
+ * MySessionActivitySubscription — **내가 참여한 모든 세션**의 활동 알림.
7
+ *
8
+ * 기존 chatMessageActivity 는 그 세션을 열어 둔 구독자에게 본문을 보낸다 → 창을 닫아 두면 새 메시지를
9
+ * 알 수 없었다. 이 구독은 배지·목록 갱신용으로 최소 정보만 보낸다(본문 없음).
10
+ *
11
+ * 도메인 격리는 필터가, "내 것만" 은 참여자 검사가 성립시킨다(session-activity-broadcast.ts).
12
+ */
13
+ const type_graphql_1 = require("type-graphql");
14
+ const graphql_yoga_1 = require("graphql-yoga");
15
+ const shell_1 = require("@things-factory/shell");
16
+ const session_activity_broadcast_js_1 = require("./session-activity-broadcast.js");
17
+ const subscribe_auth_js_1 = require("../subscribe-auth.js");
18
+ var session_activity_publish_js_1 = require("./session-activity-publish.js");
19
+ Object.defineProperty(exports, "publishSessionActivity", { enumerable: true, get: function () { return session_activity_publish_js_1.publishSessionActivity; } });
20
+ let SessionActivity = class SessionActivity {
21
+ };
22
+ exports.SessionActivity = SessionActivity;
23
+ tslib_1.__decorate([
24
+ (0, type_graphql_1.Field)({ description: 'Session that had activity.' }),
25
+ tslib_1.__metadata("design:type", String)
26
+ ], SessionActivity.prototype, "sessionId", void 0);
27
+ tslib_1.__decorate([
28
+ (0, type_graphql_1.Field)({
29
+ description: "Kind of activity: 'message' (new message — raise the unread badge) or 'meta' (session itself changed, e.g. renamed — refresh the list but do not badge)."
30
+ }),
31
+ tslib_1.__metadata("design:type", String)
32
+ ], SessionActivity.prototype, "kind", void 0);
33
+ tslib_1.__decorate([
34
+ (0, type_graphql_1.Field)({ nullable: true, description: "What the session is anchored to: 'board' | 'space' | 'instance' | 'none'." }),
35
+ tslib_1.__metadata("design:type", String)
36
+ ], SessionActivity.prototype, "anchorType", void 0);
37
+ tslib_1.__decorate([
38
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Id of the anchored target.' }),
39
+ tslib_1.__metadata("design:type", String)
40
+ ], SessionActivity.prototype, "anchorId", void 0);
41
+ tslib_1.__decorate([
42
+ (0, type_graphql_1.Field)({ description: 'Activity timestamp (ISO).' }),
43
+ tslib_1.__metadata("design:type", String)
44
+ ], SessionActivity.prototype, "at", void 0);
45
+ tslib_1.__decorate([
46
+ (0, type_graphql_1.Field)({ nullable: true, description: 'One-line preview (not the full body).' }),
47
+ tslib_1.__metadata("design:type", String)
48
+ ], SessionActivity.prototype, "preview", void 0);
49
+ tslib_1.__decorate([
50
+ (0, type_graphql_1.Field)({ nullable: true, description: 'User who caused the activity — clients skip badging their own.' }),
51
+ tslib_1.__metadata("design:type", String)
52
+ ], SessionActivity.prototype, "actorId", void 0);
53
+ exports.SessionActivity = SessionActivity = tslib_1.__decorate([
54
+ (0, type_graphql_1.ObjectType)({ description: 'Activity notice for a session the current user participates in — badge/list refresh only, no message body.' })
55
+ ], SessionActivity);
56
+ let MySessionActivitySubscription = class MySessionActivitySubscription {
57
+ mySessionActivity(payload) {
58
+ return {
59
+ sessionId: payload.sessionId,
60
+ kind: payload.kind ?? 'message',
61
+ anchorType: payload.anchorType,
62
+ anchorId: payload.anchorId,
63
+ at: payload.at,
64
+ preview: payload.preview,
65
+ actorId: payload.actorId
66
+ };
67
+ }
68
+ };
69
+ exports.MySessionActivitySubscription = MySessionActivitySubscription;
70
+ tslib_1.__decorate([
71
+ (0, type_graphql_1.Subscription)(() => SessionActivity, {
72
+ description: 'Subscribes to activity in every session the current user participates in, so a closed chat surface can still show unread badges and refresh its list.',
73
+ subscribe: async ({ context }) => {
74
+ const { domain, user } = context.state;
75
+ await (0, subscribe_auth_js_1.assertDomainSubscribeAllowed)(domain, user, (d, u) => process.superUserGranted(d, u));
76
+ return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe(session_activity_broadcast_js_1.SESSION_ACTIVITY_TOPIC), (0, graphql_yoga_1.filter)((0, session_activity_broadcast_js_1.sessionActivityFilter)(domain.id, user.id)));
77
+ }
78
+ }),
79
+ tslib_1.__param(0, (0, type_graphql_1.Root)()),
80
+ tslib_1.__metadata("design:type", Function),
81
+ tslib_1.__metadata("design:paramtypes", [Object]),
82
+ tslib_1.__metadata("design:returntype", SessionActivity)
83
+ ], MySessionActivitySubscription.prototype, "mySessionActivity", null);
84
+ exports.MySessionActivitySubscription = MySessionActivitySubscription = tslib_1.__decorate([
85
+ (0, type_graphql_1.Resolver)(SessionActivity)
86
+ ], MySessionActivitySubscription);
87
+ //# sourceMappingURL=session-activity-subscription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-activity-subscription.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-activity-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;GAOG;AACH,+CAA8E;AAC9E,+CAA2C;AAC3C,iDAA8C;AAE9C,mFAA4H;AAC5H,4DAAmE;AAEnE,6EAAsE;AAA7D,qIAAA,sBAAsB,OAAA;AAGxB,IAAM,eAAe,GAArB,MAAM,eAAe;CAwB3B,CAAA;AAxBY,0CAAe;AAE1B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;kDACnC;AAMlB;IAJC,IAAA,oBAAK,EAAC;QACL,WAAW,EACT,0JAA0J;KAC7J,CAAC;;6CACW;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2EAA2E,EAAE,CAAC;;mDACjG;AAGnB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,4BAA4B,EAAE,CAAC;;iDACpD;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2BAA2B,EAAE,CAAC;;2CACzC;AAGX;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uCAAuC,EAAE,CAAC;;gDAChE;AAGhB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;;gDACzF;0BAvBL,eAAe;IAD3B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,4GAA4G,EAAE,CAAC;GAC7H,eAAe,CAwB3B;AAGM,IAAM,6BAA6B,GAAnC,MAAM,6BAA6B;IAUxC,iBAAiB,CAAS,OAA+B;QACvD,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,SAAS;YAC/B,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO,EAAE,OAAO,CAAC,OAAO;SACzB,CAAA;IACH,CAAC;CACF,CAAA;AArBY,sEAA6B;AAUxC;IATC,IAAA,2BAAY,EAAC,GAAG,EAAE,CAAC,eAAe,EAAE;QACnC,WAAW,EACT,uJAAuJ;QACzJ,SAAS,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;YAC/B,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,IAAA,gDAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAC1F,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,sDAAsB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,qDAAqB,EAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QAC1G,CAAC;KACF,CAAC;IACiB,mBAAA,IAAA,mBAAI,GAAE,CAAA;;;4CAAmC,eAAe;sEAU1E;wCApBU,6BAA6B;IADzC,IAAA,uBAAQ,EAAC,eAAe,CAAC;GACb,6BAA6B,CAqBzC","sourcesContent":["/*\n * MySessionActivitySubscription — **내가 참여한 모든 세션**의 활동 알림.\n *\n * 기존 chatMessageActivity 는 그 세션을 열어 둔 구독자에게 본문을 보낸다 → 창을 닫아 두면 새 메시지를\n * 알 수 없었다. 이 구독은 배지·목록 갱신용으로 최소 정보만 보낸다(본문 없음).\n *\n * 도메인 격리는 필터가, \"내 것만\" 은 참여자 검사가 성립시킨다(session-activity-broadcast.ts).\n */\nimport { Resolver, Subscription, Root, ObjectType, Field } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { pubsub } from '@things-factory/shell'\n\nimport { SESSION_ACTIVITY_TOPIC, sessionActivityFilter, type SessionActivityPayload } from './session-activity-broadcast.js'\nimport { assertDomainSubscribeAllowed } from '../subscribe-auth.js'\n\nexport { publishSessionActivity } from './session-activity-publish.js'\n\n@ObjectType({ description: 'Activity notice for a session the current user participates in — badge/list refresh only, no message body.' })\nexport class SessionActivity {\n @Field({ description: 'Session that had activity.' })\n sessionId!: string\n\n @Field({\n description:\n \"Kind of activity: 'message' (new message — raise the unread badge) or 'meta' (session itself changed, e.g. renamed — refresh the list but do not badge).\"\n })\n kind!: string\n\n @Field({ nullable: true, description: \"What the session is anchored to: 'board' | 'space' | 'instance' | 'none'.\" })\n anchorType?: string\n\n @Field({ nullable: true, description: 'Id of the anchored target.' })\n anchorId?: string\n\n @Field({ description: 'Activity timestamp (ISO).' })\n at!: string\n\n @Field({ nullable: true, description: 'One-line preview (not the full body).' })\n preview?: string\n\n @Field({ nullable: true, description: 'User who caused the activity — clients skip badging their own.' })\n actorId?: string\n}\n\n@Resolver(SessionActivity)\nexport class MySessionActivitySubscription {\n @Subscription(() => SessionActivity, {\n description:\n 'Subscribes to activity in every session the current user participates in, so a closed chat surface can still show unread badges and refresh its list.',\n subscribe: async ({ context }) => {\n const { domain, user } = context.state\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n return pipe(pubsub.subscribe(SESSION_ACTIVITY_TOPIC), filter(sessionActivityFilter(domain.id, user.id)))\n }\n })\n mySessionActivity(@Root() payload: SessionActivityPayload): SessionActivity {\n return {\n sessionId: payload.sessionId,\n kind: payload.kind ?? 'message',\n anchorType: payload.anchorType,\n anchorId: payload.anchorId,\n at: payload.at,\n preview: payload.preview,\n actorId: payload.actorId\n }\n }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 대화가 매달릴 수 있는 것. **저장되는 값이므로 늘리는 것은 아키텍트 판단이다.**
3
+ *
4
+ * board 보드 저작 대화 — 그 보드의 편집 이력
5
+ * space 공간 운영 대화 — 사이트와 그 공간에서 도는 트윈들
6
+ * instance 트윈 하나에 대한 대화
7
+ * domain 테넌트 전체가 주제인 대화 — 공장 전체 문답처럼 더 좁게 가리킬 것이 없을 때
8
+ * none 앵커로 찾을 수 없는 대화. 「아직 아무것도 고르지 않았다」는 화면 상태다
9
+ */
10
+ export declare const SESSION_ANCHOR_TYPES: readonly ["board", "space", "instance", "domain", "none"];
11
+ export type SessionAnchorType = (typeof SESSION_ANCHOR_TYPES)[number];
12
+ export declare function isSessionAnchorType(value: unknown): value is SessionAnchorType;
13
+ /** 앵커 → 저장할 컬럼 값. 보드 앵커는 boardId 에도 같은 값을 넣어 기존 조회가 그대로 동작하게 한다. */
14
+ export declare function anchorColumns(anchorType: string | undefined, anchorId: string | undefined): {
15
+ anchorType: SessionAnchorType;
16
+ anchorId?: string;
17
+ boardId?: string;
18
+ };
19
+ /**
20
+ * 앵커 조회 조건. 보드 앵커는 **앵커 컬럼이 빈 옛 행도 잡아야** 하므로 boardId 로 찾는다
21
+ * (dual-write 덕분에 신규 행도 같은 조건에 걸린다). 그 밖 앵커는 앵커 컬럼으로 찾는다.
22
+ *
23
+ * 여기서도 앵커 이름을 검사한다 — 읽기 경로에서 틀린 이름은 오류가 아니라 **0 행**으로 나오고,
24
+ * 화면은 「대화가 없다」로 그린다. 그 증상으로는 원인을 못 찾는다.
25
+ */
26
+ export declare function anchorWhere(domainId: string, anchorType: string, anchorId: string): any;