Spaces:
Sleeping
Sleeping
| 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, | |
| }; | |
| } | |