Skip to content

feat!: 室員集約の再設計#94

Merged
KinjiKawaguchi merged 22 commits intodevelopfrom
feat/member-usecases
Mar 23, 2026
Merged

feat!: 室員集約の再設計#94
KinjiKawaguchi merged 22 commits intodevelopfrom
feat/member-usecases

Conversation

@KinjiKawaguchi
Copy link
Copy Markdown
Member

@KinjiKawaguchi KinjiKawaguchi commented Mar 22, 2026

Why

室員管理システムのドメインモデル設計書に基づき、Member集約を全レイヤーにわたって再設計する。
現行のMemberは単一クラスで全状態を扱い、Discord連携やDepartment(CS/BI/IA等)を集約内に持つ構造だが、仕様で求められる室員/未確認/元室員の状態遷移やAffiliationベースの所属管理に対応できていない。

BREAKING CHANGE: Member集約のAPI、DBスキーマ、ユースケースI/Oが全面的に変更される。

What

ドメイン層

Member集約の3状態モデル

  • ActiveMember(室員): MemberId, email, 名前, 個人メール, 学籍番号, 所属
  • UnconfirmedMember(未確認): MemberId, email, 名前, 個人メール
  • FormerMember(元室員): MemberId, email, 名前, 個人メール
  • type Member = ActiveMember | UnconfirmedMember | FormerMember

設計上の特徴

  • イミュータブル集約: 全操作が新インスタンスを返す
  • 型レベルでの状態保証: 元室員に学籍番号でアクセスしようとするとコンパイルエラー
  • 二重識別子: MemberId(UUID, Branded Type)は技術的識別子、UniversityEmailはビジネス識別子
  • Affiliationによる区分表現: 学部生/修士/博士の区分はAffiliationの型が内包
  • Recorded<T>: personalEmailの記録有無をnullではなく型で明示(shared kernelに配置)
  • ドメインイベント収集: 12種のイベントを集約内で蓄積しpullDomainEvents()で取り出し

DiscordAccount集約(独立した境界づけられたコンテキスト)

  • 旧MemberからDiscordAccountを分離し、独自の集約ルートとして再設計
  • DiscordId(Branded Type)で型安全に識別
  • nickNameをDiscord固有の属性として保持
  • MemberIdでMember集約を参照

削除・整理

  • DiscordAccount(Member集約内のエンティティ)→ 独立集約に移行
  • Departments(CS/BI/IA等のenum)→ Affiliation(shared kernel)に置き換え
  • Discord関連例外、InvalidDepartmentException を削除

インフラ層

DBスキーマ変更

  • member_status enum追加(active/unconfirmed/former)
  • status列追加、affiliation列追加(JSONB, SerializedAffiliation型)
  • department列削除
  • student_id: text NOT NULL → varchar(8) nullable
  • timestamp: precision(3)削除

リポジトリ

  • DrizzleMemberRepository: 新Member型(ActiveMember/UnconfirmedMember/FormerMember)に対応。SerializedAffiliationでAffiliationを型安全にシリアライズ。
  • DrizzleDiscordAccountRepository: 新規実装

アプリケーション層

ユースケースI/O更新

  • 全InputをBranded Type/ドメイン型に統一(MemberId, DiscordId, UniversityEmail, StudentId, Recorded<Email>等)
  • RegisterMember: Department→Affiliationベース
  • UpdateMember: イミュータブル集約のchangeName/changeStudentId/changePersonalEmail使用
  • Discord系: DiscordAccountRepositoryを使う形に書き直し

例外設計改善

  • InvalidAffiliationOperationException: operation, currentAffiliationType, reasonフィールドで原因を明示
  • DiscordAccountNotFoundException, MemberNotActiveExceptionを新規追加
  • 汎用Errorの使用を排除

Test plan

  • ActiveMember: 登録、復元、除籍、未確認移行、名前変更、学籍番号変更
  • ActiveMember: 内部進学(学部→修士、修士→博士、不正遷移のエラー)
  • ActiveMember: 転学部、転学科(同一学部制約)、転専攻(同一研究科制約)
  • UnconfirmedMember: 確認復帰、除籍、名前変更
  • FormerMember: 再登録、名前変更
  • ライフサイクル全体のイベント蓄積(登録→除籍→再登録、登録→未確認→確認)
  • ドメイン層の型エラー: 0件
  • 全64テスト合格

