コンテンツにスキップ

飛行計画機能 forDemo コーディング規約

本ドキュメントは、飛行計画機能 forDemo(10月デモ向け。対象APIの範囲はArchitecturePolicy_Flightplanning_forDemo.md冒頭参照)の固有コーディング規約を定義する。共通のコーディング規約(CodingConventions_Common.md)と合わせて参照すること。


1. パッケージ構成規約(飛行計画機能固有)

Section titled “1. パッケージ構成規約(飛行計画機能固有)”

基本パッケージは、UTM Backends共通の以下とする(ArchitecturePolicy_Flightplanning_forDemo.md3節参照。飛行計画機能専用の基本パッケージは設けない)。

com.intent_exchange.utm

飛行計画機能のクラスは、既存の共通レイヤへ以下のように配置する。

com.intent_exchange.utm.handler ← FlightPlanningHandler
com.intent_exchange.utm.handler.response ← FlightPlanResponseMapper 等
com.intent_exchange.utm.usecase ← CreateFlightPlanUseCase 等(1操作1クラス)
com.intent_exchange.utm.domain.model ← FlightPlan・FlightPlanRevision・FlightPlanArea 等
com.intent_exchange.utm.domain.port ← FlightPlanRepository 等
com.intent_exchange.utm.domain.exception ← FlightPlanNotFoundException 等
com.intent_exchange.utm.infrastructure.persistence.postgres ← FlightPlanRepositoryImpl 等
com.intent_exchange.utm.infrastructure.persistence.postgres.mapper ← FlightPlanMapper(+ XML)等

禁止:

com.example.FlightPlan.Handler
com.intent_exchange.flightplanningApp.Service
com.intent_exchange.flightplanning.* (飛行計画機能専用の基本パッケージを新設しない)

各パッケージには既存のpackage-info.javaがある(新規パッケージを追加する場合のみ作成し、パッケージの責務を記述する)。


2. API バージョニング規約(飛行計画機能固有)

Section titled “2. API バージョニング規約(飛行計画機能固有)”

対象APIはUI向けfrontend APIのみで、パスは以下とする(docs/openapi/frontend/flight-planning.yaml準拠)。

UI向け外部 API: POST /api/v1/fp/flight-plans (仮登録)
UI向け外部 API: GET /api/v1/fp/flight-plans (一覧取得)
UI向け外部 API: GET /api/v1/fp/flight-plans/{flightPlanId} (詳細取得)
UI向け外部 API: PUT /api/v1/fp/flight-plans/{flightPlanId} (更新)
UI向け外部 API: DELETE /api/v1/fp/flight-plans/{flightPlanId} (削除)
UI向け外部 API: POST /api/v1/fp/flight-plans/{flightPlanId}/registration (本登録)
UI向け外部 API: POST /api/v1/fp/flight-plans/{flightPlanId}/report (通報)

飛行計画機能の論理削除は、ER図(flight-plan-ER.md)の設計に従う。

  • 論理削除カラムを持つのはflight_plan.deleted_atTIMESTAMPTZ、nullable)のみ。flight_plan_revision以下は不変レコード(INSERT only)のため論理削除カラムを持たない。
  • deleteFlightPlanは物理削除を行わず、flight_plan.deleted_atに削除時刻を設定する(statusは変更しないため状態遷移イベントも追記しない)。対象がDRAFTの場合のみ、あわせてflight_plan_draftの行を物理削除する(BusinessLogicSpecifications.md5.5節手順1-3)。
  • deleted_atを設定するのはdeleteFlightPlanだけではない。DRAFTからのキャンセル(cancelFlightPlan。対象API外のため実装対象外)は削除と同等に扱い、statusDRAFTのままdeleted_atのみを設定する(リビジョンもDRAFT内容も持たない行を取得対象から外すため。状態遷移イベントも追記しない。ER図の「一時保存は専用テーブルで表す」節)。
  • 検索処理(listFlightPlansgetFlightPlan)ではdeleted_at IS NULLのデータのみを対象とする。
<!-- MyBatis: 論理削除フィルタ例(一覧・詳細取得では常にこのフィルタを適用する) -->
<select id="findById" resultType="FlightPlanRecord">
SELECT <include refid="flightPlanColumns"/>
FROM flight_planning.flight_plan
WHERE id = #{id} AND deleted_at IS NULL
</select>

