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.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/src/analysis.d.ts +8 -0
- package/dist/src/analysis.js +406 -0
- package/dist/src/analysis.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +177 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +59 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/date.d.ts +7 -0
- package/dist/src/date.js +32 -0
- package/dist/src/date.js.map +1 -0
- package/dist/src/encoding.d.ts +5 -0
- package/dist/src/encoding.js +53 -0
- package/dist/src/encoding.js.map +1 -0
- package/dist/src/parser.d.ts +3 -0
- package/dist/src/parser.js +206 -0
- package/dist/src/parser.js.map +1 -0
- package/dist/src/providers/brewpage.d.ts +8 -0
- package/dist/src/providers/brewpage.js +96 -0
- package/dist/src/providers/brewpage.js.map +1 -0
- package/dist/src/providers/cloudflare.d.ts +5 -0
- package/dist/src/providers/cloudflare.js +7 -0
- package/dist/src/providers/cloudflare.js.map +1 -0
- package/dist/src/providers/index.d.ts +3 -0
- package/dist/src/providers/index.js +20 -0
- package/dist/src/providers/index.js.map +1 -0
- package/dist/src/providers/tempfile.d.ts +8 -0
- package/dist/src/providers/tempfile.js +60 -0
- package/dist/src/providers/tempfile.js.map +1 -0
- package/dist/src/providers/types.d.ts +32 -0
- package/dist/src/providers/types.js +2 -0
- package/dist/src/providers/types.js.map +1 -0
- package/dist/src/report.d.ts +2 -0
- package/dist/src/report.js +227 -0
- package/dist/src/report.js.map +1 -0
- package/dist/src/types.d.ts +97 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +3 -0
- package/dist/src/version.js.map +1 -0
- 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,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,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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/providers/types.ts"],"names":[],"mappings":""}
|
|
@@ -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, "&")
|
|
214
|
+
.replace(/</g, "<")
|
|
215
|
+
.replace(/>/g, ">")
|
|
216
|
+
.replace(/"/g, """)
|
|
217
|
+
.replace(/'/g, "'");
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -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"}
|