kakaotalk-chat-analyzer 0.2.19 → 0.2.21
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/dist/src/aggregator.d.ts +17 -0
- package/dist/src/aggregator.js +290 -87
- package/dist/src/aggregator.js.map +1 -1
- package/dist/src/bubble-layout.d.ts +11 -0
- package/dist/src/bubble-layout.js +40 -0
- package/dist/src/bubble-layout.js.map +1 -0
- package/dist/src/keyword-counter.d.ts +2 -0
- package/dist/src/keyword-counter.js +6 -0
- package/dist/src/keyword-counter.js.map +1 -1
- package/dist/src/repeat-phrase-counter.d.ts +8 -0
- package/dist/src/repeat-phrase-counter.js +33 -0
- package/dist/src/repeat-phrase-counter.js.map +1 -0
- package/dist/src/report-enrichment.d.ts +5 -0
- package/dist/src/report-enrichment.js +105 -0
- package/dist/src/report-enrichment.js.map +1 -0
- package/dist/src/report-story.d.ts +1 -1
- package/dist/src/report-story.js +6 -5
- package/dist/src/report-story.js.map +1 -1
- package/dist/src/report.js +119 -23
- package/dist/src/report.js.map +1 -1
- package/dist/src/room-events.d.ts +2 -10
- package/dist/src/room-events.js +2 -34
- package/dist/src/room-events.js.map +1 -1
- package/dist/src/story.d.ts +5 -1
- package/dist/src/story.js +63 -16
- package/dist/src/story.js.map +1 -1
- package/dist/src/system-notices.d.ts +26 -0
- package/dist/src/system-notices.js +166 -0
- package/dist/src/system-notices.js.map +1 -0
- package/dist/src/types.d.ts +54 -3
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/dist/src/aggregator.d.ts
CHANGED
|
@@ -19,10 +19,17 @@ export declare class ReportAggregator {
|
|
|
19
19
|
private readonly attachments;
|
|
20
20
|
private readonly domains;
|
|
21
21
|
private readonly keywordCounter;
|
|
22
|
+
private readonly repeatPhraseCounter;
|
|
23
|
+
private readonly shopSearchTopics;
|
|
22
24
|
private readonly gapStats;
|
|
23
25
|
private readonly dailySenderCounts;
|
|
24
26
|
private readonly laughBySender;
|
|
25
27
|
private readonly shortBySender;
|
|
28
|
+
private readonly dailyJoin;
|
|
29
|
+
private readonly dailyLeave;
|
|
30
|
+
private readonly dailyHidden;
|
|
31
|
+
private readonly dailyKick;
|
|
32
|
+
private readonly dailyNewSenders;
|
|
26
33
|
private total;
|
|
27
34
|
private totalCharacters;
|
|
28
35
|
private messagesWithLinks;
|
|
@@ -38,6 +45,15 @@ export declare class ReportAggregator {
|
|
|
38
45
|
private roomJoinMessages;
|
|
39
46
|
private roomLeaveMessages;
|
|
40
47
|
private roomDeletedMessages;
|
|
48
|
+
private roomHiddenMessages;
|
|
49
|
+
private roomKickMessages;
|
|
50
|
+
private roomSlowOnMessages;
|
|
51
|
+
private roomSlowOffMessages;
|
|
52
|
+
private roomSubManagerMessages;
|
|
53
|
+
private roomManagerMessages;
|
|
54
|
+
private roomShopSearchMessages;
|
|
55
|
+
private roomPhotoBundleMessages;
|
|
56
|
+
private pureLaughMessages;
|
|
41
57
|
private prevMs;
|
|
42
58
|
private prevSender;
|
|
43
59
|
private runSender;
|
|
@@ -46,5 +62,6 @@ export declare class ReportAggregator {
|
|
|
46
62
|
private lastDate;
|
|
47
63
|
constructor(filePath: string, privacy: PrivacyMode, top: number);
|
|
48
64
|
consume(record: ChatRecord): void;
|
|
65
|
+
private bumpSystemNotice;
|
|
49
66
|
finalize(meta: FinalizeSourceMeta): ReportData;
|
|
50
67
|
}
|
package/dist/src/aggregator.js
CHANGED
|
@@ -2,7 +2,9 @@ import { formatDate, formatDateTime, partsToUtcMs, weekdayIndex } from "./date.j
|
|
|
2
2
|
import { maskPartialDisplayName, parseChatRoomNameFromExportPath, safeInputName } from "./analysis-labels.js";
|
|
3
3
|
import { GapStreamStats } from "./gap-stats.js";
|
|
4
4
|
import { KeywordCounter } from "./keyword-counter.js";
|
|
5
|
-
import {
|
|
5
|
+
import { RepeatPhraseCounter } from "./repeat-phrase-counter.js";
|
|
6
|
+
import { buildRoomPulse, computeActivityArc, computeBurstDays, computeConversationPace, } from "./report-enrichment.js";
|
|
7
|
+
import { isOpenChatBoilerplate, splitMessageForAnalysis, SYSTEM_NOTICE_KEYWORD_STOP, } from "./system-notices.js";
|
|
6
8
|
import { buildReportStory } from "./story.js";
|
|
7
9
|
const ATTACHMENT_MARKERS = [
|
|
8
10
|
"사진",
|
|
@@ -16,7 +18,9 @@ const ATTACHMENT_MARKERS = [
|
|
|
16
18
|
"음성메시지",
|
|
17
19
|
"삭제된 메시지",
|
|
18
20
|
];
|
|
19
|
-
const KEYWORD_EXCLUDE = new Set([...ATTACHMENT_MARKERS, ...
|
|
21
|
+
const KEYWORD_EXCLUDE = new Set([...ATTACHMENT_MARKERS, ...SYSTEM_NOTICE_KEYWORD_STOP]);
|
|
22
|
+
const PHOTO_BUNDLE_RE = /^사진\s+\d+\s*장$/;
|
|
23
|
+
const PURE_LAUGH_RE = /^[ㅋㅎㅠㅜ]+$/u;
|
|
20
24
|
const WEEKDAY_LABELS_KO = ["일", "월", "화", "수", "목", "금", "토"];
|
|
21
25
|
const URL_RE = /\bhttps?:\/\/[^\s<>"']+|www\.[^\s<>"']+/gi;
|
|
22
26
|
const EMAIL_RE = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
@@ -53,6 +57,13 @@ const STOPWORDS = new Set([
|
|
|
53
57
|
"from",
|
|
54
58
|
"http",
|
|
55
59
|
"https",
|
|
60
|
+
"정치성향",
|
|
61
|
+
"강퇴됩니다",
|
|
62
|
+
"가려짐",
|
|
63
|
+
"초중반",
|
|
64
|
+
"비속어",
|
|
65
|
+
"반가워",
|
|
66
|
+
"닉네임",
|
|
56
67
|
]);
|
|
57
68
|
const NIGHT_HOURS = new Set([23, 0, 1, 2, 3, 4, 5]);
|
|
58
69
|
const EMOJI_RE = /\p{Extended_Pictographic}/u;
|
|
@@ -73,10 +84,17 @@ export class ReportAggregator {
|
|
|
73
84
|
attachments = new Map();
|
|
74
85
|
domains = new Map();
|
|
75
86
|
keywordCounter = new KeywordCounter();
|
|
87
|
+
repeatPhraseCounter = new RepeatPhraseCounter();
|
|
88
|
+
shopSearchTopics = new Map();
|
|
76
89
|
gapStats = new GapStreamStats();
|
|
77
90
|
dailySenderCounts = new Map();
|
|
78
91
|
laughBySender = new Map();
|
|
79
92
|
shortBySender = new Map();
|
|
93
|
+
dailyJoin = new Map();
|
|
94
|
+
dailyLeave = new Map();
|
|
95
|
+
dailyHidden = new Map();
|
|
96
|
+
dailyKick = new Map();
|
|
97
|
+
dailyNewSenders = new Map();
|
|
80
98
|
total = 0;
|
|
81
99
|
totalCharacters = 0;
|
|
82
100
|
messagesWithLinks = 0;
|
|
@@ -92,6 +110,15 @@ export class ReportAggregator {
|
|
|
92
110
|
roomJoinMessages = 0;
|
|
93
111
|
roomLeaveMessages = 0;
|
|
94
112
|
roomDeletedMessages = 0;
|
|
113
|
+
roomHiddenMessages = 0;
|
|
114
|
+
roomKickMessages = 0;
|
|
115
|
+
roomSlowOnMessages = 0;
|
|
116
|
+
roomSlowOffMessages = 0;
|
|
117
|
+
roomSubManagerMessages = 0;
|
|
118
|
+
roomManagerMessages = 0;
|
|
119
|
+
roomShopSearchMessages = 0;
|
|
120
|
+
roomPhotoBundleMessages = 0;
|
|
121
|
+
pureLaughMessages = 0;
|
|
95
122
|
prevMs = null;
|
|
96
123
|
prevSender = null;
|
|
97
124
|
runSender = null;
|
|
@@ -107,20 +134,21 @@ export class ReportAggregator {
|
|
|
107
134
|
if (this.prevSender !== null && record.sender !== this.prevSender) {
|
|
108
135
|
this.speakerSwitches += 1;
|
|
109
136
|
}
|
|
137
|
+
const dayKey = formatDate(record.date);
|
|
110
138
|
const stat = getParticipantStat(this.senderStats, record.sender);
|
|
111
139
|
if (!this.sendersRegistered.has(record.sender)) {
|
|
112
140
|
this.sendersRegistered.add(record.sender);
|
|
113
141
|
this.senderNamesNormalized.add(normalizeToken(record.sender));
|
|
142
|
+
increment(this.dailyNewSenders, dayKey);
|
|
114
143
|
}
|
|
115
|
-
const
|
|
144
|
+
const split = splitMessageForAnalysis(record.message);
|
|
145
|
+
for (const kind of split.notices)
|
|
146
|
+
this.bumpSystemNotice(kind, dayKey);
|
|
147
|
+
for (const tag of split.shopSearchTags)
|
|
148
|
+
increment(this.shopSearchTopics, tag);
|
|
149
|
+
const msg = split.userText.length > 0 ? split.userText : record.message;
|
|
116
150
|
const messageLength = msg.length;
|
|
117
|
-
const
|
|
118
|
-
if (systemNotice === "join")
|
|
119
|
-
this.roomJoinMessages += 1;
|
|
120
|
-
if (systemNotice === "leave")
|
|
121
|
-
this.roomLeaveMessages += 1;
|
|
122
|
-
if (systemNotice === "deleted")
|
|
123
|
-
this.roomDeletedMessages += 1;
|
|
151
|
+
const isPureSystem = split.notices.length > 0 && split.userText.length === 0;
|
|
124
152
|
const foundAttachments = getAttachmentMarkers(msg);
|
|
125
153
|
const foundDomains = LINK_HINT_RE.test(msg) ? getDomains(msg) : [];
|
|
126
154
|
const ms = partsToUtcMs(record.date);
|
|
@@ -128,75 +156,81 @@ export class ReportAggregator {
|
|
|
128
156
|
this.firstDate = record.date;
|
|
129
157
|
this.lastDate = record.date;
|
|
130
158
|
this.total += 1;
|
|
131
|
-
if (messageLength > 0 && EMOJI_RE.test(msg)) {
|
|
132
|
-
this.emojiMessages += 1;
|
|
133
|
-
}
|
|
134
|
-
if (messageLength > 0 && LAUGH_RE.test(msg)) {
|
|
135
|
-
this.laughMessages += 1;
|
|
136
|
-
increment(this.laughBySender, record.sender);
|
|
137
|
-
}
|
|
138
|
-
const trimmed = msg.trim();
|
|
139
|
-
if (trimmed.length > 0 && trimmed.length <= 3) {
|
|
140
|
-
this.shortMessages += 1;
|
|
141
|
-
increment(this.shortBySender, record.sender);
|
|
142
|
-
}
|
|
143
|
-
if (msg.includes("?") || msg.includes("?")) {
|
|
144
|
-
this.questionMessages += 1;
|
|
145
|
-
}
|
|
146
159
|
const wi = weekdayIndex(record.date);
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (NIGHT_HOURS.has(record.date.hour)) {
|
|
151
|
-
this.nightMessages += 1;
|
|
152
|
-
stat.nightMessages += 1;
|
|
153
|
-
}
|
|
154
|
-
if (this.prevMs !== null) {
|
|
155
|
-
const delta = ms - this.prevMs;
|
|
156
|
-
this.gapStats.add(delta);
|
|
157
|
-
}
|
|
158
|
-
this.prevMs = ms;
|
|
159
|
-
if (record.sender === this.prevSender) {
|
|
160
|
-
this.runLen += 1;
|
|
161
|
-
if (this.runLen >= 3) {
|
|
162
|
-
this.monologueMessages += 1;
|
|
160
|
+
if (!isPureSystem) {
|
|
161
|
+
if (messageLength > 0 && EMOJI_RE.test(msg)) {
|
|
162
|
+
this.emojiMessages += 1;
|
|
163
163
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const prevStat = getParticipantStat(this.senderStats, this.prevSender);
|
|
168
|
-
prevStat.maxConsecutive = Math.max(prevStat.maxConsecutive, this.runLen);
|
|
164
|
+
if (messageLength > 0 && LAUGH_RE.test(msg)) {
|
|
165
|
+
this.laughMessages += 1;
|
|
166
|
+
increment(this.laughBySender, record.sender);
|
|
169
167
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.
|
|
168
|
+
if (messageLength > 0 && PURE_LAUGH_RE.test(msg.trim())) {
|
|
169
|
+
this.pureLaughMessages += 1;
|
|
170
|
+
}
|
|
171
|
+
const trimmed = msg.trim();
|
|
172
|
+
if (trimmed.length > 0 && trimmed.length <= 3) {
|
|
173
|
+
this.shortMessages += 1;
|
|
174
|
+
increment(this.shortBySender, record.sender);
|
|
175
|
+
}
|
|
176
|
+
if (msg.includes("?") || msg.includes("?")) {
|
|
177
|
+
this.questionMessages += 1;
|
|
178
|
+
}
|
|
179
|
+
if (wi === 0 || wi === 6) {
|
|
180
|
+
this.weekendMessages += 1;
|
|
181
|
+
}
|
|
182
|
+
if (NIGHT_HOURS.has(record.date.hour)) {
|
|
183
|
+
this.nightMessages += 1;
|
|
184
|
+
stat.nightMessages += 1;
|
|
185
|
+
}
|
|
186
|
+
if (this.prevMs !== null) {
|
|
187
|
+
const delta = ms - this.prevMs;
|
|
188
|
+
this.gapStats.add(delta);
|
|
189
|
+
}
|
|
190
|
+
this.prevMs = ms;
|
|
191
|
+
if (record.sender === this.prevSender) {
|
|
192
|
+
this.runLen += 1;
|
|
193
|
+
if (this.runLen >= 3) {
|
|
194
|
+
this.monologueMessages += 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
if (this.prevSender !== null && this.runSender !== null) {
|
|
199
|
+
const prevStat = getParticipantStat(this.senderStats, this.prevSender);
|
|
200
|
+
prevStat.maxConsecutive = Math.max(prevStat.maxConsecutive, this.runLen);
|
|
201
|
+
}
|
|
202
|
+
this.runSender = record.sender;
|
|
203
|
+
this.runLen = 1;
|
|
204
|
+
}
|
|
205
|
+
this.prevSender = record.sender;
|
|
206
|
+
stat.messages += 1;
|
|
207
|
+
stat.characters += messageLength;
|
|
208
|
+
this.totalCharacters += messageLength;
|
|
209
|
+
if (foundAttachments.length > 0) {
|
|
210
|
+
stat.attachmentMessages += 1;
|
|
211
|
+
this.messagesWithAttachments += 1;
|
|
212
|
+
for (const marker of foundAttachments)
|
|
213
|
+
increment(this.attachments, marker);
|
|
214
|
+
}
|
|
215
|
+
if (foundDomains.length > 0) {
|
|
216
|
+
stat.linkMessages += 1;
|
|
217
|
+
this.messagesWithLinks += 1;
|
|
218
|
+
for (const domain of foundDomains)
|
|
219
|
+
increment(this.domains, domain);
|
|
220
|
+
}
|
|
221
|
+
if (messageLength >= 2 &&
|
|
222
|
+
HAS_TOKEN_CHAR_RE.test(msg) &&
|
|
223
|
+
!isOpenChatBoilerplate(msg) &&
|
|
224
|
+
shouldExtractKeywords(msg, foundAttachments)) {
|
|
225
|
+
for (const keyword of extractKeywords(msg, this.senderNamesNormalized)) {
|
|
226
|
+
this.keywordCounter.add(keyword);
|
|
227
|
+
}
|
|
228
|
+
if (messageLength >= 12 && !isOpenChatBoilerplate(msg))
|
|
229
|
+
this.repeatPhraseCounter.add(msg);
|
|
195
230
|
}
|
|
196
231
|
}
|
|
197
|
-
const dayKey = formatDate(record.date);
|
|
198
232
|
increment(this.daily, dayKey);
|
|
199
|
-
{
|
|
233
|
+
if (!isPureSystem) {
|
|
200
234
|
let perDay = this.dailySenderCounts.get(dayKey);
|
|
201
235
|
if (!perDay) {
|
|
202
236
|
perDay = new Map();
|
|
@@ -208,6 +242,49 @@ export class ReportAggregator {
|
|
|
208
242
|
this.hourly[record.date.hour] = (this.hourly[record.date.hour] ?? 0) + 1;
|
|
209
243
|
this.weekdays[wi] = (this.weekdays[wi] ?? 0) + 1;
|
|
210
244
|
}
|
|
245
|
+
bumpSystemNotice(kind, dayKey) {
|
|
246
|
+
switch (kind) {
|
|
247
|
+
case "join":
|
|
248
|
+
this.roomJoinMessages += 1;
|
|
249
|
+
increment(this.dailyJoin, dayKey);
|
|
250
|
+
break;
|
|
251
|
+
case "leave":
|
|
252
|
+
this.roomLeaveMessages += 1;
|
|
253
|
+
increment(this.dailyLeave, dayKey);
|
|
254
|
+
break;
|
|
255
|
+
case "deleted":
|
|
256
|
+
this.roomDeletedMessages += 1;
|
|
257
|
+
break;
|
|
258
|
+
case "hidden":
|
|
259
|
+
this.roomHiddenMessages += 1;
|
|
260
|
+
increment(this.dailyHidden, dayKey);
|
|
261
|
+
break;
|
|
262
|
+
case "kick":
|
|
263
|
+
this.roomKickMessages += 1;
|
|
264
|
+
increment(this.dailyKick, dayKey);
|
|
265
|
+
break;
|
|
266
|
+
case "slowModeOn":
|
|
267
|
+
this.roomSlowOnMessages += 1;
|
|
268
|
+
break;
|
|
269
|
+
case "slowModeOff":
|
|
270
|
+
this.roomSlowOffMessages += 1;
|
|
271
|
+
break;
|
|
272
|
+
case "subManager":
|
|
273
|
+
this.roomSubManagerMessages += 1;
|
|
274
|
+
break;
|
|
275
|
+
case "manager":
|
|
276
|
+
this.roomManagerMessages += 1;
|
|
277
|
+
break;
|
|
278
|
+
case "shopSearch":
|
|
279
|
+
this.roomShopSearchMessages += 1;
|
|
280
|
+
break;
|
|
281
|
+
case "photoBundle":
|
|
282
|
+
this.roomPhotoBundleMessages += 1;
|
|
283
|
+
break;
|
|
284
|
+
default:
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
211
288
|
finalize(meta) {
|
|
212
289
|
if (this.prevSender !== null && this.runSender !== null) {
|
|
213
290
|
const prevStat = getParticipantStat(this.senderStats, this.prevSender);
|
|
@@ -303,6 +380,7 @@ export class ReportAggregator {
|
|
|
303
380
|
const uniqueDomainCount = this.domains.size;
|
|
304
381
|
const replyGapCoeffVariation = this.gapStats.coeffVariation();
|
|
305
382
|
const monologueMessagesPercent = total > 0 ? round((this.monologueMessages / total) * 100, 1) : 0;
|
|
383
|
+
const lexicalTypeRichnessPercent = this.keywordCounter.typeTokenRichnessPercent();
|
|
306
384
|
const insights = {
|
|
307
385
|
weekendSharePercent,
|
|
308
386
|
participantGini,
|
|
@@ -327,8 +405,13 @@ export class ReportAggregator {
|
|
|
327
405
|
peakDaySharePercent,
|
|
328
406
|
uniqueDomainCount,
|
|
329
407
|
replyGapCoeffVariation,
|
|
408
|
+
lexicalTypeRichnessPercent,
|
|
330
409
|
};
|
|
331
410
|
const dailySorted = [...this.daily.entries()].map(([date, count]) => ({ date, count })).sort((a, b) => a.date.localeCompare(b.date));
|
|
411
|
+
const burstDays = computeBurstDays(dailySorted);
|
|
412
|
+
const activityArc = computeActivityArc(dailySorted);
|
|
413
|
+
const conversationPace = computeConversationPace(insights);
|
|
414
|
+
const roomPulse = buildRoomPulse(sortedDays, this.dailyJoin, this.dailyLeave, this.dailyHidden, this.dailyKick, this.dailyNewSenders);
|
|
332
415
|
const laughByAlias = new Map();
|
|
333
416
|
const shortByAlias = new Map();
|
|
334
417
|
for (const [raw, c] of this.laughBySender) {
|
|
@@ -361,6 +444,10 @@ export class ReportAggregator {
|
|
|
361
444
|
shortMessages: this.shortMessages,
|
|
362
445
|
laughBySender: laughByAlias,
|
|
363
446
|
shortBySender: shortByAlias,
|
|
447
|
+
burstDays,
|
|
448
|
+
activityArc,
|
|
449
|
+
conversationPace,
|
|
450
|
+
roomPulse,
|
|
364
451
|
});
|
|
365
452
|
const highlights = buildHighlights({
|
|
366
453
|
total,
|
|
@@ -383,6 +470,16 @@ export class ReportAggregator {
|
|
|
383
470
|
roomJoinMessages: this.roomJoinMessages,
|
|
384
471
|
roomLeaveMessages: this.roomLeaveMessages,
|
|
385
472
|
roomDeletedMessages: this.roomDeletedMessages,
|
|
473
|
+
roomHiddenMessages: this.roomHiddenMessages,
|
|
474
|
+
roomKickMessages: this.roomKickMessages,
|
|
475
|
+
pureLaughMessages: this.pureLaughMessages,
|
|
476
|
+
repeatedPhraseCount: this.repeatPhraseCounter.top(1, 3)[0]?.count ?? 0,
|
|
477
|
+
burstDays,
|
|
478
|
+
activityArc,
|
|
479
|
+
conversationPace,
|
|
480
|
+
roomPulse,
|
|
481
|
+
lexicalTypeRichnessPercent,
|
|
482
|
+
speakerSwitchRatePer100,
|
|
386
483
|
});
|
|
387
484
|
return {
|
|
388
485
|
generatedAt: new Date().toISOString(),
|
|
@@ -423,15 +520,26 @@ export class ReportAggregator {
|
|
|
423
520
|
attachments: topCounts(this.attachments, this.top),
|
|
424
521
|
domains: topCounts(this.domains, this.top),
|
|
425
522
|
keywords: this.keywordCounter.topCounts(this.top),
|
|
426
|
-
roomEvents: {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
523
|
+
roomEvents: buildRoomEventStats(total, {
|
|
524
|
+
join: this.roomJoinMessages,
|
|
525
|
+
leave: this.roomLeaveMessages,
|
|
526
|
+
deleted: this.roomDeletedMessages,
|
|
527
|
+
hidden: this.roomHiddenMessages,
|
|
528
|
+
kick: this.roomKickMessages,
|
|
529
|
+
slowModeOn: this.roomSlowOnMessages,
|
|
530
|
+
slowModeOff: this.roomSlowOffMessages,
|
|
531
|
+
subManager: this.roomSubManagerMessages,
|
|
532
|
+
manager: this.roomManagerMessages,
|
|
533
|
+
shopSearch: this.roomShopSearchMessages,
|
|
534
|
+
photoBundle: this.roomPhotoBundleMessages,
|
|
535
|
+
}),
|
|
536
|
+
repeatedPhrases: this.repeatPhraseCounter.top(8, 3),
|
|
537
|
+
shopSearchTopics: topCounts(this.shopSearchTopics, 10),
|
|
538
|
+
pureLaughMessages: this.pureLaughMessages,
|
|
539
|
+
conversationPace,
|
|
540
|
+
burstDays,
|
|
541
|
+
activityArc,
|
|
542
|
+
roomPulse,
|
|
435
543
|
highlights,
|
|
436
544
|
story,
|
|
437
545
|
};
|
|
@@ -486,7 +594,45 @@ function shouldExtractKeywords(message, attachmentMarkers) {
|
|
|
486
594
|
return true;
|
|
487
595
|
}
|
|
488
596
|
function getAttachmentMarkers(message) {
|
|
489
|
-
|
|
597
|
+
const found = ATTACHMENT_MARKERS.filter((marker) => message.includes(marker));
|
|
598
|
+
const t = message.trim();
|
|
599
|
+
if (PHOTO_BUNDLE_RE.test(t) && !found.includes("사진")) {
|
|
600
|
+
found.push("사진");
|
|
601
|
+
}
|
|
602
|
+
return found;
|
|
603
|
+
}
|
|
604
|
+
function buildRoomEventStats(total, c) {
|
|
605
|
+
const sum = c.join +
|
|
606
|
+
c.leave +
|
|
607
|
+
c.deleted +
|
|
608
|
+
c.hidden +
|
|
609
|
+
c.kick +
|
|
610
|
+
c.slowModeOn +
|
|
611
|
+
c.slowModeOff +
|
|
612
|
+
c.subManager +
|
|
613
|
+
c.manager +
|
|
614
|
+
c.shopSearch +
|
|
615
|
+
c.photoBundle;
|
|
616
|
+
const pct = (n) => (total > 0 ? round((n / total) * 100, 2) : 0);
|
|
617
|
+
return {
|
|
618
|
+
joinCount: c.join,
|
|
619
|
+
leaveCount: c.leave,
|
|
620
|
+
deletedCount: c.deleted,
|
|
621
|
+
hiddenCount: c.hidden,
|
|
622
|
+
kickCount: c.kick,
|
|
623
|
+
slowModeOnCount: c.slowModeOn,
|
|
624
|
+
slowModeOffCount: c.slowModeOff,
|
|
625
|
+
subManagerCount: c.subManager,
|
|
626
|
+
managerCount: c.manager,
|
|
627
|
+
shopSearchCount: c.shopSearch,
|
|
628
|
+
photoBundleCount: c.photoBundle,
|
|
629
|
+
total: sum,
|
|
630
|
+
joinSharePercent: pct(c.join),
|
|
631
|
+
leaveSharePercent: pct(c.leave),
|
|
632
|
+
deletedSharePercent: pct(c.deleted),
|
|
633
|
+
hiddenSharePercent: pct(c.hidden),
|
|
634
|
+
kickSharePercent: pct(c.kick),
|
|
635
|
+
};
|
|
490
636
|
}
|
|
491
637
|
function getDomains(message) {
|
|
492
638
|
const matches = message.match(URL_RE) ?? [];
|
|
@@ -714,15 +860,72 @@ function buildHighlights(input) {
|
|
|
714
860
|
if (input.monologueMessagesPercent >= 25) {
|
|
715
861
|
out.push(`같은 사람 **3연속 이상** 메시지가 전체의 **${input.monologueMessagesPercent}%** — 긴 설명·정리 구간이 잦을 수 있어요.`);
|
|
716
862
|
}
|
|
717
|
-
const sysTotal = input.roomJoinMessages +
|
|
863
|
+
const sysTotal = input.roomJoinMessages +
|
|
864
|
+
input.roomLeaveMessages +
|
|
865
|
+
input.roomDeletedMessages +
|
|
866
|
+
input.roomHiddenMessages +
|
|
867
|
+
input.roomKickMessages;
|
|
718
868
|
if (sysTotal > 0) {
|
|
719
869
|
const parts = [
|
|
720
870
|
input.roomJoinMessages > 0 ? `들어옴 ${input.roomJoinMessages}` : "",
|
|
721
871
|
input.roomLeaveMessages > 0 ? `나감 ${input.roomLeaveMessages}` : "",
|
|
722
|
-
input.roomDeletedMessages > 0 ? `삭제
|
|
872
|
+
input.roomDeletedMessages > 0 ? `삭제 ${input.roomDeletedMessages}` : "",
|
|
873
|
+
input.roomHiddenMessages > 0 ? `가림 ${input.roomHiddenMessages}` : "",
|
|
874
|
+
input.roomKickMessages > 0 ? `강퇴 ${input.roomKickMessages}` : "",
|
|
723
875
|
].filter(Boolean);
|
|
724
|
-
out.push(`카카오톡
|
|
876
|
+
out.push(`카카오톡 **시스템·운영 알림** **${sysTotal}**건(${parts.join(" · ")}) — 본문 키워드와 분리했어요.`);
|
|
877
|
+
}
|
|
878
|
+
if (input.pureLaughMessages > 0) {
|
|
879
|
+
out.push(`**ㅋㅋ만** 보낸 리액션 메시지는 **${input.pureLaughMessages}**건이에요.`);
|
|
725
880
|
}
|
|
726
|
-
|
|
881
|
+
if (input.repeatedPhraseCount >= 10) {
|
|
882
|
+
out.push(`같은 문장이 **${input.repeatedPhraseCount}회** 반복된 카피페asta급 문구도 있어요.`);
|
|
883
|
+
}
|
|
884
|
+
if (input.burstDays.length > 0) {
|
|
885
|
+
const top = input.burstDays[0];
|
|
886
|
+
const labels = input.burstDays
|
|
887
|
+
.slice(0, 3)
|
|
888
|
+
.map((d) => formatDayMdHighlight(d.date))
|
|
889
|
+
.join(" · ");
|
|
890
|
+
out.push(`메시지가 평소보다 몰린 날 **${input.burstDays.length}일** — 최고는 **${formatDayMdHighlight(top.date)}**(${formatCompactCount(top.count)}건). ${labels}`);
|
|
891
|
+
}
|
|
892
|
+
const head = input.activityArc.find((a) => a.id === "head");
|
|
893
|
+
const tail = input.activityArc.find((a) => a.id === "tail");
|
|
894
|
+
if (head && tail && head.messages > 0 && tail.messages > 0) {
|
|
895
|
+
const ratio = round(tail.messages / head.messages, 2);
|
|
896
|
+
if (ratio >= 1.25) {
|
|
897
|
+
out.push(`마지막 7일이 처음 7일보다 **${ratio}배** 활발 — 대화가 뜨거워지는 구간이 있었어요.`);
|
|
898
|
+
}
|
|
899
|
+
else if (ratio <= 0.8) {
|
|
900
|
+
out.push(`처음 7일이 마지막보다 더 붐볐어요(후반은 처음의 **${Math.round(ratio * 100)}%** 수준).`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (input.lexicalTypeRichnessPercent !== null && input.lexicalTypeRichnessPercent >= 18) {
|
|
904
|
+
out.push(`본문 단어는 **${input.lexicalTypeRichnessPercent}%** 정도로 서로 다른 표현이 많이 섞였어요.`);
|
|
905
|
+
}
|
|
906
|
+
const pace = input.conversationPace;
|
|
907
|
+
out.push(`대화 템포는 **${pace.emoji} ${pace.label}** — ${pace.detail}`);
|
|
908
|
+
const peakHidden = [...input.roomPulse].sort((a, b) => b.hidden - a.hidden)[0];
|
|
909
|
+
if (peakHidden && peakHidden.hidden >= 5) {
|
|
910
|
+
out.push(`가림 알림이 가장 많았던 날은 **${formatDayMdHighlight(peakHidden.date)}**(${peakHidden.hidden}건)이에요.`);
|
|
911
|
+
}
|
|
912
|
+
const peakJoin = [...input.roomPulse].sort((a, b) => b.join - a.join)[0];
|
|
913
|
+
if (peakJoin && peakJoin.join >= 20) {
|
|
914
|
+
out.push(`입장이 가장 몰린 날은 **${formatDayMdHighlight(peakJoin.date)}** — **${peakJoin.join}**명 들어왔어요.`);
|
|
915
|
+
}
|
|
916
|
+
return out.slice(0, 14);
|
|
917
|
+
}
|
|
918
|
+
function formatDayMdHighlight(ymd) {
|
|
919
|
+
const p = ymd.split("-");
|
|
920
|
+
if (p.length === 3)
|
|
921
|
+
return `${Number(p[1])}/${Number(p[2])}`;
|
|
922
|
+
return ymd;
|
|
923
|
+
}
|
|
924
|
+
function formatCompactCount(n) {
|
|
925
|
+
if (n >= 10_000)
|
|
926
|
+
return `${Math.round(n / 1000) / 10}만`;
|
|
927
|
+
if (n >= 1000)
|
|
928
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
929
|
+
return String(n);
|
|
727
930
|
}
|
|
728
931
|
//# sourceMappingURL=aggregator.js.map
|