FE_Dev / server /services /html-preview /contents /contents-html.service.ts
GitHub Actions
Deploy from GitHub Actions [dev] - 2025-10-31 07:28:50
68f7925
import type { ContentSection } from '@/schema/proposal';
import type { ThemeExtractionResult } from '@/schema/theme-extraction';
import { baseCSS } from '@/server/lib/styles/base-css';
import { ContentsHtmlGenerator } from '@/server/lib/templates/ContentsHtmlGenerator';
import { ThemeCustomizer } from '@/server/lib/theme/theme-customizer';
/**
* ダミーモード用:プレースホルダー画像をcontents画像に置換
*/
function replacePlaceholdersWithDummyImages(html: string, tabName: string): string {
// タブ名からアルファベットを取得 (A案 -> A)
const tabLetter = tabName.replace('案', '');
try {
const fs = require('fs');
const path = require('path');
// URLパスモードを使用(Base64は使用しない)
const contentsImagesDir = path.join(process.cwd(), 'public', 'dummy', 'contents-images');
console.log(`[CONTENTS DUMMY MODE - URL Mode] Looking for images at: ${contentsImagesDir}`);
console.log(`[CONTENTS DUMMY MODE] Current working directory: ${process.cwd()}`);
// ディレクトリが存在するか確認
if (!fs.existsSync(contentsImagesDir)) {
console.error(`[CONTENTS DUMMY MODE] Images directory not found: ${contentsImagesDir}`);
return html;
}
const files = fs.readdirSync(contentsImagesDir);
// タブアルファベットで始まるjpgファイルのみを抽出してソート
const tabImages = files.filter((file: string) => file.startsWith(`${tabLetter}-`) && file.endsWith('.jpg')).sort();
console.log(`[ダミーモード - URL Mode] ${tabLetter}案の画像ファイル: ${tabImages.length}個 (URLパス形式)`);
let imageIndex = 0;
// loading-placeholder.svgを順番にcontents画像のURLパスで置換
html = html.replace(/src="\/loading-placeholder\.svg"/g, () => {
if (imageIndex < tabImages.length) {
const imageFile = tabImages[imageIndex];
const imageUrl = `/dummy/contents-images/${imageFile}`;
imageIndex++;
console.log(`[ダミーモード] 画像置換(URL): ${imageIndex} -> ${imageUrl}`);
return `src="${imageUrl}"`;
} else {
// 画像が足りない場合はループ
const loopIndex = imageIndex % tabImages.length;
const imageFile = tabImages[loopIndex];
const imageUrl = `/dummy/contents-images/${imageFile}`;
imageIndex++;
console.log(`[ダミーモード] 画像置換(URLループ): ${imageIndex} -> ${imageUrl}`);
return `src="${imageUrl}"`;
}
});
// data-image-statusをloadedに変更
html = html.replace(/data-image-status="loading"/g, 'data-image-status="loaded"');
} catch (error) {
console.error(`[ダミーモード] contents画像の読み込みに失敗:`, error);
}
return html;
}
/**
* コンテンツHTML生成サービス
*/
export class ContentsHtmlService {
private generator: ContentsHtmlGenerator;
constructor() {
this.generator = new ContentsHtmlGenerator();
}
/**
* コンテンツHTMLを生成(テンプレートベース版)
* 既存版: 後方互換性維持
*/
generateTemplateBasedHtml(_tabName: string, sections?: ContentSection[], isDummyMode?: boolean): { html: string; css: string };
/**
* コンテンツHTMLを生成(テンプレートベース版)
* テーマ対応版: 新機能
*/
generateTemplateBasedHtml(
_tabName: string,
sections?: ContentSection[],
isDummyMode?: boolean,
theme?: ThemeExtractionResult,
): { html: string; css: string };
/**
* 実装: オーバーロード対応
*/
generateTemplateBasedHtml(
_tabName: string,
sections?: ContentSection[],
isDummyMode?: boolean,
theme?: ThemeExtractionResult,
): { html: string; css: string } {
if (!sections || sections.length === 0) {
return {
html: '<div class="content-section"><p>コンテンツデータがありません</p></div>',
css: baseCSS,
};
}
// コンテンツデータを整形(会社名を取得)
const companyName = sections[0]?.大区分 || 'サンプル企業';
const requestData = {
contentSections: sections,
companyName: companyName,
};
// ContentsHtmlGeneratorでコンテンツセクションとフッターのみを生成
let contentHtml = this.generator.generateContentSectionsOnly(requestData);
// ダミーモードの場合、プレースホルダーを実際のfirefly画像に置換
if (isDummyMode) {
contentHtml = this.replacePlaceholdersWithFireflyImages(contentHtml, _tabName);
}
// CSSの生成
let finalCSS: string;
// ダミーモードでのモックテーマ自動適用を無効化
// テーマカスタマイズ機能を無効化するため、themeが明示的に渡されない限りデフォルトCSSを使用
let effectiveTheme = theme;
// if (isDummyMode && !theme) {
// effectiveTheme = this.createMockTheme();
// console.log('[ContentsHtmlService] ダミーモード: モックテーマを適用');
// }
if (effectiveTheme) {
try {
// テーマが指定されている場合、ThemeCustomizerでカスタムCSSを生成
const themeCustomizer = new ThemeCustomizer({
checkAccessibility: true,
enableCache: true,
});
const cssResult = themeCustomizer.generateThemedCSS(effectiveTheme);
finalCSS = cssResult.fullCSS;
console.log('[ContentsHtmlService] テーマ適用済みCSS生成完了');
} catch (error) {
console.error('[ContentsHtmlService] テーマCSS生成エラー、デフォルトCSSにフォールバック:', error);
finalCSS = baseCSS;
}
} else {
// テーマが指定されていない場合はデフォルトCSS
finalCSS = baseCSS;
console.log('[ContentsHtmlService] デフォルトCSSを使用');
}
return {
html: contentHtml,
css: finalCSS,
};
}
/**
* ダミーモード用:プレースホルダー画像を実際のfirefly画像に置換
*/
private replacePlaceholdersWithFireflyImages(html: string, tabName: string): string {
return replacePlaceholdersWithDummyImages(html, tabName);
}
/**
* ダミーモード用のモックテーマを生成
*/
private createMockTheme(): ThemeExtractionResult {
return {
colors: {
// メインカラー(ブルー系)
primary_color: '#3B82F6', // 鮮やかなブルー
secondary_color: '#E0F2FE', // ライトブルー
accent_color: '#F59E0B', // アクセントオレンジ
// セマンティックカラー
success_semantic_color: '#10B981', // グリーン
warning_semantic_color: '#F59E0B', // オレンジ
error_semantic_color: '#EF4444', // レッド
info_semantic_color: '#3B82F6', // ブルー
// 背景色
primary_background_color: '#FFFFFF', // ホワイト
secondary_background_color: '#F8FAFC', // ライトグレー
tertiary_background_color: '#1E293B', // ダークグレー
overlay_background_color: '#000000', // ブラック
// テキスト色
primary_text_color: '#1F2937', // ダークグレー
secondary_text_color: '#6B7280', // ミディアムグレー
disabled_text_color: '#9CA3AF', // ライトグレー
inverse_text_color: '#FFFFFF', // ホワイト
},
design: {
heading_font_family: 'YuGothic, "Yu Gothic Medium", sans-serif',
main_font_family: 'YuGothic, "Yu Gothic Medium", sans-serif',
special_font_family: 'YuGothic, "Yu Gothic Medium", sans-serif',
design_style: 'modern',
layout_type: 'standard',
},
brand: {
brand_impression: ['modern', 'tech', 'professional'],
industry_characteristics: 'technology oriented design with vibrant colors',
},
analysis_notes: 'Mock theme for dummy mode demonstration',
};
}
}
// シングルトンインスタンス
let serviceInstance: ContentsHtmlService | null = null;
/**
* サービスのシングルトンインスタンスを取得
*/
export function getContentsHtmlService(): ContentsHtmlService {
if (!serviceInstance) {
serviceInstance = new ContentsHtmlService();
}
return serviceInstance;
}
/**
* コンテンツHTML生成関数(互換性のためのエクスポート)
* 既存版: 後方互換性維持
*/
export async function generateContentsHtmlWithImages(
tabName: string,
sections?: ContentSection[],
isDummyMode?: boolean,
forceImageGeneration?: boolean, // 新しいオプション:開発環境でも画像生成を強制
forceEnableInDummyMode?: boolean, // 新しいパラメータ:ダミーモードでも画像生成を強制
ownUrl?: string, // エラーログ用:スクリーンショット元URL
userEmail?: string, // エラーログ用:ユーザーメールアドレス
): Promise<{ html: string; css: string; batchId?: string }>;
/**
* コンテンツHTML生成関数(テーマ対応版)
*/
export async function generateContentsHtmlWithImages(
tabName: string,
sections?: ContentSection[],
isDummyMode?: boolean,
forceImageGeneration?: boolean,
forceEnableInDummyMode?: boolean,
ownUrl?: string,
userEmail?: string,
theme?: ThemeExtractionResult, // テーマパラメータ追加
): Promise<{ html: string; css: string; batchId?: string }>;
/**
* 実装: オーバーロード対応
*/
export async function generateContentsHtmlWithImages(
tabName: string,
sections?: ContentSection[],
isDummyMode?: boolean,
forceImageGeneration?: boolean, // 新しいオプション:開発環境でも画像生成を強制
forceEnableInDummyMode?: boolean, // 新しいパラメータ:ダミーモードでも画像生成を強制
ownUrl?: string, // エラーログ用:スクリーンショット元URL
userEmail?: string, // エラーログ用:ユーザーメールアドレス
theme?: ThemeExtractionResult, // テーマパラメータ追加
): Promise<{ html: string; css: string; batchId?: string }> {
const service = getContentsHtmlService();
// まずHTMLテンプレートを生成(テーマ対応)
const { html: baseHtml, css } = service.generateTemplateBasedHtml(tabName, sections, isDummyMode, theme);
// ダミーモード以外の場合、かつ(本番環境または強制フラグが有効)の場合、画像生成バッチを開始
if (!isDummyMode && (process.env.NODE_ENV !== 'development' || forceImageGeneration)) {
try {
// ContentImagesGeneratorのインスタンスを取得
const fireflyClientId = process.env.D_FIREFLY_CLIENT_ID || '';
const fireflyClientSecret = process.env.D_FIREFLY_SECRET || '';
if (fireflyClientId && fireflyClientSecret) {
const { ContentImagesGenerator } = await import('./content-images.service');
const imageGenerator = ContentImagesGenerator.getInstance(fireflyClientId, fireflyClientSecret);
// ContentContextを作成
const contentContext = {
companyName: sections?.[0]?.大区分 || 'サンプル企業',
businessType: 'general',
targetAudience: 'general',
};
// 画像生成バッチを開始(非同期)
const result = await imageGenerator.startImageGenerationBatch(
baseHtml,
contentContext,
'openai', // promptProvider (ClaudeからOpenAIに変更)
'openai', // imageProvider (OpenAI優先)
undefined, // callbackUrl - ポーリングベースなので不要
ownUrl, // referenceUrl(エラーログ用)
tabName,
forceEnableInDummyMode, // ダミーモードでも画像生成を強制するパラメータを渡す
userEmail, // エラーログ用:ユーザーメールアドレス
);
return {
html: result.htmlWithPlaceholders,
css,
batchId: result.batchId,
};
} else {
const { ContentImagesGenerator } = await import('./content-images.service');
// ダミーのFirefly認証情報でインスタンス作成(OpenAI優先なので問題ない)
const imageGenerator = ContentImagesGenerator.getInstance('dummy', 'dummy');
const contentContext = {
companyName: sections?.[0]?.大区分 || 'サンプル企業',
businessType: 'general',
targetAudience: 'general',
};
const result = await imageGenerator.startImageGenerationBatch(
baseHtml,
contentContext,
'openai', // promptProvider
'openai', // imageProvider (OpenAIを強制使用)
undefined,
ownUrl, // referenceUrl(エラーログ用)
tabName,
forceEnableInDummyMode,
userEmail, // エラーログ用:ユーザーメールアドレス
);
return {
html: result.htmlWithPlaceholders,
css,
batchId: result.batchId,
};
}
} catch (error) {
const timestamp = new Date().toLocaleString('ja-JP', { hour12: false });
console.error(`[${timestamp}] [CONTENTS HTML] Error starting image generation:`, error);
// エラーが発生してもベースHTMLを返す
}
}
return {
html: baseHtml,
css,
};
}