MySQLでの文字化けトラブル解決記録(Docker + Express + MySQL で作る AI コードフォーマッター)

MySQLでの文字化けの発生

当初、VBA Formatterプロジェクトで日本語を含むプロンプトデータを表示した際に文字化けが発生していました。
具体的には「あいうえお」が「縺ゅ>縺」のように表示される状態でした。

文字コードとは

文字コードの基本

文字コードとは、コンピュータが文字を扱うための約束事です。
コンピュータは内部的には全て数値(バイナリ)で処理するため、
「あ」という文字を「あ」として認識するためには、決まった規則が必要になります。

主な文字コード

  • ASCII: 英数字のみ(128文字)
  • Shift-JIS: 日本語用(Windows系)
  • EUC-JP: 日本語用(UNIX系)
  • UTF-8: 世界中の文字に対応(現在の標準)
  • UTF-8mb4: UTF-8の拡張版(絵文字にも対応)

なぜ文字化けが起きたのか

文字化けの主な原因は、データの流れる過程で文字コードの解釈が一貫していなかったことです:

  1. データベースでの保存時の文字コード
  2. アプリケーションでの処理時の文字コード
  3. ブラウザでの表示時の文字コード

これらの設定が異なると、例えば:

  • UTF-8で保存したデータを
  • Shift-JISとして読み込み
  • UTF-8として表示しようとする

というような不整合が発生し、文字化けの原因となります。

解決方法

データベース層での対応

Node.js(Express)とMySQLをDockerで構築する手順を、順を追って解説します。

-- データベースの文字コード設定
SET NAMES utf8mb4;
SET CHARACTER SET utf8mb4;

-- テーブル作成時の設定
CREATE TABLE prompts (
    -- カラム定義
) CHARACTER SET utf8mb4 
  COLLATE utf8mb4_unicode_ci;

アプリケーション層での対応

// データベース接続設定
const config = {
    charset: 'utf8mb4',
    collation: 'utf8mb4_unicode_ci'
};

// クエリ実行時の設定
await db.query("SET NAMES utf8mb4");

フロントエンド層での対応

// バイナリデータとして受け取り、適切にデコード
const buffer = await response.arrayBuffer();
const decoder = new TextDecoder('utf-8');
const jsonString = decoder.decode(buffer);

プロジェクト構成

project/
├── docker-compose.yml
├── Dockerfile
├── package.json
├── .env
└── src/
    ├── index.js
    ├── bedrock/
    │   ├── analyzer.js
    │   └── client.js
    ├── db/
    │   ├── connection.js
    │   └── init/
    │       └── 01-schema.sql
    └── public/
        ├── index.html
        ├── style.css
        └── script.js

主要ファイルの実装

Dockerコンテナ作成

docker-compose.yml

version: '3.8'
services:
  app:
    build: .
    ports:
      - "4000:4000"
    volumes:
      - .:/usr/src/app
      - /usr/src/app/node_modules
    environment:
      - PORT=4000
      - DB_HOST=db
      - DB_USER=vbauser
      - DB_PASSWORD=vbapassword
      - DB_NAME=vba_formatter
    depends_on:
      - db

  db:
    image: mysql:8.0
    platform: linux/amd64

    # 文字コードの設定:日本語を正しく扱うためのMySQLサーバー設定
    command: 
        - --character-set-server=utf8mb4
        - --collation-server=utf8mb4_unicode_ci
    ports:
      - "3307:3306"
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: vba_formatter
      MYSQL_USER: vbauser
      MYSQL_PASSWORD: vbapassword
      TZ: Asia/Tokyo
    volumes:
      - mysql_data:/var/lib/mysql
      - ./src/db/init:/docker-entrypoint-initdb.d

volumes:
  mysql_data:

Dockerfile

FROM node:20-slim

WORKDIR /usr/src/app

# ロケールのインストールと設定 (Debianベース)
RUN apt-get update && apt-get install -y locales && \
    sed -i 's/# ja_JP.UTF-8/ja_JP.UTF-8/' /etc/locale.gen && \
    locale-gen ja_JP.UTF-8 && \
    apt-get clean

ENV LANG=ja_JP.UTF-8
ENV LC_ALL=ja_JP.UTF-8
ENV LANGUAGE=ja_JP:ja

# MySQLクライアントパッケージの名前を修正
RUN apt-get update && apt-get install -y default-mysql-client

# アプリケーションの依存関係をコピー
COPY package*.json ./

# 依存関係のインストール
RUN npm install

# アプリケーションのソースをコピー
COPY . .

EXPOSE 4000

CMD ["npm", "run", "dev"]

package.json

{
  "name": "vba-formatter",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "dependencies": {
    "@aws-sdk/client-bedrock-runtime": "^3.0.0",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "iconv-lite": "^0.6.3",
    "mysql2": "^3.6.5",
    "nodemon": "^3.0.2"
  }
}

Express サーバーの実装

データベース接続設定 (src/db/connection.js)

// src/db/connection.js

/**
 * MySQL2のPromise版を使用
 * Promise-basedなAPIでデータベース操作を行う
 */
const mysql = require('mysql2/promise');

/**
 * データベース接続の修正履歴 
 * 
 * 1. 接続エラーの解消
 *    - 接続タイムアウトの設定追加
 *    - リトライロジックの実装
 *    - エラーハンドリングの強化
 * 
 * 2. コネクションプールの最適化
 *    - プール設定の調整
 *    - 接続数の制限設定
 * 
 * 3. デバッグ機能の強化
 *    - 詳細なログ出力の追加
 *    - 接続状態の監視機能
 */

/**
 * データベース接続の文字コード処理の詳細説明
 * 
 * 【文字化けが発生理由】
 * データベースとアプリケーション間でデータをやり取りする際、
 * 以下の3つのポイントで文字コードの変換が発生します:
 * 
 * 1. アプリケーション → データベース(データ送信時)
 * 2. データベース内でのデータ保存
 * 3. データベース → アプリケーション(データ取得時)
 * 
 * 【設定項目の説明】
 * 1. charset: 'utf8mb4'
 *    - 接続時の文字コードを指定
 *    - データベースとの通信で使用する文字コードを決定
 * 
 * 2. collation: 'utf8mb4_unicode_ci'
 *    - 文字の照合順序を指定
 *    - 「ci」は Case Insensitive(大文字小文字を区別しない)
 * 
 * 3. initializationCommands
 *    - 接続確立直後に実行されるコマンド
 *    - セッションごとに文字コード設定を確実に行う
 * 
 * 【改善の仕組み】
 * - 接続時に文字コード設定を強制的に行う
 * - セッションごとに設定を初期化
 * - バイナリデータ経由で確実な文字コード変換を実現
 */

