@mathrunet/masamune_firebase_workflow_marketing 3.8.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 (146) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/LICENSE +21 -0
  3. package/README.md +70 -0
  4. package/assets/fonts/NotoSans-Bold.ttf +1439 -0
  5. package/assets/fonts/NotoSans-Regular.ttf +1439 -0
  6. package/assets/fonts/NotoSans.ttf +0 -0
  7. package/assets/fonts/NotoSansJP-Bold.ttf +0 -0
  8. package/assets/fonts/NotoSansJP-Regular.ttf +0 -0
  9. package/assets/fonts/NotoSansKR-Bold.ttf +1439 -0
  10. package/assets/fonts/NotoSansKR-Regular.ttf +1439 -0
  11. package/assets/fonts/NotoSansKR.ttf +0 -0
  12. package/assets/fonts/NotoSansSC-Bold.ttf +1439 -0
  13. package/assets/fonts/NotoSansSC-Regular.ttf +1439 -0
  14. package/assets/fonts/NotoSansSC.ttf +0 -0
  15. package/dist/clients/app_store_client.d.ts +54 -0
  16. package/dist/clients/app_store_client.js +280 -0
  17. package/dist/clients/app_store_client.js.map +1 -0
  18. package/dist/clients/firebase_analytics_client.d.ts +67 -0
  19. package/dist/clients/firebase_analytics_client.js +411 -0
  20. package/dist/clients/firebase_analytics_client.js.map +1 -0
  21. package/dist/clients/github_content_client.d.ts +83 -0
  22. package/dist/clients/github_content_client.js +443 -0
  23. package/dist/clients/github_content_client.js.map +1 -0
  24. package/dist/clients/google_play_client.d.ts +49 -0
  25. package/dist/clients/google_play_client.js +275 -0
  26. package/dist/clients/google_play_client.js.map +1 -0
  27. package/dist/functions/analyze_github_init.d.ts +37 -0
  28. package/dist/functions/analyze_github_init.js +299 -0
  29. package/dist/functions/analyze_github_init.js.map +1 -0
  30. package/dist/functions/analyze_github_process.d.ts +25 -0
  31. package/dist/functions/analyze_github_process.js +268 -0
  32. package/dist/functions/analyze_github_process.js.map +1 -0
  33. package/dist/functions/analyze_github_summary.d.ts +32 -0
  34. package/dist/functions/analyze_github_summary.js +270 -0
  35. package/dist/functions/analyze_github_summary.js.map +1 -0
  36. package/dist/functions/analyze_market_research.d.ts +40 -0
  37. package/dist/functions/analyze_market_research.js +346 -0
  38. package/dist/functions/analyze_market_research.js.map +1 -0
  39. package/dist/functions/analyze_marketing_data.d.ts +96 -0
  40. package/dist/functions/analyze_marketing_data.js +990 -0
  41. package/dist/functions/analyze_marketing_data.js.map +1 -0
  42. package/dist/functions/collect_from_app_store.d.ts +25 -0
  43. package/dist/functions/collect_from_app_store.js +148 -0
  44. package/dist/functions/collect_from_app_store.js.map +1 -0
  45. package/dist/functions/collect_from_firebase_analytics.d.ts +25 -0
  46. package/dist/functions/collect_from_firebase_analytics.js +142 -0
  47. package/dist/functions/collect_from_firebase_analytics.js.map +1 -0
  48. package/dist/functions/collect_from_google_play_console.d.ts +25 -0
  49. package/dist/functions/collect_from_google_play_console.js +142 -0
  50. package/dist/functions/collect_from_google_play_console.js.map +1 -0
  51. package/dist/functions/generate_marketing_markdown.d.ts +89 -0
  52. package/dist/functions/generate_marketing_markdown.js +653 -0
  53. package/dist/functions/generate_marketing_markdown.js.map +1 -0
  54. package/dist/functions/generate_marketing_pdf.d.ts +25 -0
  55. package/dist/functions/generate_marketing_pdf.js +175 -0
  56. package/dist/functions/generate_marketing_pdf.js.map +1 -0
  57. package/dist/functions/research_market.d.ts +56 -0
  58. package/dist/functions/research_market.js +434 -0
  59. package/dist/functions/research_market.js.map +1 -0
  60. package/dist/functions.d.ts +571 -0
  61. package/dist/functions.js +648 -0
  62. package/dist/functions.js.map +1 -0
  63. package/dist/index.d.ts +13 -0
  64. package/dist/index.js +30 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/locales/de_DE.d.ts +5 -0
  67. package/dist/locales/de_DE.js +111 -0
  68. package/dist/locales/de_DE.js.map +1 -0
  69. package/dist/locales/en.d.ts +5 -0
  70. package/dist/locales/en.js +111 -0
  71. package/dist/locales/en.js.map +1 -0
  72. package/dist/locales/es_ES.d.ts +5 -0
  73. package/dist/locales/es_ES.js +111 -0
  74. package/dist/locales/es_ES.js.map +1 -0
  75. package/dist/locales/fr_FR.d.ts +5 -0
  76. package/dist/locales/fr_FR.js +111 -0
  77. package/dist/locales/fr_FR.js.map +1 -0
  78. package/dist/locales/id_ID.d.ts +5 -0
  79. package/dist/locales/id_ID.js +111 -0
  80. package/dist/locales/id_ID.js.map +1 -0
  81. package/dist/locales/index.d.ts +59 -0
  82. package/dist/locales/index.js +155 -0
  83. package/dist/locales/index.js.map +1 -0
  84. package/dist/locales/ja.d.ts +5 -0
  85. package/dist/locales/ja.js +111 -0
  86. package/dist/locales/ja.js.map +1 -0
  87. package/dist/locales/ko_KR.d.ts +5 -0
  88. package/dist/locales/ko_KR.js +111 -0
  89. package/dist/locales/ko_KR.js.map +1 -0
  90. package/dist/locales/pt_PT.d.ts +5 -0
  91. package/dist/locales/pt_PT.js +111 -0
  92. package/dist/locales/pt_PT.js.map +1 -0
  93. package/dist/locales/ru_RU.d.ts +5 -0
  94. package/dist/locales/ru_RU.js +111 -0
  95. package/dist/locales/ru_RU.js.map +1 -0
  96. package/dist/locales/types.d.ts +105 -0
  97. package/dist/locales/types.js +8 -0
  98. package/dist/locales/types.js.map +1 -0
  99. package/dist/locales/zh_CN.d.ts +5 -0
  100. package/dist/locales/zh_CN.js +111 -0
  101. package/dist/locales/zh_CN.js.map +1 -0
  102. package/dist/models/app_store_data.d.ts +87 -0
  103. package/dist/models/app_store_data.js +8 -0
  104. package/dist/models/app_store_data.js.map +1 -0
  105. package/dist/models/chart_data.d.ts +104 -0
  106. package/dist/models/chart_data.js +8 -0
  107. package/dist/models/chart_data.js.map +1 -0
  108. package/dist/models/common.d.ts +42 -0
  109. package/dist/models/common.js +8 -0
  110. package/dist/models/common.js.map +1 -0
  111. package/dist/models/firebase_analytics_data.d.ts +134 -0
  112. package/dist/models/firebase_analytics_data.js +8 -0
  113. package/dist/models/firebase_analytics_data.js.map +1 -0
  114. package/dist/models/github_analysis_data.d.ts +266 -0
  115. package/dist/models/github_analysis_data.js +101 -0
  116. package/dist/models/github_analysis_data.js.map +1 -0
  117. package/dist/models/google_play_data.d.ts +115 -0
  118. package/dist/models/google_play_data.js +8 -0
  119. package/dist/models/google_play_data.js.map +1 -0
  120. package/dist/models/index.d.ts +14 -0
  121. package/dist/models/index.js +36 -0
  122. package/dist/models/index.js.map +1 -0
  123. package/dist/models/market_research_data.d.ts +259 -0
  124. package/dist/models/market_research_data.js +8 -0
  125. package/dist/models/market_research_data.js.map +1 -0
  126. package/dist/models/marketing_analysis_data.d.ts +200 -0
  127. package/dist/models/marketing_analysis_data.js +8 -0
  128. package/dist/models/marketing_analysis_data.js.map +1 -0
  129. package/dist/models/pdf_data.d.ts +112 -0
  130. package/dist/models/pdf_data.js +8 -0
  131. package/dist/models/pdf_data.js.map +1 -0
  132. package/dist/services/chart_service.d.ts +54 -0
  133. package/dist/services/chart_service.js +436 -0
  134. package/dist/services/chart_service.js.map +1 -0
  135. package/dist/services/github_analysis_service.d.ts +73 -0
  136. package/dist/services/github_analysis_service.js +488 -0
  137. package/dist/services/github_analysis_service.js.map +1 -0
  138. package/dist/services/pdf_service.d.ts +112 -0
  139. package/dist/services/pdf_service.js +1397 -0
  140. package/dist/services/pdf_service.js.map +1 -0
  141. package/dist/utils/github_analysis_storage.d.ts +88 -0
  142. package/dist/utils/github_analysis_storage.js +154 -0
  143. package/dist/utils/github_analysis_storage.js.map +1 -0
  144. package/package.json +64 -0
  145. package/scripts/cleanup_test_data.ts +304 -0
  146. package/scripts/list_test_data.ts +85 -0