飛行計画機能は、テーブルごとに意味の異なる日時カラムを持つ(ArchitecturePolicy_Flightplanning_forDemo.md5節参照)。

カラム名Java フィールド内容
flight_plan.created_atcreatedAt飛行計画自体の作成日時(不変)
flight_plan.created_bycreatedBy作成者(USERへの参照)
flight_plan_revision.changed_atchangedAt当該リビジョンの作成日時(不変。「更新日時」は最新リビジョンのこの値で表現する)
flight_plan_revision.changed_bychangedBy変更者(USERへの参照)
// INSERT時のレコード生成例(Repository実装内。仮登録時の初版リビジョン作成)
private FlightPlanRevisionRecord toInitialRevisionRecord(FlightPlanRevision revision) {
var now = OffsetDateTime.now(ZoneOffset.UTC);
return new FlightPlanRevisionRecord(
revision.id().value(),
revision.flightPlanId().value(),
1, // revision_no: 初版は1
null, // parent_revision_id: 初版はNULL
revision.changedBy().value(),
ChangeType.CREATE,
revision.title(),
revision.status(),
now);
}

5. 例外クラス規約(飛行計画機能固有)

Section titled “5. 例外クラス規約(飛行計画機能固有)”

共通の例外実装規約(CodingConventions_Common.md)に加え、飛行計画機能では以下の方針とする。

模擬DIPS連携で発生する例外は5.4節に定める。それ以外の外部システム連携は対象APIの範囲では行わない(deleteFlightPlanのDIPS取り下げは対象外。ArchitecturePolicy_Flightplanning_forDemo.md冒頭「対象範囲」参照)。

5.1 見つからない場合の例外(404 Not Found)

Section titled “5.1 見つからない場合の例外(404 Not Found)”

既存のCoordinationNotFoundExceptionConflictionNotFoundExceptionと同じパターンでFlightPlanNotFoundExceptionを追加する。

/// 対象の飛行計画が見つからない場合の例外(HTTP 404)。
public class FlightPlanNotFoundException extends BusinessRuleException {
public FlightPlanNotFoundException(String message) {
super(message);
}
}

GlobalExceptionHandlerに、既存のhandleCoordinationNotFoundと同型の@ExceptionHandler(FlightPlanNotFoundException.class)を追加し、ProblemTypes.NOT_FOUND(既存の共通定数)で404を返す。

5.2 状態不正の例外(409 Conflict)

Section titled “5.2 状態不正の例外(409 Conflict)”

registerFlightPlan(対象がDRAFT以外)・updateFlightPlan(対象がCANCELLED/ENDED)・deleteFlightPlan(対象がACTIVATED/ENDED)・reportFlightPlan(対象がACCEPTED以外、またはreportStatusUNREPORTED以外)が返す409は、handler.problem.ProblemTypesに既に定義済みのINVALID_TRANSITION定数を使用する。GlobalExceptionHandlerにはFlightPlanInvalidTransitionException用のマッピングを追加済みである。ロック競合(FlightPlanConflictException)は専用定数を設けずBUSINESS_RULE_VIOLATIONを流用する(OASのregisterFlightPlan/updateFlightPlan/deleteFlightPlan 409のlockConflict例が/problems/business-rule-violationのため。優先順位「OpenAPI yaml > コーディング規約」に従う)。

/// 状態不正により要求を受理できない場合の例外(HTTP 409)。
public class FlightPlanInvalidTransitionException extends BusinessRuleException {
public FlightPlanInvalidTransitionException(String message) {
super(message);
}
}
@ExceptionHandler(FlightPlanInvalidTransitionException.class)
public ResponseEntity<ProblemDetail> handleFlightPlanInvalidTransition(
FlightPlanInvalidTransitionException e) {
return problemResponse(
buildProblem(
HttpStatus.CONFLICT, ProblemTypes.INVALID_TRANSITION, "Invalid transition", e.getMessage()));
}

5.3 入力値不正(422 Unprocessable Content)

Section titled “5.3 入力値不正(422 Unprocessable Content)”

