From 3ed9e912e647e8e6ff469ce3cd78b05e4fe440c5 Mon Sep 17 00:00:00 2001 From: kyy Date: Tue, 9 Jun 2026 11:40:33 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EB=84=8C=ED=8A=B8=20=EB=B9=84?= =?UTF-8?q?=EC=86=8C=EC=86=8D=20=EA=B0=9C=EB=B0=9C=EC=9E=90=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=8B=A0=EC=B2=AD/=EB=B6=80=EC=97=AC=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/dev_handler.go | 35 ++++---- .../src/features/audit/AuditLogsPage.test.tsx | 8 +- devfront/src/features/audit/AuditLogsPage.tsx | 29 ++----- .../src/features/clients/ClientsPage.test.tsx | 53 +++++++++++- devfront/src/features/clients/ClientsPage.tsx | 55 +----------- .../developer-access/developerAccessGate.ts | 6 +- .../developer-grants/DeveloperGrantsPage.tsx | 24 +++--- .../DeveloperRequestPage.test.tsx | 86 ++++++++++++++++++- .../DeveloperRequestPage.tsx | 41 ++------- .../features/overview/GlobalOverviewPage.tsx | 29 ++----- devfront/src/locales/en.toml | 15 ++-- devfront/src/locales/ko.toml | 15 ++-- 12 files changed, 208 insertions(+), 188 deletions(-) diff --git a/backend/internal/handler/dev_handler.go b/backend/internal/handler/dev_handler.go index e5d771e5..f2a29f46 100644 --- a/backend/internal/handler/dev_handler.go +++ b/backend/internal/handler/dev_handler.go @@ -3883,16 +3883,16 @@ func (h *DevHandler) RequestDeveloperAccess(c *fiber.Ctx) error { if req.TenantID == "" && profile.TenantID != nil { req.TenantID = *profile.TenantID } - if req.TenantID == "" { - return errorJSON(c, fiber.StatusBadRequest, "tenantId is required") - } name := strings.TrimSpace(profile.Name) if name == "" { name = strings.TrimSpace(req.Name) } organization := strings.TrimSpace(req.Organization) - if h.TenantSvc != nil { + if organization == "" { + organization = strings.TrimSpace(profile.CompanyCode) + } + if req.TenantID != "" && h.TenantSvc != nil { if tenant, err := h.TenantSvc.GetTenant(c.Context(), req.TenantID); err == nil && tenant != nil && strings.TrimSpace(tenant.Name) != "" { organization = strings.TrimSpace(tenant.Name) } @@ -3927,9 +3927,6 @@ func (h *DevHandler) GetDeveloperRequestStatus(c *fiber.Ctx) error { if tenantID == "" && profile.TenantID != nil { tenantID = *profile.TenantID } - if tenantID == "" { - return errorJSON(c, fiber.StatusBadRequest, "tenantId is required") - } status, err := h.DeveloperSvc.GetRequestStatus(c.Context(), profile.ID, tenantID) if err != nil { @@ -4096,10 +4093,10 @@ func (h *DevHandler) CreateDeveloperGrant(c *fiber.Ctx) error { userID := strings.TrimSpace(reqBody.UserID) tenantID := strings.TrimSpace(reqBody.TenantID) - if userID == "" || tenantID == "" { - return errorJSON(c, fiber.StatusBadRequest, "userId and tenantId are required") + if userID == "" { + return errorJSON(c, fiber.StatusBadRequest, "userId is required") } - if h.KratosAdmin == nil || h.TenantSvc == nil { + if h.KratosAdmin == nil { return errorJSON(c, fiber.StatusServiceUnavailable, "required services are unavailable") } @@ -4107,18 +4104,22 @@ func (h *DevHandler) CreateDeveloperGrant(c *fiber.Ctx) error { if err != nil || identity == nil { return errorJSON(c, fiber.StatusNotFound, "user not found") } - tenant, err := h.TenantSvc.GetTenant(c.Context(), tenantID) - if err != nil || tenant == nil { - return errorJSON(c, fiber.StatusNotFound, "tenant not found") - } name := strings.TrimSpace(extractTraitString(identity.Traits, "name")) if name == "" { name = userID } - organization := strings.TrimSpace(tenant.Name) - if organization == "" { - organization = tenantID + organization := strings.TrimSpace(extractTraitString(identity.Traits, "companyCode")) + if tenantID != "" && h.TenantSvc != nil { + tenant, err := h.TenantSvc.GetTenant(c.Context(), tenantID) + if err != nil || tenant == nil { + return errorJSON(c, fiber.StatusNotFound, "tenant not found") + } + if strings.TrimSpace(tenant.Name) != "" { + organization = strings.TrimSpace(tenant.Name) + } else if organization == "" { + organization = tenantID + } } email := strings.TrimSpace(extractTraitString(identity.Traits, "email")) phone := strings.TrimSpace(extractTraitString(identity.Traits, "phone")) diff --git a/devfront/src/features/audit/AuditLogsPage.test.tsx b/devfront/src/features/audit/AuditLogsPage.test.tsx index 284094d2..cb23e050 100644 --- a/devfront/src/features/audit/AuditLogsPage.test.tsx +++ b/devfront/src/features/audit/AuditLogsPage.test.tsx @@ -174,20 +174,20 @@ describe("AuditLogsPage", () => { expect(navigateMock).toHaveBeenCalledWith("/developer-requests"); }); - it("shows a tenant-required notice when tenant context is missing", async () => { + it("renders the generic access request card when tenant context is missing", async () => { gateState = { hasDeveloperAccess: false, isDeveloperRequestPending: false, - canRequestDeveloperAccess: false, + canRequestDeveloperAccess: true, isLoadingDeveloperAccessGate: false, isTenantContextMissing: true, }; const container = await renderPage(); expect(container.textContent).toContain( - "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.", + "감사 로그는 개발자 권한이 있어야 볼 수 있습니다.", ); - expect(container.textContent).not.toContain("개발자 권한 신청"); + expect(container.textContent).toContain("개발자 권한 신청"); }); it("exports the fetched logs as CSV", async () => { diff --git a/devfront/src/features/audit/AuditLogsPage.tsx b/devfront/src/features/audit/AuditLogsPage.tsx index 2d665ff5..f7d939bb 100644 --- a/devfront/src/features/audit/AuditLogsPage.tsx +++ b/devfront/src/features/audit/AuditLogsPage.tsx @@ -97,7 +97,6 @@ function AuditLogsPage() { isDeveloperRequestPending, canRequestDeveloperAccess, isLoadingDeveloperAccessGate, - isTenantContextMissing, } = useDeveloperAccessGate({ hasAccessToken, profileRole, @@ -143,24 +142,6 @@ function AuditLogsPage() { } if (!hasDeveloperAccess) { - const deniedMessage = isTenantContextMissing - ? t( - "msg.dev.request.tenant_required", - "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.", - ) - : t( - "msg.dev.audit.access_denied", - "감사 로그는 개발자 권한이 있어야 볼 수 있습니다.", - ); - const deniedDetailMessage = isTenantContextMissing - ? t( - "msg.dev.request.tenant_required_detail", - "현재 계정은 테넌트와 연결되어 있지 않아 개발자 권한을 신청할 수 없습니다.", - ) - : t( - "msg.dev.audit.access_denied_detail", - "개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요.", - ); return ( navigate("/developer-requests")} /> diff --git a/devfront/src/features/clients/ClientsPage.test.tsx b/devfront/src/features/clients/ClientsPage.test.tsx index 6a958ff6..e5964584 100644 --- a/devfront/src/features/clients/ClientsPage.test.tsx +++ b/devfront/src/features/clients/ClientsPage.test.tsx @@ -278,7 +278,7 @@ describe("ClientsPage", () => { expect(navigateMock).toHaveBeenCalledWith("/developer-requests"); }); - it("shows a tenant-required notice when tenant context is missing", async () => { + it("allows a user without tenant context to request developer access", async () => { authState = { user: { access_token: "access-token", @@ -297,11 +297,56 @@ describe("ClientsPage", () => { email: "requester@example.com", phone: "010-1234-5678", }); + fetchDeveloperRequestStatusMock.mockResolvedValue({ status: "none" }); const container = await renderPage(); - expect(container.textContent).toContain( - "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.", + expect(container.textContent).toContain("개발자 등록 신청하기"); + + const requestButton = Array.from(container.querySelectorAll("button")).find( + (button) => button.textContent === "개발자 등록 신청하기", ); - expect(fetchDeveloperRequestStatusMock).not.toHaveBeenCalled(); + expect(requestButton).toBeTruthy(); + + await act(async () => { + requestButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(navigateMock).toHaveBeenCalledWith("/developer-requests"); + expect(fetchDeveloperRequestStatusMock).toHaveBeenCalled(); + }); + + it("shows the create app button for a super admin without tenant context", async () => { + authState = { + user: { + access_token: "access-token", + profile: { + role: "super_admin", + companyCode: "HANMAC", + name: "Dev Admin", + email: "dev@example.com", + phone: "010-0000-0000", + }, + }, + }; + fetchMeMock.mockResolvedValue({ + role: "super_admin", + name: "Dev Admin", + email: "dev@example.com", + phone: "010-0000-0000", + }); + + const container = await renderPage(); + expect(container.textContent).toContain("연동 앱 추가"); + + const createButton = Array.from(container.querySelectorAll("button")).find( + (button) => button.textContent === "연동 앱 추가", + ); + expect(createButton).toBeTruthy(); + + await act(async () => { + createButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + expect(navigateMock).toHaveBeenCalledWith("/clients/new"); }); }); diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx index e82b2c5e..6c22bc88 100644 --- a/devfront/src/features/clients/ClientsPage.tsx +++ b/devfront/src/features/clients/ClientsPage.tsx @@ -67,7 +67,6 @@ function ClientsPage() { const role = resolveProfileRole(userProfile); const tenantId = userProfile?.tenant_id as string | undefined; const companyCode = userProfile?.companyCode as string | undefined; - const isTenantContextMissing = !tenantId?.trim(); const { data, @@ -94,8 +93,7 @@ function ClientsPage() { } = useQuery({ queryKey: ["developer-request", tenantId], queryFn: () => fetchDeveloperRequestStatus(tenantId), - enabled: - hasAccessToken && profileRole === "user" && !isTenantContextMissing, + enabled: hasAccessToken && profileRole === "user", }); const { data: tenants } = useQuery({ queryKey: ["myTenants"], @@ -110,9 +108,7 @@ function ClientsPage() { const canCreateClient = createAccessState === "can_create"; const isDeveloperRequestPending = createAccessState === "pending"; const canRequestDeveloperAccess = - createAccessState === "request_required" && - !isLoadingRequest && - !isTenantContextMissing; + createAccessState === "request_required" && !isLoadingRequest; const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); @@ -233,20 +229,7 @@ function ClientsPage() { "OIDC 클라이언트, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다.", )} actions={ - isTenantContextMissing ? ( -
-

- {t( - "msg.dev.clients.create_requires_tenant", - "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.", - )} -

- -
- ) : canCreateClient ? ( + canCreateClient ? ( - )} {!isFilteredOut && canRequestDeveloperAccess && (