1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeだけで世界地図ゲームを開発→Stripe審査→本番ローンチした全記録

1
Posted at

はじめに

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.ts5,094 行 に膨らみました。トピック投稿、カウントアップ、Stripe 決済、Webhook 処理、管理者 API、ランキング集計——すべてが 1 ファイルに入っています。

フロントエンドも同様で、PageClient.tsx2,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 が生成しました。構成の指示・事実確認・修正は人間が行っています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?