🤖 Generated with Claude Code


Open with Devin

KinjiKawaguchi and others added 13 commits March 22, 2026 04:46
室員管理の仕様に基づきMember集約をリモデリング:
- 識別子をUUIDから大学メールアドレスに変更
- discriminated unionで室員/未確認/元室員の3状態を型レベルで表現
- 状態に応じた属性の有無を型システムで強制
- イミュータブルな集約(操作は新インスタンスを返す)
- ドメインイベントの定義と集約内での収集機構を追加
- DiscordAccountを集約から除外(外部サービス連携コンテキストへ移行予定)
- Departmentを削除しAffiliation(shared kernel)で所属を表現

BREAKING CHANGE: Member集約のAPIが全面的に変更

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ExternalServiceLink集約: 外部サービスとのアカウント紐付けを独立した境界づけられたコンテキストとして新設
- Faculty集約: 学部・学科のマスターデータ管理(学科の追加・削除、同名重複チェック)
- GraduateSchool集約: 研究科・専攻のマスターデータ管理(課程別管理、専攻の追加・削除)
- 不要な例外を削除(DiscordAccount系、InvalidDepartment)し、マスターデータ用例外を追加

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
組織構造はuniversityStructure.tsの型定義で管理されており、
ランタイムCRUD用のエンティティは不要。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MemberSuspendedをMemberUnconfirmedにリネーム(クラス名・メソッド名との整合性)
- 未使用のExternalServiceUnlinkedイベントを削除(YAGNI)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
対応サービスを型レベルで制限し、不正な値の混入を防ぐ。
現時点ではDiscordのみ対応。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UUIDをDBのPKおよびEvent集約からの参照用の技術的識別子として保持する。
emailはビジネス上の識別子として併存する。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
汎用的なExternalServiceLinkはSpeculative Generalityだったため削除し、
Discord固有のDiscordAccount集約として再設計:
- DiscordId(Branded Type)で型安全に識別
- nickNameをDiscord固有の属性として保持
- MemberIdでMember集約を参照

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Member集約リモデリングのExpand-Contract移行のPhase 1。
既存のdepartment列は維持したまま、新列を非破壊的に追加する。
- status: member_status enum(active/unconfirmed/former)、デフォルト'active'
- affiliation: 所属情報(JSONB、SerializedAffiliation型で型安全に保存)
- DrizzleMemberRepositoryを新Member型(ActiveMember/UnconfirmedMember/FormerMember)に対応
- SerializedAffiliation型をKarteリポジトリと同じパターンで定義

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CS/BI/IA → 情報学部の各学科affiliationに自動マッピング。
ALUMNI → status='former'に変更。
GRADUATE/OTHERSは手動対応としてスキップ。
yearはメールアドレスの入学年から自動計算。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expand-Contract移行のPhase 3:
- department列を削除(Affiliationに置き換え済み)
- student_idをvarchar(8) nullableに変更(未確認/元室員は持たない)
- timestamp precisionを削除(ミリ秒精度は不要)
- リポジトリのTODOコメントを解消

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
personalEmailは任意項目のため、null/undefinedではなくRecorded<Email>で
記録有無を明示する。Recorded型をKarte集約からshared kernelに移動。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Recorded型をshared kernelに追加
- DrizzleMemberRepositoryのpersonalEmailをRecorded<Email>でマッピング
- membersスキーマ: department列削除、student_id varchar(8) nullable化、timestamp precision削除

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@KinjiKawaguchi KinjiKawaguchi force-pushed the feat/member-usecases branch 4 times, most recently from 25b54f9 to 1dfdbcf Compare March 22, 2026 16:25
@KinjiKawaguchi KinjiKawaguchi force-pushed the feat/member-usecases branch 4 times, most recently from 49b7baf to 4de8b3c Compare March 22, 2026 21:45
既存ユースケースのI/Oを新Member型に合わせて書き直し:
- RegisterMember: Department→Affiliationベース、イミュータブル集約のregister()使用
- UpdateMember: イミュータブル集約のchangeName/changeStudentId/changePersonalEmail使用
- Discord系: DiscordAccountRepositoryを使う形に書き直し
- GetMember/GetMemberByEmail/GetMemberList: 型互換のため変更なし

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@KinjiKawaguchi KinjiKawaguchi changed the base branch from feat/member-schema-expand to develop March 22, 2026 21:48
KinjiKawaguchi and others added 5 commits March 23, 2026 06:50
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
member_status enum追加、status列・affiliation列追加、department列削除、
student_idをvarchar(8) nullableに変更、timestamp precision削除。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- InvalidAffiliationOperationExceptionに操作名・現在の所属種別・理由を持たせ、
  エラー原因を特定しやすくした
