package repository import ( "baron-sso-backend/internal/domain" "context" "time" "gorm.io/gorm" ) type DataIntegrityChecker interface { CheckDataIntegrity(ctx context.Context) (domain.DataIntegrityReport, error) } type dataIntegrityChecker struct { db *gorm.DB } func NewDataIntegrityChecker(db *gorm.DB) DataIntegrityChecker { return &dataIntegrityChecker{db: db} } func (c *dataIntegrityChecker) CheckDataIntegrity(ctx context.Context) (domain.DataIntegrityReport, error) { return CheckDataIntegrity(ctx, c.db) } func CheckDataIntegrity(ctx context.Context, db *gorm.DB) (domain.DataIntegrityReport, error) { tenantChecks := []domain.DataIntegrityCheck{ { Key: "duplicate_tenant_slugs", Label: "중복 테넌트 slug", Description: "삭제되지 않은 tenant의 slug를 대소문자 무시 기준으로 검사합니다.", Severity: "error", Count: 0, }, { Key: "orphan_tenant_parents", Label: "유령 상위 테넌트 참조", Description: "tenant.parent_id가 없거나 삭제된 tenant를 참조하는지 검사합니다.", Severity: "error", Count: 0, }, } userChecks := []domain.DataIntegrityCheck{ { Key: "orphan_user_tenant_memberships", Label: "유령 테넌트 사용자 소속", Description: "users.tenant_id가 없거나 삭제된 tenant를 참조하는지 검사합니다.", Severity: "error", Count: 0, }, { Key: "orphan_user_login_id_tenants", Label: "유령 테넌트 로그인 ID", Description: "user_login_ids.tenant_id가 없거나 삭제된 tenant를 참조하는지 검사합니다.", Severity: "error", Count: 0, }, { Key: "orphan_user_login_id_users", Label: "유령 사용자 로그인 ID", Description: "user_login_ids.user_id가 없거나 삭제된 user를 참조하는지 검사합니다.", Severity: "error", Count: 0, }, } counts := []struct { target *int64 query string }{ { target: &tenantChecks[0].Count, query: ` SELECT COUNT(*) FROM ( SELECT LOWER(TRIM(slug)) AS normalized_slug FROM tenants WHERE deleted_at IS NULL AND status <> 'deleted' AND TRIM(slug) <> '' GROUP BY LOWER(TRIM(slug)) HAVING COUNT(*) > 1 ) AS duplicate_slugs `, }, { target: &tenantChecks[1].Count, query: ` SELECT COUNT(*) FROM tenants AS child WHERE child.deleted_at IS NULL AND child.parent_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM tenants AS parent WHERE parent.id = child.parent_id AND parent.deleted_at IS NULL ) `, }, { target: &userChecks[0].Count, query: ` SELECT COUNT(*) FROM users AS u WHERE u.deleted_at IS NULL AND u.tenant_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM tenants AS t WHERE t.id = u.tenant_id AND t.deleted_at IS NULL ) `, }, { target: &userChecks[1].Count, query: ` SELECT COUNT(*) FROM user_login_ids AS uli WHERE NOT EXISTS ( SELECT 1 FROM tenants AS t WHERE t.id = uli.tenant_id AND t.deleted_at IS NULL ) `, }, { target: &userChecks[2].Count, query: ` SELECT COUNT(*) FROM user_login_ids AS uli WHERE NOT EXISTS ( SELECT 1 FROM users AS u WHERE u.id = uli.user_id AND u.deleted_at IS NULL ) `, }, } for _, item := range counts { if err := db.WithContext(ctx).Raw(item.query).Scan(item.target).Error; err != nil { return domain.DataIntegrityReport{}, err } } tenantChecks = applyIntegrityStatuses(tenantChecks) userChecks = applyIntegrityStatuses(userChecks) sections := []domain.DataIntegritySection{ { Key: "tenant_integrity", Label: "테넌트 정합성", Status: summarizeIntegrityStatus(tenantChecks), Checks: tenantChecks, }, { Key: "user_integrity", Label: "사용자 정합성", Status: summarizeIntegrityStatus(userChecks), Checks: userChecks, }, } summary := domain.DataIntegritySummary{} for _, section := range sections { for _, check := range section.Checks { summary.TotalChecks++ switch check.Status { case domain.DataIntegrityStatusFail: summary.Failures += check.Count case domain.DataIntegrityStatusWarning: summary.Warnings++ default: summary.Passed++ } } } return domain.DataIntegrityReport{ Status: summarizeSectionStatus(sections), CheckedAt: time.Now().UTC(), Summary: summary, Sections: sections, }, nil } func applyIntegrityStatuses(checks []domain.DataIntegrityCheck) []domain.DataIntegrityCheck { for i := range checks { if checks[i].Count > 0 { checks[i].Status = domain.DataIntegrityStatusFail } else { checks[i].Status = domain.DataIntegrityStatusPass } } return checks } func summarizeIntegrityStatus(checks []domain.DataIntegrityCheck) domain.DataIntegrityStatus { status := domain.DataIntegrityStatusPass for _, check := range checks { if check.Status == domain.DataIntegrityStatusFail { return domain.DataIntegrityStatusFail } if check.Status == domain.DataIntegrityStatusWarning { status = domain.DataIntegrityStatusWarning } } return status } func summarizeSectionStatus(sections []domain.DataIntegritySection) domain.DataIntegrityStatus { status := domain.DataIntegrityStatusPass for _, section := range sections { if section.Status == domain.DataIntegrityStatusFail { return domain.DataIntegrityStatusFail } if section.Status == domain.DataIntegrityStatusWarning { status = domain.DataIntegrityStatusWarning } } return status }