User Management 도메인 비즈니스 규칙
Event Storming 기반으로 도출된 User Management Domain의 비즈니스 규칙을 정의합니다.
1. 불변식 (Invariants)
1.1 App-User
사이트 ID 필수성
- 규칙: siteId는 필수 입력 값이며 빈 문자열일 수 없음
- 위반 시: InvalidSiteIdProvided 이벤트 발행, BAD_REQUEST 에러 반환
운영자 사이트 접근 제약
- 규칙: operators.site.admin 또는 operators.site.member 그룹의 운영자는 본인 사이트의 app-user만 조회 가능
- 위반 시: AccessDenied 이벤트 발행, BAD_REQUEST 에러 반환
조회 결과 완전성
- 규칙: app-user 목록 조회 결과는 반드시 totalCount와 count를 포함해야 함
- 위반 시: 시스템 오류
1.2 AppUser Report
리포트 생성 데이터 일관성
- 규칙: 리포트 생성 시 사용자 주기 정보, 수면 일기, 목표 로그, 설문 응답은 동일한 날짜 범위(startDate~endDate)로 조회되어야 함
- 위반 시: 데이터 불일치
일관성 퍼센티지 상한
- 규칙: LOT/AET 일관성 퍼센티지는 최대 100을 초과할 수 없음
- 위반 시: 자동으로 100으로 설정
총 사용 일수 고정값
- 규칙: totalUsageDays는 항상 app-user의 plan에 정의된 값으로 사용함
- 위반 시: 없음 (하드코딩된 값)
1.3 ExternalAccess
토큰-사용자 주기 일치
- 규칙: 외부 토큰의 "sub" 클레임은 요청한 userCycleId와 일치해야 함
- 위반 시: SubjectMismatch 이벤트 발행, BAD_REQUEST 에러 반환
2. 유효성 규칙 (Validation Rules)
2.1 App-User
페이징 파라미터 검증
- 규칙: limit이 0이면 기본값 10으로 설정, page가 0이면 기본값 1로 설정
- 위반 시: 자동 보정
정렬 파라미터 검증
- 규칙: sortBy가 빈 문자열이면 "createdAt"로, sortOrder는 "desc"로 기본 설정
- 위반 시: 자동 보정
사이트 ID 검증
- 규칙: siteId가 빈 문자열인지 검증 필수
- 위반 시: BAD_REQUEST 에러 반환
2.2 AppUserReport
사용자 주기 존재 검증
- 규칙: userCycleId로 사용자 주기 정보 조회 가능 여부 검증
- 위반 시: 조회 실패 에러 반환
날짜 범위 유효성
- 규칙: startDate와 endDate가 유효한 날짜 범위인지 검증
- 위반 시: 데이터 조회 실패
2.3 ExternalAccess
토큰 존재 검증
- 규칙: 토큰이 빈 문자열인지 검증 필수
- 위반 시: TokenValidationFailed 이벤트 발행, BAD_REQUEST 에러 반환
토큰 타입 검증
- 규칙: 토큰 타입이 TokenTypeMdAccess인지 검증 필수
- 위반 시: TokenValidationFailed 이벤트 발행, 토큰 검증 실패
Subject 클레임 타입 검증
- 규칙: "sub" 클레임이 float64 타입으로 변환 가능한지 검증
- 위반 시: SubjectMismatch 이벤트 발행, BAD_REQUEST 에러 반환
Subject 클레임 존재 검증
- 규칙: 토큰 클레임에 "sub"가 존재하는지 검증 필수
- 위반 시: SubjectMismatch 이벤트 발행, BAD_REQUEST 에러 반환
3. 상태 전이 규칙 (State Transition Rules)
3.1 AppUser 조회 상태 전이
[조회 요청]
├─→ (operators.medi-dashboard.admin 그룹)
│ [권한 검증 생략]
│ ↓
│ [DB 조회]
│
└─→ (operators.site.admin/member 그룹)
[운영자 정보 조회]
├─→ (사이트 일치)
│ [권한 검증 성공]
│ ↓
│ [DB 조회]
│
└─→ (사이트 불일치)
[접근 거부]
전환 조건:
- 조회 요청 → 권한 검증: AppUsersQueryRequested 이벤트 발행
- 권한 검증 성공 → DB 조회: AccessVerified 이벤트 발행, AppUsersQueried 이벤트 발행
- 권한 검증 실패 → 접근 거부: AccessDenied 이벤트 발행
3.2 AppUserReport 생성 상태 전이
[리포트 요청]
↓
[사용자 주기 조회]
↓
[현재 일차 계산]
↓
[데이터 조회] (수면 일기, 목표, 설문)
↓
[통계 계산] (성공 횟수, 분모)
↓
[일관성 계산] (LOT, AET)
↓
[요약 생성]
↓
[리포트 생성 완료]
전환 조건:
- 리포트 요청 → 사용자 주기 조회: AppUserReportRequested 이벤트 발행
- 사용자 주기 조회 → 데이터 조회: UserCycleRetrieved 이벤트 발행
- 통계 계산 → 일관성 계산: SuccessCountCalculated 이벤트 발행
- 요약 생성 → 완료: ReportSummaryGenerated 이벤트 발행
3.3 ExternalAccess 인증 상태 전이
[외부 토큰 수신]
↓
[토큰 검증]
├─→ (검증 성공)
│ [클레임 추출]
│ ├─→ ("sub" 존재 및 일치)
│ │ [Subject 검증 성공]
│ │ ↓
│ │ [리포트 조회]
│ │
│ └─→ ("sub" 없음 또는 불일치)
│ [Subject 불일치]
│
└─→ (검증 실패)
[인증 거부]
전환 조건:
- 토큰 수신 → 토큰 검증: ExternalTokenReceived 이벤트 발행
- 토큰 검증 성공 → 클레임 추출: TokenValidated 이벤트 발행
- Subject 검증 성공 → 리포트 조회: SubjectVerified 이벤트 발행
- Subject 불일치 → 거부: SubjectMismatch 이벤트 발행
4. 권한 규칙 (Authorization Rules)
4.1 app-user 목록 조회 권한
| API | operators.medi-dashboard.admin | operators.site.admin | operators.site.member | 외부 시스템 | 비고 |
|---|---|---|---|---|---|
| GetAppUsers | ✓ (모든 사이트) | ✓ (본인 사이트만) | ✓ (본인 사이트만) | ✗ | 사이트 기반 접근 제어 |
4.2 app-user 리포트 조회 권한
| API | operators.medi-dashboard.admin | operators.site.admin | operators.site.member | 외부 시스템 | 비고 |
|---|---|---|---|---|---|
| GetAppUserReport | ✓ | ✓ | ✓ | ✗ | 인증된 운영자만 |
4.3 외부 토큰 기반 리포트 조회 권한
| API | operators.medi-dashboard.admin | operators.site.admin | operators.site.member | 외부 시스템 | 비고 |
|---|---|---|---|---|---|
| GetAppUserReportWithExternalAccessToken | ✗ | ✗ | ✗ | ✓ | 유효한 TokenTypeMdAccess 토큰 필요 |
5. 계산/파생 규칙 (Derivation Rules)
5.1 app-user 목록 조회 계산
그룹 정보 표시 플래그
- 파생: 운영자가 Welt 사이트 소속인 경우 그룹 정보 표시 플래그 설정
- 계산식:
IF operationUser.SiteId == WeltSiteId THEN
showGroup := true
ELSE
showGroup := false
5.2 리포트 일차 계산
현재 일차 계산
-
파생: 사용자 주기 시작일부터 현재(또는 종료일)까지의 일차 자동 계산
-
계산식:
IF NOW() > userCycle.EndAt THEN
calculateFrom := userCycle.EndAt
ELSE
calculateFrom := NOW()
day := GetDaysDiff(userCycle.StartAt, calculateFrom) + 1
5.3 리포트 통계 계산
성공 횟수 계산
- 파생: LOT/AET 목표 성공 기록을 집계하여 각각의 성공 횟수 계산
- 계산 방식:
successCount.Lot := COUNT(userGoalSuccesses WHERE LotSuccess = true)
successCount.Aet := COUNT(userGoalSuccesses WHERE AetSuccess = true)
분모용 수면 일기 개수 계산
- 파생: 첫 번째 rTIB 알고리즘 목표 이후의 수면 일기 개수 계산
- 계산 방식:
sleepDiaryCountForDenominator := calculateSleepDiaryCountForDenominator(
firstRtibAlgorithmUserSleepGoalLog,
goals,
diaries
)
LOT 일관성 퍼센티지 계산
- 파생: LOT 성공 횟수를 분모로 나눈 백분율 계산
- 계산식:
IF sleepDiaryCountForDenominator > 0 THEN
IF successCount.Lot <= sleepDiaryCountForDenominator THEN
lotConsistencyPercentage := ROUND(successCount.Lot / sleepDiaryCountForDenominator * 100)
ELSE
lotConsistencyPercentage := 100
ELSE
lotConsistencyPercentage := 0
AET 일관성 퍼센티지 계산
- 파생: AET 성공 횟수를 분모로 나눈 백분율 계산
- 계산식:
IF sleepDiaryCountForDenominator > 0 THEN
IF successCount.Aet <= sleepDiaryCountForDenominator THEN
aetConsistencyPercentage := ROUND(successCount.Aet / sleepDiaryCountForDenominator * 100)
ELSE
aetConsistencyPercentage := 100
ELSE
aetConsistencyPercentage := 0
수면 기록 퍼센티지 계산
- 파생: 수면 일기 작성 비율 계산
- 계산식:
sleepRecordPercentage := ROUND(totalCount / day * 100)
6. 제약 사항 (Constraints)
6.1 데이터 조회 제약
페이징 기본값
- 제약: limit 기본값 10, page 기본값 1
- 적용: 파라미터가 0인 경우 자동 적용
정렬 기본값
- 제약: sortBy 기본값 "createdAt", sortOrder 기본값 "desc"
- 적용: 파라미터가 빈 문자열인 경우 자동 적용
lastLogin 정렬 특별 처리
- 제약: sortBy가 "lastLogin"인 경우 별도의 정렬 로직(searchUsersByLastLogin) 사용 필수
6.2 리포트 생성 제약
총 사용 일수 고정
- 제약: totalUsageDays는 항상 app-user의 plan에 정의된 값으로 사용함
- 이유: 시스템 정책에 따른 고정값
날짜 범위 일관성
- 제약: 수면 일기, 목표 로그 조회는 동일한 startDate~endDate 범위 사용 필수
- 이유: 데이터 일관성 보장
6.3 보안 제약
전화번호 암호화 조회
- 제약: 운영자 정보 조회 시 전화번호는 pgp_sym_decrypt로 복호화하여 조회 필수
- 적용: GetOperationUserById 실행 시
토큰 타입 제한
- 제약: 외부 액세스 토큰은 TokenTypeMdAccess 타입만 허용
- 이유: 보안 정책에 따른 접근 제어
Subject 클레임 타입 제한
- 제약: "sub" 클레임은 float64 타입으로 파싱 가능해야 함
- 이유: userCycleId(int32)와 비교를 위한 타입 일관성
7. 정책 상세 (Policy Details from Event Storming)
7.1 app-user 목록 조회 정책
| When | Then |
|---|---|
| AppUsersQueryRequested (operators.site.admin/member) | OperationUserRetrieved 발행, AccessVerified 발행 |
| AppUsersQueryRequested (operators.medi-dashboard.admin) | AppUsersQueried 발행 (권한 검증 생략) |
| AccessDenied | InvalidSiteIdProvided 발행, BAD_REQUEST 에러 반환 |
| AppUsersQueried | LastLoginTimeRetrieved 발행 (각 app-user별) |
| LastLoginTimeRetrieved | AppUsersRetrieved 발행 |
7.2 app-user 리포트 생성 정책
| When | Then |
|---|---|
| AppUserReportRequested | UserCycleRetrieved 발행, CurrentDayCalculated 발행 |
| UserCycleRetrieved | SleepDiariesRetrieved 발행, RtibAlgorithmGoalRetrieved 발행, SleepGoalsRetrieved 발행, QuestionnairesRetrieved 발행 |
| GoalSuccessRecordsRetrieved | SuccessCountCalculated 발행 |
| SuccessCountCalculated | SleepDiaryCountCalculated 발행 |
| SleepDiaryCountCalculated | LotConsistencyCalculated 발행, AetConsistencyCalculated 발행 |
| AetConsistencyCalculated | ReportSummaryGenerated 발행 |
| ReportSummaryGenerated | AppUserReportGenerated 발행 |
7.3 외부 토큰 인증 정책
| When | Then |
|---|---|
| ExternalTokenReceived (빈 문자열) | TokenValidationFailed 발행, BAD_REQUEST 에러 반환 |
| TokenValidationRequested | TokenValidated 발행 또는 TokenValidationFailed 발행 |
| TokenValidated | TokenClaimsExtracted 발행 |
| TokenClaimsExtracted ("sub" 없음) | SubjectMismatch 발행, BAD_REQUEST 에러 반환 |
| TokenClaimsExtracted ("sub" 불일치) | SubjectMismatch 발행, BAD_REQUEST 에러 반환 |
| SubjectVerified | AppUserReportRequested 발행 |
변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-12-31 | dalia@weltcorp.com | 문서 최초 작성 - GetAppUsers, GetAppUserReport, GetAppUserReportWithExternalAccessToken 기반 |