- 汎用ErrorをApplicationExceptionに置き換え:
  - DiscordAccountNotFoundException(Discord検索失敗時)
  - MemberNotActiveException(室員以外への操作時)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
findById: string → MemberId
findByEmail: string → UniversityEmail

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@KinjiKawaguchi KinjiKawaguchi changed the title feat: Memberユースケースを新ドメインモデルに対応 feat!: Member集約を3状態モデルに再設計し、DiscordAccountを独立集約に分離 Mar 22, 2026
drizzle-kit生成ファイルのフォーマットはdrizzle管轄のため。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@KinjiKawaguchi KinjiKawaguchi marked this pull request as ready for review March 22, 2026 23:14
Copilot AI review requested due to automatic review settings March 22, 2026 23:14
@KinjiKawaguchi KinjiKawaguchi changed the title feat!: Member集約を3状態モデルに再設計し、DiscordAccountを独立集約に分離 feat!: 室員集約の再設計 Mar 22, 2026
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Devin Review found 7 potential issues.

Open in Devin Review

Comment thread src/domain/aggregates/member/Member.ts
Comment thread src/domain/shared/Recorded.ts
Comment thread src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts
Comment thread src/infrastructure/drizzle/DrizzleMemberRepository.ts
Comment thread src/application/usecase/member/UpdateMember.ts Outdated
Comment thread src/domain/base/index.ts
Comment thread src/application/usecase/member/RegisterMember.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Member集約を「室員/未確認/元室員」の3状態モデルへ再設計し、DiscordAccountを独立集約として分離することで、状態遷移と所属(Affiliation)を型レベルで安全に扱えるようにするPRです(ドメイン〜アプリケーション〜インフラまでBREAKING)。

