Node.js(Express)とMySQLをDockerで構築する手順を、順を追って解説します。
プロジェクト構成
project/
├── docker-compose.yml
├── Dockerfile
├── package.json
└── src/
├── index.js
├── db/
│ ├── connection.js
│ └── init/
│ └── 01-schema.sql
└── public/
├── index.html
├── style.css
└── script.js
主要ファイルの実装
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
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
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",
"express": "^4.18.2",
"mysql2": "^3.6.5",
"nodemon": "^3.0.2",
"dotenv": "^16.3.1"
}
}
Express サーバーの実装
データベース接続設定 (src/db/connection.js)
const express = require('express');
const path = require('path');
const db = require('./db/connection');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
// API エンドポイント
app.get('/api/data', async (req, res) => {
try {
const [rows] = await db.query('SELECT * FROM your_table');
res.json({ success: true, data: rows });
} catch (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(`Server running on http://localhost:${PORT}`);
});
メインサーバーファイル (src/index.js)
// サーバーのメインファイル (index.js)
const path = require('path');
const express = require('express');
const { analyzeCode } = require('./bedrock/analyzer');
const db = require('./db/connection');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
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設定
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');
}
next();
});
// プロンプト一覧取得API
app.get('/api/prompt-info', async (req, res) => {
try {
const [rows] = await db.query(`
SELECT id, name, content, updated_at
FROM prompts
ORDER BY id
`);
res.json({ success: true, prompts: rows });
} catch (error) {
res.status(500).json({
success: false,
error: 'プロンプト情報の取得に失敗しました'
});
}
});
// フォーマットAPI
app.post('/format', async (req, res) => {
try {
const { code, promptId } = req.body;
if (!code) {
return res.status(400).json({
success: false,
error: 'コードが提供されていません'
});
}
const [rows] = await db.query(
'SELECT content FROM prompts WHERE id = ?',
[promptId]
);
if (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) {
res.status(500).json({
success: false,
error: error.message
});
}
});
// プロンプト更新API
app.put('/prompts/:id', async (req, res) => {
try {
const { id } = req.params;
const { content } = req.body;
const [result] = await db.query(
'UPDATE prompts SET content = ? WHERE id = ?',
[content, id]
);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
error: 'プロンプトが見つかりません'
});
}
res.json({ success: true, message: '更新しました' });
} catch (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(`Server running on http://localhost:${PORT}`);
});
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>
<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>
<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>
<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/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: .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;
}
.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-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;
}
.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;
}
.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 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;
}
.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;
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);
}
Docker環境の起動
現在の状況
DockerでVBAフォーマッターを作成中で、日本語が文字化けする問題に直面しています。
- 関連するファイルを共有:
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形式でのデータ内容確認