/**
 * データベース接続設定の拡張
 * - 環境変数から設定を読み込み、なければデフォルト値を使用
 * - utf8mb4を使用して絵文字を含む多言語対応
 */
const config = {
    host: process.env.DB_HOST || 'db',
    user: process.env.DB_USER || 'vbauser',
    password: process.env.DB_PASSWORD || 'vbapassword',
    database: process.env.DB_NAME || 'vba_formatter',
    charset: 'utf8mb4',
    collation: 'utf8mb4_unicode_ci',
    // 文字コード関連の設定を追加
    connectionLimit: 10,
    supportBigNumbers: true,
    bigNumberStrings: true,
    dateStrings: true,
    // 明示的な文字コード設定
    charset: 'utf8mb4',
    // コネクション確立時の初期化コマンド
    initializationCommands: [
        'SET NAMES utf8mb4',
        'SET CHARACTER SET utf8mb4',
        'SET SESSION collation_connection = utf8mb4_unicode_ci'
    ]
};

// コネクションプールのインスタンス
let pool;
let connectionAttempts = 0;
const MAX_RETRIES = 5;

/**
 * データベース接続を試行する関数
 * リトライロジック付き
 */
const initPool = async () => {
    while (connectionAttempts < MAX_RETRIES) {
        try {
            console.log(`データベース接続を試行中... (試行回数: ${connectionAttempts + 1})`);
            
            // プールの作成
            pool = mysql.createPool(config);
            
            // 接続テスト
            await pool.query('SELECT 1');
            
            // 文字コード設定の確認
            const [charsetResults] = await pool.query('SHOW VARIABLES LIKE "character%"');
            console.log('データベース文字コード設定:', charsetResults);
            
            console.log('データベース接続成功');
            return pool;

        } catch (error) {
            connectionAttempts++;
            console.error(`データベース接続エラー (試行回数: ${connectionAttempts}):`, error);
            
            if (connectionAttempts >= MAX_RETRIES) {
                throw new Error(`データベース接続に失敗しました。最大試行回数(${MAX_RETRIES})を超えました。`);
            }
            
            // 再試行前に待機
            await new Promise(resolve => setTimeout(resolve, 2000 * connectionAttempts));
        }
    }
};

/**
 * クエリ実行用の関数
 * 接続エラー時の再接続を含む
 */
const query = async (...args) => {
    try {
        if (!pool) {
            await initPool();
        }
        return await pool.query(...args);
    } catch (error) {
        console.error('クエリ実行エラー:', error);
        // 接続エラーの場合は再接続を試みる
        if (error.code === 'PROTOCOL_CONNECTION_LOST') {
            pool = null;
            return query(...args);
        }
        throw error;
    }
};

/**
 * データベース操作用の関数をエクスポート
 * - query: SQL実行用の関数
 * - getPool: プールインスタンス取得用の関数
 */
module.exports = {
    // クエリ実行関数
    query,
    // プール取得関数
    getPool: async () => {
        if (!pool) {
            await initPool();  // プールが未初期化なら初期化
        }
        return pool;
    }
};

src\db\init\01-schema.sql

/**
 * 文字コード設定の詳細説明
 * 
 * 【文字化け発生の背景】
 * データベースでは文字データを保存する際に「文字コード」という形式を使用します。
 * 日本語などの多言語文字を正しく扱うにはUTF-8mb4という文字コードが必要です。
 * 
 * 【各設定の役割】
 * 1. SET NAMES utf8mb4
 *    - クライアントとサーバー間の通信で使用する文字コードを設定
 *    - データの送受信時の文字化けを防ぐ
 * 
 * 2. SET CHARACTER SET utf8mb4
 *    - データベースが使用する文字コードを設定
 *    - データ保存時の文字化けを防ぐ
 * 
 * 3. COLLATE utf8mb4_unicode_ci
 *    - 文字の照合順序(ソート順)を設定
 *    - 日本語を含む多言語での正しい並び順を保証
 * 
 * 【utf8mb4を使用する理由】
 * - 絵文字を含むすべてのUnicode文字を扱える
 * - 従来のutf8より広い文字範囲をサポート
 * - 将来的な文字コードの拡張にも対応可能
 */

-- データベースの文字コード設定を確実に行う
SET NAMES utf8mb4;
SET CHARACTER SET utf8mb4;