@@ -0,0 +1,1397 @@
1
+ "use strict";
2
+ /**
3
+ * PDF Service
4
+ *
5
+ * Generates PDF marketing reports using pdfkit.
6
+ * Supports multilingual text with appropriate fonts.
7
+ *
8
+ * @see https://pdfkit.org/
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ var __importDefault = (this && this.__importDefault) || function (mod) {
44
+ return (mod && mod.__esModule) ? mod : { "default": mod };
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.PDFService = void 0;
48
+ const pdfkit_1 = __importDefault(require("pdfkit"));
49
+ const path = __importStar(require("path"));
50
+ const fs = __importStar(require("fs"));
51
+ const locales_1 = require("../locales");
52
+ /**
53
+ * Font family configurations for multilingual support.
54
+ */
55
+ const FONT_CONFIGS = [
56
+ { name: "NotoSansJP", regularFile: "NotoSansJP-Regular.ttf", boldFile: "NotoSansJP-Bold.ttf" },
57
+ { name: "NotoSansSC", regularFile: "NotoSansSC.ttf", boldFile: "NotoSansSC.ttf", isVariable: true },
58
+ { name: "NotoSansKR", regularFile: "NotoSansKR.ttf", boldFile: "NotoSansKR.ttf", isVariable: true },
59
+ { name: "NotoSans", regularFile: "NotoSans.ttf", boldFile: "NotoSans.ttf", isVariable: true },
60
+ ];
61
+ /**
62
+ * Light color scheme (default).
63
+ */
64
+ const LIGHT_SCHEME = {
65
+ background: "#ffffff",
66
+ text: "#000000",
67
+ textSecondary: "#757575",
68
+ primary: "#1565c0",
69
+ primaryLight: "#e3f2fd",
70
+ success: "#2e7d32",
71
+ successLight: "#e8f5e9",
72
+ successDark: "#1b5e20",
73
+ error: "#c62828",
74
+ errorLight: "#ffebee",
75
+ errorDark: "#b71c1c",
76
+ warning: "#f57c00",
77
+ neutral: "#757575",
78
+ googlePlay: "#4CAF50",
79
+ appStore: "#007AFF",
80
+ boxBackground: "#f5f5f5",
81
+ summaryBackground: "#e3f2fd",
82
+ highlightBackground: "#e8f5e9",
83
+ concernBackground: "#ffebee",
84
+ competitorHeader: "#37474f",
85
+ };
86
+ /**
87
+ * Dark color scheme.
88
+ */
89
+ const DARK_SCHEME = {
90
+ background: "#212121",
91
+ text: "#ffffff",
92
+ textSecondary: "#b0b0b0",
93
+ primary: "#64b5f6",
94
+ primaryLight: "#1e3a5f",
95
+ success: "#81c784",
96
+ successLight: "#1b3d1e",
97
+ successDark: "#4caf50",
98
+ error: "#ef5350",
99
+ errorLight: "#3d1b1b",
100
+ errorDark: "#f44336",
101
+ warning: "#ffb74d",
102
+ neutral: "#9e9e9e",
103
+ googlePlay: "#66bb6a",
104
+ appStore: "#42a5f5",
105
+ boxBackground: "#1e1e1e",
106
+ summaryBackground: "#1e3a5f",
107
+ highlightBackground: "#1b3d1e",
108
+ concernBackground: "#3d1b1b",
109
+ competitorHeader: "#263238",
110
+ };
111
+ /**
112
+ * PDF Service for generating marketing reports.
113
+ */
114
+ class PDFService {
115
+ pageWidth = 595.28; // A4 width in points
116
+ pageHeight = 841.89; // A4 height in points
117
+ margin = 50;
118
+ headerHeight = 45; // Header height in points
119
+ footerHeight = 30; // Footer height in points
120
+ contentWidth;
121
+ maxContentY; // Maximum Y position for content (before footer)
122
+ fontDir;
123
+ registeredFonts = new Set();
124
+ currentFontFamily = "Helvetica";
125
+ translations;
126
+ colors = LIGHT_SCHEME;
127
+ styleOptions = {};
128
+ headerIconBuffer = null;
129
+ constructor() {
130
+ this.contentWidth = this.pageWidth - this.margin * 2;
131
+ // Maximum Y for content: page height - bottom margin - footer height
132
+ this.maxContentY = this.pageHeight - this.margin - this.footerHeight;
133
+ // Default font directory: assets/fonts relative to package root
134
+ this.fontDir = path.join(__dirname, "..", "..", "assets", "fonts");
135
+ // Default to English translations
136
+ this.translations = (0, locales_1.getTranslations)("en");
137
+ }
138
+ /**
139
+ * Download image from URL and return as Buffer.
140
+ */
141
+ async downloadImage(url) {
142
+ try {
143
+ // Convert GitHub blob URL to raw URL
144
+ let rawUrl = url;
145
+ if (url.includes("github.com") && url.includes("/blob/")) {
146
+ rawUrl = url
147
+ .replace("github.com", "raw.githubusercontent.com")
148
+ .replace("/blob/", "/");
149
+ }
150
+ const response = await fetch(rawUrl);
151
+ if (!response.ok) {
152
+ console.warn(`Failed to download image from ${rawUrl}: ${response.status}`);
153
+ return null;
154
+ }
155
+ const arrayBuffer = await response.arrayBuffer();
156
+ return Buffer.from(arrayBuffer);
157
+ }
158
+ catch (error) {
159
+ console.warn(`Error downloading image from ${url}:`, error);
160
+ return null;
161
+ }
162
+ }
163
+ /**
164
+ * Draw page background for dark mode.
165
+ */
166
+ drawPageBackground(doc) {
167
+ if (this.styleOptions.colorScheme === "dark") {
168
+ doc.save();
169
+ doc.rect(0, 0, this.pageWidth, this.pageHeight).fill(this.colors.background);
170
+ doc.restore();
171
+ }
172
+ }
173
+ /**
174
+ * Draw page header with icon and organization title.
175
+ */
176
+ drawPageHeader(doc) {
177
+ let y = this.margin;
178
+ // Only draw header if we have icon or organization title
179
+ if (!this.headerIconBuffer && !this.styleOptions.organizationTitle) {
180
+ return y;
181
+ }
182
+ // Draw icon on the left
183
+ if (this.headerIconBuffer) {
184
+ try {
185
+ const iconSize = 28;
186
+ doc.image(this.headerIconBuffer, this.margin, y, {
187
+ width: iconSize,
188
+ height: iconSize,
189
+ fit: [iconSize, iconSize],
190
+ });
191
+ }
192
+ catch (error) {
193
+ console.warn("Failed to draw header icon:", error);
194
+ }
195
+ }
196
+ // Draw organization title on the right
197
+ if (this.styleOptions.organizationTitle) {
198
+ doc.fontSize(11).font(this.getFont(true));
199
+ doc.fillColor(this.colors.text);
200
+ doc.text(this.styleOptions.organizationTitle, this.margin, y + 8, { width: this.contentWidth, align: "right" });
201
+ }
202
+ return y + this.headerHeight;
203
+ }
204
+ /**
205
+ * Draw page footer with copyright.
206
+ * Uses direct graphics commands to avoid auto-pagination.
207
+ */
208
+ drawPageFooter(doc) {
209
+ if (!this.styleOptions.copyright) {
210
+ return;
211
+ }
212
+ const footerText = `© ${new Date().getFullYear()} ${this.styleOptions.copyright}`;
213
+ const footerY = this.pageHeight - this.footerHeight;
214
+ const fontSize = 8;
215
+ // Save current position
216
+ const savedY = doc.y;
217
+ // Set font and color
218
+ doc.fontSize(fontSize).font(this.getFont());
219
+ doc.fillColor(this.colors.textSecondary);
220
+ // Calculate text width for centering
221
+ const textWidth = doc.widthOfString(footerText);
222
+ const centerX = this.margin + (this.contentWidth - textWidth) / 2;
223
+ // Use save/restore and direct positioning to avoid auto-pagination
224
+ doc.save();
225
+ // Draw text using _fragment or simpleText approach
226
+ // Move to position and draw without triggering pagination
227
+ doc._fragment(footerText, centerX, footerY, {
228
+ lineBreak: false,
229
+ textWidth: textWidth,
230
+ wordSpacing: 0,
231
+ characterSpacing: 0,
232
+ });
233
+ doc.restore();
234
+ // Restore Y position to prevent state pollution
235
+ doc.y = savedY;
236
+ }
237
+ /**
238
+ * Initialize a new page with background, header, and return starting Y position.
239
+ * When isNewPage is true, draws footer on current page before adding new page.
240
+ */
241
+ initPage(doc, isNewPage = false) {
242
+ if (isNewPage) {
243
+ // Draw footer on current page before creating new page
244
+ this.drawPageFooter(doc);
245
+ doc.addPage();
246
+ }
247
+ this.drawPageBackground(doc);
248
+ const y = this.drawPageHeader(doc);
249
+ return y;
250
+ }
251
+ /**
252
+ * Register fonts to PDFDocument based on locale.
253
+ */
254
+ registerFonts(doc, locale) {
255
+ // Determine font family from locale
256
+ this.currentFontFamily = (0, locales_1.getFontFamily)(locale);
257
+ this.translations = (0, locales_1.getTranslations)(locale);
258
+ // Register all available font families
259
+ for (const config of FONT_CONFIGS) {
260
+ const regularPath = path.join(this.fontDir, config.regularFile);
261
+ const boldPath = path.join(this.fontDir, config.boldFile);
262
+ if (fs.existsSync(regularPath)) {
263
+ doc.registerFont(config.name, regularPath);
264
+ this.registeredFonts.add(config.name);
265
+ }
266
+ if (!config.isVariable && fs.existsSync(boldPath)) {
267
+ doc.registerFont(`${config.name}-Bold`, boldPath);
268
+ this.registeredFonts.add(`${config.name}-Bold`);
269
+ }
270
+ }
271
+ }
272
+ /**
273
+ * Get font name based on current locale.
274
+ */
275
+ getFont(bold = false) {
276
+ if (this.currentFontFamily === "Helvetica") {
277
+ return bold ? "Helvetica-Bold" : "Helvetica";
278
+ }
279
+ // Check if the font was registered
280
+ if (this.registeredFonts.has(this.currentFontFamily)) {
281
+ // For variable fonts, bold is simulated or same font is used
282
+ if (bold && this.registeredFonts.has(`${this.currentFontFamily}-Bold`)) {
283
+ return `${this.currentFontFamily}-Bold`;
284
+ }
285
+ return this.currentFontFamily;
286
+ }
287
+ // Fallback to Helvetica
288
+ return bold ? "Helvetica-Bold" : "Helvetica";
289
+ }
290
+ /**
291
+ * Generate a complete marketing report PDF.
292
+ */
293
+ async generateReport(options) {
294
+ // Initialize style options
295
+ this.styleOptions = options.style || {};
296
+ this.colors = this.styleOptions.colorScheme === "dark" ? DARK_SCHEME : LIGHT_SCHEME;
297
+ // Download header icon if specified
298
+ if (this.styleOptions.headerIconUrl) {
299
+ this.headerIconBuffer = await this.downloadImage(this.styleOptions.headerIconUrl);
300
+ }
301
+ else {
302
+ this.headerIconBuffer = null;
303
+ }
304
+ return new Promise((resolve, reject) => {
305
+ try {
306
+ const appName = options.appName ||
307
+ options.data.appStore?.appName ||
308
+ options.data.googlePlayConsole?.packageName ||
309
+ "Marketing Report";
310
+ const doc = new pdfkit_1.default({
311
+ size: "A4",
312
+ margins: {
313
+ top: this.margin,
314
+ bottom: this.margin,
315
+ left: this.margin,
316
+ right: this.margin,
317
+ },
318
+ info: {
319
+ Title: `${appName} Marketing Report`,
320
+ Author: "Masamune Workflow Marketing",
321
+ Subject: options.dateRange
322
+ ? `${options.dateRange.startDate} - ${options.dateRange.endDate}`
323
+ : new Date().toISOString().split("T")[0],
324
+ },
325
+ });
326
+ // Register fonts based on locale
327
+ const locale = typeof options.locale === "object"
328
+ ? options.locale["@language"]
329
+ : options.locale;
330
+ this.registerFonts(doc, locale);
331
+ const chunks = [];
332
+ doc.on("data", (chunk) => chunks.push(chunk));
333
+ doc.on("end", () => resolve(Buffer.concat(chunks)));
334
+ doc.on("error", reject);
335
+ // Draw background for first page (cover page)
336
+ this.drawPageBackground(doc);
337
+ // Cover page
338
+ this.addCoverPage(doc, options, appName);
339
+ // Executive Summary page
340
+ if (options.data.marketingAnalytics?.overallAnalysis) {
341
+ const summaryY = this.initPage(doc, true);
342
+ this.addSummaryPage(doc, options, summaryY);
343
+ // Highlights & Concerns page (separate page to avoid overflow)
344
+ const analysis = options.data.marketingAnalytics.overallAnalysis;
345
+ if ((analysis.highlights?.length || 0) > 0 || (analysis.concerns?.length || 0) > 0) {
346
+ const highlightsY = this.initPage(doc, true);
347
+ this.addHighlightsConcernsPage(doc, options, highlightsY);
348
+ }
349
+ }
350
+ // User Analytics page
351
+ if (options.data.firebaseAnalytics) {
352
+ const analyticsY = this.initPage(doc, true);
353
+ this.addUserAnalyticsPage(doc, options, analyticsY);
354
+ }
355
+ // Ratings & Reviews page
356
+ if (options.data.googlePlayConsole || options.data.appStore || options.data.marketingAnalytics?.reviewAnalysis) {
357
+ const ratingsY = this.initPage(doc, true);
358
+ this.addRatingsReviewsPage(doc, options, ratingsY);
359
+ }
360
+ // Competitive Positioning page (market research data)
361
+ if (options.data.marketingAnalytics?.competitivePositioning) {
362
+ const competitiveY = this.initPage(doc, true);
363
+ this.addCompetitivePositioningPage(doc, options, competitiveY);
364
+ }
365
+ // Market Opportunity Priority page (market research data)
366
+ if (options.data.marketingAnalytics?.marketOpportunityPriority) {
367
+ const opportunityY = this.initPage(doc, true);
368
+ this.addMarketOpportunityPriorityPage(doc, options, opportunityY);
369
+ }
370
+ // Trend Analysis page
371
+ if (options.data.marketingAnalytics?.trendAnalysis) {
372
+ const trendY = this.initPage(doc, true);
373
+ this.addTrendAnalysisPage(doc, options, trendY);
374
+ }
375
+ // Improvement Suggestions page
376
+ if (options.data.marketingAnalytics?.improvementSuggestions?.length) {
377
+ const improvementsY = this.initPage(doc, true);
378
+ this.addImprovementsPage(doc, options, improvementsY);
379
+ }
380
+ // GitHub-based Code Improvements page(s)
381
+ if (options.data.githubImprovements?.improvements?.length) {
382
+ const githubY = this.initPage(doc, true);
383
+ this.addGitHubImprovementsPage(doc, options, githubY);
384
+ }
385
+ // Draw footer on the last page
386
+ this.drawPageFooter(doc);
387
+ doc.end();
388
+ }
389
+ catch (error) {
390
+ reject(error);
391
+ }
392
+ });
393
+ }
394
+ /**
395
+ * Add cover page to the PDF.
396
+ */
397
+ addCoverPage(doc, options, appName) {
398
+ let y = 200;
399
+ // App name
400
+ doc.fontSize(32).font(this.getFont(true));
401
+ doc.fillColor(this.colors.text);
402
+ doc.text(appName, this.margin, y, {
403
+ width: this.contentWidth,
404
+ align: "center",
405
+ });
406
+ y += 60;
407
+ // Report type
408
+ doc.fontSize(20).font(this.getFont());
409
+ doc.fillColor(this.colors.text);
410
+ const t = this.translations;
411
+ const reportTypeLabel = options.reportType === "daily"
412
+ ? t.dailyReport
413
+ : options.reportType === "weekly"
414
+ ? t.weeklyReport
415
+ : t.monthlyReport;
416
+ doc.text(reportTypeLabel, this.margin, y, {
417
+ width: this.contentWidth,
418
+ align: "center",
419
+ });
420
+ y += 40;
421
+ // Date range
422
+ if (options.dateRange) {
423
+ doc.fontSize(16);
424
+ doc.fillColor(this.colors.text);
425
+ doc.text(`${options.dateRange.startDate} - ${options.dateRange.endDate}`, this.margin, y, { width: this.contentWidth, align: "center" });
426
+ y += 80;
427
+ }
428
+ else {
429
+ y += 40;
430
+ }
431
+ // Generated date
432
+ doc.fontSize(12).font(this.getFont());
433
+ doc.fillColor(this.colors.text);
434
+ doc.text(`${t.generated}: ${new Date().toISOString().split("T")[0]}`, this.margin, y, { width: this.contentWidth, align: "center" });
435
+ // Data sources at the bottom
436
+ y = this.pageHeight - 150;
437
+ doc.fontSize(10).font(this.getFont());
438
+ doc.fillColor(this.colors.textSecondary);
439
+ const sources = [];
440
+ if (options.data.googlePlayConsole)
441
+ sources.push("Google Play");
442
+ if (options.data.appStore)
443
+ sources.push("App Store");
444
+ if (options.data.firebaseAnalytics)
445
+ sources.push("Firebase Analytics");
446
+ if (options.data.marketingAnalytics)
447
+ sources.push(t.aiAnalysis);
448
+ doc.text(`${t.dataSources}: ${sources.join(" | ")}`, this.margin, y, {
449
+ width: this.contentWidth,
450
+ align: "center",
451
+ });
452
+ doc.fillColor(this.colors.text);
453
+ }
454
+ /**
455
+ * Add summary page with AI analysis.
456
+ */
457
+ addSummaryPage(doc, options, startY = this.margin) {
458
+ let y = startY;
459
+ const analysis = options.data.marketingAnalytics?.overallAnalysis;
460
+ const t = this.translations;
461
+ // Page title
462
+ doc.fontSize(20).font(this.getFont(true));
463
+ doc.text(t.executiveSummary, this.margin, y);
464
+ y += 35;
465
+ // Summary text
466
+ if (analysis?.summary) {
467
+ doc.fontSize(11).font(this.getFont());
468
+ doc.text(analysis.summary, this.margin, y, {
469
+ width: this.contentWidth,
470
+ align: "justify",
471
+ });
472
+ y = doc.y + 25;
473
+ }
474
+ // Engagement chart
475
+ if (options.charts?.engagement) {
476
+ try {
477
+ const chartWidth = 300;
478
+ const chartHeight = 200;
479
+ const chartX = this.margin + (this.contentWidth - chartWidth) / 2;
480
+ doc.image(options.charts.engagement, chartX, y, {
481
+ width: chartWidth,
482
+ height: chartHeight,
483
+ fit: [chartWidth, chartHeight],
484
+ });
485
+ y += chartHeight + 25;
486
+ }
487
+ catch {
488
+ // Skip if image fails
489
+ }
490
+ }
491
+ // Key Metrics
492
+ if (analysis?.keyMetrics?.length) {
493
+ doc.fontSize(14).font(this.getFont(true));
494
+ doc.text(t.metric, this.margin, y);
495
+ y += 20;
496
+ const metricsPerRow = 3;
497
+ const metricBoxWidth = this.contentWidth / metricsPerRow;
498
+ for (let i = 0; i < analysis.keyMetrics.length; i++) {
499
+ const metric = analysis.keyMetrics[i];
500
+ const col = i % metricsPerRow;
501
+ const x = this.margin + col * metricBoxWidth;
502
+ if (col === 0 && i > 0) {
503
+ y += 55;
504
+ }
505
+ // Metric box
506
+ doc.fillColor(this.colors.boxBackground);
507
+ doc.rect(x + 2, y, metricBoxWidth - 4, 50).fill();
508
+ // Metric title
509
+ doc.fillColor(this.colors.primary);
510
+ doc.fontSize(9).font(this.getFont(true));
511
+ doc.text(metric.metric, x + 8, y + 8, {
512
+ width: metricBoxWidth - 16,
513
+ });
514
+ // Trend indicator and value
515
+ const trendColor = metric.trend === "up" ? this.colors.success : metric.trend === "down" ? this.colors.error : this.colors.textSecondary;
516
+ const trendIcon = metric.trend === "up" ? "+" : metric.trend === "down" ? "-" : "~";
517
+ doc.fillColor(trendColor);
518
+ doc.fontSize(13).font(this.getFont(true));
519
+ doc.text(`${metric.value} ${trendIcon}`, x + 8, y + 28, {
520
+ width: metricBoxWidth - 16,
521
+ });
522
+ }
523
+ y += 70;
524
+ }
525
+ doc.fillColor(this.colors.text);
526
+ }
527
+ /**
528
+ * Add highlights and concerns page.
529
+ * This page displays highlights and concerns in a 2-column layout.
530
+ * Dynamically calculates item height and adds pages as needed.
531
+ */
532
+ addHighlightsConcernsPage(doc, options, startY = this.margin) {
533
+ let y = startY;
534
+ const analysis = options.data.marketingAnalytics?.overallAnalysis;
535
+ const t = this.translations;
536
+ // Page title
537
+ doc.fontSize(20).font(this.getFont(true));
538
+ doc.text(t.highlightsAndConcerns, this.margin, y);
539
+ y += 40;
540
+ const halfWidth = this.contentWidth / 2 - 15;
541
+ const boxPadding = 16; // Padding inside boxes
542
+ const itemSpacing = 10; // Space between items
543
+ // Get all items
544
+ const highlights = analysis?.highlights || [];
545
+ const concerns = analysis?.concerns || [];
546
+ // Headers
547
+ doc.fontSize(14).font(this.getFont(true));
548
+ doc.fillColor(this.colors.successDark);
549
+ doc.text(t.highlights, this.margin, y);
550
+ doc.fillColor(this.colors.errorDark);
551
+ doc.text(t.concerns, this.margin + halfWidth + 30, y);
552
+ y += 25;
553
+ // Calculate heights for each item pair
554
+ doc.fontSize(10).font(this.getFont());
555
+ const maxItems = Math.max(highlights.length, concerns.length);
556
+ for (let i = 0; i < maxItems; i++) {
557
+ const highlightText = i < highlights.length ? highlights[i] : "";
558
+ const concernText = i < concerns.length ? concerns[i] : "";
559
+ // Calculate actual text heights
560
+ const highlightHeight = highlightText
561
+ ? doc.heightOfString(highlightText, { width: halfWidth - boxPadding }) + boxPadding
562
+ : 0;
563
+ const concernHeight = concernText
564
+ ? doc.heightOfString(concernText, { width: halfWidth - boxPadding }) + boxPadding
565
+ : 0;
566
+ // Use the larger height for this row
567
+ const rowHeight = Math.max(highlightHeight, concernHeight, 40);
568
+ // Check if we need a new page
569
+ if (y + rowHeight > this.maxContentY) {
570
+ this.drawPageFooter(doc);
571
+ doc.addPage();
572
+ this.drawPageBackground(doc);
573
+ y = this.drawPageHeader(doc);
574
+ // Re-add headers on new page
575
+ doc.fontSize(16).font(this.getFont(true));
576
+ doc.fillColor(this.colors.text);
577
+ doc.text(`${t.highlightsAndConcerns} ${t.continued}`, this.margin, y);
578
+ y += 30;
579
+ doc.fontSize(14).font(this.getFont(true));
580
+ doc.fillColor(this.colors.successDark);
581
+ doc.text(t.highlights, this.margin, y);
582
+ doc.fillColor(this.colors.errorDark);
583
+ doc.text(t.concerns, this.margin + halfWidth + 30, y);
584
+ y += 25;
585
+ }
586
+ // Draw highlight item
587
+ if (highlightText) {
588
+ doc.fillColor(this.colors.highlightBackground);
589
+ doc.roundedRect(this.margin, y, halfWidth, rowHeight, 4).fill();
590
+ doc.fillColor(this.colors.successDark);
591
+ doc.fontSize(10).font(this.getFont());
592
+ doc.text(highlightText, this.margin + 8, y + 8, {
593
+ width: halfWidth - boxPadding,
594
+ });
595
+ }
596
+ // Draw concern item
597
+ if (concernText) {
598
+ doc.fillColor(this.colors.concernBackground);
599
+ doc.roundedRect(this.margin + halfWidth + 30, y, halfWidth, rowHeight, 4).fill();
600
+ doc.fillColor(this.colors.errorDark);
601
+ doc.fontSize(10).font(this.getFont());
602
+ doc.text(concernText, this.margin + halfWidth + 38, y + 8, {
603
+ width: halfWidth - boxPadding,
604
+ });
605
+ }
606
+ y += rowHeight + itemSpacing;
607
+ }
608
+ doc.fillColor(this.colors.text);
609
+ }
610
+ /**
611
+ * Add user analytics page.
612
+ */
613
+ addUserAnalyticsPage(doc, options, startY = this.margin) {
614
+ let y = startY;
615
+ const firebase = options.data.firebaseAnalytics;
616
+ const t = this.translations;
617
+ // Page title
618
+ doc.fontSize(20).font(this.getFont(true));
619
+ doc.text(t.userAnalytics, this.margin, y);
620
+ y += 35;
621
+ if (firebase) {
622
+ // DAU/WAU/MAU stats
623
+ const dau = firebase.dau?.toLocaleString() || "N/A";
624
+ const wau = firebase.wau?.toLocaleString() || "N/A";
625
+ const mau = firebase.mau?.toLocaleString() || "N/A";
626
+ const newUsersVal = firebase.newUsers?.toLocaleString() || "N/A";
627
+ doc.fillColor(this.colors.primary);
628
+ doc.fontSize(14).font(this.getFont(true));
629
+ doc.text(`DAU: ${dau} | WAU: ${wau} | MAU: ${mau} | ${t.newUsers}: ${newUsersVal}`, this.margin, y);
630
+ doc.fillColor(this.colors.text);
631
+ y += 25;
632
+ // Retention ratio
633
+ if (firebase.dau && firebase.mau && firebase.mau > 0) {
634
+ const retentionRatio = ((firebase.dau / firebase.mau) * 100).toFixed(1);
635
+ doc.fontSize(11).font(this.getFont());
636
+ doc.text(`${t.retention} (DAU/MAU): ${retentionRatio}%`, this.margin, y);
637
+ y += 20;
638
+ }
639
+ // Session stats
640
+ if (firebase.averageSessionDuration || firebase.sessionsPerUser) {
641
+ doc.fontSize(11).font(this.getFont());
642
+ const stats = [];
643
+ if (firebase.averageSessionDuration) {
644
+ const minutes = Math.floor(firebase.averageSessionDuration / 60);
645
+ const seconds = Math.floor(firebase.averageSessionDuration % 60);
646
+ stats.push(`${t.avgSessionDuration}: ${minutes}m ${seconds}s`);
647
+ }
648
+ if (firebase.sessionsPerUser) {
649
+ stats.push(`${t.sessionsPerUser}: ${firebase.sessionsPerUser.toFixed(1)}`);
650
+ }
651
+ doc.text(stats.join(" | "), this.margin, y);
652
+ y += 20;
653
+ }
654
+ }
655
+ y += 20;
656
+ // Charts
657
+ const chartWidth = this.contentWidth / 2 - 10;
658
+ const chartHeight = 200;
659
+ // Engagement and Retention charts
660
+ if (options.charts?.engagement) {
661
+ try {
662
+ doc.fontSize(12).font(this.getFont(true));
663
+ doc.text(t.activeUsers, this.margin, y);
664
+ doc.image(options.charts.engagement, this.margin, y + 18, {
665
+ width: chartWidth,
666
+ height: chartHeight,
667
+ fit: [chartWidth, chartHeight],
668
+ });
669
+ }
670
+ catch {
671
+ // Skip if image fails
672
+ }
673
+ }
674
+ if (options.charts?.retentionRatio) {
675
+ try {
676
+ doc.fontSize(12).font(this.getFont(true));
677
+ doc.text(t.retention, this.margin + chartWidth + 20, y);
678
+ doc.image(options.charts.retentionRatio, this.margin + chartWidth + 20, y + 18, {
679
+ width: chartWidth,
680
+ height: chartHeight,
681
+ fit: [chartWidth, chartHeight],
682
+ });
683
+ }
684
+ catch {
685
+ // Skip if image fails
686
+ }
687
+ }
688
+ y += chartHeight + 50;
689
+ // Demographics charts
690
+ if (options.charts?.demographics) {
691
+ try {
692
+ doc.fontSize(12).font(this.getFont(true));
693
+ doc.text(t.ageDemographics, this.margin, y);
694
+ doc.image(options.charts.demographics, this.margin, y + 18, {
695
+ width: chartWidth,
696
+ height: chartHeight,
697
+ fit: [chartWidth, chartHeight],
698
+ });
699
+ }
700
+ catch {
701
+ // Skip if image fails
702
+ }
703
+ }
704
+ if (options.charts?.countryDistribution) {
705
+ try {
706
+ doc.fontSize(12).font(this.getFont(true));
707
+ doc.text(t.countryDistribution, this.margin + chartWidth + 20, y);
708
+ doc.image(options.charts.countryDistribution, this.margin + chartWidth + 20, y + 18, {
709
+ width: chartWidth,
710
+ height: chartHeight,
711
+ fit: [chartWidth, chartHeight],
712
+ });
713
+ }
714
+ catch {
715
+ // Skip if image fails
716
+ }
717
+ }
718
+ }
719
+ /**
720
+ * Add ratings and reviews page.
721
+ */
722
+ addRatingsReviewsPage(doc, options, startY = this.margin) {
723
+ let y = startY;
724
+ const t = this.translations;
725
+ // Page title
726
+ doc.fontSize(20).font(this.getFont(true));
727
+ doc.text(t.ratingsAndReviews, this.margin, y);
728
+ y += 35;
729
+ // Rating summary
730
+ const googlePlay = options.data.googlePlayConsole;
731
+ const appStore = options.data.appStore;
732
+ const chartWidth = this.contentWidth / 2 - 10;
733
+ const chartHeight = 180;
734
+ if (googlePlay?.averageRating) {
735
+ doc.fillColor(this.colors.googlePlay);
736
+ doc.fontSize(24).font(this.getFont(true));
737
+ doc.text(`${googlePlay.averageRating.toFixed(1)}`, this.margin, y);
738
+ doc.fillColor(this.colors.text);
739
+ doc.fontSize(11).font(this.getFont());
740
+ doc.text(`Google Play (${googlePlay.totalRatings?.toLocaleString() || 0} ratings)`, this.margin + 50, y + 8);
741
+ }
742
+ if (appStore?.averageRating) {
743
+ doc.fillColor(this.colors.appStore);
744
+ doc.fontSize(24).font(this.getFont(true));
745
+ doc.text(`${appStore.averageRating.toFixed(1)}`, this.margin + chartWidth + 20, y);
746
+ doc.fillColor(this.colors.text);
747
+ doc.fontSize(11).font(this.getFont());
748
+ doc.text(`App Store (${appStore.totalRatings?.toLocaleString() || 0} ratings)`, this.margin + chartWidth + 70, y + 8);
749
+ }
750
+ y += 50;
751
+ // Charts
752
+ if (options.charts?.ratingDistribution) {
753
+ try {
754
+ doc.fontSize(12).font(this.getFont(true));
755
+ doc.fillColor(this.colors.text);
756
+ doc.text(t.ratingDistribution, this.margin, y);
757
+ doc.image(options.charts.ratingDistribution, this.margin, y + 18, {
758
+ width: chartWidth,
759
+ height: chartHeight,
760
+ fit: [chartWidth, chartHeight],
761
+ });
762
+ }
763
+ catch {
764
+ // Skip if image fails
765
+ }
766
+ }
767
+ if (options.charts?.sentiment) {
768
+ try {
769
+ doc.fontSize(12).font(this.getFont(true));
770
+ doc.text(t.sentimentAnalysis, this.margin + chartWidth + 20, y);
771
+ doc.image(options.charts.sentiment, this.margin + chartWidth + 20, y + 18, {
772
+ width: chartWidth,
773
+ height: chartHeight,
774
+ fit: [chartWidth, chartHeight],
775
+ });
776
+ }
777
+ catch {
778
+ // Skip if image fails
779
+ }
780
+ }
781
+ y += chartHeight + 50;
782
+ // Review Analysis
783
+ const reviewAnalysis = options.data.marketingAnalytics?.reviewAnalysis;
784
+ if (reviewAnalysis) {
785
+ doc.fontSize(14).font(this.getFont(true));
786
+ doc.fillColor(this.colors.text);
787
+ doc.text(t.sentimentAnalysis, this.margin, y);
788
+ y += 20;
789
+ // Sentiment breakdown
790
+ if (reviewAnalysis.sentiment) {
791
+ doc.fontSize(11).font(this.getFont());
792
+ doc.fillColor(this.colors.googlePlay);
793
+ doc.text(`${t.positive}: ${reviewAnalysis.sentiment.positive}%`, this.margin, y);
794
+ doc.fillColor(this.colors.neutral);
795
+ doc.text(`${t.neutral}: ${reviewAnalysis.sentiment.neutral}%`, this.margin + 120, y);
796
+ doc.fillColor(this.colors.error);
797
+ doc.text(`${t.negative}: ${reviewAnalysis.sentiment.negative}%`, this.margin + 230, y);
798
+ y += 25;
799
+ }
800
+ // Common themes
801
+ if (reviewAnalysis.commonThemes?.length) {
802
+ doc.fillColor(this.colors.text);
803
+ doc.fontSize(12).font(this.getFont(true));
804
+ doc.text(`${t.commonThemes}:`, this.margin, y);
805
+ y += 18;
806
+ doc.fontSize(10).font(this.getFont());
807
+ for (const theme of reviewAnalysis.commonThemes) {
808
+ doc.text(`- ${theme}`, this.margin + 10, y, { width: this.contentWidth - 20 });
809
+ y = doc.y + 4;
810
+ }
811
+ y += 10;
812
+ }
813
+ // Actionable insights
814
+ if (reviewAnalysis.actionableInsights?.length) {
815
+ doc.fontSize(12).font(this.getFont(true));
816
+ doc.text(`${t.actionableInsights}:`, this.margin, y);
817
+ y += 18;
818
+ doc.fontSize(10).font(this.getFont());
819
+ for (const insight of reviewAnalysis.actionableInsights) {
820
+ doc.fillColor(this.colors.primary);
821
+ doc.text(`-> ${insight}`, this.margin + 10, y, { width: this.contentWidth - 20 });
822
+ y = doc.y + 4;
823
+ }
824
+ }
825
+ }
826
+ doc.fillColor(this.colors.text);
827
+ }
828
+ /**
829
+ * Add improvements page.
830
+ */
831
+ addImprovementsPage(doc, options, startY = this.margin) {
832
+ let y = startY;
833
+ const suggestions = options.data.marketingAnalytics?.improvementSuggestions || [];
834
+ const t = this.translations;
835
+ // Page title
836
+ doc.fontSize(20).font(this.getFont(true));
837
+ doc.text(t.improvementSuggestions, this.margin, y);
838
+ y += 40;
839
+ for (const suggestion of suggestions) {
840
+ // Check if we need a new page
841
+ if (y > this.maxContentY - 100) {
842
+ this.drawPageFooter(doc);
843
+ doc.addPage();
844
+ this.drawPageBackground(doc);
845
+ y = this.drawPageHeader(doc);
846
+ }
847
+ // Priority badge
848
+ const priorityColor = suggestion.priority === "high"
849
+ ? this.colors.error
850
+ : suggestion.priority === "medium"
851
+ ? this.colors.warning
852
+ : this.colors.success;
853
+ doc.fillColor(priorityColor);
854
+ doc.rect(this.margin, y, 60, 20).fill();
855
+ doc.fillColor("#ffffff");
856
+ doc.fontSize(10).font(this.getFont(true));
857
+ doc.text(suggestion.priority.toUpperCase(), this.margin + 5, y + 5);
858
+ // Category
859
+ doc.fillColor(this.colors.textSecondary);
860
+ doc.fontSize(10).font(this.getFont());
861
+ doc.text(`[${suggestion.category}]`, this.margin + 70, y + 5);
862
+ y += 25;
863
+ // Title
864
+ doc.fillColor(this.colors.text);
865
+ doc.fontSize(13).font(this.getFont(true));
866
+ doc.text(suggestion.title, this.margin, y);
867
+ y += 20;
868
+ // Description
869
+ doc.fontSize(11).font(this.getFont());
870
+ doc.text(suggestion.description, this.margin + 10, y, {
871
+ width: this.contentWidth - 10,
872
+ });
873
+ y = doc.y + 10;
874
+ // Expected impact
875
+ if (suggestion.expectedImpact) {
876
+ doc.fillColor(this.colors.primary);
877
+ doc.text(`${t.expectedImpact}: ${suggestion.expectedImpact}`, this.margin + 10, y, {
878
+ width: this.contentWidth - 10,
879
+ });
880
+ y = doc.y + 10;
881
+ }
882
+ doc.fillColor(this.colors.text);
883
+ y += 15;
884
+ }
885
+ }
886
+ /**
887
+ * Add competitive positioning page.
888
+ *
889
+ * 競合ポジショニング分析ページを追加。
890
+ */
891
+ addCompetitivePositioningPage(doc, options, startY = this.margin) {
892
+ let y = startY;
893
+ const positioning = options.data.marketingAnalytics?.competitivePositioning;
894
+ const t = this.translations;
895
+ // Page title
896
+ doc.fontSize(20).font(this.getFont(true));
897
+ doc.text(t.competitivePositioning, this.margin, y);
898
+ y += 35;
899
+ // Market Position
900
+ if (positioning?.marketPosition) {
901
+ doc.fontSize(14).font(this.getFont(true));
902
+ doc.fillColor(this.colors.primary);
903
+ doc.text(t.marketPosition, this.margin, y);
904
+ y += 20;
905
+ doc.fontSize(11).font(this.getFont());
906
+ doc.fillColor(this.colors.text);
907
+ doc.text(positioning.marketPosition, this.margin, y, {
908
+ width: this.contentWidth,
909
+ });
910
+ y = doc.y + 25;
911
+ }
912
+ // Competitor Comparison
913
+ if (positioning?.competitorComparison?.length) {
914
+ doc.fontSize(14).font(this.getFont(true));
915
+ doc.fillColor(this.colors.primary);
916
+ doc.text(t.competitorComparison, this.margin, y);
917
+ y += 25;
918
+ for (const comp of positioning.competitorComparison) {
919
+ // Check if we need a new page
920
+ if (y > this.maxContentY - 150) {
921
+ this.drawPageFooter(doc);
922
+ doc.addPage();
923
+ this.drawPageBackground(doc);
924
+ y = this.drawPageHeader(doc);
925
+ doc.fontSize(16).font(this.getFont(true));
926
+ doc.fillColor(this.colors.text);
927
+ doc.text(`${t.competitivePositioning} ${t.continued}`, this.margin, y);
928
+ y += 30;
929
+ }
930
+ // Competitor name header
931
+ doc.fillColor("#ffffff");
932
+ doc.rect(this.margin, y, this.contentWidth, 22).fill(this.colors.competitorHeader);
933
+ doc.fontSize(12).font(this.getFont(true));
934
+ doc.fillColor("#ffffff");
935
+ doc.text(`vs ${comp.competitor}`, this.margin + 10, y + 5);
936
+ y += 28;
937
+ const halfWidth = this.contentWidth / 2 - 10;
938
+ const itemSpacing = 4;
939
+ const headerHeight = 22;
940
+ const boxPadding = 10;
941
+ // Calculate heights for both sections first
942
+ doc.fontSize(9).font(this.getFont());
943
+ // Calculate Strengths heights
944
+ const strengthHeights = [];
945
+ let totalStrengthsHeight = headerHeight;
946
+ for (const strength of (comp.ourStrengths || []).slice(0, 4)) {
947
+ const h = doc.heightOfString(`✓ ${strength}`, { width: halfWidth - 16 });
948
+ strengthHeights.push(h);
949
+ totalStrengthsHeight += h + itemSpacing;
950
+ }
951
+ totalStrengthsHeight += boxPadding;
952
+ // Calculate Weaknesses heights (with extra right padding)
953
+ const weaknessHeights = [];
954
+ let totalWeaknessesHeight = headerHeight;
955
+ for (const weakness of (comp.ourWeaknesses || []).slice(0, 4)) {
956
+ const h = doc.heightOfString(`△ ${weakness}`, { width: halfWidth - 24 });
957
+ weaknessHeights.push(h);
958
+ totalWeaknessesHeight += h + itemSpacing;
959
+ }
960
+ totalWeaknessesHeight += boxPadding;
961
+ // Sync box heights (use the larger one)
962
+ const boxHeight = Math.max(totalStrengthsHeight, totalWeaknessesHeight, 80);
963
+ // Strengths (left side)
964
+ if (comp.ourStrengths?.length) {
965
+ doc.fillColor(this.colors.highlightBackground);
966
+ doc.rect(this.margin, y, halfWidth, boxHeight).fill();
967
+ doc.fillColor(this.colors.successDark);
968
+ doc.fontSize(10).font(this.getFont(true));
969
+ doc.text(t.ourStrengths, this.margin + 8, y + 5);
970
+ doc.fontSize(9).font(this.getFont());
971
+ let sy = y + headerHeight;
972
+ let i = 0;
973
+ for (const strength of comp.ourStrengths.slice(0, 4)) {
974
+ doc.text(`✓ ${strength}`, this.margin + 8, sy, {
975
+ width: halfWidth - 16,
976
+ });
977
+ sy += strengthHeights[i] + itemSpacing;
978
+ i++;
979
+ }
980
+ }
981
+ // Weaknesses (right side)
982
+ if (comp.ourWeaknesses?.length) {
983
+ doc.fillColor(this.colors.concernBackground);
984
+ doc.rect(this.margin + halfWidth + 20, y, halfWidth, boxHeight).fill();
985
+ doc.fillColor(this.colors.errorDark);
986
+ doc.fontSize(10).font(this.getFont(true));
987
+ doc.text(t.ourWeaknesses, this.margin + halfWidth + 28, y + 5);
988
+ doc.fontSize(9).font(this.getFont());
989
+ let wy = y + headerHeight;
990
+ let i = 0;
991
+ for (const weakness of comp.ourWeaknesses.slice(0, 4)) {
992
+ doc.text(`△ ${weakness}`, this.margin + halfWidth + 28, wy, {
993
+ width: halfWidth - 24,
994
+ });
995
+ wy += weaknessHeights[i] + itemSpacing;
996
+ i++;
997
+ }
998
+ }
999
+ y += boxHeight + 10;
1000
+ // Battle Strategy
1001
+ if (comp.battleStrategy) {
1002
+ doc.fillColor(this.colors.summaryBackground);
1003
+ doc.rect(this.margin, y, this.contentWidth, 45).fill();
1004
+ doc.fillColor(this.colors.primary);
1005
+ doc.fontSize(9).font(this.getFont(true));
1006
+ doc.text(`${t.battleStrategy}:`, this.margin + 8, y + 5);
1007
+ doc.fontSize(9).font(this.getFont());
1008
+ doc.text(comp.battleStrategy, this.margin + 8, y + 18, {
1009
+ width: this.contentWidth - 16,
1010
+ height: 24,
1011
+ ellipsis: true,
1012
+ });
1013
+ y += 55;
1014
+ }
1015
+ y += 15;
1016
+ }
1017
+ }
1018
+ // Check if we need a new page for remaining sections
1019
+ if (y > this.maxContentY - 100) {
1020
+ this.drawPageFooter(doc);
1021
+ doc.addPage();
1022
+ this.drawPageBackground(doc);
1023
+ y = this.drawPageHeader(doc);
1024
+ }
1025
+ // Differentiation Strategy
1026
+ if (positioning?.differentiationStrategy) {
1027
+ doc.fontSize(14).font(this.getFont(true));
1028
+ doc.fillColor(this.colors.primary);
1029
+ doc.text(t.differentiationStrategy, this.margin, y);
1030
+ y += 20;
1031
+ doc.fontSize(10).font(this.getFont());
1032
+ doc.fillColor(this.colors.text);
1033
+ doc.text(positioning.differentiationStrategy, this.margin, y, {
1034
+ width: this.contentWidth,
1035
+ });
1036
+ y = doc.y + 25;
1037
+ }
1038
+ // Quick Wins
1039
+ if (positioning?.quickWins?.length) {
1040
+ doc.fontSize(14).font(this.getFont(true));
1041
+ doc.fillColor(this.colors.primary);
1042
+ doc.text(t.quickWins, this.margin, y);
1043
+ y += 20;
1044
+ doc.fontSize(10).font(this.getFont());
1045
+ for (const quickWin of positioning.quickWins) {
1046
+ doc.fillColor(this.colors.success);
1047
+ doc.text(`→ ${quickWin}`, this.margin + 10, y, { width: this.contentWidth - 20 });
1048
+ y = doc.y + 5;
1049
+ }
1050
+ }
1051
+ doc.fillColor(this.colors.text);
1052
+ }
1053
+ /**
1054
+ * Add market opportunity priority page.
1055
+ *
1056
+ * 市場機会優先度分析ページを追加。
1057
+ */
1058
+ addMarketOpportunityPriorityPage(doc, options, startY = this.margin) {
1059
+ let y = startY;
1060
+ const priority = options.data.marketingAnalytics?.marketOpportunityPriority;
1061
+ const t = this.translations;
1062
+ // Page title
1063
+ doc.fontSize(20).font(this.getFont(true));
1064
+ doc.text(t.marketOpportunityPriority, this.margin, y);
1065
+ y += 35;
1066
+ // Prioritized Opportunities
1067
+ if (priority?.prioritizedOpportunities?.length) {
1068
+ const fitScoreColors = {
1069
+ excellent: this.colors.success,
1070
+ good: this.colors.primary,
1071
+ moderate: this.colors.warning,
1072
+ poor: this.colors.error,
1073
+ };
1074
+ const effortLabels = {
1075
+ low: t.low,
1076
+ medium: t.medium,
1077
+ high: t.high,
1078
+ };
1079
+ for (const opp of priority.prioritizedOpportunities) {
1080
+ // Check if we need a new page
1081
+ if (y > this.maxContentY - 130) {
1082
+ this.drawPageFooter(doc);
1083
+ doc.addPage();
1084
+ this.drawPageBackground(doc);
1085
+ y = this.drawPageHeader(doc);
1086
+ doc.fontSize(16).font(this.getFont(true));
1087
+ doc.fillColor(this.colors.text);
1088
+ doc.text(`${t.marketOpportunityPriority} ${t.continued}`, this.margin, y);
1089
+ y += 30;
1090
+ }
1091
+ const fitColor = fitScoreColors[opp.fitScore] || this.colors.textSecondary;
1092
+ // Opportunity header with fit score badge
1093
+ doc.fillColor(fitColor);
1094
+ doc.rect(this.margin, y, 70, 20).fill();
1095
+ doc.fillColor("#ffffff");
1096
+ doc.fontSize(9).font(this.getFont(true));
1097
+ doc.text(opp.fitScore.toUpperCase(), this.margin + 5, y + 5);
1098
+ // Effort badge
1099
+ doc.fillColor(this.colors.textSecondary);
1100
+ doc.rect(this.margin + 75, y, 70, 20).fill();
1101
+ doc.fillColor("#ffffff");
1102
+ doc.fontSize(9).font(this.getFont());
1103
+ doc.text(`${t.effort}:${effortLabels[opp.estimatedEffort] || opp.estimatedEffort}`, this.margin + 80, y + 5, {
1104
+ width: 60,
1105
+ height: 12,
1106
+ ellipsis: true,
1107
+ });
1108
+ y += 25;
1109
+ // Opportunity title
1110
+ doc.fillColor(this.colors.text);
1111
+ doc.fontSize(13).font(this.getFont(true));
1112
+ doc.text(opp.opportunity, this.margin, y);
1113
+ y += 20;
1114
+ // Fit reason
1115
+ if (opp.fitReason) {
1116
+ doc.fontSize(10).font(this.getFont());
1117
+ doc.fillColor(this.colors.textSecondary);
1118
+ doc.text(`${t.reason}: ${opp.fitReason}`, this.margin + 10, y, {
1119
+ width: this.contentWidth - 10,
1120
+ });
1121
+ y = doc.y + 10;
1122
+ }
1123
+ // Required changes
1124
+ if (opp.requiredChanges?.length) {
1125
+ doc.fontSize(10).font(this.getFont(true));
1126
+ doc.fillColor(this.colors.text);
1127
+ doc.text(`${t.requiredChanges}:`, this.margin + 10, y);
1128
+ y += 15;
1129
+ doc.fontSize(9).font(this.getFont());
1130
+ for (const change of opp.requiredChanges.slice(0, 3)) {
1131
+ doc.fillColor(this.colors.primary);
1132
+ doc.text(`• ${change}`, this.margin + 20, y, { width: this.contentWidth - 30 });
1133
+ y = doc.y + 3;
1134
+ }
1135
+ if (opp.requiredChanges.length > 3) {
1136
+ doc.fillColor(this.colors.textSecondary);
1137
+ doc.text(`+ ${opp.requiredChanges.length - 3} more...`, this.margin + 20, y);
1138
+ y = doc.y + 3;
1139
+ }
1140
+ y += 5;
1141
+ }
1142
+ // Recommended action
1143
+ if (opp.recommendedAction) {
1144
+ doc.fillColor(this.colors.highlightBackground);
1145
+ doc.rect(this.margin + 10, y, this.contentWidth - 20, 40).fill();
1146
+ doc.fillColor(this.colors.successDark);
1147
+ doc.fontSize(9).font(this.getFont(true));
1148
+ doc.text(`${t.recommendedAction}:`, this.margin + 15, y + 5);
1149
+ doc.fontSize(9).font(this.getFont());
1150
+ doc.text(opp.recommendedAction, this.margin + 15, y + 17, {
1151
+ width: this.contentWidth - 30,
1152
+ height: 20,
1153
+ ellipsis: true,
1154
+ });
1155
+ y += 50;
1156
+ }
1157
+ y += 15;
1158
+ }
1159
+ }
1160
+ // Check if we need a new page for strategic recommendation
1161
+ if (y > this.maxContentY - 50) {
1162
+ this.drawPageFooter(doc);
1163
+ doc.addPage();
1164
+ this.drawPageBackground(doc);
1165
+ y = this.drawPageHeader(doc);
1166
+ }
1167
+ // Strategic Recommendation
1168
+ if (priority?.strategicRecommendation) {
1169
+ doc.fontSize(14).font(this.getFont(true));
1170
+ doc.fillColor(this.colors.primary);
1171
+ doc.text(t.strategicRecommendations, this.margin, y);
1172
+ y += 20;
1173
+ doc.fillColor(this.colors.summaryBackground);
1174
+ doc.rect(this.margin, y, this.contentWidth, 80).fill();
1175
+ doc.fontSize(10).font(this.getFont());
1176
+ doc.fillColor(this.colors.primary);
1177
+ doc.text(priority.strategicRecommendation, this.margin + 10, y + 10, {
1178
+ width: this.contentWidth - 20,
1179
+ height: 65,
1180
+ ellipsis: true,
1181
+ });
1182
+ }
1183
+ doc.fillColor(this.colors.text);
1184
+ }
1185
+ /**
1186
+ * Add trend analysis page.
1187
+ */
1188
+ addTrendAnalysisPage(doc, options, startY = this.margin) {
1189
+ let y = startY;
1190
+ const trendAnalysis = options.data.marketingAnalytics?.trendAnalysis;
1191
+ const t = this.translations;
1192
+ // Page title
1193
+ doc.fontSize(20).font(this.getFont(true));
1194
+ doc.text(t.trendAnalysisAndPredictions, this.margin, y);
1195
+ y += 35;
1196
+ // User Growth Trend
1197
+ if (trendAnalysis?.userGrowthTrend) {
1198
+ doc.fontSize(12).font(this.getFont(true));
1199
+ doc.text(t.userGrowth, this.margin, y);
1200
+ y += 18;
1201
+ doc.fontSize(10).font(this.getFont());
1202
+ doc.text(trendAnalysis.userGrowthTrend, this.margin, y, {
1203
+ width: this.contentWidth,
1204
+ });
1205
+ y = doc.y + 20;
1206
+ }
1207
+ // Engagement Trend
1208
+ if (trendAnalysis?.engagementTrend) {
1209
+ doc.fontSize(12).font(this.getFont(true));
1210
+ doc.text(t.engagement, this.margin, y);
1211
+ y += 18;
1212
+ doc.fontSize(10).font(this.getFont());
1213
+ doc.text(trendAnalysis.engagementTrend, this.margin, y, {
1214
+ width: this.contentWidth,
1215
+ });
1216
+ y = doc.y + 20;
1217
+ }
1218
+ // Rating Trend
1219
+ if (trendAnalysis?.ratingTrend) {
1220
+ doc.fontSize(12).font(this.getFont(true));
1221
+ doc.text(t.ratings, this.margin, y);
1222
+ y += 18;
1223
+ doc.fontSize(10).font(this.getFont());
1224
+ doc.text(trendAnalysis.ratingTrend, this.margin, y, {
1225
+ width: this.contentWidth,
1226
+ });
1227
+ y = doc.y + 30;
1228
+ }
1229
+ // Predictions
1230
+ if (trendAnalysis?.predictions?.length) {
1231
+ doc.fontSize(14).font(this.getFont(true));
1232
+ doc.text(t.predictions, this.margin, y);
1233
+ y += 22;
1234
+ doc.fontSize(10).font(this.getFont());
1235
+ for (const prediction of trendAnalysis.predictions) {
1236
+ doc.fillColor(this.colors.primary);
1237
+ doc.text(`-> ${prediction}`, this.margin + 10, y, { width: this.contentWidth - 20 });
1238
+ y = doc.y + 6;
1239
+ }
1240
+ doc.fillColor(this.colors.text);
1241
+ }
1242
+ }
1243
+ /**
1244
+ * Add GitHub-based code improvements page.
1245
+ *
1246
+ * GitHubベースのコード改善提案ページを追加。
1247
+ */
1248
+ addGitHubImprovementsPage(doc, options, startY = this.margin) {
1249
+ let y = startY;
1250
+ const githubImprovements = options.data.githubImprovements;
1251
+ const improvements = githubImprovements?.improvements || [];
1252
+ const t = this.translations;
1253
+ // Page title
1254
+ doc.fontSize(20).font(this.getFont(true));
1255
+ doc.text(t.codebaseImprovements, this.margin, y);
1256
+ y += 25;
1257
+ // Repository info
1258
+ doc.fontSize(10).font(this.getFont());
1259
+ doc.fillColor(this.colors.textSecondary);
1260
+ const repo = githubImprovements?.repository || "";
1261
+ const framework = githubImprovements?.framework || "";
1262
+ doc.text(`${t.repository}: ${repo} | ${t.framework}: ${framework}`, this.margin, y);
1263
+ y += 20;
1264
+ // Summary
1265
+ if (githubImprovements?.improvementSummary) {
1266
+ doc.fillColor(this.colors.text);
1267
+ doc.fontSize(11).font(this.getFont());
1268
+ doc.text(githubImprovements.improvementSummary, this.margin, y, {
1269
+ width: this.contentWidth,
1270
+ });
1271
+ y = doc.y + 25;
1272
+ }
1273
+ // Each improvement
1274
+ for (const improvement of improvements) {
1275
+ // Check if we need a new page
1276
+ if (y > this.maxContentY - 150) {
1277
+ this.drawPageFooter(doc);
1278
+ doc.addPage();
1279
+ this.drawPageBackground(doc);
1280
+ y = this.drawPageHeader(doc);
1281
+ doc.fontSize(16).font(this.getFont(true));
1282
+ doc.fillColor(this.colors.text);
1283
+ doc.text(`${t.codebaseImprovements} ${t.continued}`, this.margin, y);
1284
+ y += 30;
1285
+ }
1286
+ // Priority badge
1287
+ const priorityColor = improvement.priority === "high"
1288
+ ? this.colors.error
1289
+ : improvement.priority === "medium"
1290
+ ? this.colors.warning
1291
+ : this.colors.success;
1292
+ doc.fillColor(priorityColor);
1293
+ doc.rect(this.margin, y, 60, 18).fill();
1294
+ doc.fillColor("#ffffff");
1295
+ doc.fontSize(9).font(this.getFont(true));
1296
+ doc.text(improvement.priority?.toUpperCase() || "MEDIUM", this.margin + 5, y + 4);
1297
+ // Category badge
1298
+ doc.fillColor(this.colors.primary);
1299
+ doc.rect(this.margin + 65, y, 80, 18).fill();
1300
+ doc.fillColor("#ffffff");
1301
+ doc.fontSize(9).font(this.getFont());
1302
+ doc.text(improvement.category || "", this.margin + 70, y + 4);
1303
+ y += 25;
1304
+ // Title
1305
+ doc.fillColor(this.colors.text);
1306
+ doc.fontSize(13).font(this.getFont(true));
1307
+ doc.text(improvement.title || "", this.margin, y);
1308
+ y += 20;
1309
+ // Description
1310
+ doc.fontSize(10).font(this.getFont());
1311
+ doc.text(improvement.description || "", this.margin + 10, y, {
1312
+ width: this.contentWidth - 10,
1313
+ });
1314
+ y = doc.y + 10;
1315
+ // Related Feature
1316
+ if (improvement.relatedFeature) {
1317
+ doc.fillColor("#6a1b9a");
1318
+ doc.fontSize(9).font(this.getFont());
1319
+ doc.text(`${t.relatedFeature}: ${improvement.relatedFeature}`, this.margin + 10, y);
1320
+ y = doc.y + 8;
1321
+ }
1322
+ // Code References
1323
+ const codeRefs = improvement.codeReferences || [];
1324
+ if (codeRefs.length > 0) {
1325
+ doc.fillColor(this.colors.text);
1326
+ doc.fontSize(10).font(this.getFont(true));
1327
+ doc.text(`${t.fileModifications}:`, this.margin + 10, y);
1328
+ y += 15;
1329
+ for (const ref of codeRefs) {
1330
+ // Check page break
1331
+ if (y > this.maxContentY - 50) {
1332
+ this.drawPageFooter(doc);
1333
+ doc.addPage();
1334
+ this.drawPageBackground(doc);
1335
+ y = this.drawPageHeader(doc);
1336
+ doc.fontSize(16).font(this.getFont(true));
1337
+ doc.fillColor(this.colors.text);
1338
+ doc.text(`${t.codebaseImprovements} ${t.continued}`, this.margin, y);
1339
+ y += 30;
1340
+ }
1341
+ // Modification type icon
1342
+ const modIcon = {
1343
+ add: "+",
1344
+ modify: "~",
1345
+ refactor: "R",
1346
+ optimize: "O",
1347
+ };
1348
+ const modColor = {
1349
+ add: this.colors.success,
1350
+ modify: this.colors.warning,
1351
+ refactor: this.colors.primary,
1352
+ optimize: "#6a1b9a",
1353
+ };
1354
+ const icon = modIcon[ref.modificationType] || "?";
1355
+ const color = modColor[ref.modificationType] || this.colors.textSecondary;
1356
+ // Modification type badge
1357
+ doc.fillColor(color);
1358
+ doc.roundedRect(this.margin + 15, y, 20, 16, 2).fill();
1359
+ doc.fillColor("#ffffff");
1360
+ doc.fontSize(9).font(this.getFont(true));
1361
+ doc.text(icon, this.margin + 21, y + 3);
1362
+ // File path
1363
+ doc.fillColor(this.colors.primary);
1364
+ doc.fontSize(9).font(this.getFont());
1365
+ doc.text(ref.filePath || "", this.margin + 40, y + 3, {
1366
+ width: this.contentWidth - 50,
1367
+ });
1368
+ y += 18;
1369
+ // Current functionality
1370
+ doc.fillColor(this.colors.textSecondary);
1371
+ doc.fontSize(8).font(this.getFont());
1372
+ doc.text(`${t.current}: ${ref.currentFunctionality || ""}`, this.margin + 40, y, {
1373
+ width: this.contentWidth - 50,
1374
+ });
1375
+ y = doc.y + 3;
1376
+ // Proposed change
1377
+ doc.fillColor(this.colors.success);
1378
+ doc.text(`${t.proposed}: ${ref.proposedChange || ""}`, this.margin + 40, y, {
1379
+ width: this.contentWidth - 50,
1380
+ });
1381
+ y = doc.y + 8;
1382
+ }
1383
+ }
1384
+ // Expected Impact
1385
+ if (improvement.expectedImpact) {
1386
+ doc.fillColor(this.colors.primary);
1387
+ doc.fontSize(9).font(this.getFont());
1388
+ doc.text(`${t.expectedImpact}: ${improvement.expectedImpact}`, this.margin + 10, y);
1389
+ y = doc.y + 5;
1390
+ }
1391
+ doc.fillColor(this.colors.text);
1392
+ y += 20;
1393
+ }
1394
+ }
1395
+ }
1396
+ exports.PDFService = PDFService;
1397
+ //# sourceMappingURL=pdf_service.js.map