Skip to content

feat: PC相談室カルテ集約のドメインモデルを追加#82

Merged
KinjiKawaguchi merged 26 commits into
developfrom
feat/karte-domain-model
Mar 20, 2026
Merged

feat: PC相談室カルテ集約のドメインモデルを追加#82
KinjiKawaguchi merged 26 commits into
developfrom
feat/karte-domain-model

Conversation

@KinjiKawaguchi

@KinjiKawaguchi KinjiKawaguchi commented Mar 12, 2026

Copy link
Copy Markdown
Member

概要

PC相談室のカルテ(相談記録)を表すKarte集約を追加する。

背景

PC相談室では相談対応の記録をカルテとして管理している。このPRではカルテのドメインモデルを定義し、ドメイン層に集約として追加する。

集約の構造

Karte(集約ルート / immutable)
├── Client          相談者(Student | Teacher | Staff | Other)
├── Consent         同意事項
├── Consultation    相談事
│   ├── categories      相談カテゴリ(複数選択)
│   ├── targetDevice    対象機器
│   └── troubleDetails  トラブル詳細
└── SupportRecord   対応記録
    ├── assignedMemberIds  担当メンバー
    ├── content            対応内容
    ├── resolution         解決ステータス(resolved | unresolved → FollowUp)
    └── workDuration       作業時間

設計の要点

1. immutableな「記録型」集約

カルテは一度書いたら変わらない「記録」としてimmutableに設計した。

  • create() で新規作成、correct() で訂正(記録ミスの修正用)
  • correct()recordedAt を保持し lastUpdatedAt のみ更新する
  • reconstruct() は永続化データからの復元用(バリデーションなし)

2. Recorded<T> — 過去データの欠損を型で表現

過去のカルテには一部フィールドが存在しない。null/undefinedに暗黙の意味を持たせず、{ type: "recorded", value: T } | { type: "notRecorded" } の判別共用体で明示する。

  • create() では全フィールドを生の値で受け取り、内部で recorded() に包む → 新規カルテは常に完全
  • reconstruct() では Recorded<T> をそのまま受け取る → 過去データの欠損を許容

3. Branded型 + ファクトリ関数によるVO実装

WorkDuration, KarteId などの値オブジェクトは、既存の ValueObject<T> 基底クラスを使わずbranded型 + ファクトリ関数で実装した。

理由: ValueObject<T> 基底クラスは equals() が JSON.stringify 比較に依存しており実装として脆い。今後他の集約でも基底クラスから剥がしていく可能性があるため、新規コードでは採用しなかった。

4. KarteRepositoryは find/save のみ

ユースケースが明確なメソッドだけ定義し、CRUD網羅はしない(YAGNI)。検索や一覧のユースケースが出た時点で追加する。

5. Recorded<T> の値にはドメインルールを適用

Recorded<T>recorded であれば、その値はドメインルールに従うべき。

  • assignedMemberIds: Recorded<NonEmptyArray<string>> — 記録されているなら1人以上
  • categories: Recorded<NonEmptyArray<ConsultationCategory>> — 記録されているなら1つ以上

議論したいこと

  • カルテは本当に変更すべきでないか? 現在の設計は「記録型(immutable)」を前提としている。correct()は記録ミスの訂正用として用意したが、そもそもカルテは一度記録したら変更不可とすべきか、それとも訂正を許容すべきか。
  • 変更履歴を残すべきか? 現在のcorrect()は最新の状態だけを保持し、変更前の内容は残らない。訂正を許容する場合、誰がいつ何を変更したかの履歴(監査ログ)が必要ではないか。
  • FollowUp「その他」選択時の自由記述フィールド が必要か(ドメインエキスパートとの議論待ち)

テスト

  • WorkDuration: バリデーション境界値(0分OK、負数/小数/NaN → 例外)
  • Karte: create/correctのドメインロジック(タイムスタンプ、Recorded包装、訂正時のid/recordedAt保持)

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings March 12, 2026 05:22

This comment was marked as resolved.

@KinjiKawaguchi KinjiKawaguchi force-pushed the feat/karte-domain-model branch from 2a06f68 to 914eec6 Compare March 12, 2026 07:31
@KinjiKawaguchi KinjiKawaguchi changed the base branch from develop to feat/shared-kernel-affiliation March 12, 2026 07:31
devin-ai-integration[bot]

This comment was marked as resolved.

@KinjiKawaguchi KinjiKawaguchi left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

相談カテゴリは何処に書かれている?

