Contents
MySQLでの文字化けの発生
当初、VBA Formatterプロジェクトで日本語を含むプロンプトデータを表示した際に文字化けが発生していました。
具体的には「あいうえお」が「縺ゅ>縺」のように表示される状態でした。
文字コードとは
文字コードの基本
文字コードとは、コンピュータが文字を扱うための約束事です。
コンピュータは内部的には全て数値(バイナリ)で処理するため、
「あ」という文字を「あ」として認識するためには、決まった規則が必要になります。
主な文字コード
- ASCII: 英数字のみ(128文字)
- Shift-JIS: 日本語用(Windows系)
- EUC-JP: 日本語用(UNIX系)
- UTF-8: 世界中の文字に対応(現在の標準)
- UTF-8mb4: UTF-8の拡張版(絵文字にも対応)
なぜ文字化けが起きたのか
文字化けの主な原因は、データの流れる過程で文字コードの解釈が一貫していなかったことです:
- データベースでの保存時の文字コード
- アプリケーションでの処理時の文字コード
- ブラウザでの表示時の文字コード
これらの設定が異なると、例えば:
- 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>
日本語が文字化けする問題
- 関連するファイルを共有:
docker-compose.yml
connection.js
index.js
package.json
- エラーログやスクリーンショット
- これまでに試したこと:
- 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向けの文字コード
多重エンコードとは:
- 既にエンコード(変換)された文字を、さらに変換してしまう状態
- 例:
- 「あ」→
E38182
(1回目の変換:正常) E38182
→C3A3E28182
(2回目の変換:不要)
- 「あ」→
- この状態だと文字化けの原因となる
現在の問題:
- DBに保存する際に多重エンコードが発生
- Node.jsでデータを取得する際に文字化けして表示
試したアプローチ:
- アプリケーション層での対応:
- Content-Type設定
- Buffer経由のデコード
- iconv-liteでのデコード
- DB層での対応:
- CONVERTを使用した変換
- CAST AS BINARYの使用
- HEX形式での取得
- 接続設定での対応:
- charsetの設定
- エンコーディングオプション
- セッション文字コードの設定
未試行と考えられるアプローチ:
- DB接続時のクエリ結果処理: