はじめに
Claude Code(Anthropic の AI コーディングツール)だけで、位置情報ゲーム LocoCount を開発し、本番ローンチしました。
人間が書いたコードは0行。この記事では、AI だけでプロダクトを作る過程で得た技術的な知見を共有します。
完成したもの
- 世界地図上にトピックを投稿し、応援(Cheer)を集めるゲーム
- 月間で最も応援した人がその地域の「エリアスター」になれる
- Stripe 決済(5 種)、Google OAuth、EXP/レベルシステム搭載
技術スタック
| レイヤー | 技術 |
|---|---|
| フロントエンド | Next.js 16 + React 19 + Tailwind CSS v4 |
| 地図 | MapLibre GL + OpenFreeMap (Liberty) |
| 位置検索 | Nominatim (OpenStreetMap) + geohash |
| 認証 | AWS Cognito + Google OAuth + TOTP MFA |
| API | AWS Lambda (Node.js 20) × 8 関数 |
| DB | DynamoDB (PAY_PER_REQUEST) × 9 テーブル |
| 決済 | Stripe Checkout + Adaptive Pricing |
| インフラ | AWS CDK v2 (TypeScript) |
| AI | Claude Code |
アーキテクチャ
Claude Code の開発スタイルで気づいたこと
1. AI はファイルを分割しない
開発を進めるうち、Lambda のメインハンドラー counter-handler.ts が 5,094 行 に膨らみました。トピック投稿、カウントアップ、Stripe 決済、Webhook 処理、管理者 API、ランキング集計——すべてが 1 ファイルに入っています。
フロントエンドも同様で、PageClient.tsx は 2,679 行 の単一コンポーネントです。
人間の開発なら「ファイル分割しよう」と考えるところですが、Claude Code は一度も提案しませんでした。
なぜか? AI にとって 1 ファイルに全コードがある方が、変更の影響範囲を正確に把握でき、ファイル間の整合性バグを起こしにくいからだと考えています。実際のパフォーマンスも問題なし:
Memory: 117MB / 1024MB (11%)
Response: 38ms average
Cold start: ~2s
「人間のベストプラクティス」と「AI のベストプラクティス」は違う。 これが最初の学びでした。
2. 開発フロー
1. 日本語で「〇〇の機能を追加して」と指示
2. Claude Code がコードを書く
3. Claude Code が npm run deploy:prod を実行
4. ブラウザで確認
5. 「ここがおかしい」とフィードバック
6. 修正 → デプロイ → 確認(1日に数十回)
3. AI が苦手だったこと
| 問題 | 原因 | 解決までの時間 |
|---|---|---|
| 管理コンソールにログインできない | Cognito Client ID の新旧混在 | 丸1日 |
| 県境の都道府県判定が間違う |
fetch() の User-Agent ヘッダーが CORS preflight を発生させ Nominatim API が失敗 |
半日 |
| SSR ページが真っ白 |
reuseMaps 有効時に onLoad が再発火しない |
数時間 |
共通して言えるのは、ブラウザのセキュリティモデルや AWS サービスの細かい仕様 に起因するバグは AI が遠回りしやすいということ。「CORS」「SSR ハイドレーション」「localStorage のスコープ」あたりは人間のヒントが必要でした。
実装の詳細
geohash による近傍検索
トピックの位置情報を geohash でインデックスし、DynamoDB の GSI で検索可能に。
// 9桁 geohash(~2.4m 精度)で保存
const geoHashValue = geohash.encode(latitude, longitude, 9);
// 6桁 prefix(~1.2km)で近傍検索
const geoHashPrefix = geohash.encode(latitude, longitude, 6);
検索時は対象の 6 桁 prefix + 周囲 8 セル = 9 セルを並列 Query:
const prefixes = getNearbyPrefixes(lat, lon); // 9 prefixes
const results = await Promise.all(
prefixes.map(prefix =>
ddb.send(new QueryCommand({
TableName: TABLE,
IndexName: 'GeoHashPrefixIndex',
KeyConditionExpression: 'geoHashPrefix = :prefix',
ExpressionAttributeValues: { ':prefix': prefix },
ScanIndexForward: false,
Limit: 20,
}))
)
);
同名トピックの座標重複検知
DynamoDB のキーは area::topicName。同名トピックが別の場所にある場合、座標の差が 0.0001 度(約 11m)以上なら別トピックとして作成:
const COORD_THRESHOLD = 0.0001; // ~11m
if (latDiff > COORD_THRESHOLD || lngDiff > COORD_THRESHOLD) {
topicName = topicName + `_${latitude.toFixed(4)}_${longitude.toFixed(4)}`;
}
Stripe Webhook の冪等性
全 5 種の決済タイプで、ProcessedEventsTable + TransactWrite による冪等性を確保:
await ddb.send(new TransactWriteCommand({
TransactItems: [
{
Put: {
TableName: PROCESSED_EVENTS_TABLE,
Item: { eventId, ttl, timestamp: Date.now() },
ConditionExpression: 'attribute_not_exists(eventId)',
},
},
{
Update: {
TableName: USAGE_TABLE,
Key: { pk: `USER#${sub}`, sk: 'PURCHASED' },
UpdateExpression: 'ADD balance :counts',
ExpressionAttributeValues: { ':counts': counts },
},
},
],
}));
ConditionExpression: 'attribute_not_exists(eventId)' で重複を弾き、TTL 30 日で自動クリーンアップ。
楽観的更新 + 長押し加速
カウントアップは楽観的更新で即座に UI 反映。長押し時は 200ms → 50ms まで加速:
const tick = () => {
if (!pressingRef.current) return;
if (pendingRef.current >= limit) {
pressingRef.current = false;
return; // 上限で自動停止
}
pendingRef.current++;
setPendingDisplay(pendingRef.current);
if (intervalRef.current > 50)
intervalRef.current = Math.max(50, intervalRef.current * 0.85);
holdRef.current = setTimeout(tick, intervalRef.current);
};
運営責任者名の検索避け(Stripe 審査対策)
特定商取引法に基づく表記で本名の表示が必要だが、検索インデックスに載せたくない。SVG を Base64 エンコードして <img> タグで埋め込み:
<img
src={`data:image/svg+xml;base64,PHN2Zy...`}
alt=""
width={90} height={20}
draggable={false}
style={{ userSelect: 'none', pointerEvents: 'none' }}
/>
-
alt=""→ 検索エンジンにインデックスされない -
<img>→ テキスト選択・コピー不可 - Base64 → ソースコードに名前文字列が残らない
- Stripe 審査員には視覚的に見える → 要件を満たす
注意: スクリーンリーダーでは読み上げられません。アクセシビリティを重視する場合は
aria-labelの付与を検討してください。
数字で見る LocoCount
| 指標 | 値 |
|---|---|
| 開発期間 | 約 2 週間 |
| コード行数(Lambda) | 5,094 行 |
| コード行数(フロント) | 2,679 行 |
| 管理コンソール | 2,720 行 |
| TypeScript ファイル数 | 100 ファイル |
| DynamoDB テーブル | 9 個 |
| Lambda 関数 | 8 個 |
| Stripe 決済タイプ | 5 種 |
| 人間が書いたコード | 0 行 |
まとめ
- Claude Code は、決済・認証・地図を含むフルスタックプロダクトを1人分の工数で生成できる
- AI はファイル分割より「1 ファイルに全部入れる」スタイルを好む。パフォーマンスに問題がなければそのままで OK
- ブラウザのセキュリティモデル(CORS、Cookie スコープ)や AWS の細かい仕様は人間のヒントが必要
- Stripe 審査は特商法ページの整備で突破可能。個人開発でも諦めなくていい
この記事の文章も Claude Code が生成しました。構成の指示・事実確認・修正は人間が行っています。