kakaotalk-chat-analyzer 0.2.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +190 -0
  3. package/dist/src/analysis.d.ts +8 -0
  4. package/dist/src/analysis.js +406 -0
  5. package/dist/src/analysis.js.map +1 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +177 -0
  8. package/dist/src/cli.js.map +1 -0
  9. package/dist/src/config.d.ts +19 -0
  10. package/dist/src/config.js +59 -0
  11. package/dist/src/config.js.map +1 -0
  12. package/dist/src/date.d.ts +7 -0
  13. package/dist/src/date.js +32 -0
  14. package/dist/src/date.js.map +1 -0
  15. package/dist/src/encoding.d.ts +5 -0
  16. package/dist/src/encoding.js +53 -0
  17. package/dist/src/encoding.js.map +1 -0
  18. package/dist/src/parser.d.ts +3 -0
  19. package/dist/src/parser.js +206 -0
  20. package/dist/src/parser.js.map +1 -0
  21. package/dist/src/providers/brewpage.d.ts +8 -0
  22. package/dist/src/providers/brewpage.js +96 -0
  23. package/dist/src/providers/brewpage.js.map +1 -0
  24. package/dist/src/providers/cloudflare.d.ts +5 -0
  25. package/dist/src/providers/cloudflare.js +7 -0
  26. package/dist/src/providers/cloudflare.js.map +1 -0
  27. package/dist/src/providers/index.d.ts +3 -0
  28. package/dist/src/providers/index.js +20 -0
  29. package/dist/src/providers/index.js.map +1 -0
  30. package/dist/src/providers/tempfile.d.ts +8 -0
  31. package/dist/src/providers/tempfile.js +60 -0
  32. package/dist/src/providers/tempfile.js.map +1 -0
  33. package/dist/src/providers/types.d.ts +32 -0
  34. package/dist/src/providers/types.js +2 -0
  35. package/dist/src/providers/types.js.map +1 -0
  36. package/dist/src/report.d.ts +2 -0
  37. package/dist/src/report.js +227 -0
  38. package/dist/src/report.js.map +1 -0
  39. package/dist/src/types.d.ts +97 -0
  40. package/dist/src/types.js +2 -0
  41. package/dist/src/types.js.map +1 -0
  42. package/dist/src/version.d.ts +2 -0
  43. package/dist/src/version.js +3 -0
  44. package/dist/src/version.js.map +1 -0
  45. package/package.json +51 -0
