import { readFileSync } from 'node:fs'; import { join } from 'node:path'; // 大区分の型定義 import type { ContentSection, SectionType } from '@/schema/proposal'; type RequestData = { companyInfo: { name: string; title?: string; }; heroSection: { コピー: { メインコピー: string; サブコピー1?: string; サブコピー2?: string; サブコピー3?: string; }; CTA: { ボタンテキスト: string; マイクロコピー?: string; }; ビジュアル?: { 内容: string; 作成指示?: string; }; 権威付け?: { 内容: string; }; }; contentSections?: ContentSection[]; }; // aria-label マッピング const ariaLabelMapping: Record = { '問題提起/共感': '問題提起/共感', シミュレーション: 'シミュレーション', '商品/サービスの特徴': '商品/サービスの特徴', '解決策/ベネフィット提示': '解決策/ベネフィット提示', 'SPオファー/限定特典': 'SPオファー/限定特典', '料金/プラン': '料金/プラン', '商品/サービスの詳細情報': '商品/サービスの詳細情報', '成功事例/ユーザーボイス紹介': '成功事例/ユーザーボイス紹介', 'ご利用の流れ/ステップ': 'ご利用の流れ/ステップ', メディア掲載情報: 'メディア掲載情報', 競合比較: '競合比較', 'スタッフ・メンバー紹介': 'スタッフ・メンバー紹介', 権威訴求: '権威訴求', 店舗情報: '店舗情報', 活用シーン: '活用シーン', 'FAQ/よくある質問': 'FAQ/よくある質問', 'News/更新情報': 'News/更新情報', }; // セクションタイプマッピング const sectionMapping: Record = { '問題提起/共感': 'problem-solution', シミュレーション: 'simulation', '商品/サービスの特徴': 'features', '解決策/ベネフィット提示': 'benefits', 'SPオファー/限定特典': 'special-offer', '料金/プラン': 'pricing', '商品/サービスの詳細情報': 'product-details', '成功事例/ユーザーボイス紹介': 'testimonials', 'ご利用の流れ/ステップ': 'usage-flow', メディア掲載情報: 'media-coverage', 競合比較: 'comparison', 'スタッフ・メンバー紹介': 'staff', 権威訴求: 'authority', 店舗情報: 'store-info', 活用シーン: 'use-cases', 'FAQ/よくある質問': 'faq', 'News/更新情報': 'news-updates', }; // HTMLテンプレート生成クラス export class ContentsHtmlGenerator { private baseTemplate: string; private imageIdCounter: number; constructor() { this.baseTemplate = this.loadTemplateFromFile(); this.imageIdCounter = 0; } /** * ユニークな画像IDを生成 */ private generateUniqueImageId(sectionType: string, index: number): string { this.imageIdCounter++; const uniqueId = `${sectionType}-img-${index}-${this.imageIdCounter}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; return uniqueId; } // メインのHTML生成メソッド generateHTML(data: RequestData): string { let html = this.baseTemplate; // 会社情報の置換 html = html.replace(/{{COMPANY_NAME}}/g, data.companyInfo.name); html = html.replace(/{{COMPANY_TITLE}}/g, data.companyInfo.title || data.heroSection.コピー.メインコピー); // ヒーローセクションの置換 html = html.replace(/{{HERO_MAIN_COPY}}/g, this.formatHeroTitle(data.heroSection.コピー.メインコピー)); html = html.replace(/{{HERO_FEATURES}}/g, this.generateHeroFeatures(data.heroSection.コピー)); html = html.replace(/{{CTA_BUTTON_TEXT}}/g, data.heroSection.CTA.ボタンテキスト); html = html.replace(/{{CTA_MICRO_COPY}}/g, data.heroSection.CTA.マイクロコピー || ''); // ヒーロー画像の置換は削除(CNデータには通常ヒーロー画像はなく、コンテンツ画像のIDが上書きされる問題を防ぐため) // コンテンツセクションの生成 const contentSections = this.generateContentSections(data.contentSections || []); html = html.replace(/{{CONTENT_SECTIONS}}/g, contentSections); // 権威付けセクションの生成 const authoritySection = this.generateAuthoritySection(data.heroSection.権威付け); html = html.replace(/{{AUTHORITY_SECTION}}/g, authoritySection); return html; } /** * ヒーロータイトルのフォーマット */ private formatHeroTitle(title: string): string { return title || ''; } /** * ヒーローフィーチャーの生成 */ private generateHeroFeatures(copyData: { メインコピー: string; サブコピー1?: string; サブコピー2?: string; サブコピー3?: string }): string { const features = []; if (copyData.サブコピー1) features.push(copyData.サブコピー1); if (copyData.サブコピー2) features.push(copyData.サブコピー2); if (copyData.サブコピー3) features.push(copyData.サブコピー3); if (features.length === 0) { return ''; } return features.map((feature) => `
  • ${feature}
  • `).join(''); } /** * コンテンツセクションとフッターのみを生成(ヘッダーとヒーローセクションを除外) */ generateContentSectionsOnly(data: { contentSections?: any[]; companyName?: string }): string { // コンテンツセクションの生成 const contentSections = this.generateContentSections(data.contentSections || []); // フッターを追加(コンテンツ単体で表示される場合のため) const footer = this.generateFooterSection(data.companyName); const html = contentSections + footer; return html; } // コンテンツセクションの生成 private generateContentSections(sections: ContentSection[]): string { const results = sections.map((section) => { const sectionType = this.mapSectionType(section.大区分); const html = this.generateSectionByType(sectionType, section); return html; }); // すべてのセクションを返す(空のチェックを削除) return results.join(''); } // 大区分からセクションタイプへのマッピング private mapSectionType(type: SectionType): string { return sectionMapping[type] || 'generic'; } // セクションタイプ別のHTML生成 private generateSectionByType(type: string, section: ContentSection): string { switch (type) { case 'features': return this.generateFeaturesSection(section); case 'benefits': return this.generateBenefitsSection(section); case 'authority': return this.generateAuthorityContentSection(section); case 'problem-solution': return this.generateProblemSolutionSection(section); case 'usage-flow': return this.generateUsageFlowSection(section); case 'product-details': return this.generateProductDetailsSection(section); case 'simulation': return this.generateSimulationSection(section); case 'special-offer': return this.generateSpecialOfferSection(section); case 'pricing': return this.generatePricingSection(section); case 'testimonials': return this.generateTestimonialsSection(section); case 'media-coverage': return this.generateMediaCoverageSection(section); case 'comparison': return this.generateComparisonSection(section); case 'staff': return this.generateStaffSection(section); case 'store-info': return this.generateStoreInfoSection(section); case 'use-cases': return this.generateUseCasesSection(section); case 'faq': return this.generateFaqSection(section); case 'news-updates': return this.generateNewsUpdatesSection(section); default: return this.generateGenericSection(section); } } // 特徴セクションの生成 private generateFeaturesSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // ユニークIDを生成(タイムスタンプ + インデックス + ランダム文字列) const imageId = this.generateUniqueImageId('feature', index); // 変数を即座にテンプレートに埋め込む(クロージャの問題を回避) const imageAlt = section.大区分 || '画像生成中...'; const imageTitle = section.大区分 || ''; const itemNumber = String(index + 1).padStart(2, '0'); const itemHeading = item.見出し?.value || ''; const itemContent = item.内容?.value || ''; // 4:3のプレースホルダーを生成 const placeholderHtml = `${imageAlt}`; return `
    ${itemNumber}
    ${placeholderHtml}

    ${itemHeading}

    ${itemContent}

    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // ベネフィットセクションの生成 private generateBenefitsSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map( (item, index) => `
    ${item.見出し?.value ? `

    ${item.見出し.value}

    ` : ''}
    ${item.内容?.value || ''} ${item.注釈?.value ? `${item.注釈.value}` : ''}
    `, ) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 権威付けセクションの生成 private generateAuthorityContentSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map( (item, index) => `
    ${item.見出し?.value || ''}

    ${item.内容?.value || ''}

    `, ) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 問題解決セクションの生成 private generateProblemSolutionSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { const imageId = this.generateUniqueImageId('problem', index); // 変数を即座にキャプチャ const imageAlt = item.見出し?.value || '画像生成中...'; const imageTitle = item.見出し?.value?.substring(0, 200) + '...' || ''; const itemHeading = item.見出し?.value || ''; const itemContent = item.内容?.value || ''; const itemNote = item.注釈?.value || ''; return `
    ${imageAlt}

    ${itemHeading}

    ${itemContent}

    ${itemNote ? `

    ${itemNote}

    ` : ''}
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 利用フローセクションの生成 private generateUsageFlowSection(section: ContentSection): string { const steps = (section.小区分json ?? []) .map((item, index: number) => { const imageId = this.generateUniqueImageId('flow', index); // 動的フィールドをチェック const itemAny = item as any; const hasStep = itemAny.ステップ?.value; const hasIntro = itemAny.導入?.value; const title = hasStep ? itemAny.ステップ.value : item.見出し?.value || ''; return `
    ${String(index + 1).padStart(2, '0')}
    ${title || '画像生成中...'}

    ${title}

    ${hasIntro ? `
    ${itemAny.導入.value}
    ` : ''}

    ${item.内容?.value || ''}

    ${item.注釈?.value ? `

    ${item.注釈?.value}

    ` : ''}
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${steps}
    `; } // 商品詳細セクションの生成 private generateProductDetailsSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { const imageId = this.generateUniqueImageId('product', index); // 変数を即座にキャプチャ const imageAlt = item.見出し?.value || '画像生成中...'; const imageTitle = item.見出し?.value?.substring(0, 200) + '...' || ''; const productSubtitle = item.見出し?.value || ''; const productText = item.内容?.value || ''; return `
    ${imageAlt}

    ${productSubtitle}

    ${productText}

    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // シミュレーションセクションの生成 private generateSimulationSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // 動的フィールドをチェック const itemAny = item as any; const hasQA = itemAny.質疑?.value; const hasButton = itemAny.ボタン?.value; const hasIntro = itemAny.導入?.value; const title = item.見出し?.value || ''; const content = hasQA ? itemAny.質疑.value : item.内容?.value || ''; const intro = hasIntro ? itemAny.導入.value : ''; const buttonText = hasButton ? itemAny.ボタン.value : ''; return `

    ${title}

    ${intro ? `
    ${intro}
    ` : ''}
    ${content}
    ${buttonText ? `` : ''} ${item.注釈?.value ? `

    ${item.注釈?.value}

    ` : ''}
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 特別オファーセクションの生成 private generateSpecialOfferSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // 動的フィールドをチェック const itemAny = item as any; const hasOfferContent = itemAny['オファー/特典内容']?.value; const hasButton = itemAny.ボタン?.value; const hasIntro = itemAny.導入?.value; const title = item.見出し?.value || ''; const content = hasOfferContent ? itemAny['オファー/特典内容'].value : item.内容?.value || ''; const intro = hasIntro ? itemAny.導入.value : ''; const buttonText = hasButton ? itemAny.ボタン.value : ''; return `

    ${title}

    ${intro ? `
    ${intro}
    ` : ''}

    ${content}

    ${buttonText ? `` : ''} ${item.注釈?.value ? `

    ${item.注釈?.value}

    ` : ''}
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 料金セクションの生成 private generatePricingSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // 動的フィールドをチェック(互換性のため型アサーションを使用) const itemAny = item as any; const hasPrice = itemAny.料金?.value; const hasPlan = itemAny.プラン名?.value; // 動的フィールドがある場合は特別な処理 if (hasPrice || hasPlan) { return `
    ${ hasPlan ? `

    ${itemAny.プラン名.value}

    ` : `

    ${item.見出し?.value || ''}

    ` } ${hasPrice ? `
    ${itemAny.料金.value}
    ` : ''}

    ${item.内容?.value || ''}

    ${item.注釈?.value ? `

    ${item.注釈.value}

    ` : ''}
    `; } // 標準フィールドのみの場合 return `

    ${item.見出し?.value || ''}

    ${item.内容?.value || ''}

    ${item.注釈?.value ? `

    ${item.注釈?.value}

    ` : ''}
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // お客様の声セクションの生成 private generateTestimonialsSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map( (item, index) => `
    👤
    ${item.見出し?.value || ''}

    ${item.内容?.value || ''}

    ${item.ユーザー情報?.value ? `
    ${item.ユーザー情報?.value}
    ` : ''} ${item.注釈?.value ? `

    ${item.注釈?.value}

    ` : ''}
    `, ) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // メディア掲載セクションの生成 private generateMediaCoverageSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // 動的フィールドをチェック const itemAny = item as any; const hasTitle = itemAny.タイトル?.value; const title = hasTitle ? itemAny.タイトル.value : item.見出し?.value || ''; return `

    ${title}

    ${item.内容?.value || ''}

    ${item.注釈?.value ? `

    ${item.注釈?.value}

    ` : ''}
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 競合比較セクションの生成 private generateComparisonSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // 動的フィールドをチェック const itemAny = item as any; const hasComparisonItem = itemAny.比較項目?.value; const title = hasComparisonItem ? itemAny.比較項目.value : item.見出し?.value || ''; return `
    🔍

    ${title}

    ${item.内容?.value || ''}

    ${item.注釈?.value ? `

    ${item.注釈?.value}

    ` : ''}
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // スタッフ紹介セクションの生成 private generateStaffSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // 動的フィールドをチェック const itemAny = item as any; const hasName = itemAny.名前?.value; const hasIntro = itemAny.導入?.value; const hasDescription = itemAny.紹介文?.value; const name = hasName ? itemAny.名前.value : item.ユーザー情報?.value || ''; const role = item.見出し?.value || ''; const intro = hasIntro ? itemAny.導入.value : ''; const bio = hasDescription ? itemAny.紹介文.value : item.内容?.value || ''; return `
    👤
    ${role}
    ${name}
    ${intro ? `
    ${intro}
    ` : ''}

    ${bio}

    ${item.注釈?.value ? `

    ${item.注釈?.value}

    ` : ''}
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 店舗情報セクションの生成 private generateStoreInfoSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // 動的フィールドをチェック const itemAny = item as any; const hasStore = itemAny.店舗?.value; const hasIntro = itemAny.導入?.value; const hasDetails = itemAny.詳細情報?.value; const storeName = hasStore ? itemAny.店舗.value : item.見出し?.value || ''; const description = item.内容?.value || ''; const intro = hasIntro ? itemAny.導入.value : ''; const details = hasDetails ? itemAny.詳細情報.value : ''; // 地図画像用のユニークIDを生成 const mapImageId = this.generateUniqueImageId('map', index); // 変数を即座にキャプチャ const mapAlt = `地図: ${storeName}`; const mapTitle = `地図: ${storeName}`; const noteValue = item.注釈?.value || ''; return `
    ${mapAlt}

    ${storeName}

    ${intro ? `

    ${intro}

    ` : ''}

    ${description}

    ${ details || noteValue ? `
    詳細情報
    ${details || noteValue}
    ` : '' }
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 活用シーンセクションの生成 private generateUseCasesSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map( (item, index) => `
    ${item.見出し?.value || ''}

    ${item.内容?.value || ''}

    ${ item.注釈?.value ? `
    ${item.注釈?.value}
    ` : '' }
    `, ) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // FAQ/よくある質問セクションの生成 private generateFaqSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // 動的フィールドをチェック const itemAny = item as any; const hasQA = itemAny.質疑?.value; if (hasQA) { // 質疑フィールドがある場合(Q:とA:を分離して処理) const qaValue = itemAny.質疑.value; const qaLines = qaValue.split('
    ').map((line: string) => line.trim()); let question = ''; let answer = ''; // Q:とA:を分離 qaLines.forEach((line: string) => { if (line.startsWith('Q:')) { question = line.substring(2).trim(); } else if (line.startsWith('A:')) { answer = line.substring(2).trim(); } }); // 注釈がある場合は追加 const annotation = itemAny.注釈?.value ? `

    ${itemAny.注釈.value}

    ` : ''; return `
    Q ${question || 'よくあるご質問'}
    A
    ${answer || ''} ${annotation}
    `; } // 標準フィールドのみの場合(見出し・内容) const questionText = item.見出し?.value || ''; const answerText = item.内容?.value || ''; const annotation = item.注釈?.value ? `

    ${item.注釈.value}

    ` : ''; return `
    Q ${questionText}
    A
    ${answerText} ${annotation}
    `; }) .join('') || ''; // FAQセクション用のスタイル(回答を常に表示) return `

    ${section.中区分}

    ${items}
    `; } // News/更新情報セクションの生成 private generateNewsUpdatesSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map((item, index) => { // 動的フィールドをチェック const itemAny = item as any; const hasTitle = itemAny.タイトル?.value; const title = hasTitle ? itemAny.タイトル.value : item.見出し?.value || ''; return `
    2024.12.${String(15 - index).padStart(2, '0')}
    プレスリリース

    ${title}

    ${item.内容?.value || ''}

    ${item.注釈?.value ? `

    ${item.注釈?.value}

    ` : ''}
    `; }) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 汎用セクションの生成 private generateGenericSection(section: ContentSection): string { const items = (section.小区分json ?? []) .map( (item, index) => `

    ${item.見出し?.value || ''}

    ${item.内容?.value || ''}

    ${item.注釈?.value ? `${item.注釈?.value}` : ''}
    `, ) .join('') || ''; return `

    ${section.中区分}

    ${items}
    `; } // 権威付けセクションの生成(ヒーローセクション用) private generateAuthoritySection(authority?: { 内容: string }): string { if (!authority) return ''; return `
    ${authority.内容}
    `; } // フッターセクションの生成 private generateFooterSection(companyName?: string): string { return ` `; } // template-generator.htmlファイルからテンプレートを読み込み private loadTemplateFromFile(): string { try { const templatePath = join(process.cwd(), 'server', 'lib', 'templates', 'template-generator.html'); return readFileSync(templatePath, 'utf-8'); } catch (error) { console.error('template-generator.htmlの読み込みに失敗しました:', error); // フォールバック用の簡単なテンプレート return this.getFallbackTemplate(); } } // フォールバック用テンプレート private getFallbackTemplate(): string { return ` {{COMPANY_TITLE}}

    {{HERO_MAIN_COPY}}

    {{HERO_FEATURES}}

    {{CTA_MICRO_COPY}}

    {{CTA_BUTTON_TEXT}}
    {{CONTENT_SECTIONS}} {{AUTHORITY_SECTION}} `; } }