feat: PC相談室カルテ集約のドメインモデルを追加#82
Merged
Merged
Conversation
2a06f68 to
914eec6
Compare
KinjiKawaguchi
commented
Mar 12, 2026
KinjiKawaguchi
left a comment
Member
Author
There was a problem hiding this comment.
相談カテゴリは何処に書かれている?
KinjiKawaguchi
commented
Mar 13, 2026
静岡大学の全学部・研究科の組織構造を型レベルで表現し、 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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
同意事項・相談事・対応事という3つのドメイン概念で構成される 深いモデルとしてKarte集約を設計した。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
7c8c87f to
5e192b1
Compare
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This was referenced Mar 15, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
|
カルテは基本的にimmutableで良いです |
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概要
PC相談室のカルテ(相談記録)を表すKarte集約を追加する。
背景
PC相談室では相談対応の記録をカルテとして管理している。このPRではカルテのドメインモデルを定義し、ドメイン層に集約として追加する。
集約の構造
設計の要点
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つ以上議論したいこと
テスト
WorkDuration: バリデーション境界値(0分OK、負数/小数/NaN → 例外)Karte: create/correctのドメインロジック(タイムスタンプ、Recorded包装、訂正時のid/recordedAt保持)🤖 Generated with Claude Code