-- データベースの文字コードを強制的にUTF8mb4に設定
-- これによりデータベースレベルで日本語を正しく扱える
ALTER DATABASE vba_formatter CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- カラム
CREATE TABLE IF NOT EXISTS prompts (
    id VARCHAR(50) PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB 
  CHARACTER SET utf8mb4 
  COLLATE utf8mb4_unicode_ci;

/**
 * プロンプトの表示順序の制御について
 * 
 * 【表示順序の制御方法】
 * 1. nameカラムに接頭辞を付けて制御
 *    - 数字の場合はゼロ埋めして2桁で統一
 *    - 例:'01_', '02_' など
 * 
 * 2. 意図した順序:
 *    - "01_VBAプロンプト1" (最初に表示)
 *    - "02_VBAプロンプト2" (2番目に表示)
 *    - "03_HTML BEMの命名規則" (3番目に表示)
 *    - "04_開発テスト用" (4番目に表示)
 */

-- 初期データの挿入(順序を制御するため、nameを修正)
INSERT INTO prompts (id, name, content) VALUES 
    ('prompt01', '01_VBAプロンプト1', 
    '# ここからがAIへの指示内容です
=============================================

# AIの役割定義
あなたはVBAコードの専門家として、以下のコードを分析し、優先順位に従って改善してください。

# 入力コード部分
```vba
{code}
```

# 改善要件(優先順位:高 - 第一弾)
=============================================
1. インデント処理
  - SUBからENDSUBまでTABひとつ分のインデント
  - IF文、With文、For文等の制御構造にもインデント
  - ネストレベルに応じて適切なインデント

2. ヘッダー・フッター追加
  - Sub開始時のヘッダー:
    Option Explicit
    ''***************************************************
    '' [プロシージャ名]
    ''***************************************************
    ''【機  能】[機能の説明]
    ''【引  数】[引数の説明]
    ''【戻 り 値】[戻り値の説明]
    ''【機能説明】[詳細な説明]
    ''【備  考】[その他特記事項]
    '' Copyright(c) 2024 Your Company All Rights Reserved.
    ''***************************************************
    
  - 処理の区切り:
    ''***************************************************
    '' 変数宣言
    ''***************************************************
    [変数宣言部分]

  - Sub終了時のフッター:
    ''***************************************************
    ''---------------------------------------------------
    '' Version
    ''---------------------------------------------------
    '' 1.00 | 2024.01.01 | ********* | *********
    ''***************************************************

# 改善要件(優先順位:中 - 第二弾)
=============================================
3. 変数名の改善
  - 一文字の変数を禁止
  - セル参照の変数は意味のある名前に変更
    (例: Last_Row, Last_Column)
  - 配列はArrayを付ける
  - その他の変数はDataを付ける

4. Public変数・Call文の処理
  - Public変数にはPublic関数であることをコメントで記載
  - Call文にはモジュール名を追加
    (例: Call Module1.印刷)

5. 変数宣言のコメント
  - 宣言した変数の横にコメントを追加
  - 使用目的や内容を簡潔に説明

# 改善要件(優先順位:中 - 第三弾)
=============================================
6. 変数・シート名の処理
  - 日本語変数名を英語に変更
  - 未宣言変数を「変数宣言」セクションに追加
  - 型判定できない変数はVariantに
  - シート名は定数(Const)で定義
  - シート追加時の名前はVariantで処理

7. コメント追加
  - IF文、With文などの制御構造にコメント
  - 配列の内容説明
  - SET文の説明
  - Offsetのコメント必須
    (参照セルと目的を明記)

8. コードの最適化
  - 類似コードが3回以上続く場合はループ化
  - パスの直書きは避け、pathに置換
    (元のパスはコメントとして保持)

# 出力形式
=============================================
優先順位の高い要件(第一弾)から順に適用し、
改善したVBAコードのみを出力してください。'),

    ('prompt02', '02_VBAプロンプト2',
    '# ここからがAIへの指示内容です(コードレビュー版)
=============================================

# AIの役割定義
あなたはシニアVBA開発者として、以下のコードをレビューし、ベストプラクティスに基づいて改善してください。

# 入力コード部分
```vba
{code}
```

# レビュー観点
=============================================
1. コーディング規約準拠
2. バグの可能性
3. パフォーマンスボトルネック
4. セキュリティリスク

# 出力形式
=============================================
コードレビューコメントと改善後のコードを提供してください。'),

    ('prompt03', '03_HTML BEMの命名規則',
    '# ここからがAIへの指示内容です(HTML BEM分析版)
=============================================

# AIの役割定義
あなたはHTMLとCSSの専門家として、クラス名をBEM命名規則に基づいて改善します。

# 入力コード部分
```html
{code}
```

# 改善要件
=============================================
1. BEMの基本ルール適用
  - Block: 独立したコンポーネント(例: header, menu)
  - Element: Blockの一部(例: menu__item)
  - Modifier: 状態や見た目の変更(例: menu__item--active)

2. 命名規則の統一
  - Blockは意味のある名前を使用
  - Element は __ (アンダースコア2つ) で接続
  - Modifier は -- (ハイフン2つ) で接続
  - 全て小文字、ハイフンで単語を区切る

3. クラス名の階層構造
  - 最大2階層までの要素の入れ子
  - Block内のElement同士の依存関係を避ける
  - 共通の機能はMixinとして抽出

4. コンポーネントの分割
  - 再利用可能なBlockの特定
  - 共通のModifierパターンの抽出
  - コンポーネント間の依存関係の最小化

5. レスポンシブ対応
  - Modifierでのブレイクポイント管理
  - コンテナクエリの活用
  - フレックスボックス/グリッドの適切な使用

# 出力形式
=============================================
1. 改善後のHTML
2. 各クラス名の説明とBEMルールとの対応
3. コンポーネント構造の解説'),

    ('prompt04', '04_開発テスト用',
    '# ここからがAIへの指示内容です
=============================================

# 以下のコードをレビューし、改善要件に基づいて修正してください。
-
# 入力コード部分
```
{code}
```
# 改善要件
=============================================
1. xx
  - xx
  - xx
  - xx

2. xx
  - xx
  - xx
  - xx

3. xx
  - xx
  - xx
  - xx

# 出力形式
=============================================
1. xx
2. xx
3. xx')
ON DUPLICATE KEY UPDATE 
    name = VALUES(name),
    content = VALUES(content);

メインサーバーファイル (src/index.js)

// ========================================
// サーバーのメインファイル (index.js)
// VBAフォーマッターのバックエンド処理を担当
// ========================================

// 必要なモジュールのインポート
const path = require("path"); // pathモジュールを追加
const express = require("express");
const {analyzeCode} = require("./bedrock/analyzer"); // Bedrock AIの分析機能
// const { formatVBA } = require('./formatter'); // 基本的なフォーマット、現在機能使用していない
const db = require("./db/connection"); // データベース接続を追加

// Expressアプリケーションの初期化
const app = express();

// ミドルウェアの設定
// 静的ファイルのパスを修正- Docker環境での絶対パス指定に変更
app.use(express.static(path.join(__dirname, "public")));
// JSONリクエストの解析を有効化
app.use(express.json());

// CORSの設定を追加
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");
  res.header("Access-Control-Allow-Headers", "Content-Type");
  next();
});

// Content-Typeの設定(APIエンドポイントのみに適用)
app.use((req, res, next) => {
  if (
    req.path.startsWith("/api") ||
    req.path.startsWith("/prompts") ||
    req.path === "/format" ||
    req.path === "/prompt-list"
  ) {
    res.setHeader("Content-Type", "application/json; charset=utf-8");
    // 追加: レスポンスエンコーディングを設定
    res.setHeader("Transfer-Encoding", "chunked");
  }
  next();
});

/**
 * プロンプト一覧取得APIをDB対応に修正
 * - MySQLからデータを取得するように変更
 */
app.get("/prompt-list", async (req, res) => {
  try {
    const [rows] = await db.query("SELECT id, name, description FROM prompts");
    res.json({
      success: true,
      prompts: rows,
    });
  } catch (error) {
    console.error("データベースエラー:", error);
    res.status(500).json({
      success: false,
      error: "プロンプト一覧の取得に失敗しました",
    });
  }
});

app.get("/about", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "about.html"));
});

