@solhun/feedback-kit-core 0.1.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.
@@ -0,0 +1,109 @@
1
+ // 스크린샷 용량 정책.
2
+ //
3
+ // 캡처는 "있으면 좋은 것"이지 제출의 전제가 아니다. 그래서 이 모듈의 모든 실패 경로는
4
+ // 예외를 던지지 않고 `failed: true` 로 내려온다 — 호출부가 제출을 막지 않게 하려는 것이다.
5
+
6
+ import type { FeedbackScreenshot } from "../types.js";
7
+
8
+ /** 계약상 스크린샷 상한. base64 문자열 기준 8 MiB. */
9
+ export const SCREENSHOT_MAX_BASE64_BYTES = 8 * 1024 * 1024;
10
+
11
+ /** 캡처가 불가능할 때 사용자에게 보여줄 문구. */
12
+ export const SCREENSHOT_FAILED_MESSAGE = "캡처 실패 — 파일로 첨부할 수 있습니다";
13
+
14
+ /** 상한을 넘겼을 때 차례로 시도할 재인코딩 품질. 앞에서부터 시도하고 처음 통과하는 값을 쓴다. */
15
+ export const SCREENSHOT_QUALITY_STEPS: readonly number[] = [0.7, 0.5, 0.3];
16
+
17
+ /**
18
+ * base64 페이로드의 바이트 수.
19
+ * base64 는 ASCII 라 문자 수가 곧 바이트 수다(디코딩 후 원본 크기가 아니라 전송량 기준).
20
+ */
21
+ export function screenshotBytes(shot: FeedbackScreenshot | null | undefined): number {
22
+ if (!shot || typeof shot.base64 !== "string") return 0;
23
+ return shot.base64.length;
24
+ }
25
+
26
+ export type ScreenshotCapture = () =>
27
+ | FeedbackScreenshot
28
+ | null
29
+ | Promise<FeedbackScreenshot | null>;
30
+
31
+ export type ScreenshotReencode = (
32
+ shot: FeedbackScreenshot,
33
+ quality: number,
34
+ ) => FeedbackScreenshot | null | Promise<FeedbackScreenshot | null>;
35
+
36
+ export interface CaptureWithinLimitOpts {
37
+ capture: ScreenshotCapture;
38
+ /** 상한 초과 시 다시 인코딩하는 훅. 없으면 재인코딩 없이 바로 포기한다. */
39
+ reencode?: ScreenshotReencode | null;
40
+ qualitySteps?: readonly number[];
41
+ limitBytes?: number;
42
+ }
43
+
44
+ export interface ScreenshotOutcome {
45
+ screenshot: FeedbackScreenshot | null;
46
+ /** 캡처를 끝내 얻지 못했다. 제출은 계속 가능해야 한다. */
47
+ failed: boolean;
48
+ /** 실패했을 때만 채워지는 안내 문구. */
49
+ message: string | null;
50
+ /** 품질을 낮춰 다시 인코딩한 결과인가. */
51
+ reencoded: boolean;
52
+ }
53
+
54
+ function failure(): ScreenshotOutcome {
55
+ return {
56
+ screenshot: null,
57
+ failed: true,
58
+ message: SCREENSHOT_FAILED_MESSAGE,
59
+ reencoded: false,
60
+ };
61
+ }
62
+
63
+ function usable(shot: FeedbackScreenshot | null | undefined): shot is FeedbackScreenshot {
64
+ return !!shot && typeof shot.base64 === "string" && shot.base64.length > 0;
65
+ }
66
+
67
+ /**
68
+ * 캡처한 뒤 용량 상한 안으로 들여보낸다.
69
+ *
70
+ * 1. 캡처 → 실패하거나 빈 결과면 곧장 실패로 내린다.
71
+ * 2. 상한 이내면 그대로 쓴다.
72
+ * 3. 초과하면 품질을 단계별로 낮춰 재인코딩하고, 처음 통과한 결과를 쓴다.
73
+ * 4. 끝내 못 맞추면 스크린샷을 버리고 실패 문구로 대체한다(제출은 막지 않는다).
74
+ */
75
+ export async function captureWithinLimit(
76
+ opts: CaptureWithinLimitOpts,
77
+ ): Promise<ScreenshotOutcome> {
78
+ const limit = opts.limitBytes ?? SCREENSHOT_MAX_BASE64_BYTES;
79
+
80
+ let shot: FeedbackScreenshot | null = null;
81
+ try {
82
+ shot = (await opts.capture()) ?? null;
83
+ } catch {
84
+ // 캡처 구현이 던지는 예외는 여기서 흡수한다. 위로 새면 모달이 열리다 만다.
85
+ shot = null;
86
+ }
87
+ if (!usable(shot)) return failure();
88
+
89
+ if (screenshotBytes(shot) <= limit) {
90
+ return { screenshot: shot, failed: false, message: null, reencoded: false };
91
+ }
92
+
93
+ const steps = opts.qualitySteps ?? SCREENSHOT_QUALITY_STEPS;
94
+ if (opts.reencode) {
95
+ for (const quality of steps) {
96
+ let next: FeedbackScreenshot | null = null;
97
+ try {
98
+ next = (await opts.reencode(shot, quality)) ?? null;
99
+ } catch {
100
+ next = null;
101
+ }
102
+ if (usable(next) && screenshotBytes(next) <= limit) {
103
+ return { screenshot: next, failed: false, message: null, reencoded: true };
104
+ }
105
+ }
106
+ }
107
+
108
+ return failure();
109
+ }