【初・個人開発】AIペアプロで「万年・作る作る詐欺」を卒業し、CSV文字化け解消ツールを2日で公開した話
はじめに:なぜ「今」これを作ったのか
万年「作る作る詐欺」からの脱却
2025年(というかこれまで毎年)の私の目標は、「未完成のコードを積み上げるのをやめて、小さくてもいいから『完了』させること」でした。
これまで私は、「せっかく作るなら立派なアプリを作りたい!」と設計で悩み、デザインで悩み、結局一度もローカル環境から出ることなく挫折するということを繰り返していました。
そんな負のループを脱却すべく、今回は一旦「自分で考える」ことを放棄。Geminiに企画から立案してもらった上でスパルタ指導をお願いし、「機能は1つだけで絶対に1週間以内に公開URLを発行する」 という制約を自分に課しました。
解決したかった課題:CSVの文字化け
実務でCSV出力を実装した際に、テキストでは問題ないけれどExcelで開くと文字化けするという現象に遭遇しました。Shift-JIS変換やBOM(Byte Order Mark)の有無など、知っていればなんてことのない事象ですが個人的には頻繁に引っかかる問題です。
これについて、テキストを投げたら整形して返してくれるAPIがあれば毎度実装せずともよいのではと思い、ドラッグ&ドロップで非エンジニアでも解決することができるUIも合わせて自分用に作ってみることにしました。
作ったもの
CSV文字化け解消ツール
https://csv-converter-mu.vercel.app/
機能は極めてシンプルです。
- ファイルをドラッグ&ドロップ
- 自動で文字コード(Shift-JIS, EUC-JP等)を判定
- Excelが確実に読み込める「BOM付きUTF-8」に変換してダウンロード
技術選定と「やらないこと」
今回は「1週間以内の公開」を最優先にするため、技術スタックは慣れていてモダンな構成であるNext.js(App Router)とVercelを選択しました。 一方で、MVPとしてリリースするため以下は意図的にやりませんでした。
- データベースの実装(ファイルは即時変換して破棄するため不要)
- ユーザー認証機能
- 複雑なエラーハンドリング
- 多機能化
技術的に困難だったこと1:文字コード判定
UTF-8への変換自体はそこまで難しい実装ではありませんが、今回は文字コード判定も実装に入れており、その部分でやや手こずる場面がありました。
jschardetの誤判定問題
文字コード判定にはjschardetライブラリを使用しましたが、短い日本語テキストの場合、Shift-JISではなく Windows-1252(欧米コード)と誤判定されるケースがありました。
これに対し、「欧米コードと判定されたら、あえてShift-JISとして扱う」という日本特化のロジックを入れることで解決しました。
// 実際のコードの断片(判定ロジックの一部)
let encoding = 'utf8';
// 文字コードを検出
const detected = jschardet.detect(buffer);
if (detected && detected.encoding) {
const detectedEncoding = detected.encoding.toLowerCase();
if (['windows-1252', 'iso-8859-1'].includes(detectedEncoding)) {
// 日本語環境では誤判定の可能性が高いためShift_JISとみなす
encoding = 'shift_jis';
}
}
BOM(Byte Order Mark)の重要性
単純にUTF-8にすればよいのだと思っていましたが、ExcelはBOMがないUTF-8をデフォルトでShift-JISとして開こうとしてしまい正しく認識してくれません。iconv-liteを使い、ファイルの先頭に0xEF, 0xBB, 0xBFというバイナリデータを付与することで、Excelでも問題なく開けるようにしました。
技術的に困難だったこと2:AI開発における「人間の判断」
今回、もっとも技術的に困難(チャレンジ)だったことは、コードを書くことそのものではありませんでした。「AIが書いたコードをどこまで信頼し、どこからを自身が補完するか」という判断です。
開発には、Cursorを活用しました。AIのAgent Modeで「動くコード」は一瞬で実装してもらえますが、「安全なコード」かどうかまでは自発的に考えてくれません。
デプロイ直前のセキュリティチェック
挙動の確認をしていたところ、バリデーションの甘い部分がいくつか見つかりました。プロンプトに明示的に記載しなかったためですが、AIが出してきたコードに対し自身で以下のバリデーションを追加しました。
- 拡張子の厳密なチェック:画像ファイルなどを投げられないように制限
- ファイルサイズ制限:Vercel Serverless Functionの制限(4.5MB)を超えないよう、フロント/バック両方でガード
-
ファイル名のサニタイズ:
../などの特殊文字によるパストラバーサル攻撃(ディレクトリ遡り)を防止 - レート制限:短時間に大量のリクエストを送信してサーバーを落とす攻撃(DoS)を防ぐため、同一IPからのアクセス頻度を制限
はじめての「セキュリティヘッダー」
また、AIにセキュリティレビューを依頼した際、「next.config.ts にセキュリティヘッダーを追加すべき」という提案を受けました。
正直、言われるまでセキュリティヘッダーという存在すら知りませんでしたが、AIに提示された設定内容を精査し、最終的に以下のような設定を導入しました。
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains;'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
},
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self'",
].join('; ')
}
],
},
];
},
};
export default nextConfig;
※ 2026/01/01 上記コードからX-XSS-Protection[1]とStrict-Transport-Securityのpreload[2]を削除する修正をしました。
特に勉強になったのは以下の設定です。
-
X-Frame-Options: DENY
自分のサイトが他のサイトの <iframe> 内に埋め込まれるのを防ぎ、クリックジャッキング攻撃を回避 -
Permissions-Policy
camera=(), microphone=() と書くことで、ブラウザのカメラやマイクへのアクセスを明示的に禁止 -
Content-Security-Policy (CSP)
読み込んでよいスクリプトや画像のソース元を厳格に指定し、XSS(クロスサイトスクリプティング)などの攻撃を緩和
脆弱性の問題が騒がれている今、自分の知識が及ばない部分をAIに指摘してもらいながら埋めていくプロセスこそが、今回の最大の学びでした。
まとめ:2日で公開して感じたこと
これまでは「こんな稚拙なツールを公開するなんて恥ずかしい」と思っていました。公開後の今も正直ちょっと思っています。でも、アプリを作って公開して誰かに知ってもらうためにGoogle Search Consoleに登録したり記事を書いてみたり、そういう一連のプロセスをただ知っているのと実際に体験するのとでは大きな差があると感じました。
AIという強力なペアプロパートナーのおかげで、ずっとできなかった「0→1」の壁を一旦突破できました。1週間以内を目標にして最終的にはたった2日の開発期間で作った小さなツールですが、私にとっては「個人開発者としての一歩」を踏み出す大きな成果物です。
これから「完成品」を少しずつでも公開していき最終的には自分の作りたい立派なアプリを作れるようになれることを目標に、個人開発を続けていきたいと思います。
内容に間違い等あればご指摘いただけましたら幸いです!
-
X-XSS-Protection
以前は設定推奨されていたヘッダーでしたが、現代のブラウザ(ChromeやEdgeなど)では非標準・非推奨となっているため、削除しました。現在はContent-Security-Policy (CSP)がその役割を担っています。 ↩︎ -
preload
現代のブラウザは HTTPS-First Mode を持っているため安全性は向上しています。逆に preload は一度設定すると解除が困難でサブドメインにも影響するなど運用リスクが非常に高いため、個人開発では削除しました。次の記事のHSTSヘッダー解説セクションにて詳しくまとめましたのでよければご一読ください。 ↩︎
Discussion