Changes:

  • Member集約を ActiveMember | UnconfirmedMember | FormerMember のUnionに刷新し、ドメインイベント収集(MemberEvent)を追加
  • DiscordAccountを独立集約化し、UseCase/Repositoryを分離して注入経路を更新
  • DBスキーマを member_status enum + affiliation(JSONB) へ移行し、Drizzle永続化を対応

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/domain/aggregates/member/Member.test.ts 3状態の振る舞い・遷移・イベント蓄積のテスト追加
src/infrastructure/drizzle/schema.ts membersにstatus/affiliationを追加しdepartmentを削除
src/infrastructure/drizzle/index.ts DrizzleDiscordAccountRepositoryを公開
src/infrastructure/drizzle/DrizzleMemberRepository.ts 3状態Memberの復元/保存とAffiliationの(逆)シリアライズ
src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts DiscordAccountの永続化リポジトリ新規追加
src/executable/member.ts Member/Discord reposの生成とUseCaseへのDI更新
src/domain/shared/index.ts sharedからRecordedを再export
src/domain/shared/Recorded.ts null回避のためのRecorded型を追加
src/domain/exceptions/DomainExceptions.ts InvalidAffiliationOperationException追加、旧Discord/Department例外削除
src/domain/base/index.ts DomainEventをexport対象に追加
src/domain/base/DomainEvent.ts DomainEvent基底インターフェース追加
src/domain/aggregates/member/index.ts MemberEventをexport、旧DiscordAccount/Departments export削除
src/domain/aggregates/member/MemberRepository.ts findByEmailをUniversityEmail受けに変更、Discord検索/delete削除
src/domain/aggregates/member/MemberEvent.ts Member集約のドメインイベント群を新規追加
src/domain/aggregates/member/Member.ts Memberを3クラス+Unionに再構成し状態遷移/変更操作を実装
src/domain/aggregates/member/DiscordAccount.ts Member内DiscordAccountエンティティを削除
src/domain/aggregates/member/Departments.ts Department ValueObjectを削除
src/domain/aggregates/index.ts discord-account export追加(集約公開範囲更新)
src/domain/aggregates/discord-account/index.ts discord-account集約のexport追加
src/domain/aggregates/discord-account/DiscordId.ts DiscordIdのBranded Type追加
src/domain/aggregates/discord-account/DiscordAccountRepository.ts DiscordAccountRepository追加
src/domain/aggregates/discord-account/DiscordAccount.ts DiscordAccount集約実装(link/reconstruct等)追加
src/application/usecase/member/index.ts memberユースケースexport順更新
src/application/usecase/member/UpdateMember.ts Active限定更新・Immutable更新へ変更
src/application/usecase/member/RegisterMember.ts ActiveMember.registerを用いるI/Oへ変更
src/application/usecase/member/GetMemberByEmail.ts 入力をUniversityEmailへ変更
src/application/usecase/member/GetMemberByDiscordId.ts DiscordRepo→MemberRepoの2段検索へ変更
src/application/usecase/member/ConnectDiscordAccount.ts DiscordAccount集約を保存する形へ変更
src/application/usecase/member/ChangeDiscordNickName.ts DiscordAccountRepositoryベースへ変更
src/application/exceptions/ApplicationExceptions.ts DiscordAccountNotFound/MemberNotActive追加
drizzle/meta/_journal.json Drizzleジャーナル更新
drizzle/meta/0001_snapshot.json Drizzle snapshot追加
drizzle/0001_skinny_cannonball.sql membersのstatus/affiliation等マイグレーション追加
biome.json drizzleディレクトリをformatter対象外に追加

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/application/usecase/member/UpdateMember.ts Outdated
Comment thread src/domain/aggregates/member/Member.ts
Comment thread src/infrastructure/drizzle/DrizzleMemberRepository.ts
Comment thread src/infrastructure/drizzle/DrizzleMemberRepository.ts Outdated
Comment thread src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts
Comment thread src/application/usecase/member/ConnectDiscordAccount.ts
- UpdateMember: 型アサーションをナローイングで置き換え
- pullDomainEvents → getDomainEventsにリネーム(イミュータブル集約ではクリアしない)
- DrizzleMemberRepository: personalEmailのnullチェックを明示的に、deserializeAffiliationにexhaustive check追加
- ConnectDiscordAccount: 既存紐付けの上書き防止チェックを追加
- DrizzleDiscordAccountRepository: save()のonConflictからmemberId更新を除外
- DiscordAccountAlreadyLinkedException を新規追加

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

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

Choose a reason for hiding this comment

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

Devin Review found 4 new potential issues.

Open in Devin Review

Comment thread scripts/migrate-members.ts
Comment thread src/infrastructure/drizzle/DrizzleDiscordAccountRepository.ts
Comment thread src/domain/aggregates/index.ts
Comment thread scripts/migrate-members.ts
@KinjiKawaguchi KinjiKawaguchi merged commit e489194 into develop Mar 23, 2026
3 of 4 checks passed
@KinjiKawaguchi KinjiKawaguchi deleted the feat/member-usecases branch March 23, 2026 10:42
KinjiKawaguchi added a commit that referenced this pull request Mar 23, 2026
## Why

室員管理システムのドメインモデル設計書に基づき、Member集約を全レイヤーにわたって再設計する。

現行のMemberは単一クラスで全状態を扱い、Discord連携やDepartment(CS/BI/IA等)を集約内に持つ構造だが、仕様で求められる室員/未確認/元室員の状態遷移やAffiliationベースの所属管理に対応できていない。