DIPS必須項目のビジネスルール違反(registerFlightPlanの必須項目チェック等)・Idempotency-Keyの異なるリクエストボディでの再利用は、既存の共通例外(BusinessRuleExceptionのフォールバックハンドラー、IdempotencyKeyConflictException)をそのまま使用する。飛行計画機能固有の新規例外クラスは不要。

通報義務のない飛行計画(reportFlightPlanreportRequired=false)への通報要求は業務ルール違反ではなく、通報を受け付ける。reportRequiredは通報が必須かどうかを表すフラグであり通報の可否を決めない(DIPSは義務のない飛行の通報も受理する)。専用の例外クラスは設けない。

5.4 模擬DIPS連携の例外(502 / 503 / 504)

Section titled “5.4 模擬DIPS連携の例外(502 / 503 / 504)”

模擬DIPSの呼び出しで発生する失敗は、DipsApiClientが3種のドメイン例外(ExternalDipsInvalidResponseExceptionExternalDipsUnavailableExceptionExternalDipsTimeoutException)に詰め替える。GlobalExceptionHandlerのマッピングとtypeArchitecturePolicy_Flightplanning_forDemo.md18節の表に従い、DIPS専用のtypeProblemTypes.DIPS_INVALID_RESPONSEDIPS_CALL_FAILEDDIPS_TIMEOUT)を使う。汎用のBAD_GATEWAYSERVICE_UNAVAILABLEGATEWAY_TIMEOUTは運航調整のUTM間通信(ExternalUtm*)が使っており、同じ値を返すとクライアントがDIPS起因の失敗を判別できない。

HTTP 200でも業務的に失敗している場合(flightPlanRegistrationResult1以外)は、上記3種のいずれにも当たらない。UseCase層で判定し、503(/problems/dips-call-failed)に対応する例外を送出する。

いずれの例外もdetailにはe.getMessage()だけを載せ、原因例外(cause)は載せない。模擬DIPSの応答本文には個人情報が含まれうるため、クライアントへ返る値に混入させない。


6. ドメインモデル実装規約(不変リビジョン)

Section titled “6. ドメインモデル実装規約(不変リビジョン)”

方針の背景はArchitecturePolicy_Flightplanning_forDemo.md19節を参照。飛行計画はFlightPlan(不変ヘッダ)+FlightPlanRevision(1リビジョン分のスナップショット、INSERT only)の2集約とする。

6.1 FlightPlanFlightPlanRevision(通常のrecordパターン)

Section titled “6.1 FlightPlan・FlightPlanRevision(通常のrecordパターン)”
/// 飛行計画(不変ヘッダ)。
/// current_revision_id が指す最新リビジョンへのポインタを持つのみで、内容自体は保持しない。
public record FlightPlan(
FlightPlanId id,
OrganizationId organizationId,
UserId createdBy,
FlightPlanRevisionId currentRevisionId,
OffsetDateTime createdAt,
Optional<OffsetDateTime> deletedAt) {}
/// 飛行計画リビジョン(1リビジョン分のスナップショット。INSERT onlyで不変)。
/// 内容変更・状態遷移のいずれも新しいインスタンスの追加として表現し、UPDATEは行わない。
public record FlightPlanRevision(
FlightPlanRevisionId id,
FlightPlanId flightPlanId,
int revisionNo,
Optional<FlightPlanRevisionId> parentRevisionId,
UserId changedBy,
ChangeType changeType,
OffsetDateTime changedAt,
String title,
FlightPlanStatus status,
// ... 以下、飛行日時・飛行領域・操縦者機体割当・DIPS通報固有属性等が続く(OAS `FlightPlanFields`参照)
List<FlightPlanArea> areas,
List<PilotAssignment> pilotAssignments) {
/// 仮登録時の初版リビジョンを組み立てる便宜メソッド。
public static FlightPlanRevision createInitial(
FlightPlanId flightPlanId, UserId changedBy, String title, /* ... */ OffsetDateTime now) {
return new FlightPlanRevision(
FlightPlanRevisionId.generate(), flightPlanId, 1, Optional.empty(),
changedBy, ChangeType.CREATE, now, title, FlightPlanStatus.DRAFT, /* ... */);
}
/// 本登録時の新リビジョンを組み立てる便宜メソッド(既存の内容をそのまま引き継ぎ、status・changeTypeのみ更新)。
public FlightPlanRevision withAccepted(UserId changedBy, OffsetDateTime now) {
return new FlightPlanRevision(
FlightPlanRevisionId.generate(), flightPlanId, revisionNo + 1, Optional.of(id),
changedBy, ChangeType.ACCEPT, now, title, FlightPlanStatus.ACCEPTED, areas, pilotAssignments);
}
}

