diff --git a/adminfront/src/features/users/UserListPage.tsx b/adminfront/src/features/users/UserListPage.tsx index 8468d18e..3a6f165e 100644 --- a/adminfront/src/features/users/UserListPage.tsx +++ b/adminfront/src/features/users/UserListPage.tsx @@ -53,6 +53,7 @@ import { } from "../../lib/adminApi"; import { t } from "../../lib/i18n"; import { UserBulkUploadModal } from "./components/UserBulkUploadModal"; +import { UserBulkMoveGroupModal } from "./components/UserBulkMoveGroupModal"; type UserSchemaField = { key: string; @@ -552,6 +553,13 @@ function UserListPage() { > {t("ui.common.status.inactive", "비활성화")} + { + query.refetch(); + setSelectedUserIds([]); + }} + />
+ + + + {t("ui.admin.users.bulk.move_title", "사용자 부서 이동")} + + {t("msg.admin.users.bulk.move_description", "선택한 {{count}}명의 사용자를 이동할 테넌트와 부서를 선택하세요.", { count: userIds.length })} + + + +
+
+ + +
+ + {selectedTenantSlug && ( +
+ +
+ + setSearchTerm(e.target.value)} + /> +
+ +
+ + {isGroupsLoading ? ( +
+ ) : ( + filteredGroups.map((group) => ( + + )) + )} +
+
+
+ )} +
+ + + + + +
+ + ); +} diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 368c8f8b..db734038 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -673,9 +673,11 @@ func (h *UserHandler) ExportUsersCSV(c *fiber.Ctx) error { func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error { var req struct { - UserIDs []string `json:"userIds"` - Status *string `json:"status"` - Role *string `json:"role"` + UserIDs []string `json:"userIds"` + Status *string `json:"status"` + Role *string `json:"role"` + CompanyCode *string `json:"companyCode"` + Department *string `json:"department"` } if err := c.BodyParser(&req); err != nil { return errorJSON(c, fiber.StatusBadRequest, "invalid request body") @@ -690,7 +692,13 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error { return errorJSON(c, fiber.StatusUnauthorized, "unauthorized") } - // Build manageable slugs map if tenant_admin + // [New] Pre-fetch tenant cache if companyCode is being changed + type tenantCacheItem struct { + ID string + Schema []interface{} + } + tenantCache := make(map[string]tenantCacheItem) + manageableSlugs := make(map[string]bool) if requester.Role == domain.RoleTenantAdmin { for _, t := range requester.ManageableTenants { @@ -711,12 +719,19 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error { } // Authorization check + userComp := strings.ToLower(extractTraitString(identity.Traits, "companyCode")) if requester.Role == domain.RoleTenantAdmin { - userComp := strings.ToLower(extractTraitString(identity.Traits, "companyCode")) if !manageableSlugs[userComp] { results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: user belongs to another tenant"}) continue } + // If changing companyCode, must be to a manageable one + if req.CompanyCode != nil { + if !manageableSlugs[strings.ToLower(*req.CompanyCode)] { + results = append(results, map[string]any{"id": id, "success": false, "message": "forbidden: target tenant not manageable"}) + continue + } + } } // Prepare updates @@ -724,6 +739,24 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error { if req.Role != nil { traits["role"] = *req.Role } + if req.CompanyCode != nil { + traits["companyCode"] = *req.CompanyCode + + // Resolve and update tenant_id in traits if changed + if tItem, exists := tenantCache[*req.CompanyCode]; exists { + traits["tenant_id"] = tItem.ID + } else if h.TenantService != nil { + tenant, err := h.TenantService.GetTenantBySlug(c.Context(), *req.CompanyCode) + if err == nil && tenant != nil { + tItem.ID = tenant.ID + tenantCache[*req.CompanyCode] = tItem + traits["tenant_id"] = tenant.ID + } + } + } + if req.Department != nil { + traits["department"] = *req.Department + } state := identity.State if req.Status != nil { @@ -743,12 +776,10 @@ func (h *UserHandler) BulkUpdateUsers(c *fiber.Ctx) error { // Sync to local DB if h.UserRepo != nil { localUser := h.mapToLocalUser(*identity) - if req.Role != nil { - localUser.Role = *req.Role - } - if req.Status != nil { - localUser.Status = *req.Status - } + if req.Role != nil { localUser.Role = *req.Role } + if req.Status != nil { localUser.Status = *req.Status } + if req.CompanyCode != nil { localUser.CompanyCode = *req.CompanyCode } + if req.Department != nil { localUser.Department = *req.Department } _ = h.UserRepo.Update(c.Context(), localUser) }