Comment thread src/domain/aggregates/karte/Client.ts Outdated
Comment thread src/domain/aggregates/karte/Client.ts Outdated
Comment thread src/domain/aggregates/karte/FollowUpDestination.ts Outdated
Comment thread src/domain/aggregates/karte/Karte.ts Outdated
Comment thread src/domain/aggregates/karte/Resolution.ts Outdated
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Comment thread src/domain/aggregates/karte/ConsultationCategory.ts
Comment thread src/domain/aggregates/karte/FollowUpDestination.ts Outdated
Comment thread src/domain/aggregates/karte/FollowUpDestination.ts Outdated
Comment thread src/domain/aggregates/karte/Karte.ts Outdated
Comment thread src/domain/aggregates/karte/Response.ts Outdated
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Base automatically changed from feat/shared-kernel-affiliation to develop March 14, 2026 13:55
devin-ai-integration[bot]

This comment was marked as resolved.

KinjiKawaguchi and others added 10 commits March 14, 2026 22:58
静岡大学の全学部・研究科の組織構造を型レベルで表現し、
ValueObject<T> パターンでランタイムバリデーション付きの
Affiliation クラス群を実装。

- UndergraduateAffiliation / MasterAffiliation / DoctoralAffiliation / ProfessionalAffiliation
- 学部ごとに異なる分類子(学科・課程・専攻・コース・専修)を discriminated union で表現
- StudentId 値オブジェクト(旧形式8桁 / 新形式3桁+英字+4桁)
- 関連するドメイン例外を追加

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
型レベルの判別共用体で制約が保証されるため、
ランタイムバリデーションはValueObjectの責務ではなくシステム境界の責務とする。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
各学部・研究科の型定義に日本語名のJSDocコメントと
公式組織構成ページへの@seeリンクを追加。
バリデーター削除で不要になった例外クラスも削除。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
型レベルで安全性を保証する設計のため不要だったInvalidAffiliationExceptionを削除。
universityStructure.tsのフィールドにユビキタス言語との対応を示すJSDocを追加。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
教育学部のURLは EducationFacultyValue の型JSDocに既にあるため重複を解消。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
同意事項・相談事・対応事という3つのドメイン概念で構成される
深いモデルとしてKarte集約を設計した。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- StaffClientからaffiliationを除去
- OtherClient(学外者等)を追加
- FollowUpDestinationに生協・情報基盤センター・その他を追加
- ResolutionをDUに変更し、未解決時にfollowUpDestinationを必須化
- ResponseからfollowUpDestination optional fieldを除去
- KarteにconsultedAt(相談日時)を追加
- WorkDuration/Resolutionのフォーマットを修正

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
KinjiKawaguchi and others added 11 commits March 14, 2026 22:58
ValueObjectクラスからリテラル型ID+定数一覧に置き換え。
タグは増えることはあるが減ることはない性質のため、ドメイン層に定義する。
不要になったInvalidConsultationCategoryExceptionも削除。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CSVデータにない6カテゴリを追加し、problem_osを廃止。
正式な19カテゴリに統一。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- NonEmptyArray<T>型を導入し、空集合の禁止を型レベルで強制
- Recorded<T>型を導入し、レガシーデータの欠損をnull/undefinedなしで表現
- ゲッターを廃止し全フィールドをpublic readonlyに変更
- correct()の引数をcreate()と同じ生の値に統一し完全性を型で保証
- Response→SupportRecordリネーム、Client型にFacultyClientを追加
- KarteSnapshot/KarteId/MemberIdブランド型を新設
- 実行時バリデーション例外を型による保証に置き換え

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MemberIdはmember集約の概念であり、karte集約内に定義すべきではない。
既存のmember集約がstringで扱っているため合わせる。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
facultyは教員の集合名詞であり個人を指すには不適切。
student/staff/otherと並べたときteacherが最も自然。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
全フィールドがpublic readonlyであるため、永続化層は
Karteを直接読めばよく、Snapshotによる変換層は不要。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0以上の整数という不変条件をファクトリ関数で保証する。
過去データで未記録の場合があるためRecorded<WorkDuration>に変更。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- wifi_succesのタイポをwifi_successに修正
- InvalidWorkDurationExceptionのエラーメッセージを「0以上の整数」に修正
- Karte.create()のDate共有によるミュータブル参照問題を修正

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- FollowUpDestination → FollowUp にリネーム(「見送り」はdestinationではない)
- Recorded<T> を domain/base から karte集約内に移動(現時点でkarte固有)
- SupportRecord.assignedMemberIds を Recorded<NonEmptyArray<string>> に修正
- KarteRepository を findById + save のみに絞る(YAGNI)
- KarteContentProps 抽出と toConsultation/toSupportRecord 関数化(Shotgun Surgery対策)
- Karte.create/correct と WorkDuration のユニットテストを追加

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Recorded<T>のドメインルール整合性原則に基づき、categories型を修正。
DD-001〜DD-008の設計判断をdocs/design-decisions.mdに記録。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
設計判断の記録はPR本文に記載する。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@KinjiKawaguchi KinjiKawaguchi force-pushed the feat/karte-domain-model branch from 7c8c87f to 5e192b1 Compare March 14, 2026 13:58
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