BREAKING CHANGE: Member集約のAPI、DBスキーマ、ユースケースI/Oが全面的に変更される。

## What

### ドメイン層

#### Member集約の3状態モデル
- `ActiveMember`(室員): MemberId, email, 名前, 個人メール, 学籍番号, 所属
- `UnconfirmedMember`(未確認): MemberId, email, 名前, 個人メール
- `FormerMember`(元室員): MemberId, email, 名前, 個人メール
- `type Member = ActiveMember | UnconfirmedMember | FormerMember`

#### 設計上の特徴
- **イミュータブル集約**: 全操作が新インスタンスを返す
- **型レベルでの状態保証**: 元室員に学籍番号でアクセスしようとするとコンパイルエラー
- **二重識別子**: `MemberId`(UUID, Branded
Type)は技術的識別子、`UniversityEmail`はビジネス識別子
- **Affiliationによる区分表現**: 学部生/修士/博士の区分はAffiliationの型が内包
- **Recorded\<T\>**: personalEmailの記録有無をnullではなく型で明示(shared kernelに配置)
- **ドメインイベント収集**: 12種のイベントを集約内で蓄積し`pullDomainEvents()`で取り出し

#### DiscordAccount集約(独立した境界づけられたコンテキスト)
- 旧MemberからDiscordAccountを分離し、独自の集約ルートとして再設計
- `DiscordId`(Branded Type)で型安全に識別
- `nickName`をDiscord固有の属性として保持
- `MemberId`でMember集約を参照

#### 削除・整理
- 旧`DiscordAccount`(Member集約内のエンティティ)→ 独立集約に移行
- `Departments`(CS/BI/IA等のenum)→ Affiliation(shared kernel)に置き換え
- Discord関連例外、InvalidDepartmentException を削除

### インフラ層

#### DBスキーマ変更
- `member_status` enum追加(active/unconfirmed/former)
- `status`列追加、`affiliation`列追加(JSONB, SerializedAffiliation型)
- `department`列削除
- `student_id`: text NOT NULL → varchar(8) nullable
- `timestamp`: precision(3)削除

#### リポジトリ
- `DrizzleMemberRepository`:
新Member型(ActiveMember/UnconfirmedMember/FormerMember)に対応。SerializedAffiliationでAffiliationを型安全にシリアライズ。
- `DrizzleDiscordAccountRepository`: 新規実装

### アプリケーション層

#### ユースケースI/O更新
- 全InputをBranded Type/ドメイン型に統一(MemberId, DiscordId, UniversityEmail,
StudentId, Recorded\<Email\>等)
- RegisterMember: Department→Affiliationベース
- UpdateMember:
イミュータブル集約のchangeName/changeStudentId/changePersonalEmail使用
- Discord系: DiscordAccountRepositoryを使う形に書き直し

#### 例外設計改善
- `InvalidAffiliationOperationException`: operation,
currentAffiliationType, reasonフィールドで原因を明示
- `DiscordAccountNotFoundException`, `MemberNotActiveException`を新規追加
- 汎用Errorの使用を排除

## Test plan
- [x] ActiveMember: 登録、復元、除籍、未確認移行、名前変更、学籍番号変更
- [x] ActiveMember: 内部進学(学部→修士、修士→博士、不正遷移のエラー)
- [x] ActiveMember: 転学部、転学科(同一学部制約)、転専攻(同一研究科制約)
- [x] UnconfirmedMember: 確認復帰、除籍、名前変更
- [x] FormerMember: 再登録、名前変更
- [x] ライフサイクル全体のイベント蓄積(登録→除籍→再登録、登録→未確認→確認)
- [x] ドメイン層の型エラー: 0件
- [x] 全64テスト合格

🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- devin-review-badge-begin -->

---

<a href="https://app.devin.ai/review/su-its/core/pull/94"
target="_blank">
  <picture>
<source media="(prefers-color-scheme: dark)"
srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1">
<img
src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1"
alt="Open with Devin">
  </picture>
</a>
<!-- devin-review-badge-end -->

---------

Co-authored-by: Claude Opus 4.6 (1M context) <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.

2 participants