6.2 状態遷移の妥当性判定はUseCase層で行う

Section titled “6.2 状態遷移の妥当性判定はUseCase層で行う”

statusの遷移可否(例:registerFlightPlanは対象がDRAFT以外の場合FlightPlanInvalidTransitionException)は、flight-plan-statemachine.mdの条件をそのままUseCase層のガード節として実装する。FlightPlanRevision自身は新しいインスタンスを組み立てる便宜メソッドを提供するのみで、遷移可否そのものは判定しない。

/// 「飛行計画本登録」ユースケース。
public class RegisterFlightPlanUseCase {
private final FlightPlanRepository flightPlanRepository;
private final TransactionManager transactionManager;
public FlightPlan execute(FlightPlanId flightPlanId, UserId actorUserId) {
return transactionManager.execute(() -> {
var flightPlan = flightPlanRepository.findByIdForUpdate(flightPlanId)
.orElseThrow(() -> new FlightPlanNotFoundException(flightPlanId.toString()));
var currentRevision = flightPlanRepository.findRevision(flightPlan.currentRevisionId())
.orElseThrow(() -> new FlightPlanNotFoundException(flightPlanId.toString()));
// 状態遷移の妥当性判定はUseCase層で行う(sealed interfaceの型では強制しない)
if (currentRevision.status() != FlightPlanStatus.DRAFT) {
throw new FlightPlanInvalidTransitionException(
"flight plan cannot be registered from status: " + currentRevision.status());
}
// 必須項目チェック(OAS registerFlightPlan の説明参照。422)は別途実施
var newRevision = currentRevision.withAccepted(actorUserId, OffsetDateTime.now(ZoneOffset.UTC));
flightPlanRepository.insertRevision(newRevision);
return flightPlanRepository.updateCurrentRevision(flightPlan, newRevision);
});
}
}

flight_plan_area.geometryはPostGIS型(WGS84/EPSG:4326)を使用する。Domain層はPostGISに依存しないため、Infrastructure層(MyBatis Mapper・TypeHandler)でジオメトリ⇔ドメイン型の変換を行う。現時点でリポジトリ内に類似の実装例が無いため、具体的な型(座標配列を持つ自前の値オブジェクトを使うか、JTS等のライブラリを導入するか)は実装時に決定する(判断不可。要確認)。

6.4 部分更新(JSON Merge Patch)の実装規約

Section titled “6.4 部分更新(JSON Merge Patch)の実装規約”