@KikyoNanakusa

Copy link
Copy Markdown
Contributor

カルテは基本的にimmutableで良いです
カルテの内容の変更は基本的にspreadsheet経由なので細かい監査ログは残しようがありません。
Follow-upのその他は過去20年使用実績がほとんどないのでそもそも必要か分かりません。とりあえず過去の記録と整合性を取るために存在させておけば良いです

KinjiKawaguchi and others added 3 commits March 21, 2026 00:17
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dateはミュータブルなため、public readonlyでは破壊的メソッド
(setTime()等)による変更を防げない。privateで保持しgetterで
新しいDateインスタンスを返すことで集約の不変性を保証する。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

Open in Devin Review

Comment thread tests/domain/aggregates/karte/Karte.test.ts
Comment thread src/domain/aggregates/karte/Karte.ts
Comment thread src/domain/aggregates/karte/Karte.ts

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment thread src/domain/aggregates/karte/Karte.ts
@KinjiKawaguchi KinjiKawaguchi merged commit 1a93b48 into develop Mar 20, 2026
4 checks passed
@KinjiKawaguchi KinjiKawaguchi deleted the feat/karte-domain-model branch March 20, 2026 16:52
KinjiKawaguchi added a commit that referenced this pull request Mar 23, 2026
## 概要

PC相談室のカルテ(相談記録)を表すKarte集約を追加する。

## 背景

PC相談室では相談対応の記録をカルテとして管理している。このPRではカルテのドメインモデルを定義し、ドメイン層に集約として追加する。

## 集約の構造

```
Karte(集約ルート / immutable)
├── Client          相談者(Student | Teacher | Staff | Other)
├── Consent         同意事項
├── Consultation    相談事
│   ├── categories      相談カテゴリ(複数選択)
│   ├── targetDevice    対象機器
│   └── troubleDetails  トラブル詳細
└── SupportRecord   対応記録
    ├── assignedMemberIds  担当メンバー
    ├── content            対応内容
    ├── resolution         解決ステータス(resolved | unresolved → FollowUp)
    └── workDuration       作業時間
```

## 設計の要点

### 1. immutableな「記録型」集約

カルテは一度書いたら変わらない「記録」としてimmutableに設計した。
- `create()` で新規作成、`correct()` で訂正(記録ミスの修正用)
- `correct()` は `recordedAt` を保持し `lastUpdatedAt` のみ更新する
- `reconstruct()` は永続化データからの復元用(バリデーションなし)

### 2. Recorded\<T\> — 過去データの欠損を型で表現

過去のカルテには一部フィールドが存在しない。null/undefinedに暗黙の意味を持たせず、`{ type: "recorded",
value: T } | { type: "notRecorded" }` の判別共用体で明示する。

- `create()` では全フィールドを生の値で受け取り、内部で `recorded()` に包む → 新規カルテは常に完全
- `reconstruct()` では `Recorded<T>` をそのまま受け取る → 過去データの欠損を許容

### 3. Branded型 + ファクトリ関数によるVO実装

`WorkDuration`, `KarteId` などの値オブジェクトは、既存の `ValueObject<T>`
基底クラスを使わずbranded型 + ファクトリ関数で実装した。

理由: `ValueObject<T>` 基底クラスは `equals()` が JSON.stringify
比較に依存しており実装として脆い。今後他の集約でも基底クラスから剥がしていく可能性があるため、新規コードでは採用しなかった。

### 4. KarteRepositoryは find/save のみ

ユースケースが明確なメソッドだけ定義し、CRUD網羅はしない(YAGNI)。検索や一覧のユースケースが出た時点で追加する。

### 5. Recorded\<T\> の値にはドメインルールを適用

`Recorded<T>` が `recorded` であれば、その値はドメインルールに従うべき。
- `assignedMemberIds`: `Recorded<NonEmptyArray<string>>` — 記録されているなら1人以上
- `categories`: `Recorded<NonEmptyArray<ConsultationCategory>>` —
記録されているなら1つ以上

## 議論したいこと

- **カルテは本当に変更すべきでないか?**
現在の設計は「記録型(immutable)」を前提としている。correct()は記録ミスの訂正用として用意したが、そもそもカルテは一度記録したら変更不可とすべきか、それとも訂正を許容すべきか。
- **変更履歴を残すべきか?**
現在のcorrect()は最新の状態だけを保持し、変更前の内容は残らない。訂正を許容する場合、誰がいつ何を変更したかの履歴(監査ログ)が必要ではないか。
- **FollowUp「その他」選択時の自由記述フィールド** が必要か(ドメインエキスパートとの議論待ち)

## テスト

- `WorkDuration`: バリデーション境界値(0分OK、負数/小数/NaN → 例外)
- `Karte`:
create/correctのドメインロジック(タイムスタンプ、Recorded包装、訂正時のid/recordedAt保持)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants