forked from baron/baron-sso
테넌트 비소속 개발자 권한 신청/부여 가능
This commit is contained in:
@@ -3883,16 +3883,16 @@ func (h *DevHandler) RequestDeveloperAccess(c *fiber.Ctx) error {
|
|||||||
if req.TenantID == "" && profile.TenantID != nil {
|
if req.TenantID == "" && profile.TenantID != nil {
|
||||||
req.TenantID = *profile.TenantID
|
req.TenantID = *profile.TenantID
|
||||||
}
|
}
|
||||||
if req.TenantID == "" {
|
|
||||||
return errorJSON(c, fiber.StatusBadRequest, "tenantId is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
name := strings.TrimSpace(profile.Name)
|
name := strings.TrimSpace(profile.Name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = strings.TrimSpace(req.Name)
|
name = strings.TrimSpace(req.Name)
|
||||||
}
|
}
|
||||||
organization := strings.TrimSpace(req.Organization)
|
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) != "" {
|
if tenant, err := h.TenantSvc.GetTenant(c.Context(), req.TenantID); err == nil && tenant != nil && strings.TrimSpace(tenant.Name) != "" {
|
||||||
organization = 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 {
|
if tenantID == "" && profile.TenantID != nil {
|
||||||
tenantID = *profile.TenantID
|
tenantID = *profile.TenantID
|
||||||
}
|
}
|
||||||
if tenantID == "" {
|
|
||||||
return errorJSON(c, fiber.StatusBadRequest, "tenantId is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err := h.DeveloperSvc.GetRequestStatus(c.Context(), profile.ID, tenantID)
|
status, err := h.DeveloperSvc.GetRequestStatus(c.Context(), profile.ID, tenantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4096,10 +4093,10 @@ func (h *DevHandler) CreateDeveloperGrant(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
userID := strings.TrimSpace(reqBody.UserID)
|
userID := strings.TrimSpace(reqBody.UserID)
|
||||||
tenantID := strings.TrimSpace(reqBody.TenantID)
|
tenantID := strings.TrimSpace(reqBody.TenantID)
|
||||||
if userID == "" || tenantID == "" {
|
if userID == "" {
|
||||||
return errorJSON(c, fiber.StatusBadRequest, "userId and tenantId are required")
|
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")
|
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 {
|
if err != nil || identity == nil {
|
||||||
return errorJSON(c, fiber.StatusNotFound, "user not found")
|
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"))
|
name := strings.TrimSpace(extractTraitString(identity.Traits, "name"))
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = userID
|
name = userID
|
||||||
}
|
}
|
||||||
organization := strings.TrimSpace(tenant.Name)
|
organization := strings.TrimSpace(extractTraitString(identity.Traits, "companyCode"))
|
||||||
if organization == "" {
|
if tenantID != "" && h.TenantSvc != nil {
|
||||||
organization = tenantID
|
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"))
|
email := strings.TrimSpace(extractTraitString(identity.Traits, "email"))
|
||||||
phone := strings.TrimSpace(extractTraitString(identity.Traits, "phone"))
|
phone := strings.TrimSpace(extractTraitString(identity.Traits, "phone"))
|
||||||
|
|||||||
@@ -174,20 +174,20 @@ describe("AuditLogsPage", () => {
|
|||||||
expect(navigateMock).toHaveBeenCalledWith("/developer-requests");
|
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 = {
|
gateState = {
|
||||||
hasDeveloperAccess: false,
|
hasDeveloperAccess: false,
|
||||||
isDeveloperRequestPending: false,
|
isDeveloperRequestPending: false,
|
||||||
canRequestDeveloperAccess: false,
|
canRequestDeveloperAccess: true,
|
||||||
isLoadingDeveloperAccessGate: false,
|
isLoadingDeveloperAccessGate: false,
|
||||||
isTenantContextMissing: true,
|
isTenantContextMissing: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const container = await renderPage();
|
const container = await renderPage();
|
||||||
expect(container.textContent).toContain(
|
expect(container.textContent).toContain(
|
||||||
"개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.",
|
"감사 로그는 개발자 권한이 있어야 볼 수 있습니다.",
|
||||||
);
|
);
|
||||||
expect(container.textContent).not.toContain("개발자 권한 신청");
|
expect(container.textContent).toContain("개발자 권한 신청");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exports the fetched logs as CSV", async () => {
|
it("exports the fetched logs as CSV", async () => {
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ function AuditLogsPage() {
|
|||||||
isDeveloperRequestPending,
|
isDeveloperRequestPending,
|
||||||
canRequestDeveloperAccess,
|
canRequestDeveloperAccess,
|
||||||
isLoadingDeveloperAccessGate,
|
isLoadingDeveloperAccessGate,
|
||||||
isTenantContextMissing,
|
|
||||||
} = useDeveloperAccessGate({
|
} = useDeveloperAccessGate({
|
||||||
hasAccessToken,
|
hasAccessToken,
|
||||||
profileRole,
|
profileRole,
|
||||||
@@ -143,24 +142,6 @@ function AuditLogsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasDeveloperAccess) {
|
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 (
|
return (
|
||||||
<DeveloperAccessRequestCard
|
<DeveloperAccessRequestCard
|
||||||
title={t("ui.common.audit.title", "Audit Logs")}
|
title={t("ui.common.audit.title", "Audit Logs")}
|
||||||
@@ -170,12 +151,18 @@ function AuditLogsPage() {
|
|||||||
"msg.dev.dashboard.access_pending",
|
"msg.dev.dashboard.access_pending",
|
||||||
"개발자 권한 신청을 검토 중입니다.",
|
"개발자 권한 신청을 검토 중입니다.",
|
||||||
)}
|
)}
|
||||||
deniedMessage={deniedMessage}
|
deniedMessage={t(
|
||||||
|
"msg.dev.audit.access_denied",
|
||||||
|
"감사 로그는 개발자 권한이 있어야 볼 수 있습니다.",
|
||||||
|
)}
|
||||||
pendingDetailMessage={t(
|
pendingDetailMessage={t(
|
||||||
"msg.dev.dashboard.access_pending_detail",
|
"msg.dev.dashboard.access_pending_detail",
|
||||||
"super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다.",
|
"super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다.",
|
||||||
)}
|
)}
|
||||||
deniedDetailMessage={deniedDetailMessage}
|
deniedDetailMessage={t(
|
||||||
|
"msg.dev.audit.access_denied_detail",
|
||||||
|
"개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요.",
|
||||||
|
)}
|
||||||
actionLabel={t("ui.dev.nav.developer_request", "개발자 권한 신청")}
|
actionLabel={t("ui.dev.nav.developer_request", "개발자 권한 신청")}
|
||||||
onAction={() => navigate("/developer-requests")}
|
onAction={() => navigate("/developer-requests")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ describe("ClientsPage", () => {
|
|||||||
expect(navigateMock).toHaveBeenCalledWith("/developer-requests");
|
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 = {
|
authState = {
|
||||||
user: {
|
user: {
|
||||||
access_token: "access-token",
|
access_token: "access-token",
|
||||||
@@ -297,11 +297,56 @@ describe("ClientsPage", () => {
|
|||||||
email: "requester@example.com",
|
email: "requester@example.com",
|
||||||
phone: "010-1234-5678",
|
phone: "010-1234-5678",
|
||||||
});
|
});
|
||||||
|
fetchDeveloperRequestStatusMock.mockResolvedValue({ status: "none" });
|
||||||
|
|
||||||
const container = await renderPage();
|
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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ function ClientsPage() {
|
|||||||
const role = resolveProfileRole(userProfile);
|
const role = resolveProfileRole(userProfile);
|
||||||
const tenantId = userProfile?.tenant_id as string | undefined;
|
const tenantId = userProfile?.tenant_id as string | undefined;
|
||||||
const companyCode = userProfile?.companyCode as string | undefined;
|
const companyCode = userProfile?.companyCode as string | undefined;
|
||||||
const isTenantContextMissing = !tenantId?.trim();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -94,8 +93,7 @@ function ClientsPage() {
|
|||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["developer-request", tenantId],
|
queryKey: ["developer-request", tenantId],
|
||||||
queryFn: () => fetchDeveloperRequestStatus(tenantId),
|
queryFn: () => fetchDeveloperRequestStatus(tenantId),
|
||||||
enabled:
|
enabled: hasAccessToken && profileRole === "user",
|
||||||
hasAccessToken && profileRole === "user" && !isTenantContextMissing,
|
|
||||||
});
|
});
|
||||||
const { data: tenants } = useQuery({
|
const { data: tenants } = useQuery({
|
||||||
queryKey: ["myTenants"],
|
queryKey: ["myTenants"],
|
||||||
@@ -110,9 +108,7 @@ function ClientsPage() {
|
|||||||
const canCreateClient = createAccessState === "can_create";
|
const canCreateClient = createAccessState === "can_create";
|
||||||
const isDeveloperRequestPending = createAccessState === "pending";
|
const isDeveloperRequestPending = createAccessState === "pending";
|
||||||
const canRequestDeveloperAccess =
|
const canRequestDeveloperAccess =
|
||||||
createAccessState === "request_required" &&
|
createAccessState === "request_required" && !isLoadingRequest;
|
||||||
!isLoadingRequest &&
|
|
||||||
!isTenantContextMissing;
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [typeFilter, setTypeFilter] = useState("all");
|
const [typeFilter, setTypeFilter] = useState("all");
|
||||||
@@ -233,20 +229,7 @@ function ClientsPage() {
|
|||||||
"OIDC 클라이언트, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다.",
|
"OIDC 클라이언트, 인증 방식, 리다이렉트 URI, 비밀키 재발행을 감사 로그와 함께 관리합니다.",
|
||||||
)}
|
)}
|
||||||
actions={
|
actions={
|
||||||
isTenantContextMissing ? (
|
canCreateClient ? (
|
||||||
<div className="flex flex-col items-end gap-2 text-right">
|
|
||||||
<p className="max-w-xs text-sm text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"msg.dev.clients.create_requires_tenant",
|
|
||||||
"개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<Button type="button" variant="outline" size="sm" disabled>
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
{t("ui.dev.welcome.btn_request", "개발자 권한 신청")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : canCreateClient ? (
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="mt-1 shadow-lg shadow-primary/30"
|
className="mt-1 shadow-lg shadow-primary/30"
|
||||||
@@ -470,11 +453,6 @@ function ClientsPage() {
|
|||||||
"msg.dev.clients.empty_filtered",
|
"msg.dev.clients.empty_filtered",
|
||||||
"조건에 맞는 연동 앱이 없습니다.",
|
"조건에 맞는 연동 앱이 없습니다.",
|
||||||
)
|
)
|
||||||
: isTenantContextMissing
|
|
||||||
? t(
|
|
||||||
"msg.dev.clients.empty_tenant_missing",
|
|
||||||
"개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.",
|
|
||||||
)
|
|
||||||
: canCreateClient
|
: canCreateClient
|
||||||
? t(
|
? t(
|
||||||
"msg.dev.clients.empty_can_create",
|
"msg.dev.clients.empty_can_create",
|
||||||
@@ -497,11 +475,6 @@ function ClientsPage() {
|
|||||||
"msg.dev.clients.empty_filtered_detail",
|
"msg.dev.clients.empty_filtered_detail",
|
||||||
"검색어나 필터 조건을 변경해 보세요.",
|
"검색어나 필터 조건을 변경해 보세요.",
|
||||||
)
|
)
|
||||||
: isTenantContextMissing
|
|
||||||
? t(
|
|
||||||
"msg.dev.clients.empty_tenant_missing_detail",
|
|
||||||
"현재 계정은 테넌트와 연결되어 있지 않아 개발자 권한을 신청할 수 없습니다.",
|
|
||||||
)
|
|
||||||
: canCreateClient
|
: canCreateClient
|
||||||
? t(
|
? t(
|
||||||
"msg.dev.clients.empty_can_create_detail",
|
"msg.dev.clients.empty_can_create_detail",
|
||||||
@@ -526,18 +499,6 @@ function ClientsPage() {
|
|||||||
{t("ui.dev.clients.new", "연동 앱 추가")}
|
{t("ui.dev.clients.new", "연동 앱 추가")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!isFilteredOut && isTenantContextMissing && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="font-bold text-muted-foreground"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
"ui.dev.welcome.btn_request",
|
|
||||||
"개발자 등록 신청하기",
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!isFilteredOut && canRequestDeveloperAccess && (
|
{!isFilteredOut && canRequestDeveloperAccess && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -711,7 +672,6 @@ function RequestAccessModal({
|
|||||||
const [name, setName] = useState(initialName);
|
const [name, setName] = useState(initialName);
|
||||||
const [organization, setOrganization] = useState(initialOrg);
|
const [organization, setOrganization] = useState(initialOrg);
|
||||||
const [reason, setReason] = useState("");
|
const [reason, setReason] = useState("");
|
||||||
const isTenantContextMissing = !tenantId.trim();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
@@ -728,15 +688,6 @@ function RequestAccessModal({
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (isTenantContextMissing) {
|
|
||||||
alert(
|
|
||||||
t(
|
|
||||||
"msg.dev.clients.create_requires_tenant",
|
|
||||||
"개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
name,
|
name,
|
||||||
organization,
|
organization,
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ export function useDeveloperAccessGate({
|
|||||||
const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({
|
const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({
|
||||||
queryKey: ["developer-request", tenantId],
|
queryKey: ["developer-request", tenantId],
|
||||||
queryFn: () => fetchDeveloperRequestStatus(tenantId),
|
queryFn: () => fetchDeveloperRequestStatus(tenantId),
|
||||||
enabled:
|
enabled: hasAccessToken && shouldFetchRequestStatus,
|
||||||
hasAccessToken && shouldFetchRequestStatus && !isTenantContextMissing,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedGate = resolveDeveloperAccessGate(
|
const resolvedGate = resolveDeveloperAccessGate(
|
||||||
@@ -75,8 +74,7 @@ export function useDeveloperAccessGate({
|
|||||||
return {
|
return {
|
||||||
...resolvedGate,
|
...resolvedGate,
|
||||||
isTenantContextMissing,
|
isTenantContextMissing,
|
||||||
canRequestDeveloperAccess:
|
canRequestDeveloperAccess: resolvedGate.canRequestDeveloperAccess,
|
||||||
resolvedGate.canRequestDeveloperAccess && !isTenantContextMissing,
|
|
||||||
isLoadingDeveloperAccessGate: shouldShowDeveloperAccessLoading(
|
isLoadingDeveloperAccessGate: shouldShowDeveloperAccessLoading(
|
||||||
profileRole,
|
profileRole,
|
||||||
isLoadingIdentity,
|
isLoadingIdentity,
|
||||||
|
|||||||
@@ -201,17 +201,11 @@ export default function DeveloperGrantsPage() {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tenantId = selectedUserDetail?.tenant?.id?.trim() || "";
|
const tenantId =
|
||||||
if (!tenantId) {
|
selectedUserDetail?.tenant?.id?.trim() ||
|
||||||
toast(
|
selectedUserDetail?.tenantSlug?.trim() ||
|
||||||
t(
|
selectedUserDetail?.companyCode?.trim() ||
|
||||||
"msg.dev.grants.tenant_required",
|
"";
|
||||||
"선택한 사용자의 현재 테넌트 정보를 확인할 수 없습니다.",
|
|
||||||
),
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
createGrantMutation.mutate({
|
createGrantMutation.mutate({
|
||||||
userId: selectedUser.id,
|
userId: selectedUser.id,
|
||||||
@@ -382,7 +376,9 @@ export default function DeveloperGrantsPage() {
|
|||||||
selectedUserDetail?.tenant?.name ||
|
selectedUserDetail?.tenant?.name ||
|
||||||
selectedUserDetail?.tenantSlug ||
|
selectedUserDetail?.tenantSlug ||
|
||||||
selectedUserDetail?.companyCode ||
|
selectedUserDetail?.companyCode ||
|
||||||
""
|
(selectedUser && !isSelectedUserDetailLoading
|
||||||
|
? t("ui.common.na", "없음")
|
||||||
|
: "")
|
||||||
}
|
}
|
||||||
readOnly
|
readOnly
|
||||||
placeholder={
|
placeholder={
|
||||||
@@ -548,10 +544,10 @@ export default function DeveloperGrantsPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{grant.organization || grant.tenantId}
|
{grant.organization || grant.tenantId || t("ui.common.na", "없음")}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-xs text-muted-foreground">
|
<div className="font-mono text-xs text-muted-foreground">
|
||||||
{grant.tenantId}
|
{grant.tenantId || t("ui.common.na", "없음")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ describe("DeveloperRequestPage", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a tenant-required notice and hides the request button when tenant is missing", async () => {
|
it("allows requesting developer access even when tenant context is missing", async () => {
|
||||||
authState = {
|
authState = {
|
||||||
user: {
|
user: {
|
||||||
access_token: "access-token",
|
access_token: "access-token",
|
||||||
@@ -206,12 +206,90 @@ describe("DeveloperRequestPage", () => {
|
|||||||
phone: "010-1234-5678",
|
phone: "010-1234-5678",
|
||||||
role: "user",
|
role: "user",
|
||||||
});
|
});
|
||||||
|
fetchMyTenantsMock.mockResolvedValue([]);
|
||||||
|
|
||||||
const container = await renderPage();
|
const container = await renderPage();
|
||||||
expect(container.textContent).toContain(
|
expect(container.textContent).toContain("신규 신청하기");
|
||||||
|
expect(container.textContent).not.toContain(
|
||||||
"개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.",
|
"개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.",
|
||||||
);
|
);
|
||||||
expect(container.textContent).not.toContain("신규 신청하기");
|
|
||||||
expect(requestDeveloperAccessMock).not.toHaveBeenCalled();
|
const actionButton = Array.from(container.querySelectorAll("button")).find(
|
||||||
|
(button) => button.textContent?.includes("신규 신청하기"),
|
||||||
|
);
|
||||||
|
expect(actionButton).toBeTruthy();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
actionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.textContent).toContain("개발자 등록 신청");
|
||||||
|
|
||||||
|
const reasonField = container.querySelector(
|
||||||
|
"textarea",
|
||||||
|
) as HTMLTextAreaElement | null;
|
||||||
|
if (!reasonField) {
|
||||||
|
throw new Error("Expected reason textarea to be rendered");
|
||||||
|
}
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await setTextAreaValue(reasonField, "Need RP access");
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitButton = Array.from(container.querySelectorAll("button")).find(
|
||||||
|
(button) => button.textContent === "신청하기",
|
||||||
|
);
|
||||||
|
expect(submitButton).toBeTruthy();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
submitButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(requestDeveloperAccessMock.mock.calls[0]?.[0]).toEqual({
|
||||||
|
name: "Requester",
|
||||||
|
organization: "HANMAC",
|
||||||
|
reason: "Need RP access",
|
||||||
|
tenantId: "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows '없음' when organization is unavailable", async () => {
|
||||||
|
authState = {
|
||||||
|
user: {
|
||||||
|
access_token: "access-token",
|
||||||
|
profile: {
|
||||||
|
role: "user",
|
||||||
|
name: "Requester",
|
||||||
|
email: "requester@example.com",
|
||||||
|
phone: "010-1234-5678",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fetchMeMock.mockResolvedValue({
|
||||||
|
id: "user-1",
|
||||||
|
name: "Requester",
|
||||||
|
email: "requester@example.com",
|
||||||
|
phone: "010-1234-5678",
|
||||||
|
role: "user",
|
||||||
|
});
|
||||||
|
fetchMyTenantsMock.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const container = await renderPage();
|
||||||
|
const actionButton = Array.from(container.querySelectorAll("button")).find(
|
||||||
|
(button) => button.textContent?.includes("신규 신청하기"),
|
||||||
|
);
|
||||||
|
expect(actionButton).toBeTruthy();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
actionButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const orgField = container.querySelector("#org") as HTMLInputElement | null;
|
||||||
|
if (!orgField) {
|
||||||
|
throw new Error("Expected organization input to be rendered");
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(orgField.value).toBe("없음");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ export default function DeveloperRequestPage() {
|
|||||||
const role = resolveProfileRole(userProfile);
|
const role = resolveProfileRole(userProfile);
|
||||||
const tenantId = userProfile?.tenant_id as string | undefined;
|
const tenantId = userProfile?.tenant_id as string | undefined;
|
||||||
const companyCode = userProfile?.companyCode as string | undefined;
|
const companyCode = userProfile?.companyCode as string | undefined;
|
||||||
const isTenantContextMissing = !tenantId?.trim();
|
|
||||||
|
|
||||||
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
||||||
const [adminNotes, setAdminNotes] = useState<Record<number, string>>({});
|
const [adminNotes, setAdminNotes] = useState<Record<number, string>>({});
|
||||||
@@ -180,7 +179,7 @@ export default function DeveloperRequestPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
!isSuperAdmin && !isTenantContextMissing && !hasActiveRequest ? (
|
!isSuperAdmin && !hasActiveRequest ? (
|
||||||
<Button onClick={() => setIsRequestModalOpen(true)}>
|
<Button onClick={() => setIsRequestModalOpen(true)}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
{t("ui.dev.welcome.btn_request", "신규 신청하기")}
|
{t("ui.dev.welcome.btn_request", "신규 신청하기")}
|
||||||
@@ -189,28 +188,6 @@ export default function DeveloperRequestPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isSuperAdmin && isTenantContextMissing ? (
|
|
||||||
<Card className="border-amber-500/30 bg-amber-500/10">
|
|
||||||
<CardContent className="flex items-start gap-3 p-4">
|
|
||||||
<ShieldAlert className="mt-0.5 h-5 w-5 shrink-0 text-amber-600" />
|
|
||||||
<div className="space-y-1 text-left">
|
|
||||||
<p className="font-semibold text-foreground">
|
|
||||||
{t(
|
|
||||||
"msg.dev.request.tenant_required",
|
|
||||||
"개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"msg.dev.request.tenant_required_detail",
|
|
||||||
"현재 계정은 테넌트와 연결되어 있지 않아 개발자 권한을 신청할 수 없습니다.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Card className="glass-panel">
|
<Card className="glass-panel">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">
|
<CardTitle className="text-xl">
|
||||||
@@ -282,7 +259,9 @@ export default function DeveloperRequestPage() {
|
|||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
<TableCell>{req.organization}</TableCell>
|
<TableCell>
|
||||||
|
{req.organization?.trim() || t("ui.common.na", "없음")}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="max-w-md">
|
<TableCell className="max-w-md">
|
||||||
<div className="truncate" title={req.reason}>
|
<div className="truncate" title={req.reason}>
|
||||||
{req.reason}
|
{req.reason}
|
||||||
@@ -470,6 +449,7 @@ function RequestAccessModal({
|
|||||||
const [name, setName] = useState(initialName);
|
const [name, setName] = useState(initialName);
|
||||||
const [organization, setOrganization] = useState(initialOrg);
|
const [organization, setOrganization] = useState(initialOrg);
|
||||||
const [reason, setReason] = useState("");
|
const [reason, setReason] = useState("");
|
||||||
|
const organizationDisplay = organization.trim() || t("ui.common.na", "없음");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
@@ -486,15 +466,6 @@ function RequestAccessModal({
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!tenantId.trim()) {
|
|
||||||
alert(
|
|
||||||
t(
|
|
||||||
"msg.dev.request.tenant_required",
|
|
||||||
"개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
name,
|
name,
|
||||||
organization,
|
organization,
|
||||||
@@ -550,7 +521,7 @@ function RequestAccessModal({
|
|||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="org"
|
id="org"
|
||||||
value={organization}
|
value={organizationDisplay}
|
||||||
readOnly
|
readOnly
|
||||||
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||||
required
|
required
|
||||||
|
|||||||
@@ -968,7 +968,6 @@ function GlobalOverviewPage() {
|
|||||||
isDeveloperRequestPending,
|
isDeveloperRequestPending,
|
||||||
canRequestDeveloperAccess,
|
canRequestDeveloperAccess,
|
||||||
isLoadingDeveloperAccessGate,
|
isLoadingDeveloperAccessGate,
|
||||||
isTenantContextMissing,
|
|
||||||
} = useDeveloperAccessGate({
|
} = useDeveloperAccessGate({
|
||||||
hasAccessToken,
|
hasAccessToken,
|
||||||
profileRole,
|
profileRole,
|
||||||
@@ -1267,24 +1266,6 @@ function GlobalOverviewPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasDeveloperAccess) {
|
if (!hasDeveloperAccess) {
|
||||||
const deniedMessage = isTenantContextMissing
|
|
||||||
? t(
|
|
||||||
"msg.dev.request.tenant_required",
|
|
||||||
"개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다.",
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"msg.dev.dashboard.access_denied",
|
|
||||||
"대시보드는 개발자 권한이 있어야 볼 수 있습니다.",
|
|
||||||
);
|
|
||||||
const deniedDetailMessage = isTenantContextMissing
|
|
||||||
? t(
|
|
||||||
"msg.dev.request.tenant_required_detail",
|
|
||||||
"현재 계정은 테넌트와 연결되어 있지 않아 개발자 권한을 신청할 수 없습니다.",
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
"msg.dev.dashboard.access_denied_detail",
|
|
||||||
"개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요.",
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<DeveloperAccessRequestCard
|
<DeveloperAccessRequestCard
|
||||||
title={t("ui.common.overview.title", "운영 현황")}
|
title={t("ui.common.overview.title", "운영 현황")}
|
||||||
@@ -1298,8 +1279,14 @@ function GlobalOverviewPage() {
|
|||||||
"msg.dev.dashboard.access_pending_detail",
|
"msg.dev.dashboard.access_pending_detail",
|
||||||
"super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다.",
|
"super admin이 승인하면 개요와 개발자 기능을 사용할 수 있습니다.",
|
||||||
)}
|
)}
|
||||||
deniedMessage={deniedMessage}
|
deniedMessage={t(
|
||||||
deniedDetailMessage={deniedDetailMessage}
|
"msg.dev.dashboard.access_denied",
|
||||||
|
"대시보드는 개발자 권한이 있어야 볼 수 있습니다.",
|
||||||
|
)}
|
||||||
|
deniedDetailMessage={t(
|
||||||
|
"msg.dev.dashboard.access_denied_detail",
|
||||||
|
"개발자 권한 신청 페이지에서 신청을 등록한 뒤 승인을 받아주세요.",
|
||||||
|
)}
|
||||||
actionLabel={t("ui.dev.nav.developer_request", "개발자 권한 신청")}
|
actionLabel={t("ui.dev.nav.developer_request", "개발자 권한 신청")}
|
||||||
onAction={() => navigate("/developer-requests")}
|
onAction={() => navigate("/developer-requests")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -329,8 +329,8 @@ user_desc = "Review your request history and submit a new access request."
|
|||||||
|
|
||||||
[msg.dev.request.modal]
|
[msg.dev.request.modal]
|
||||||
desc = "Please enter the reason for your request. It will be approved after administrator review."
|
desc = "Please enter the reason for your request. It will be approved after administrator review."
|
||||||
tenant_required = "You need to be assigned to a tenant before you can request developer access."
|
tenant_required = "Please submit a developer access request."
|
||||||
tenant_required_detail = "This account is not linked to a tenant yet, so developer access cannot be requested."
|
tenant_required_detail = "Enter a request reason and submit it for administrator review."
|
||||||
|
|
||||||
[msg.dev.clients]
|
[msg.dev.clients]
|
||||||
load_error = "Error loading clients: {{error}}"
|
load_error = "Error loading clients: {{error}}"
|
||||||
@@ -344,16 +344,16 @@ empty_detail = "RPs will appear here when a relationship is assigned to your acc
|
|||||||
empty_can_create = "No linked apps have been registered yet."
|
empty_can_create = "No linked apps have been registered yet."
|
||||||
empty_can_create_detail = "Create a new RP with the Add linked app button, and it will appear here."
|
empty_can_create_detail = "Create a new RP with the Add linked app button, and it will appear here."
|
||||||
create_requires_request = "You do not have permission to create applications.\nSubmit a developer access request and wait for approval."
|
create_requires_request = "You do not have permission to create applications.\nSubmit a developer access request and wait for approval."
|
||||||
create_requires_tenant = "You need to be assigned to a tenant before you can request developer access."
|
create_requires_tenant = "Please submit a developer access request."
|
||||||
create_requires_tenant_detail = "This account is not linked to a tenant yet, so developer access cannot be requested."
|
create_requires_tenant_detail = "Enter a request reason and submit it for administrator review."
|
||||||
create_pending_detail = "Your developer access request is under review. You will be able to add applications after approval."
|
create_pending_detail = "Your developer access request is under review. You will be able to add applications after approval."
|
||||||
create_forbidden_detail = "You do not have permission to create applications. Ask an administrator to grant developer access or the appropriate RP permissions."
|
create_forbidden_detail = "You do not have permission to create applications. Ask an administrator to grant developer access or the appropriate RP permissions."
|
||||||
empty_filtered = "No linked apps match the current filters."
|
empty_filtered = "No linked apps match the current filters."
|
||||||
empty_filtered_detail = "Try changing the search text or filters."
|
empty_filtered_detail = "Try changing the search text or filters."
|
||||||
empty_pending = "Your developer access request is under review."
|
empty_pending = "Your developer access request is under review."
|
||||||
empty_pending_detail = "You can add linked apps after a super admin approves it."
|
empty_pending_detail = "You can add linked apps after a super admin approves it."
|
||||||
empty_tenant_missing = "You need to be assigned to a tenant before you can request developer access."
|
empty_tenant_missing = "Please submit a developer access request."
|
||||||
empty_tenant_missing_detail = "This account is not linked to a tenant yet, so developer access cannot be requested."
|
empty_tenant_missing_detail = "Enter a request reason and submit it for administrator review."
|
||||||
|
|
||||||
[msg.dev.clients.consents]
|
[msg.dev.clients.consents]
|
||||||
empty = "No consents found."
|
empty = "No consents found."
|
||||||
@@ -2122,3 +2122,6 @@ user.clients = "General user accounts can only use this feature if they have bee
|
|||||||
user.consents = "Viewing consent history for this App (RP) is only available when granted operational, consent view, or consent revoke relationships. If you need access, please request it from an administrator."
|
user.consents = "Viewing consent history for this App (RP) is only available when granted operational, consent view, or consent revoke relationships. If you need access, please request it from an administrator."
|
||||||
user.audit = "Viewing audit logs for this App (RP) is only available when granted operational or audit view relationships. If you need access, please request it from an administrator."
|
user.audit = "Viewing audit logs for this App (RP) is only available when granted operational or audit view relationships. If you need access, please request it from an administrator."
|
||||||
title = "Access Denied: {{resource}}"
|
title = "Access Denied: {{resource}}"
|
||||||
|
|
||||||
|
[ui.common]
|
||||||
|
na = "N/A"
|
||||||
|
|||||||
@@ -329,8 +329,8 @@ user_desc = "내 신청 내역을 확인하고 새로운 권한을 신청할 수
|
|||||||
|
|
||||||
[msg.dev.request.modal]
|
[msg.dev.request.modal]
|
||||||
desc = "신청 사유를 입력해 주세요. 관리자 확인 후 승인됩니다."
|
desc = "신청 사유를 입력해 주세요. 관리자 확인 후 승인됩니다."
|
||||||
tenant_required = "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다."
|
tenant_required = "개발자 권한 신청을 진행해 주세요."
|
||||||
tenant_required_detail = "현재 계정은 테넌트와 연결되어 있지 않아 개발자 권한을 신청할 수 없습니다."
|
tenant_required_detail = "신청 사유를 입력해 제출하면 관리자 검토 후 승인됩니다."
|
||||||
|
|
||||||
[msg.dev.clients]
|
[msg.dev.clients]
|
||||||
deleted = "앱이 삭제되었습니다."
|
deleted = "앱이 삭제되었습니다."
|
||||||
@@ -341,16 +341,16 @@ empty_detail = "RP 관계가 부여되면 이 목록에 해당 RP가 표시됩
|
|||||||
empty_can_create = "아직 등록된 연동 앱이 없습니다."
|
empty_can_create = "아직 등록된 연동 앱이 없습니다."
|
||||||
empty_can_create_detail = "연동 앱 추가 버튼으로 새 RP를 생성하면 이 목록에 표시됩니다."
|
empty_can_create_detail = "연동 앱 추가 버튼으로 새 RP를 생성하면 이 목록에 표시됩니다."
|
||||||
create_requires_request = "연동 앱을 생성할 권한이 없습니다.\n개발자 권한 신청을 요청한 뒤 승인 받아주세요."
|
create_requires_request = "연동 앱을 생성할 권한이 없습니다.\n개발자 권한 신청을 요청한 뒤 승인 받아주세요."
|
||||||
create_requires_tenant = "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다."
|
create_requires_tenant = "개발자 권한 신청을 진행해 주세요."
|
||||||
create_requires_tenant_detail = "현재 계정은 테넌트와 연결되어 있지 않아 개발자 권한을 신청할 수 없습니다."
|
create_requires_tenant_detail = "신청 사유를 입력해 제출하면 관리자 검토 후 승인됩니다."
|
||||||
create_pending_detail = "개발자 권한 신청을 검토 중입니다. 승인되면 연동 앱을 추가할 수 있습니다."
|
create_pending_detail = "개발자 권한 신청을 검토 중입니다. 승인되면 연동 앱을 추가할 수 있습니다."
|
||||||
create_forbidden_detail = "연동 앱을 생성할 권한이 없습니다. 관리자에게 개발자 권한 또는 적절한 RP 권한 부여를 요청해 주세요."
|
create_forbidden_detail = "연동 앱을 생성할 권한이 없습니다. 관리자에게 개발자 권한 또는 적절한 RP 권한 부여를 요청해 주세요."
|
||||||
empty_filtered = "조건에 맞는 연동 앱이 없습니다."
|
empty_filtered = "조건에 맞는 연동 앱이 없습니다."
|
||||||
empty_filtered_detail = "검색어나 필터 조건을 변경해 보세요."
|
empty_filtered_detail = "검색어나 필터 조건을 변경해 보세요."
|
||||||
empty_pending = "개발자 권한 신청을 검토 중입니다."
|
empty_pending = "개발자 권한 신청을 검토 중입니다."
|
||||||
empty_pending_detail = "super admin이 승인하면 연동 앱을 추가할 수 있습니다."
|
empty_pending_detail = "super admin이 승인하면 연동 앱을 추가할 수 있습니다."
|
||||||
empty_tenant_missing = "개발자 권한을 신청하려면 먼저 테넌트에 소속되어 있어야 합니다."
|
empty_tenant_missing = "개발자 권한 신청을 진행해 주세요."
|
||||||
empty_tenant_missing_detail = "현재 계정은 테넌트와 연결되어 있지 않아 개발자 권한을 신청할 수 없습니다."
|
empty_tenant_missing_detail = "신청 사유를 입력해 제출하면 관리자 검토 후 승인됩니다."
|
||||||
load_error = "앱 정보를 불러오지 못했습니다: {{error}}"
|
load_error = "앱 정보를 불러오지 못했습니다: {{error}}"
|
||||||
loading = "앱 정보를 불러오는 중..."
|
loading = "앱 정보를 불러오는 중..."
|
||||||
showing = "총 {{shown}}개의 애플리케이션이 등록되어 있습니다."
|
showing = "총 {{shown}}개의 애플리케이션이 등록되어 있습니다."
|
||||||
@@ -2118,3 +2118,6 @@ user.clients = "일반 사용자 계정은 담당 RP(앱)에 대한 운영 또
|
|||||||
user.consents = "해당 앱(RP)에 대한 동의 내역 조회는 운영, 동의 조회, 동의 회수 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
|
user.consents = "해당 앱(RP)에 대한 동의 내역 조회는 운영, 동의 조회, 동의 회수 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
|
||||||
user.audit = "해당 앱(RP)에 대한 감사 로그 조회는 운영 또는 감사 조회 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
|
user.audit = "해당 앱(RP)에 대한 감사 로그 조회는 운영 또는 감사 조회 관계가 부여된 경우에만 사용할 수 있습니다. 권한이 필요하면 관리자에게 요청하세요."
|
||||||
title = "{{resource}} 접근 권한 없음"
|
title = "{{resource}} 접근 권한 없음"
|
||||||
|
|
||||||
|
[ui.common]
|
||||||
|
na = "없음"
|
||||||
|
|||||||
Reference in New Issue
Block a user