콘텐츠로 이동

Member

길드에 속한 Discord 사용자를 관리하는 도메인. 웹 조인(join.umbra.ink/{slug}) 흐름의 주체이며, 복구 시 멤버 단위의 수동 복원 대상이다.

Bounded context

  • Type — Supporting
  • Sibling contexts — Identity, Guild, Recovery (멤버 정보 참조)
  • Location in codebaseengine/member/

Why this domain exists

Discord 길드의 멤버는 수백~수만 명 규모로 확장 가능하며, Umbra 는 두 가지 책임을 가진다.

첫째, 웹 조인 플로우join.umbra.ink/{slug} 링크로 외부에서 유저가 Discord OAuth2 후 길드에 가입하도록 돕는다. 특히 인증 시스템(나이 인증, 외부 가입 승인) 연동 포인트로 활용된다.

둘째, 멤버 상태 저장 — 복구 대상이 되는 멤버 목록과 역할 매핑을 DB 에 유지한다. 단 "자동 멤버 복구" 는 대량 DM 과 rate limit 부담으로 MVP 에서 수동 단위만 지원.

Member 는 Guild 의 정보를 참조하되, 자체적으로 소유한 데이터는 "Umbra 가 본 멤버의 시점별 상태" 다.

Domain model

Member

길드 멤버 레코드.

Member
├─ id                UUID v7      PK
├─ guild_id          UUID         → guild.guilds.id
├─ discord_user_id   TEXT         Discord snowflake
├─ user_id           UUID         → identity.users.id (nullable)
├─ joined_via        TEXT         'web_join' | 'direct' | 'restored'
├─ joined_at         TIMESTAMPTZ
├─ left_at           TIMESTAMPTZ  탈퇴 시
├─ role_ids          TEXT[]       Discord role IDs (현재 상태)
├─ nickname          TEXT         길드 내 닉네임
├─ created_at        TIMESTAMPTZ
└─ updated_at        TIMESTAMPTZ
  • user_id 는 멤버가 Umbra 에 OAuth2 로그인 이력이 있으면 연결 (웹 조인이 아니면 NULL)
  • role_ids 는 Live Sync 로 갱신되는 현재 상태 (Recovery 도메인이 참조)

WebJoinRequest

웹 조인 도중의 진행 상태.

WebJoinRequest
├─ id                UUID v7      PK
├─ guild_id          UUID         → guild.guilds.id
├─ discord_user_id   TEXT
├─ state             TEXT         'pending' | 'completed' | 'failed'
├─ discord_access_token   TEXT   암호화, OAuth2 완료 시 저장
├─ attempted_at      TIMESTAMPTZ
├─ completed_at      TIMESTAMPTZ
└─ failure_reason    TEXT

웹 조인은 guilds.join Discord API 호출이 즉시 성공하지 않을 수 있어(역할 설정, 권한 체크 등) 상태 추적이 필요.

Aggregates

  • Member aggregate — Member + 연결된 WebJoinRequest (있으면)

Invariants

  • Guild + Discord User 유일성UNIQUE(guild_id, discord_user_id) (한 길드에 같은 Discord 유저 멤버 레코드 하나)
  • Soft leave — 탈퇴 시 left_at 설정, row 유지 (이력 추적)
  • WebJoinRequest TTLpending 상태가 1시간 초과하면 failed 로 전환

State machine

Member:

stateDiagram-v2
    [*] --> Active : Joined
    Active --> Left : Left or kicked
    Left --> Active : Re-joined (새 Member row? 동일 row update?)

재가입 처리 정책: 같은 (guild_id, discord_user_id) 로 재가입 시 기존 row 의 left_at 을 NULL 로 초기화하고 joined_at 갱신. 별도 row 생성하지 않음.

WebJoinRequest:

stateDiagram-v2
    [*] --> Pending : User clicks slug link
    Pending --> Completed : guilds.join success
    Pending --> Failed : guilds.join error or TTL
    Completed --> [*]
    Failed --> [*]

Domain events

Published

Event Trigger Payload Subscribers
MemberJoinedViaWebJoin 웹 조인으로 멤버 가입 완료 member_id, guild_id, slug, user_id Audit
MemberRestored 수동 Member 복원 실행 member_id, guild_id, restored_roles Audit
MemberLeft 탈퇴/추방 감지 member_id, guild_id, reason Audit

Consumed

Member 는 Guild 이벤트를 직접 구독하지 않는다. Live Sync 가 상태를 배치로 갱신.

Ports

Supporting 도메인이므로 단순 구조.

Inbound

type Service interface {
    // 웹 조인
    StartWebJoin(ctx, slug, discordAuthCode) (*WebJoinRequest, error)
    CompleteWebJoin(ctx, requestID) error

    // 멤버 조회
    GetMember(ctx, memberID) (*Member, error)
    GetMembersByGuild(ctx, guildID, options) ([]*Member, error)

    // 복원 (수동)
    RestoreMember(ctx, memberID, snapshotMemberData) error

    // Live Sync 에서 호출
    UpsertMember(ctx, guildID, discordUserID, roleIDs, nickname) error
    MarkMemberLeft(ctx, guildID, discordUserID) error
}

Outbound

  • MemberRepository — sqlc 래퍼
  • DiscordClientguilds.join, guilds.members.roles 엔드포인트
  • IdentityReader — Identity 도메인 조회 (user_id 연결)

Adapters

  • Persistenceengine/member/repository.go
  • Discordengine/member/discord_client.go

Permission model

  • 웹 조인 — 누구나 링크로 접근 가능. Discord OAuth2 완료 + Guild 의 웹 조인 활성 여부가 조건
  • Member 복원 — 길드 owner 또는 MANAGE_GUILD 권한자, 대시보드에서 실행
  • Member 목록 조회 — Pro 플랜 이상 (Free 는 대시보드 없음)

Web Join flow

대략의 흐름:

  1. 사용자가 join.umbra.ink/{slug} 접근
  2. umbra-web 이 slug 로 Guild 조회, 웹 조인 활성 확인
  3. "가입하기" 버튼 → Discord OAuth2 authorize 리다이렉트 (scope: identify, guilds.join)
  4. Discord 콜백 → umbra-api 가 auth code 로 access token 교환
  5. WebJoinRequest 생성 (state = pending)
  6. guilds.join API 호출
  7. 성공 시 state = completed, Member row upsert, auto_role 부여
  8. 사용자에게 성공 페이지 + Discord 로 리다이렉트

상세 시퀀스는 flows/web-join.md 참조.

Failure modes

  • Discord OAuth2 scope 부족guilds.join scope 거부 시 WebJoinRequest failed, 사용자에게 재시도 유도
  • guilds.join rate limit — Discord API 가 제한하면 backoff 후 재시도, 최대 3회
  • Bot 이 guild 에 없음guilds.join 실패. Guild Removed 상태 전환 트리거
  • Auto role ID 가 유효하지 않음 — role 부여만 실패, 가입 자체는 유지
  • Live Sync 지연 — 신규 멤버가 DB 에 즉시 반영 안 됨. 복구 시 snapshot 기준이므로 문제 없음

See also

  • data/member-schema.md — DB 스키마
  • domain/guild.md — 멤버가 속한 길드
  • domain/recovery/overview.md — 멤버 복원
  • flows/web-join.md — 웹 조인 전체 흐름
  • adr/0027-message-content-exclusion.md — 메시지는 저장하지 않음