/**
 * 文字化け解消のための修正履歴 2024-03-xx
 * 
 * 1. Content-Typeヘッダーの設定を試行
 *    res.setHeader('Content-Type', 'application/json; charset=utf-8');
 *    → 部分的な効果あり、完全な解決には至らず
 *    
 * 2. Buffer経由のデコードを試行
 *    Buffer.from(rows[0].content).toString('utf8')
 *    → 一部の文字で文字化けが発生
 * 
 * 3. クエリでのCONVERT使用を試行
 *    SELECT CONVERT(content USING utf8mb4) as content
 *    SELECT CONVERT(CAST(content AS BINARY) USING utf8) as content
 *    → 特定の文字で文字化けが継続
 * 
 * 4. HEX形式での取得を試行
 *    SELECT HEX(content) as content_hex FROM prompts
 *    → デコード時に問題発生
 * 
 * 5. クエリオプションでのエンコーディング指定を試行
 *    {sql: "SELECT * FROM prompts", encoding: 'utf8mb4'}
 *    → mysql2では未サポート
 * 
 * 6. セッション文字コードの設定とバイナリ変換を試行
 *    SET NAMES utf8mb4 +
 *    SELECT CAST(CONVERT(content USING binary) AS CHAR CHARACTER SET utf8mb4)
 *    → 部分的な改善
 * 
 * 7. iconv-liteによる文字コード変換を試行
 *    const iconv = require('iconv-lite');
 *    iconv.decode(Buffer.from(content), 'utf8');
 *    → 特定のケースで例外発生
 * 
 * 8. execute()による実行を試行
 *    → db.executeは存在しない
 *    → プールインスタンスにはexecuteメソッドがない
 * 
 * 9. コネクション管理の改善を試行
 *    - getConnection()でコネクションを取得
 *    - 取得したコネクションでexecuteを実行
 *    - 処理後にコネクションを解放
 *    → コネクション管理は改善したが文字化けは解消せず
 * 
 * 10. 現在の解決策(2024-03-xx):
 *     - データベースレベルでの文字コード設定の確実な適用
 *     - 接続時の初期化コマンドによる文字コード設定
 *     - バイナリ経由での確実な文字コード変換
 *     - レスポンスヘッダーとエンコーディングの最適化
 *     → 以下のアプローチの組み合わせで解決
 *        1. データベース接続時の文字コード初期化
 *        2. バイナリ経由での文字コード変換
 *        3. Buffer処理による確実なエンコーディング
 *        4. クライアントでのデコード処理の最適化
 */

/**
 * APIエンドポイントでの文字コード処理の詳細説明
 * 
 * 【文字化け解消の3段階】
 * 1. データベースからの取得時
 *    - バイナリデータとして一旦取得
 *    - UTF-8mb4として再解釈
 * 
 * 2. JSONレスポンス生成時
 *    - Buffer経由で文字コードを確実に変換
 *    - 文字化けしやすい文字も正しく処理
 * 
 * 3. クライアントへの送信時
 *    - Content-Typeヘッダーで文字コードを明示
 *    - Transfer-Encodingの設定で大きなデータも安全に送信
 * 
 * 【なぜこの方法で解決できたのか】
 * 1. バイナリ経由の変換
 *    - 文字コードの解釈を一旦リセット
 *    - 確実にUTF-8として再解釈
 * 
 * 2. 多層的なアプローチ
 *    - DB設定
 *    - 接続設定
 *    - アプリケーション処理
 *    全ての層で文字コードを適切に処理
 * 
 * 3. ヘッダー設定の最適化
 *    - クライアントに文字コードを正しく伝達
 *    - ブラウザでの解釈を確実に制御
 */

// プロンプト一覧を取得するエンドポイント
app.get("/api/prompt-info", async (req, res) => {
  try {
    // 文字コードの明示的な設定
    await db.query("SET NAMES utf8mb4");
    await db.query("SET CHARACTER SET utf8mb4");
    await db.query("SET SESSION collation_connection = utf8mb4_unicode_ci");
    
    // バイナリ経由での文字列取得(最新の解決策)
    const [rows] = await db.query(`
      SELECT 
        id,
        CONVERT(CAST(CONVERT(name USING binary) AS BINARY) USING utf8mb4) as name,
        CONVERT(CAST(CONVERT(content USING binary) AS BINARY) USING utf8mb4) as content
      FROM prompts 
      ORDER BY name
    `);
    
    // レスポンスヘッダーの設定
    res.setHeader('Content-Type', 'application/json; charset=utf8');
    res.setHeader('Transfer-Encoding', 'chunked');
    
    // Buffer経由でのエンコーディング
    const jsonString = JSON.stringify({
      success: true,
      prompts: rows.map(row => ({
        id: row.id,
        name: Buffer.from(row.name).toString('utf8'),
        content: Buffer.from(row.content).toString('utf8')
      }))
    });
    
    res.end(Buffer.from(jsonString, 'utf8'));

  } catch (error) {
    console.error("データ取得エラー:", error);
    res.status(500).json({
      success: false,
      error: "データの取得に失敗しました"
    });
  }
});

// 個別のプロンプト内容を取得するエンドポイント
app.get("/api/prompts/:id", async (req, res) => {
  // レスポンスヘッダーを設定
  res.setHeader('Content-Type', 'application/json; charset=utf-8');

  try {
    // UTF-8エンコーディングを明示的に指定してコンテンツを取得
    const [rows] = await db.query(
      "SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) as content FROM prompts WHERE id = ?",
      [req.params.id]
    );

    if (!rows || rows.length === 0) {
      return res.status(404).json({
        success: false,
        error: "指定されたプロンプトが見つかりません"
      });
    }

    // 成功時のレスポンス形式を統一
    res.json({
      success: true,
      content: rows[0].content
    });

  } catch (error) {
    // エラーログの出力
    console.error("プロンプト取得エラー:", error);
    
    // エラー時のレスポンス形式を統一
    res.status(500).json({
      success: false,
      error: "プロンプトの取得に失敗しました",
      details: process.env.NODE_ENV === 'development' ? error.message : undefined
    });
  }
});

/**
 * フォーマットAPIにプロンプトIDの処理を追加
 * - リクエストからプロンプトIDを取得
 * - DBから対応するプロンプトを取得して使用
 */
app.post("/format", async (req, res) => {
  try {
    const {code, promptId} = req.body;
    const [rows] = await db.query(
      "SELECT CAST(content AS CHAR CHARACTER SET utf8mb4) as content FROM prompts WHERE id = ?",
      [promptId]
    );

    if (!rows || rows.length === 0) {
      return res.status(404).json({
        success: false,
        error: "プロンプトが見つかりません",
      });
    }

    const formattedCode = await analyzeCode(code, rows[0].content);
    res.json({success: true, formatted: formattedCode});
  } catch (error) {
    console.error("Format error:", error);
    res.status(500).json({
      success: false,
      error: error.message || "フォーマット処理に失敗しました",
    });
  }
});