@@ -0,0 +1,96 @@
1
+ import { USER_AGENT } from "../version.js";
2
+ const DEFAULT_ENDPOINT = "https://brewpage.app/api/html";
3
+ export class BrewPageProvider {
4
+ fetchImpl;
5
+ endpoint;
6
+ name = "brewpage";
7
+ constructor(fetchImpl = globalThis.fetch, endpoint = DEFAULT_ENDPOINT) {
8
+ this.fetchImpl = fetchImpl;
9
+ this.endpoint = endpoint;
10
+ }
11
+ async publish(request) {
12
+ const ttlDays = clampTtl(request.ttlDays);
13
+ const url = new URL(this.endpoint);
14
+ url.searchParams.set("ns", request.namespace);
15
+ url.searchParams.set("ttl", String(ttlDays));
16
+ const headers = {
17
+ "Content-Type": "application/json",
18
+ "User-Agent": USER_AGENT,
19
+ };
20
+ if (request.ownerToken) {
21
+ headers["X-Owner-Token"] = request.ownerToken;
22
+ }
23
+ const response = await this.fetchImpl(url.toString(), {
24
+ method: "POST",
25
+ headers,
26
+ body: JSON.stringify({
27
+ content: request.html,
28
+ filename: request.title,
29
+ showTopBar: true,
30
+ }),
31
+ });
32
+ const text = await response.text();
33
+ let body = null;
34
+ try {
35
+ body = text ? JSON.parse(text) : null;
36
+ }
37
+ catch {
38
+ body = null;
39
+ }
40
+ if (!response.ok) {
41
+ throw new Error(`BrewPage upload failed: HTTP ${response.status} ${response.statusText}${text ? ` - ${text.slice(0, 300)}` : ""}`);
42
+ }
43
+ const parsed = parseBrewPageResponse(body);
44
+ if (!parsed.link) {
45
+ throw new Error(`BrewPage upload response did not include a share URL: ${text.slice(0, 300)}`);
46
+ }
47
+ return {
48
+ provider: this.name,
49
+ link: parsed.link,
50
+ id: parsed.id ?? idFromLink(parsed.link),
51
+ ownerToken: parsed.ownerToken,
52
+ ownerLink: parsed.ownerLink,
53
+ expiresAt: parsed.expiresAt ?? expiryFromTtl(ttlDays),
54
+ };
55
+ }
56
+ }
57
+ function parseBrewPageResponse(body) {
58
+ const record = asRecord(body);
59
+ const data = asRecord(record?.data);
60
+ return {
61
+ link: stringField(record, "link") ?? stringField(record, "url") ?? stringField(data, "link") ?? stringField(data, "url"),
62
+ id: stringField(record, "id") ?? stringField(record, "siteId") ?? stringField(data, "id") ?? stringField(data, "siteId"),
63
+ ownerToken: stringField(record, "ownerToken") ??
64
+ stringField(record, "deleteToken") ??
65
+ stringField(data, "ownerToken") ??
66
+ stringField(data, "deleteToken"),
67
+ ownerLink: stringField(record, "ownerLink") ?? stringField(data, "ownerLink"),
68
+ expiresAt: stringField(record, "expiresAt") ?? stringField(data, "expiresAt"),
69
+ };
70
+ }
71
+ function asRecord(value) {
72
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
73
+ }
74
+ function stringField(record, key) {
75
+ const value = record?.[key];
76
+ return typeof value === "string" && value.length > 0 ? value : undefined;
77
+ }
78
+ function idFromLink(link) {
79
+ try {
80
+ const url = new URL(link);
81
+ const parts = url.pathname.split("/").filter(Boolean);
82
+ return parts.at(-1);
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
88
+ function expiryFromTtl(ttlDays) {
89
+ return new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000).toISOString();
90
+ }
91
+ function clampTtl(ttlDays) {
92
+ if (!Number.isFinite(ttlDays))
93
+ return 30;
94
+ return Math.max(1, Math.min(30, Math.round(ttlDays)));
95
+ }
96
+ //# sourceMappingURL=brewpage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brewpage.js","sourceRoot":"","sources":["../../../src/providers/brewpage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { HostName, PublishProvider, PublishRequest, PublishResult } from "./types.js";
2
+ export declare class CloudflareProvider implements PublishProvider {
3
+ readonly name: HostName;
4
+ publish(_request: PublishRequest): Promise<PublishResult>;
5
+ }
@@ -0,0 +1,7 @@
1
+ export class CloudflareProvider {
2
+ name = "cloudflare";
3
+ async publish(_request) {
4
+ throw new Error("Cloudflare Pages is not a zero-login host. Use --host brewpage for the default no-signup flow, or deploy the local HTML manually with a Cloudflare account.");
5
+ }
6
+ }
7
+ //# sourceMappingURL=cloudflare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../../src/providers/cloudflare.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,kBAAkB;IACpB,IAAI,GAAa,YAAY,CAAC;IAEvC,KAAK,CAAC,OAAO,CAAC,QAAwB;QACpC,MAAM,IAAI,KAAK,CACb,6JAA6J,CAC9J,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ import type { HostName, PublishProvider } from "./types.js";
2
+ export declare function createProvider(host: HostName): PublishProvider;
3
+ export declare function parseHostName(value: string): HostName;
@@ -0,0 +1,20 @@
1
+ import { BrewPageProvider } from "./brewpage.js";
2
+ import { CloudflareProvider } from "./cloudflare.js";
3
+ import { TempFileProvider } from "./tempfile.js";
4
+ export function createProvider(host) {
5
+ switch (host) {
6
+ case "brewpage":
7
+ return new BrewPageProvider();
8
+ case "tempfile":
9
+ return new TempFileProvider();
10
+ case "cloudflare":
11
+ return new CloudflareProvider();
12
+ }
13
+ }
14
+ export function parseHostName(value) {
15
+ if (value === "brewpage" || value === "tempfile" || value === "cloudflare") {
16
+ return value;
17
+ }
18
+ throw new Error(`Unsupported host "${value}". Expected brewpage, tempfile, or cloudflare.`);
19
+ }
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/providers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGjD,MAAM,UAAU,cAAc,CAAC,IAAc;IAC3C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU;YACb,OAAO,IAAI,gBAAgB,EAAE,CAAC;QAChC,KAAK,UAAU;YACb,OAAO,IAAI,gBAAgB,EAAE,CAAC;QAChC,KAAK,YAAY;YACf,OAAO,IAAI,kBAAkB,EAAE,CAAC;IACpC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,YAAY,EAAE,CAAC;QAC3E,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,qBAAqB,KAAK,gDAAgD,CAAC,CAAC;AAC9F,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { FetchLike, HostName, PublishProvider, PublishRequest, PublishResult } from "./types.js";
2
+ export declare class TempFileProvider implements PublishProvider {
3
+ private readonly fetchImpl;
4
+ private readonly endpoint;
5
+ readonly name: HostName;
6
+ constructor(fetchImpl?: FetchLike, endpoint?: string);
7
+ publish(request: PublishRequest): Promise<PublishResult>;
8
+ }
@@ -0,0 +1,60 @@
1
+ import { USER_AGENT } from "../version.js";
2
+ const DEFAULT_ENDPOINT = "https://tempfile.org/api/upload/local";
3
+ export class TempFileProvider {
4
+ fetchImpl;
5
+ endpoint;
6
+ name = "tempfile";
7
+ constructor(fetchImpl = globalThis.fetch, endpoint = DEFAULT_ENDPOINT) {
8
+ this.fetchImpl = fetchImpl;
9
+ this.endpoint = endpoint;
10
+ }
11
+ async publish(request) {
12
+ const form = new FormData();
13
+ form.append("files", new Blob([request.html], { type: "text/html;charset=utf-8" }), "kakao-chat-report.html");
14
+ form.append("expiryHours", String(Math.max(1, Math.round(request.ttlDays * 24))));
15
+ const response = await this.fetchImpl(this.endpoint, {
16
+ method: "POST",
17
+ headers: {
18
+ "User-Agent": USER_AGENT,
19
+ },
20
+ body: form,
21
+ });
22
+ const text = await response.text();
23
+ let body = null;
24
+ try {
25
+ body = text ? JSON.parse(text) : null;
26
+ }
27
+ catch {
28
+ body = null;
29
+ }
30
+ if (!response.ok) {
31
+ throw new Error(`TempFile upload failed: HTTP ${response.status} ${response.statusText}${text ? ` - ${text.slice(0, 300)}` : ""}`);
32
+ }
33
+ const link = parseTempFileLink(body);
34
+ if (!link) {
35
+ throw new Error(`TempFile upload response did not include a file URL: ${text.slice(0, 300)}`);
36
+ }
37
+ return {
38
+ provider: this.name,
39
+ link,
40
+ expiresAt: expiryFromTtl(request.ttlDays),
41
+ };
42
+ }
43
+ }
44
+ function parseTempFileLink(body) {
45
+ const record = asRecord(body);
46
+ const files = Array.isArray(record?.files) ? record.files : [];
47
+ const firstFile = asRecord(files[0]);
48
+ return stringField(firstFile, "url") ?? stringField(asRecord(record?.file), "url");
49
+ }
50
+ function asRecord(value) {
51
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
52
+ }
53
+ function stringField(record, key) {
54
+ const value = record?.[key];
55
+ return typeof value === "string" && value.length > 0 ? value : undefined;
56
+ }
57
+ function expiryFromTtl(ttlDays) {
58
+ return new Date(Date.now() + Math.max(1, ttlDays) * 24 * 60 * 60 * 1000).toISOString();
59
+ }
60
+ //# sourceMappingURL=tempfile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tempfile.js","sourceRoot":"","sources":["../../../src/providers/tempfile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ export type HostName = "brewpage" | "tempfile" | "cloudflare";
2
+ export interface PublishRequest {
3
+ html: string;
4
+ ttlDays: number;
5
+ namespace: string;
6
+ title: string;
7
+ ownerToken?: string;
8
+ }
9
+ export interface PublishResult {
10
+ provider: HostName;
11
+ link: string;
12
+ id?: string;
13
+ ownerToken?: string;
14
+ ownerLink?: string;
15
+ expiresAt?: string;
16
+ }
17
+ export interface PublishProvider {
18
+ readonly name: HostName;
19
+ publish(request: PublishRequest): Promise<PublishResult>;
20
+ }
21
+ export interface FetchResponseLike {
22
+ ok: boolean;
23
+ status: number;
24
+ statusText: string;
25
+ json(): Promise<unknown>;
26
+ text(): Promise<string>;
27
+ }
28
+ export type FetchLike = (url: string, init: {
29
+ method: string;
30
+ headers?: Record<string, string>;
31
+ body?: unknown;
32
+ }) => Promise<FetchResponseLike>;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/providers/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ import type { ReportData } from "./types.js";
2
+ export declare function renderReportHtml(data: ReportData): string;
@@ -0,0 +1,227 @@
1
+ const FIVE_MIB = 5 * 1024 * 1024;
2
+ export function renderReportHtml(data) {
3
+ const html = `<!doctype html>
4
+ <html lang="ko">
5
+ <head>
6
+ <meta charset="utf-8">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <title>카카오톡 대화 리포트 · kca</title>
9
+ <style>
10
+ :root {
11
+ color-scheme: light;
12
+ --bg: #f4f1ea;
13
+ --bg2: #e8e2d6;
14
+ --ink: #141a1f;
15
+ --muted: #5c6670;
16
+ --line: #d4cdc2;
17
+ --panel: #fffcf7;
18
+ --accent: #0f6b5c;
19
+ --accent2: #c45c2a;
20
+ --gold: #b8860b;
21
+ --shadow: 0 18px 50px rgba(20, 26, 31, 0.08);
22
+ font-family: "Pretendard Variable", Pretendard, "Apple SD Gothic Neo", "Malgun Gothic", ui-sans-serif, system-ui, sans-serif;
23
+ }
24
+ * { box-sizing: border-box; }
25
+ body { margin: 0; background: radial-gradient(1200px 500px at 10% -10%, rgba(15,107,92,0.12), transparent), linear-gradient(180deg, var(--bg), var(--bg2)); color: var(--ink); }
26
+ main { width: min(1200px, calc(100% - 36px)); margin: 0 auto; padding: 36px 0 56px; }
27
+ .hero { display: grid; gap: 20px; grid-template-columns: 1.35fr 1fr; align-items: stretch; padding-bottom: 28px; }
28
+ h1 { margin: 0; font-size: clamp(28px, 4.2vw, 48px); line-height: 1.08; letter-spacing: -0.03em; font-weight: 800; }
29
+ .sub { margin: 12px 0 0; color: var(--muted); line-height: 1.65; font-size: 15px; max-width: 52ch; }
30
+ .badge-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
31
+ .badge { font-size: 12px; padding: 6px 10px; border-radius: 999px; border: 1px solid var(--line); background: rgba(255,255,255,0.65); color: var(--muted); }
32
+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 18px 20px; box-shadow: var(--shadow); }
33
+ .side-card { display: flex; flex-direction: column; gap: 10px; justify-content: center; }
34
+ .side-card p { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.5; }
35
+ .side-card strong { color: var(--ink); }
36
+ h2 { margin: 0 0 12px; font-size: 17px; font-weight: 750; letter-spacing: -0.02em; }
37
+ .grid { display: grid; gap: 14px; }
38
+ .metrics { grid-template-columns: repeat(4, minmax(0, 1fr)); }
39
+ .metrics6 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
40
+ .two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
41
+ .three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
42
+ .metric .label { display: block; color: var(--muted); font-size: 12px; margin-bottom: 6px; }
43
+ .metric .value { font-size: 26px; font-weight: 800; line-height: 1; letter-spacing: -0.02em; }
44
+ .metric .sub { display: block; color: var(--muted); font-size: 11px; margin-top: 6px; }
45
+ .highlights { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
46
+ .highlights li { padding: 12px 14px; border-radius: 10px; background: linear-gradient(120deg, rgba(15,107,92,0.08), rgba(196,92,42,0.06)); border: 1px solid rgba(15,107,92,0.15); font-size: 14px; line-height: 1.55; }
47
+ .highlights strong { color: var(--accent); font-weight: 750; }
48
+ .bars { display: grid; gap: 8px; }
49
+ .bar-row { display: grid; grid-template-columns: minmax(72px, 1fr) minmax(0, 2.2fr) 52px; gap: 10px; align-items: center; min-height: 22px; }
50
+ .bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
51
+ .bar-track { height: 9px; background: #e5dfd4; border-radius: 999px; overflow: hidden; }
52
+ .bar-fill { height: 100%; width: var(--w); background: linear-gradient(90deg, var(--accent), #1a9d87); border-radius: inherit; }
53
+ .bar-value { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; font-size: 12px; }
54
+ .calendar { display: grid; gap: 3px; grid-template-columns: repeat(auto-fill, minmax(34px, 1fr)); }
55
+ .day { aspect-ratio: 1; border-radius: 6px; background: color-mix(in srgb, var(--accent) var(--level), #e5dfd4); display: grid; place-items: center; font-size: 9px; color: #0c2a24; font-weight: 650; }
56
+ .hours { display: grid; grid-template-columns: repeat(24, 1fr); gap: 3px; align-items: end; height: 140px; }
57
+ .hour { min-width: 0; background: linear-gradient(180deg, var(--accent2), #e07a45); border-radius: 3px 3px 0 0; height: var(--h); }
58
+ .table { width: 100%; border-collapse: collapse; font-size: 13px; }
59
+ .table th, .table td { text-align: left; border-bottom: 1px solid var(--line); padding: 9px 6px; }
60
+ .table th { color: var(--muted); font-weight: 650; font-size: 11px; text-transform: none; }
61
+ .table td.num { text-align: right; font-variant-numeric: tabular-nums; }
62
+ footer { margin-top: 28px; color: var(--muted); font-size: 11px; line-height: 1.5; }
63
+ @media (max-width: 900px) {
64
+ .hero, .two, .three, .metrics, .metrics6 { grid-template-columns: 1fr; }
65
+ .hours { grid-template-columns: repeat(12, 1fr); height: 120px; }
66
+ }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <main>
71
+ <header class="hero">
72
+ <div>
73
+ <h1>카카오톡 대화 리포트</h1>
74
+ <p class="sub">원문 메시지·전체 URL은 저장하지 않습니다. 참여자는 <strong>부분 마스킹된 표시명</strong>으로만 보여요. 아래는 집계·리듬·키워드 중심의 인사이트입니다.</p>
75
+ <div class="badge-row">
76
+ <span class="badge">프라이버시: ${escapeHtml(privacyLabel(data.privacy))}</span>
77
+ <span class="badge">인코딩: ${escapeHtml(data.source.encoding)}</span>
78
+ <span class="badge">경고: ${data.source.warnings}건</span>
79
+ </div>
80
+ </div>
81
+ <div class="card side-card">
82
+ <p><strong>생성 시각</strong><br>${escapeHtml(formatTimestamp(data.generatedAt))}</p>
83
+ <p><strong>첫 메시지</strong><br>${escapeHtml(data.summary.firstMessage ?? "—")}</p>
84
+ <p><strong>마지막 메시지</strong><br>${escapeHtml(data.summary.lastMessage ?? "—")}</p>
85
+ </div>
86
+ </header>
87
+
88
+ ${data.highlights.length > 0
89
+ ? `<section class="card" style="margin-bottom:16px"><h2>하이라이트</h2><ul class="highlights">${data.highlights.map((h) => `<li>${renderHighlightLine(h)}</li>`).join("")}</ul></section>`
90
+ : ""}
91
+
92
+ <section class="grid metrics" style="margin-bottom:14px">
93
+ ${metric("총 메시지", formatNumber(data.summary.totalMessages), `활동일 ${formatNumber(data.summary.activeDays)}일`)}
94
+ ${metric("참여자", formatNumber(data.summary.participants), "부분 마스킹 표시")}
95
+ ${metric("평균 길이", `${data.summary.averageMessageLength}`, "글자 수 기준")}
96
+ ${metric("URL 포함", formatNumber(data.summary.messagesWithLinks), "메시지 수")}
97
+ </section>
98
+
99
+ <section class="grid metrics6" style="margin-bottom:14px">
100
+ ${metric("활동일당 평균", `${data.summary.messagesPerActiveDay}`, "메시지 / 활동일")}
101
+ ${metric("최장 연속일", `${data.summary.longestActiveStreakDays}`, "메시지가 있었던 날 기준")}
102
+ ${metric("심야 비중", `${data.summary.nightSharePercent}%`, "23~05시")}
103
+ ${metric("응답 간격 중앙값", data.summary.medianReplyGapMinutes !== null ? `${data.summary.medianReplyGapMinutes}분` : "—", "연속 메시지 기준")}
104
+ ${metric("피크 타임", data.summary.peakHour !== null ? `${data.summary.peakHour}시` : "—", "가장 붐빈 시각")}
105
+ ${metric("이모지 느낌", formatNumber(data.summary.emojiMessages), "감지된 메시지")}
106
+ </section>
107
+
108
+ <section class="grid two" style="margin-bottom:14px">
109
+ ${panel("일별 활동 히트맵", renderDaily(data.daily))}
110
+ ${panel("시간대 리듬 (0~23시)", renderHours(data.hourly))}
111
+ </section>
112
+
113
+ <section class="grid two" style="margin-bottom:14px">
114
+ ${panel("참여자 랭킹", renderParticipants(data.participants))}
115
+ ${panel("요일별 활동", renderCountBars(data.weekdays))}
116
+ </section>
117
+
118
+ <section class="grid two" style="margin-bottom:14px">
119
+ ${panel("월별 추이", renderMonthly(data.monthly))}
120
+ ${panel("첨부 유형", renderCountBars(data.attachments))}
121
+ </section>
122
+
123
+ <section class="grid two" style="margin-bottom:14px">
124
+ ${panel("자주 나온 도메인", renderCountBars(data.domains))}
125
+ ${panel("키워드 스냅샷", renderCountBars(data.keywords))}
126
+ </section>
127
+
128
+ <script type="application/json" id="report-data">${escapeJsonForHtml(data)}</script>
129
+ <footer>${escapeHtml(data.source.fileName)} · 경고 ${data.source.warnings}건 · 본 리포트는 통계 목적이며 법적·회계적 증빙으로 사용할 수 없습니다.</footer>
130
+ </main>
131
+ </body>
132
+ </html>`;
133
+ const size = Buffer.byteLength(html, "utf8");
134
+ if (size > FIVE_MIB) {
135
+ throw new Error(`Generated HTML is ${size} bytes, which exceeds the 5 MiB BrewPage HTML limit.`);
136
+ }
137
+ return html;
138
+ }
139
+ function privacyLabel(mode) {
140
+ if (mode === "public-masked")
141
+ return "부분 마스킹(기본)";
142
+ if (mode === "public-anonymous")
143
+ return "완전 별칭(User 001)";
144
+ return mode;
145
+ }
146
+ function metric(label, value, sub) {
147
+ return `<div class="card metric"><span class="label">${escapeHtml(label)}</span><span class="value">${escapeHtml(value)}</span><span class="sub">${escapeHtml(sub)}</span></div>`;
148
+ }
149
+ function panel(title, content) {
150
+ return `<div class="card"><h2>${escapeHtml(title)}</h2>${content}</div>`;
151
+ }
152
+ function renderDaily(days) {
153
+ if (days.length === 0)
154
+ return `<p style="margin:0;color:var(--muted);font-size:13px">날짜가 있는 메시지가 없습니다.</p>`;
155
+ const max = Math.max(...days.map((day) => day.count), 1);
156
+ return `<div class="calendar">${days
157
+ .map((day) => {
158
+ const level = Math.max(8, Math.round((day.count / max) * 85));
159
+ return `<div class="day" title="${escapeHtml(day.date)} · ${day.count}건" style="--level: ${level}%">${day.count}</div>`;
160
+ })
161
+ .join("")}</div>`;
162
+ }
163
+ function renderMonthly(months) {
164
+ if (months.length === 0)
165
+ return `<p style="margin:0;color:var(--muted);font-size:13px">데이터가 없습니다.</p>`;
166
+ return renderCountBars(months.map((m) => ({ label: m.date, count: m.count })));
167
+ }
168
+ function renderHours(hours) {
169
+ const max = Math.max(...hours, 1);
170
+ return `<div class="hours">${hours
171
+ .map((count, hour) => {
172
+ const height = Math.max(2, Math.round((count / max) * 100));
173
+ return `<div class="hour" title="${hour}시 · ${count}건" style="--h: ${height}%"></div>`;
174
+ })
175
+ .join("")}</div>`;
176
+ }
177
+ function renderParticipants(participants) {
178
+ if (participants.length === 0) {
179
+ return `<p style="margin:0;color:var(--muted);font-size:13px">참여자 데이터가 없습니다.</p>`;
180
+ }
181
+ return `<table class="table"><thead><tr><th>표시명</th><th class="num">메시지</th><th class="num">비율</th><th class="num">평균 길이</th><th class="num">URL</th><th class="num">첨부</th><th class="num">심야</th><th class="num">연속 최대</th></tr></thead><tbody>${participants
182
+ .map((p) => `<tr><td>${escapeHtml(p.alias)}</td><td class="num">${formatNumber(p.messages)}</td><td class="num">${p.sharePercent}%</td><td class="num">${p.averageLength}</td><td class="num">${formatNumber(p.linkMessages)}</td><td class="num">${formatNumber(p.attachmentMessages)}</td><td class="num">${formatNumber(p.nightMessages)}</td><td class="num">${formatNumber(p.maxConsecutive)}</td></tr>`)
183
+ .join("")}</tbody></table>`;
184
+ }
185
+ function renderCountBars(items) {
186
+ if (items.length === 0)
187
+ return `<p style="margin:0;color:var(--muted);font-size:13px">데이터가 없습니다.</p>`;
188
+ const max = Math.max(...items.map((item) => item.count), 1);
189
+ return `<div class="bars">${items
190
+ .map((item) => {
191
+ const width = Math.max(2, Math.round((item.count / max) * 100));
192
+ return `<div class="bar-row"><span class="bar-label" title="${escapeHtml(item.label)}">${escapeHtml(item.label)}</span><span class="bar-track"><span class="bar-fill" style="--w: ${width}%"></span></span><span class="bar-value">${formatNumber(item.count)}</span></div>`;
193
+ })
194
+ .join("")}</div>`;
195
+ }
196
+ function renderHighlightLine(line) {
197
+ const parts = line.split("**");
198
+ return parts.map((part, i) => (i % 2 === 1 ? `<strong>${escapeHtml(part)}</strong>` : escapeHtml(part))).join("");
199
+ }
200
+ function formatNumber(value) {
201
+ return new Intl.NumberFormat("ko-KR").format(value);
202
+ }
203
+ function formatTimestamp(value) {
204
+ try {
205
+ return new Intl.DateTimeFormat("ko-KR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
206
+ }
207
+ catch {
208
+ return value;
209
+ }
210
+ }
211
+ function escapeHtml(value) {
212
+ return value
213
+ .replace(/&/g, "&amp;")
214
+ .replace(/</g, "&lt;")
215
+ .replace(/>/g, "&gt;")
216
+ .replace(/"/g, "&quot;")
217
+ .replace(/'/g, "&#39;");
218
+ }
219
+ function escapeJsonForHtml(value) {
220
+ return JSON.stringify(value)
221
+ .replace(/</g, "\\u003c")
222
+ .replace(/>/g, "\\u003e")
223
+ .replace(/&/g, "\\u0026")
224
+ .replace(/\u2028/g, "\\u2028")
225
+ .replace(/\u2029/g, "\\u2029");
226
+ }
227
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ export type EncodingName = "utf-8-bom" | "utf-8" | "cp949" | "euc-kr";
2
+ export type PrivacyMode = "public-masked" | "public-anonymous";
3
+ export interface ParsedDateParts {
4
+ year: number;
5
+ month: number;
6
+ day: number;
7
+ hour: number;
8
+ minute: number;
9
+ second: number;
10
+ }
11
+ export interface ChatRecord {
12
+ line: number;
13
+ rawDate: string;
14
+ date: ParsedDateParts;
15
+ sender: string;
16
+ message: string;
17
+ }
18
+ export interface ParseWarning {
19
+ line: number;
20
+ code: string;
21
+ message: string;
22
+ }
23
+ export interface ParseResult {
24
+ filePath: string;
25
+ encoding: EncodingName;
26
+ physicalLines: number;
27
+ records: ChatRecord[];
28
+ warnings: ParseWarning[];
29
+ header: string[];
30
+ }
31
+ export interface ParticipantStat {
32
+ alias: string;
33
+ messages: number;
34
+ characters: number;
35
+ averageLength: number;
36
+ attachmentMessages: number;
37
+ linkMessages: number;
38
+ /** 전체 메시지 대비 비율(%) */
39
+ sharePercent: number;
40
+ /** 23~05시(심야) 메시지 수 */
41
+ nightMessages: number;
42
+ /** 동일 발신자 연속 메시지 최대 길이 */
43
+ maxConsecutive: number;
44
+ }
45
+ export interface CountItem {
46
+ label: string;
47
+ count: number;
48
+ }
49
+ export interface DailyCount {
50
+ date: string;
51
+ count: number;
52
+ }
53
+ export interface ReportData {
54
+ generatedAt: string;
55
+ privacy: PrivacyMode;
56
+ source: {
57
+ fileName: string;
58
+ encoding: EncodingName;
59
+ physicalLines: number;
60
+ warnings: number;
61
+ };
62
+ summary: {
63
+ totalMessages: number;
64
+ participants: number;
65
+ activeDays: number;
66
+ firstMessage: string | null;
67
+ lastMessage: string | null;
68
+ averageMessageLength: number;
69
+ messagesWithLinks: number;
70
+ messagesWithAttachments: number;
71
+ /** 활동일 기준 하루 평균 메시지 수 */
72
+ messagesPerActiveDay: number;
73
+ /** 연속으로 메시지가 있었던 최대 일수 */
74
+ longestActiveStreakDays: number;
75
+ /** 가장 말이 많았던 시(0~23) */
76
+ peakHour: number | null;
77
+ /** 가장 활발한 요일 라벨(리포트 언어) */
78
+ busiestWeekdayLabel: string | null;
79
+ /** 연속 메시지 간격 중앙값(분). 표본 없으면 null */
80
+ medianReplyGapMinutes: number | null;
81
+ /** 심야(23~05) 메시지 비율(%) */
82
+ nightSharePercent: number;
83
+ /** 이모지/픽토그램이 포함된 메시지 수(대략적) */
84
+ emojiMessages: number;
85
+ };
86
+ participants: ParticipantStat[];
87
+ daily: DailyCount[];
88
+ hourly: number[];
89
+ weekdays: CountItem[];
90
+ /** YYYY-MM 월별 메시지 수 */
91
+ monthly: DailyCount[];
92
+ attachments: CountItem[];
93
+ domains: CountItem[];
94
+ keywords: CountItem[];
95
+ /** 리포트 상단에 보여줄 한 줄 인사이트(한국어) */
96
+ highlights: string[];
97
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export declare const USER_AGENT = "kakaotalk-chat-analyzer/0.1.0";
@@ -0,0 +1,3 @@
1
+ export const VERSION = "0.1.0";
2
+ export const USER_AGENT = `kakaotalk-chat-analyzer/${VERSION}`;
3
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAC/B,MAAM,CAAC,MAAM,UAAU,GAAG,2BAA2B,OAAO,EAAE,CAAC"}