updateFlightPlanのリクエストボディは、キー省略(=変更しない)・明示的null(=未入力に戻す)・値指定(=その値に更新する)の3値のセマンティクスを持つ(BusinessLogicSpecifications.md5.2節手順3-1)。生成DTOFlightPlanUpdateRequestopenApiNullable=falseで生成されており未設定とnullを区別できないため、以下の方針で実装する。

  • Handler層にRequestBodyAdviceFlightPlanUpdateBodyCapture)を置き、Jacksonが生成DTOへ変換する前の生JSONをリクエストスコープへ退避する。HandlerはそれをJsonNodeとして取り出し、UseCaseへ渡す。生成DTOは@ValidによるBean Validation(name長・radiusMの下限等)を効かせるためだけに受け取る。

  • パッチ適用はRFC 7386(JSON Merge Patch)に従う(JsonMergePatch)。入れ子のオブジェクトは再帰的にマージし、配列は全置換とする。

  • ただしflyRouteだけは再帰マージせず値ごと全置換する。typeによる判別子付きのoneOfで種別ごとに持つキーが異なるため(circleradiusMroutebufferM)、部分マージすると種別変更後も旧種別のキーが残るためである。

  • ACCEPTED以降の分岐では、現行リビジョンをFlightPlanContentAssembler.toPatchableFieldsでリクエストと同じ形のJSONへ復元してからパッチを適用し、assembleForRegistrationへ通す。これにより「本登録と同じ入力項目の整合性チェック」(同5.2節手順3-2)を検証ロジックの重複なく再利用する。

  • 内容が変わらない更新で次版リビジョンを作らない判定(同5.2節手順3-2)は、組み立て直した次版リビジョンと現行リビジョンの内容の比較で行う(FlightPlanRevision.hasSameContentAs)。パッチのキーの有無ではなく保存される値で判定するため、判定はドメイン(リビジョンの内容を知る側)に置き、JSONの比較では行わない。JSONの比較にすると、同一時点を指す別のオフセット表記や閉環の補い方といった表記の違いを「変更あり」と誤判定する。

  • ただしassembleForRegistrationが担うのは、渡されたJSONだけで判定できるチェック(同5.1節手順2-1-1〜2-1-5)に限られる。幾何演算(同2-1-6の自己交差)はPort(GeometryValidator)を要するため、FlightPlanContentValidator.validateForRegistrationが担い、registerFlightPlanupdateFlightPlanの両ユースケースがこれを呼ぶ。片方だけがこの検証を持つ状態を作らない(本登録で弾かれる内容が更新経由で保存できてしまうため)。両者が同じ検証を通ることをコード上で保証するために1クラスへ集約している。

  • 3値セマンティクスの説明はレイヤごとに再掲しない。 役割ごとに置き場所を1つに決め、他はそこを参照する。境界を越えるクラス(FlightPlanningHandlerUpdateFlightPlanUseCaseFlightPlanContentAssembler)は「生成DTOでは3値を表現できない」という理由を再掲せず、下表の(4)(5)を参照する。同じ理由が層ごとに再掲されると、方針を変えたときに一部だけ古くなる(Claude Code向けのルール .claude/rules/java/common.md「同じ結論が2箇所にあると片方だけ古くなる」。リポジトリ内のパスのため、ドキュメントサイトには掲載しない)。テストの@DisplayNameは検証内容の宣言であり、再掲には当たらない。

    #何を述べるか置き場所
    (1)業務としての意味論(キー省略/明示的null/値指定)BusinessLogicSpecifications.md5.2節手順3-1
    (2)API契約としての意味論(クライアント向け)OASのupdateFlightPlanのdescription(スキーマがname以外をnullableにしている理由はFlightPlanUpdateRequestのdescription)
    (3)実装方針本節
    (4)コードでの意味論(マージの規則)JsonMergePatchのJavadoc
    (5)生成DTOを経由できない理由(openApiNullable=falseによる迂回)FlightPlanUpdateBodyCaptureのJavadoc

6.5 模擬DIPSへの通報リクエストの組み立て

Section titled “6.5 模擬DIPSへの通報リクエストの組み立て”

reportFlightPlanが送る通報リクエストは、現行リビジョン(FLIGHT_PLAN_REVISIONとその配下)とPilot/Assetマスタから毎回すべて組み立て直す。模擬DIPSの更新は差分更新ではなく全置換であるため、再通報でも同じ組み立てを行う。

  • 項目単位の対応はdocs/data-model/flight-plan-field-mapping.mdに従う。模擬DIPS向けの実装は同ドキュメント11節を正とする(1〜10節はDIPS2.0のガイドラインが出典で、綴り・必須項目・実行モードの有無が異なる)。
  • 変換(コード値→数値、boolean"0"/"1"、日時→yyyyMMdd HHmm)はInfrastructure層のAdapterに置き、Domain層は模擬DIPSの表現を知らない。
  • 通報者(reporter)とpilotInfo[].privateLicenseは固定値を送る(同11-2・11-5節)。通報者の連絡先は設定値(dips.api.reporter.*)から解決し、DBのカラムは追加しない。
  • 新規登録と更新の切り替えはflightPlanIdの有無で行う。更新時に送る値はDIPS_REPORT.dips_receipt_noであり、UTMが発行するFLIGHT_PLAN.idではない。

テストメソッド名は以下の形式とする(CodingConventions_Common.mdと共通)。