// プロンプト更新API
app.put("/prompts/:id", async (req, res) => {
  console.log("Update request received:", {
    params: req.params,
    body: req.body,
  });

  try {
    const {id} = req.params;
    const {content} = req.body;

    // データベース接続テスト
    const [testConnection] = await db.query("SELECT 1");
    console.log("Database connection test:", testConnection);

    const [result] = await db.query(
      "UPDATE prompts SET content = ? WHERE id = ?",
      [content, id]
    );

    console.log("Update result:", result);

    if (result.affectedRows === 0) {
      return res.status(404).json({
        success: false,
        error: "プロンプトが見つかりません",
      });
    }

    res.json({
      success: true,
      message: "更新しました",
    });
  } catch (error) {
    console.error("Database error:", error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// サーバー起動
const PORT = process.env.PORT || 4000;
app.listen(PORT, "0.0.0.0", () => {
  console.log("========================================");
  console.log(`Server running on http://localhost:${PORT}`);

  // 登録されているルートを表示
  app._router.stack.forEach((r) => {
    if (r.route && r.route.path) {
      console.log(`Route: ${r.route.path}`);
      console.log(`Methods:`, r.route.methods);
    }
  });
  console.log("========================================");
});

bedrock関連

.envファイルで要環境変数設定

src\bedrock\analyzer.js

// AWS SDKからBedrockのクライアントとコマンドをインポート
const { InvokeModelCommand } = require("@aws-sdk/client-bedrock-runtime");
const client = require('./client');

/**
 * コードをClaudeモデルで分析する関数
 * @param {string} code - 分析対象のコード(VBAまたはHTML)
 * @param {string} promptContent - 使用するプロンプトの内容
 * @returns {Promise<string>} - 改善されたコード
 */
async function analyzeCode(code, promptContent) {
    try {
        // 入力検証
        if (!code || !promptContent) {
            throw new Error('コードとプロンプトの内容は必須です');
        }

        // プロンプト内容にコードを挿入
        // {code}をユーザー入力のコードに置換
        const fullPrompt = promptContent.replace('{code}', code);

        // Claude用のリクエストコマンドを作成
        const command = new InvokeModelCommand({
            // Claude 3 Sonnetモデルを指定(最新バージョン)
            modelId: "anthropic.claude-3-sonnet-20240229-v1:0",
            contentType: "application/json",
            accept: "application/json",
            // Bedrock API用のリクエストボディを構築
            body: JSON.stringify({
                // Bedrockのバージョンを指定
                anthropic_version: "bedrock-2023-05-31",
                // 最大トークン数(出力の長さ制限)
                max_tokens: 2048,
                // メッセージ形式でプロンプトを送信
                messages: [
                    {
                        role: "user",
                        content: fullPrompt
                    }
                ],
                // 必要に応じて追加のパラメータを設定可能
                // temperature: 0.7,  // 生成の多様性(0-1)
                // top_p: 0.9,       // 出力のランダム性
            })
        });

        // BedrockAPIを呼び出し
        const response = await client.send(command);
        
        // レスポンスの処理(バイナリからUTF-8文字列に変換)
        const responseBody = Buffer.from(response.body).toString('utf8');
        const jsonResponse = JSON.parse(responseBody);

        // Claudeのレスポンス形式から結果を抽出
        // content[0].textに実際の生成テキストが含まれる
        if (jsonResponse.content && jsonResponse.content[0]) {
            return jsonResponse.content[0].text;
        }

        throw new Error('AIモデルからの応答が不正な形式です');

    } catch (error) {
        // エラーログを出力し、上位層に伝播
        console.error("コード分析エラー:", error);
        console.error("エラー詳細:", {
            message: error.message,
            code: error.code,
            requestId: error.$metadata?.requestId
        });
        throw error;
    }
}

// analyzeCode関数をエクスポート
module.exports = {
    analyzeCode
};

src\bedrock\client.js

require('dotenv').config();
const { BedrockRuntimeClient } = require("@aws-sdk/client-bedrock-runtime");

// Bedrockクライアントの設定
const client = new BedrockRuntimeClient({ 
    region: process.env.AWS_REGION || "us-east-1",
    credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
    }
});

module.exports = client;

フロントエンド

src/public/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <!-- 文字エンコーディングとビューポートの設定 -->
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>VBA Formatter</title>
    <!-- reset.css ress -->
    <link rel="stylesheet" href="https://unpkg.com/ress/dist/ress.min.css" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link rel="stylesheet" href="style.css">
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">

</head>
<body>
    <!-- メインコンテナ -->
    <div class="container">
        <h1>VBA Formatter</h1>
        <div class="nav-links">
            <a href="index.html">ホーム</a>
            <a href="about.html">About</a>
        </div>

        <!-- AIへの指示内容表示セクション(src/bedrock/prompts.jsの内容) -->
        <div class="prompt-section">
            <h2>AIへの指示内容</h2>
            <div class="prompt-selector">
                <select id="promptSelect" onchange="changePrompt()">
                    <option value="">読み込み中...</option>
                </select>
                <button id="editButton" onclick="toggleEdit()" class="edit-button">
                    編集
                </button>
            </div>
            <!-- プロンプト内容を動的に表示する要素 -->
            <div class="prompt-display-container">
                <div class="user-icon"></div>
                <div class="prompt-display-wrapper">
                    <pre id="promptDisplay" class="prompt-display" contenteditable="false">ユーザーからの指示内容がここに表示されます</pre>
                </div>
            </div>
            <button id="saveButton" onclick="savePrompt()" class="save-button" style="display: none;">
                保存
            </button>
        </div>

        <!-- コードエディタ部分 -->
        <div class="editors">
            <!-- 入力用エディタ -->
            <div class="editor">
                <h2>分析するコード</h2>
                <!-- 入力用テキストエリア:ユーザーがコードを入力または貼り付け可能 -->
                <textarea id="input" placeholder="コードを入力してください"></textarea>
                
                <!-- ファイル選択ボタン:.basと.txtファイルのみ受付 -->
                <input type="file" id="vbaFile" accept=".bas,.txt,.html,.css,.js">
            </div>
            <!-- 出力用エディタ -->
            <div class="editor">
                <h2>改善後のコード</h2>
                <!-- 出力用テキストエリア:読み取り専用で改善されたコードを表示 -->
                <textarea id="output" readonly></textarea>
                
                <!-- フォーマット実行ボタン -->
                <button onclick="formatCode()" id="formatButton">Format</button>
                <!-- ポップアップメッセージ表示エリア -->
                <div id="popup" class="popup">クリップボードにコピーしました</div>
            </div>
        </div>
    </div>
    <script src="./script.js"></script>
</body>
</html>

src\public\script.js

/**
 * ページ初期ロード時の処理
 * 
 * 【プロンプト選択の仕組み】
 * 1. ページロード時にプロンプト一覧を取得
 * 2. セレクトボックスの最初のオプションが自動的に選択される
 * 3. changePrompt()が呼ばれ、選択されたプロンプトの内容を表示
 * 
 * 【選択の優先順位】
 * 1. セレクトボックスの最初のオプション(データベースのORDER BY name順で最初のプロンプト)
 * 2. エラー時は「プロンプトの読み込みに失敗しました」というメッセージを表示
 */
window.onload = async function() {
    try {
        // プロンプトリストの取得
        const response = await fetch('/api/prompt-info');
        const buffer = await response.arrayBuffer();
        const decoder = new TextDecoder('utf-8');
        const jsonString = decoder.decode(buffer);
        const data = JSON.parse(jsonString);
        
        if (data.success) {
            // セレクトボックスの生成
            // ORDER BY name でソートされたプロンプトリストの最初のものが自動選択される
            const select = document.getElementById('promptSelect');
            select.innerHTML = data.prompts.map(prompt => 
                `<option value="${prompt.id}">${prompt.name}</option>`
            ).join('');

            // 最初のプロンプトの内容を取得して表示
            await changePrompt();
        }
    } catch (error) {
        console.error('初期化エラー:', error);
        document.querySelector('.prompt-display').textContent = 'プロンプトの読み込みに失敗しました';
    }
};

// プロンプト切り替え関数
async function changePrompt() {
    try {
        const selectedPrompt = document.getElementById('promptSelect').value;
        console.log('Selected prompt ID:', selectedPrompt); // デバッグログ

        const response = await fetch(`/api/prompts/${selectedPrompt}`);
        const data = await response.json();
        console.log('API Response:', data); // デバッグログ
        
        const promptDisplay = document.querySelector('.prompt-display');
        if (data.success) {
            promptDisplay.textContent = data.content;
        } else {
            promptDisplay.textContent = 'プロンプトの読み込みに失敗しました';
        }
    } catch (error) {
        console.error('プロンプト取得エラー:', error);
        document.querySelector('.prompt-display').textContent = 'プロンプトの読み込みに失敗しました';
    }
}

/**
 * コードのフォーマットを実行する関数
 * 1. 入力を取得
 * 2. サーバーにリクエスト
 * 3. 結果を表示
 */
async function formatCode() {
    const input = document.getElementById('input').value;
    const promptId = document.getElementById('promptSelect').value;
    const formatButton = document.getElementById('formatButton');
    
    // デバッグログ追加
    console.log('Formatting with:', {
        promptId,
        code: input
    });

    if (!input) {
        alert('コードを入力してください');
        return;
    }

    formatButton.innerHTML = '<span class="processing"><span></span><span></span><span></span></span>';

    try {
        const response = await fetch('/format', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ 
                code: input,
                promptId: promptId
            })
        });

        // デバッグログ追加
        console.log('Response status:', response.status);
        const data = await response.json();
        console.log('API Response:', data);
        
        if (data.success) {
            document.getElementById('output').value = data.formatted;
            formatButton.innerHTML = '分析完了! クリックしてコピー<i class="fa-regular fa-copy fa-beat-fade fa-lg" style="vertical-align: 0;"></i>';
            formatButton.onclick = copyToClipboard;
        } else {
            formatButton.textContent = 'エラー: ' + data.error;
        }
    } catch (error) {
        console.error('フォーマットエラー:', error);
        formatButton.textContent = 'エラー: ' + error.message;
    }
    formatButton.classList.remove('processing');
}

