本プロジェクトの開発規約です。コードレビューや実装で新たに合意した規約を、その都度ここへ追記・更新していきます。
npm workspaces + Turborepo によるモノレポ。apps/*(アプリ)と
packages/*(共有パッケージ)で構成する。
apps/
system-manager システム管理画面organization-manager 組織管理画面user ユーザー画面www 公開画面packages/
database Prisma + zod スキーマ + ドメインヘルパーdatetime 日時ユーティリティmulpay 決済(Mulpay)eslint-config / typescript-config 共有設定packages/ に置き、@aihomestaging/* として import する。@aihomestaging/database 経由。アプリから Prisma を直接初期化しない。app/ 配下。/v/settings)にインストールボタンを持つ。actions/{domain}/ に配置。create-X.ts / get-X.ts / list-X.ts / update-X.ts / delete-X.ts。画像生成は generate-image/。_components/ = そのルート専用のコンポーネント(先頭 _ はルートにならない)。_actions/ = そのルート専用のアクション、_fields/ = フォーム部品。_components/ に置く。_fields/ に1ファイル1コンポーネントで分割する(フォーム本体の index.tsx に FormField をベタ書きしない)。{field-name}-field.tsx、エクスポートは {FieldName}Field。useFormContext<FieldInputs, unknown, FieldOutputs>() でフォームへ接続する(props でのバケツリレーをしない)。schema.ts に切り出し、schema / FieldInputs(z.input)/ FieldOutputs(z.output)をエクスポートする。index.tsx は useForm(defaultValues・submit)とフィールドの組み立てに専念する。_fields/ と schema.ts はフォーム単位で持つ(作成フォームと編集フォームで各自に配置する)。例:contract-plans/new/.../contract-plan-add-form/、system-notifications/new/.../system-notification-add-form/。(horizontal)/h = PC版、(vertical)/v = モバイル版(用語: PC版/モバイル版)。(tracking)/t、(landing-page)/lp など、レイアウト単位でルートグループを分ける。// NG: アクションの型に依存
import { BillingRecordMonthlyTotal } from "@/actions/billing-record/list-billing-record-monthly-totals";
// OK: コンポーネント側で型を定義
type BillingRecordMonthlyTotals = {
month: number;
totalAmount: number;
}[];
"use server"。action(actionName, handler) ヘルパーでラップするのを標準とする。認証チェックと try/catch を集約し、handler には認証済み user(organization 付き)が渡る。※ 一部に未適用の関数あり(既知のブレ ①)。Result<T>({ success: true, data } または { success: false, errorCode })。例外をクライアントへ投げず、failed() を返す。succeeded(data)、失敗は failed(ErrorCode.X, { log })。失敗時は logError で記録される。session.user.isSystemManager を検証してから処理する。cache() でラップ(例:listGenerationLogCached)。export async function createHomestaging(data): Promise<Result<Homestaging>> {
return action("createHomestaging", async (user) => {
// user.organization 利用可
if (!inputImageUri) return failed(ErrorCode.BadRequest, { log: { ... } });
const homestaging = await prisma.homestaging.create({ ... });
return succeeded(homestaging);
});
}
ErrorCode(lib/server-action/error-code.ts)に集中定義する。Unexpected / Unauthorized / BadRequest / FeatureUnavailable。ErrorCode.Cleaning.GenerationHasAlreadyStarted、ErrorCode.SystemNotification.HasBeenExpired。prisma/schema/*.prisma に分割する。/// ドキュメントコメントと @namespace を付ける(埋め込み用語集として機能)。cuid(2)・@db.VarChar(30)。@map("snake_case")、テーブルは @@map("snake_case_複数形")。createdAt / updatedAt。deletedAt を持ち、取得時は where: { deletedAt: null } で除外する。Json カラム+ Zod スキーマで parse(packages/database/lib/{domain}/)。kind 判別は discriminated union。auth() でセッション取得。AuthPage ラッパーを用いる。session.user は organization や isSystemManager 等のフラグを持つ。権限はこれで分岐する。@google/genai、lib/gemini)。AWS_CLOUDFRONT_ENDPOINT + key)。
lib/aws/s3/upload(base64 を Buffer.from(data, "base64") でバイナリ化して保存)。actions/s3/get-upload-url で presigned PUT URL を取得し、ブラウザから S3 へ直接 PUT する。
⚠️ PUT のボディは必ずバイナリ(Blob)にする。ImageData.data は base64 文字列なので、そのまま fetch の body にすると S3 に「base64 テキスト」が保存され画像が壊れる(S3 は base64 を自動デコードしない)。
await (await fetch(`data:${mimeType};base64,${data}`)).blob() 等でデコードしてから PUT する。
PUT の content-type は presigned の ContentType と一致させる。
画像生成(ホームステージング / DIYリフォーム / 家具消し / 家具引越し / 居抜き / 3D間取り 等)の作成フォームではこの方式を標準とし、作成サーバーアクションは inputImage(base64)ではなく inputImageUri を受け取る形に統一する(base64 をサーバーへ通さず VPC/ECS/WAF コストを抑える。2026/06/10 セッション。外装は未対応 →課題)。MD5(内容) のため、実装を変えても同一画像なら同一キーになる。S3 の中身を確認して動作検証する際は、旧実装が残したオブジェクトとのキー衝突による誤検知に注意(一度削除してから再アップロードして先頭バイトを確認するのが確実)。get-upload-url は現状クライアント指定の key を検証していない(課題)。サーバー側でのキー生成/接頭辞検証へ寄せたい。@aihomestaging/mulpay)。@aihomestaging/datetime(日時・タイムゾーン 参照)。DateTime(@aihomestaging/datetime から import { DateTime })。
内部実装は date-fns + @date-fns/tz。
生成:DateTime.of({ datetime, timezone })(datetime 文字列は "yyyy-MM-dd HH:mm:ss")。
主なメソッド:monthStart() / addMonth(n) / toString({ format, timezone }) / toDate()。
フォーマットは date-fns トークン(年は yyyy、月は M/MM。tempo の YYYY とは異なる点に注意)。
LocalDateTime(@aihomestaging/datetime/lib/local-datetime)は非推奨(@deprecated)。
タイムゾーン計算の挙動に問題があり、DateTime へ統一中。新規コードでは使用しない。
既存利用(organizations / system-notifications 配下など)は順次移行する。
gte(以上)/ lt(未満)で範囲指定する。
開発を進める中で方針が変わり、コードベースに生じている不整合のうち、把握済みのもの。 今後どちらかへ寄せる予定の項目を記録する(意図通りの差異はここには含めない)。
① サーバーアクションのラップ方法が不統一 置換予定
多くは action() ラッパーを使うが、system-manager の AI設定更新(update-ai-instruction.ts 当初7ファイル)は auth() を直書きしていた。3D間取りは対応済み(残り6ファイル)。
方針:action() に置き換えていく。該当関数には // TODO を追記済み。
→ 課題:action() ラッパー未統一
② 画像生成のトリガが API ルート 制約による意図的
CRUD はサーバーアクションだが、画像生成(generate)は API ルート(app/api/{type}/[id]/generate/route.ts)に実装。
理由:画像生成はコンポーネントではなくサービスワーカー起点で呼ばれるため。本来はサーバーアクションにしたいが、現状の制約で API ルートに置いている。
→ 課題:画像生成のトリガが API ルート
③ generateImage のシグネチャ不統一 統一予定
多くは generateImage(session, data) だが、ホームステージングのみ generateImage(data)。
方針:generateImage(session, data) へ統一する。該当関数に // TODO 追記済み。
→ 課題:generateImage シグネチャ不統一
※ ホームステージングが AI設定を使わない点は意図通り(マスタ由来)でありブレではない。
※ 本ドキュメントは継続更新します。新たに合意した規約・例外・非推奨パターンがあれば随時追記してください。