package handler import ( "baron-sso-backend/internal/domain" "context" "fmt" "slices" "strings" ) const hanmacFamilyTenantSlug = "hanmac-family" type hanmacEmailScope struct { TenantIDs map[string]bool Slugs map[string]bool IDList []string SlugList []string } type hanmacEmailEvaluation struct { Email string OriginalEmail string SuggestedEmail string Status string Warnings []string Message string Blocking bool LocalPart string } func (h *UserHandler) evaluateHanmacImportEmail(ctx context.Context, item bulkUserItem, scope *hanmacEmailScope, usedLocalParts map[string]bool) hanmacEmailEvaluation { originalEmail := strings.TrimSpace(item.Email) name := strings.TrimSpace(item.Name) evaluation := hanmacEmailEvaluation{ Email: originalEmail, OriginalEmail: originalEmail, Status: "valid", } localPart, domainPart, err := domain.SplitEmailDomain(originalEmail) if err != nil { evaluation.Status = "blockingError" evaluation.Message = "invalid email format" evaluation.Blocking = true return evaluation } base, needsReview, _ := domain.BuildKoreanNameEmailBase(name) if needsReview { evaluation.Warnings = append(evaluation.Warnings, "needsReview") evaluation.Status = "needsReview" } if localPart == "" { if base == "" { evaluation.Status = "blockingError" evaluation.Message = "이름으로 이메일 ID를 제안할 수 없습니다." evaluation.Blocking = true return evaluation } nextLocalPart := nextAvailableHanmacLocalPart(base, usedLocalParts) evaluation.Email = nextLocalPart + "@" + domainPart evaluation.SuggestedEmail = evaluation.Email evaluation.LocalPart = nextLocalPart evaluation.Status = "suggested" evaluation.Warnings = appendUniqueString(evaluation.Warnings, "suggested") return evaluation } evaluation.LocalPart = localPart if usedLocalParts[localPart] { evaluation.Status = "blockingError" evaluation.Message = "한맥가족 내에서 이미 사용 중인 이메일 ID입니다." evaluation.Blocking = true return evaluation } if base != "" && !domain.MatchesSuggestedNameRule(localPart, base) { evaluation.Status = "ruleMismatch" evaluation.Warnings = appendUniqueString(evaluation.Warnings, "ruleMismatch") } if evaluation.Status == "needsReview" && len(evaluation.Warnings) == 0 { evaluation.Warnings = append(evaluation.Warnings, "needsReview") } _ = scope return evaluation } func (h *UserHandler) ensureHanmacCreateEmailAllowed(ctx context.Context, email string, tenantSlug string, tenantID string) error { scope, err := h.resolveHanmacEmailScope(ctx) if err != nil || scope == nil || !scope.ContainsTenant(tenantID, tenantSlug) { return nil } localPart, err := domain.ExtractNormalizedEmailLocalPart(email) if err != nil { return err } usedLocalParts, err := h.loadHanmacLocalParts(ctx, scope) if err != nil { return err } if usedLocalParts[localPart] { return fmt.Errorf("한맥가족 내에서 이미 사용 중인 이메일 ID입니다.") } return nil } func (h *UserHandler) resolveHanmacEmailScope(ctx context.Context) (*hanmacEmailScope, error) { if h.TenantService == nil { return nil, nil } tenants, _, err := h.TenantService.ListTenants(ctx, 10000, 0, "") if err != nil { return nil, err } var rootID string for _, tenant := range tenants { if strings.EqualFold(strings.TrimSpace(tenant.Slug), hanmacFamilyTenantSlug) { rootID = tenant.ID break } } if rootID == "" { return nil, nil } tenantByID := make(map[string]domain.Tenant, len(tenants)) for _, tenant := range tenants { tenantByID[tenant.ID] = tenant } scope := &hanmacEmailScope{ TenantIDs: make(map[string]bool), Slugs: make(map[string]bool), } for _, tenant := range tenants { if isTenantDescendantOf(tenant, rootID, tenantByID) { scope.TenantIDs[tenant.ID] = true scope.Slugs[strings.ToLower(strings.TrimSpace(tenant.Slug))] = true scope.IDList = append(scope.IDList, tenant.ID) scope.SlugList = append(scope.SlugList, tenant.Slug) } } return scope, nil } func (h *UserHandler) loadHanmacLocalParts(ctx context.Context, scope *hanmacEmailScope) (map[string]bool, error) { used := make(map[string]bool) if h.UserRepo == nil || scope == nil { return used, nil } if len(scope.IDList) > 0 { users, err := h.UserRepo.FindByTenantIDs(ctx, scope.IDList) if err != nil { return nil, err } addUserEmailLocalParts(used, users) } if len(scope.SlugList) > 0 { users, err := h.UserRepo.FindByCompanyCodes(ctx, scope.SlugList) if err != nil { return nil, err } addUserEmailLocalParts(used, users) } return used, nil } func (s *hanmacEmailScope) ContainsTenant(tenantID string, slug string) bool { if s == nil { return false } if tenantID != "" && s.TenantIDs[tenantID] { return true } return s.Slugs[strings.ToLower(strings.TrimSpace(slug))] } func isTenantDescendantOf(tenant domain.Tenant, rootID string, tenantByID map[string]domain.Tenant) bool { if tenant.ID == rootID { return true } visited := make(map[string]bool) parentID := "" if tenant.ParentID != nil { parentID = *tenant.ParentID } for parentID != "" { if parentID == rootID { return true } if visited[parentID] { return false } visited[parentID] = true parent, ok := tenantByID[parentID] if !ok || parent.ParentID == nil { return false } parentID = *parent.ParentID } return false } func addUserEmailLocalParts(target map[string]bool, users []domain.User) { for _, user := range users { localPart, err := domain.ExtractNormalizedEmailLocalPart(user.Email) if err == nil && localPart != "" { target[localPart] = true } } } func nextAvailableHanmacLocalPart(base string, usedLocalParts map[string]bool) string { base = strings.ToLower(strings.TrimSpace(base)) if base == "" { return "" } if !usedLocalParts[base] { return base } for index := 1; ; index++ { candidate := fmt.Sprintf("%s%d", base, index) if !usedLocalParts[candidate] { return candidate } } } func appendUniqueString(values []string, value string) []string { if slices.Contains(values, value) { return values } return append(values, value) }