/**
 * クリップボードにコピーする関数
 * 出力用テキストエリアの内容をクリップボードにコピー
 */
 function copyToClipboard() {
    const outputTextarea = document.getElementById('output');
    outputTextarea.select();
    document.execCommand('copy');

    // ポップアップメッセージを表示
    const popup = document.getElementById('popup');
    popup.classList.add('show');

    // 一定時間後にポップアップメッセージを非表示にする
    setTimeout(() => {
        popup.classList.remove('show');
    }, 2000); // 2秒後に非表示
}

/**
 * ファイル選択時の処理
 * 選択されたファイルの内容を入力エリアに表示
 */
document.getElementById('vbaFile').addEventListener('change', (e) => {
    // 選択されたファイルを取得
    const file = e.target.files[0];
    if (file) {
        // FileReaderを使用してファイルの内容を読み込み
        const reader = new FileReader();
        // ファイル読み込み完了時の処理
        reader.onload = (e) => {
            // 読み込んだ内容を入力エリアに設定
            document.getElementById('input').value = e.target.result;
        };
        // ファイルをテキストとして読み込み開始
        reader.readAsText(file);
    }
});

// プロンプトの保存
async function savePrompt() {
    try {
        const promptId = document.getElementById('promptSelect').value;
        const content = document.getElementById('promptDisplay').textContent;
        
        console.log('Saving prompt:', promptId);
        console.log('Content:', content);

        const saveButton = document.getElementById('saveButton');
        saveButton.textContent = '保存中...';
        saveButton.disabled = true;

        // URLを確認のため出力
        const url = `/prompts/${promptId}`;
        console.log('Request URL:', url);

        const response = await fetch(url, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                content: content
            })
        });

        console.log('Response status:', response.status);
        const data = await response.json();
        console.log('Response data:', data);
        
        if (data.success) {
            alert('保存しました');
            document.getElementById('editButton').click();
        } else {
            alert('保存に失敗しました: ' + data.error);
        }

    } catch (error) {
        console.error('保存エラー:', error);
        alert('エラーが発生しました: ' + error.message);
    } finally {
        const saveButton = document.getElementById('saveButton');
        saveButton.textContent = '保存';
        saveButton.disabled = false;
    }
}

// 編集モードの切り替え
function toggleEdit() {
    const promptDisplay = document.getElementById('promptDisplay');
    const editButton = document.getElementById('editButton');
    const saveButton = document.getElementById('saveButton');
    const isEditing = promptDisplay.contentEditable === 'true';

    if (isEditing) {
        // 編集モード終了
        promptDisplay.contentEditable = 'false';
        editButton.textContent = '編集';
        saveButton.style.display = 'none';
        promptDisplay.classList.remove('editing');
    } else {
        // 編集モード開始
        promptDisplay.contentEditable = 'true';
        editButton.textContent = 'キャンセル';
        saveButton.style.display = 'block';
        promptDisplay.classList.add('editing');
        
        // カーソルを末尾に設定
        const range = document.createRange();
        const sel = window.getSelection();
        range.selectNodeContents(promptDisplay);
        range.collapse(false);
        sel.removeAllRanges();
        sel.addRange(range);
        promptDisplay.focus();
    }
}