<targetMethod>_when<Condition>_<ExpectedResult>

例:

registerFlightPlan_whenStatusIsDraft_returnsAcceptedFlightPlan
registerFlightPlan_whenStatusIsNotDraft_throwsInvalidTransitionException
findById_whenRecordIsDeleted_returnsEmpty

禁止:

testRegisterFlightPlan
registerFlightPlanTest
test01
飛行計画本登録成功時

JUnit 6 の @DisplayName を使用し、テストの意図を日本語で明示することを推奨する。

@Test
@DisplayName("対象がDRAFT状態の場合は本登録に成功する")
void registerFlightPlan_whenStatusIsDraft_returnsAcceptedFlightPlan() {
}

Claude Code はコード生成時に以下を遵守すること。

  • CodingConventions_Common.md および本ドキュメントに従う
  • ArchitecturePolicy_Common.md および ArchitecturePolicy_Flightplanning_forDemo.md に従う
  • OpenAPI yaml(docs/openapi/frontend/flight-planning.yaml)を API 仕様の正とする(スキーマファースト)
  • DB create SQL(db/schema/flight-planning.sql)を DB 仕様の正とする
  • Handler / UseCase / Domain / Port / Infrastructure の責務を分離する
  • Domain に ORM アノテーションを付与しない
  • Domain・UseCase に @Component/@Service を付与しない(AppConfig@Bean で登録)
  • @Transactional を使わず TransactionManager.execute を使う
  • Repository 実装内で DataAccessExceptionRepositoryException にラップする
  • SQL を文字列連結で組み立てない(MyBatis の #{} を使用)
  • ドメイン例外に HTTP ステータスを含めない
  • エラーレスポンスは RFC 9457 Problem Details 形式とする
  • 機密情報をコードに直書きしない
  • 未使用 import を含めない
  • コンパイル可能なコードを生成する
  • テストコードの雛形も生成する
  • ArchUnit テストの雛形も生成する

明示的な仕様がない限り、以下は forDemo では実装対象外とする(ArchitecturePolicy_Flightplanning_forDemo.md12節と共通)。

  • 冒頭「対象範囲」に記載したAPI以外の飛行計画関連API(周辺データ更新・飛行開始・飛行終了・キャンセル)
  • DIPS通報の取り下げ(模擬DIPSに削除APIが無いため。deleteFlightPlanの通報済み飛行計画に対する取り下げを含む)
  • フロントエンド画面
  • 認証基盤そのものの実装
  • 本番用 CI/CD
  • Kubernetes マニフェスト
  • ページング実装
  • 模擬DIPS以外の外部 API 連携
  • メッセージキュー連携
  • ファイルアップロード
  • バッチ処理

ただし、OpenAPI・DB 定義・業務仕様に記載がある場合は、その内容を優先する。


Claude Code による生成後は、以下を確認すること。

  • コンパイルできること
  • ./gradlew spotlessApply 後に差分がないこと
  • ./gradlew check がパスすること(テスト・Spotless・NullAway・ArchUnit)
  • Handler に業務ロジックがないこと
  • UseCase に HTTP 固有処理がないこと
  • Domain に Spring・MyBatis・Infrastructure・PostGISの import がないこと
  • Domain に ORM アノテーションがないこと
  • @Transactional が UseCase・Domain に使われていないこと
  • TransactionManager.execute でトランザクションが制御されていること
  • ドメイン型を API レスポンスとして返していないこと
  • 生成 DTO が Handler 層からのみ参照されていること
  • OpenAPI の項目と生成 DTO が対応していること
  • DB create SQL と MyBatis Mapper XML が対応していること
  • スマートコンストラクタで不変条件が検証されていること
  • flight_plan_revisionがUPDATEされず、常にINSERTで新リビジョンが追加されていること
  • 一覧・詳細取得でdeleted_at IS NULLのフィルタが適用されていること
  • ドメイン例外に HTTP ステータスが含まれていないこと
  • RFC 9457 Problem Details 形式のエラーレスポンスが返却されていること
  • DataAccessExceptionRepositoryException にラップされていること
  • 機密情報が直書きされていないこと
  • 未使用 import がないこと
  • テストコードが生成されていること
  • ArchUnit テストが生成されていること