// データ取得時の文字コード処理
async function fetchPromptInfo() {
  try {
    const response = await fetch('/api/prompt-info');
    const buffer = await response.arrayBuffer();
    const decoder = new TextDecoder('utf-8');
    const jsonString = decoder.decode(buffer);
    return JSON.parse(jsonString);
  } catch (error) {
    console.error('データ取得エラー:', error);
    throw error;
  }
}

src/public/style.css

body {
  font-family: "Noto Sans JP", serif;
  background-color: #f1f2f3;
}
/* メインコンテナ */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 2rem 2rem;
}

h1 {
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  text-align: center;
  letter-spacing: 0.1em;
  padding: 1rem 0;
  margin: 0 calc(50% - 50vw);
  width: 100vw;
  margin-bottom: 2rem;
}

h2 {
  margin-bottom: 1rem;
  text-align: center;
  letter-spacing: 0.1em;
}

.nav-links {
  text-align: center;
  margin-bottom: 2rem;
}

.nav-links a {
  display: inline-block;
  padding: 0.5rem 1rem;
  background-color: #e9be3b;
  color: white;
  text-decoration: none;
  border-radius: 4px;
  margin: 0 0.5rem;
}

.nav-links a:hover {
  background-color: #d3b458;
}

/* AIへの指示内容表示部分 */
.prompt-section {
  margin-bottom: 2rem;
}

.prompt-selector {
  display: flex;
  justify-content: center;
  gap: 1rem;
  margin-bottom: 1rem;
}

.edit-button {
  background-color: #4a90e2;
  height: 2.5rem;
}

.edit-button:hover {
  background-color: #357abd;
}

.save-button {
  display: none;
  margin: 1rem auto;
  background-color: #4caf50;
  width: 200px;
}

.save-button:hover {
  background-color: #45a049;
}

.prompt-display[contenteditable="true"] {
  border: 2px solid #4a90e2;
  padding: 1rem;
  outline: none;
}

.prompt-selector select {
  padding: 0.5rem 2rem 0.5rem 1rem;
  border: 1px solid #ccc;
  border-radius: 4px;
  background-color: white;
  font-size: 1rem;
  cursor: pointer;
  appearance: none;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
  background-repeat: no-repeat;
  background-position: right 0.7rem center;
  background-size: 1em;
}

.prompt-selector select:hover {
  border-color: #888;
}

.prompt-selector select:focus {
  outline: none;
  border-color: #e9be3b;
  box-shadow: 0 0 0 2px rgba(233, 190, 59, 0.2);
}

.prompt-display-container {
  display: flex;
  align-items: end;
  justify-content: center;
  gap: 2rem;
  margin: 0 auto;
}

.user-icon {
  width: 40px;
  height: 40px;
  background-color: #f6ce55;
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  margin-bottom: 3rem;
  position: relative;
}

.user-icon::after {
  content: "";
  position: absolute;
  top: 96%;
  left: 50%;
  transform: translateX(-50%);
  width: 50px;
  height: 40px;
  background-color: #f6ce55;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  border-top-left-radius: 50%;
  border-top-right-radius: 50%;
}

.prompt-display-wrapper {
  max-width: 80%;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #fff;
  border-radius: 5rem 5rem 5rem 0;
  box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
  padding: 2rem;
}

.prompt-display {
  font-family: monospace;
  font-size: 0.825rem;
  line-height: 1.5;
  white-space: pre-wrap;
  max-height: 400px;
  min-height: 300px;
  min-width: 600px;
  height: auto;
  overflow-y: auto;
  resize: vertical;
  padding: 1rem;
}

/* カスタムスクロールバーのスタイル */
.prompt-display::-webkit-scrollbar {
  width: 0.5rem; /* スクロールバーの幅 */
}

.prompt-display::-webkit-scrollbar-thumb {
  background: #888;
  border-radius: 1rem;
}

/* エディタ部分 */
.editors {
  display: flex;
  gap: 2rem;
  margin-bottom: 1rem;
  position: relative;
}

.editor {
  flex: 1;
  display: flex;
  flex-direction: column;
}

.editor > * {
  margin-bottom: 1rem;
}

.editor > *:last-child {
  margin-top: auto !important;
}

.editor input {
  height: 3rem;
  background-color: #ccc;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  width: 100%;
  transition: all 0.3s ease;
}

.editor input:hover {
  background-color: #bbb;
}

.editor .detail {
  margin-top: 0.5rem;
  color: #ff565d;
}

/* テキストエリア */
.editor textarea {
  width: 100%;
  height: 500px;
  font-family: monospace;
  padding: 10px;
  font-size: 14px;
  line-height: 1.5;
  border: 2px solid #ccc;
  background-color: #fff;
  border-radius: 4px;
}

button {
  height: 3rem;
  padding: 0.5rem 1rem;
  background-color: #e9be3b;
  border-radius: 4px;
  color: white;
  letter-spacing: 0.1em;
  border: none;
  cursor: pointer;
  position: relative;
}

button:hover {
  background-color: #d3b458;
  transition: all 0.3s ease;
}

/* アニメーションを追加するクラス */
.processing {
  display: inline-flex;
  gap: 0.5rem;
}

.processing span {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  background-color: #fff;
  border-radius: 50%;
  animation: fadeDots 1.5s infinite ease-in-out;
}

/* それぞれのドットに異なるアニメーションの遅延を設定 */
.processing span:nth-child(1) {
  animation-delay: 0s;
}

.processing span:nth-child(2) {
  animation-delay: 0.2s;
}

.processing span:nth-child(3) {
  animation-delay: 0.4s;
}

/* 薄さ(透明度)を変化させるアニメーション */
@keyframes fadeDots {
  0%,
  100% {
    opacity: 0.2;
  } /* 薄くなる */
  50% {
    opacity: 1;
  } /* 濃くなる */
}

.popup {
  visibility: hidden; /* 初期状態では非表示 */
  position: absolute;
  bottom: 4.5rem;
  right: 0.5rem;
  background-color: rgba(76, 175, 80, 0.8);
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  color: white;
  letter-spacing: 0.1em;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
  z-index: 1000;
  opacity: 0;
  transform: translateY(1rem);
  transition: opacity 0.5s ease, transform 0.5s ease, visibility 0.5s;
}

.popup.show {
  visibility: visible;
  opacity: 1;
  transform: translateY(0);
}

src\public\about.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>About VBA Formatter</title>
    <!-- <link rel="stylesheet" href="https://unpkg.com/ress/dist/ress.min.css" /> -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
    <style>
        .section {
            background: white;
            border-radius: 8px;
            padding: 2rem;
            margin-bottom: 2rem;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .prompt-card {
            border: solid 2px #ccc;
            border-radius: 8px;
            padding: 2rem;
            margin-bottom: 2rem;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        .prompt-content {
            background: #f8f9fa;
            padding: 1.5rem;
            border-radius: 4px;
            white-space: pre-wrap;
            font-family: monospace;
            font-size: 0.825rem;
            line-height: 1.5;
            overflow-x: auto;
        }

        .prompt-footer {
            margin-top: 1rem;
            padding-top: 1rem;
            border-top: 1px solid #eee;
            color: #666;
            font-size: 0.9rem;
        }

        .error {
            color: #dc3545;
            padding: 1rem;
            background: #fff;
            border-left: 4px solid #dc3545;
            margin: 1rem 0;
        }
    </style>
</head>
<body>
    <h1>VBA Formatter について</h1>

    <div class="container">
        <div class="nav-links">
            <a href="index.html">ホーム</a>
            <a href="about.html">About</a>
        </div>

        <!-- プロンプトセクション -->
        <div class="section">
            <h2>プロンプト管理</h2>
            <div id="promptInfo" class="prompt-info">
                <!-- プロンプト情報がここに動的に挿入されます -->
                <div class="loading">読み込み中...</div>
            </div>
        </div>

        <div class="section">
            <h2>システム概要</h2>
            <p>
                Code Formatterは、コードを自動的に整形し、プロンプトに従ってコードを改善するツールです。
                <br>Docker環境で動作し、Node.jsとMySQLを利用しています。
            </p>
        </div>

        <div class="section">
            <h2>Docker環境について</h2>
            <ul class="point-list">
                <li>コンテナの停止(docker-compose down): データは保持されます</li>
                <li>Docker Desktop停止: データは保持されます</li>
                <li>ボリューム削除(docker-compose down --volumes): データは消失します</li>
            </ul>
        </div>

        <div class="section">
            <h2>使用方法</h2>
            <ul class="point-list">
                <li>プロンプトの選択: ドロップダウンメニューから選択</li>
                <li>編集: 「編集」ボタンをクリックして内容を修正</li>
                <li>保存: 「保存」ボタンをクリックしてデータベースに保存</li>
                <li>確認: 保存完了のメッセージを確認</li>
            </ul>
        </div>

        <div class="section">
            <h2>開発者向け情報</h2>
            <ul class="point-list">
                <li>デバッグ: docker-compose logsでログを確認できます</li>
                <li>データベース確認:<br>
                    - docker exec -it 1be476fccbd035b76a53810a40486a0a427266c50deb5b681671dcc2557ca5be bashで接続<br>
                    - mysql -u root -p</li>
                <li>ボリューム確認: docker volume inspectで詳細を確認</li>
            </ul>
        </div>
    </div>
    <script>
        // プロンプトの表示用スクリプト
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

function formatDate(dateStr) {
    return new Date(dateStr).toLocaleString('ja-JP', {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
        hour: '2-digit',
        minute: '2-digit'
    });
}

async function loadPromptInfo() {
    try {
        const response = await fetch('/api/prompt-info');
        const data = await response.json();
        
        if (data.success) {
            const promptsHtml = data.prompts.map(prompt => `
                <div class="prompt-card">
                    <h3>${escapeHtml(prompt.name)}</h3>
                    <p class="description">${escapeHtml(prompt.description)}</p>
                    <pre class="prompt-content">${escapeHtml(prompt.content)}</pre>
                    <div class="prompt-footer">
                        最終更新: ${formatDate(prompt.updated_at)}
                    </div>
                </div>
            `).join('');
            
            document.getElementById('promptInfo').innerHTML = promptsHtml;
        }
    } catch (error) {
        console.error('プロンプト情報の取得に失敗:', error);
        document.getElementById('promptInfo').innerHTML = 
            '<div class="error">プロンプト情報の取得に失敗しました</div>';
    }
}

document.addEventListener('DOMContentLoaded', loadPromptInfo);
    </script>
</body>
</html>

日本語が文字化けする問題

  1. 関連するファイルを共有:
  • docker-compose.yml
  • connection.js
  • index.js
  • package.json
  • エラーログやスクリーンショット
  1. これまでに試したこと:
  • DBの文字コード設定確認(utf8mb4)
  • Bufferを使用したデコード処理 など

そして、先ほどのnpm installについては、Dockerコンテナ内で実行する場合は:

試したこと:

DBの文字コード設定確認

  • 文字セットが全て utf8mb4 であることを確認
  • collation が utf8mb4_unicode_ci であることを確認

connection.js での試み

  • charset の指定
  • collation, encoding の追加(警告が出たため無効)
  • typeCast オプションの追加

index.js での試み

  • Buffer を使用したデコード処理
  • Content-Type ヘッダーの設定
  • HEX形式でのデータ取得

データ確認

  • DBに直接アクセスして日本語データが正常なことを確認
  • hex形式でのデータ内容確認

日本語が文字化けする問題

文字コードとは

  • コンピュータが文字を扱うための約束事
  • 例:「あ」という文字を16進数の E38182 として表現
  • 主な文字コード:
    • UTF-8:日本語を含む世界中の文字を扱える(最も一般的)
    • latin1:英語圏で使用される基本的な文字セット
    • Shift-JIS:日本語Windows向けの文字コード

多重エンコードとは

  • 既にエンコード(変換)された文字を、さらに変換してしまう状態
  • 例:
    1. 「あ」→ E38182(1回目の変換:正常)
    2. E38182C3A3E28182(2回目の変換:不要)
  • この状態だと文字化けの原因となる

現在の問題

  • DBに保存する際に多重エンコードが発生
  • Node.jsでデータを取得する際に文字化けして表示
文字「あ」 UTF-8エンコード E38182 文字「あ」 1回目のエンコード E38182 2回目のエンコード C3A3E28182 正常な変換 多重エンコード(問題のある状態)

試したアプローチ:

  1. アプリケーション層での対応:
    • Content-Type設定
    • Buffer経由のデコード
    • iconv-liteでのデコード
  2. DB層での対応:
    • CONVERTを使用した変換
    • CAST AS BINARYの使用
    • HEX形式での取得
  3. 接続設定での対応:
    • charsetの設定
    • エンコーディングオプション
    • セッション文字コードの設定

未試行と考えられるアプローチ:

  1. DB接続時のクエリ結果処理: