1
0
forked from baron/baron-sso

개발자 등록 신청 입력 안내 및 역할 표기 개선

This commit is contained in:
2026-04-22 15:40:17 +09:00
parent 5d334069c7
commit 9e73059d2a
5 changed files with 218 additions and 18 deletions

View File

@@ -9,7 +9,7 @@ import {
ShieldHalf,
X,
} from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useAuth } from "react-oidc-context";
import { Link, useNavigate } from "react-router-dom";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
@@ -43,11 +43,13 @@ import {
fetchClients,
fetchDevStats,
fetchDeveloperRequestStatus,
fetchMyTenants,
requestDeveloperAccess,
} from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { resolveProfileRole } from "../../lib/role";
import { cn } from "../../lib/utils";
import { fetchMe } from "../auth/authApi";
function ClientsPage() {
const navigate = useNavigate();
@@ -56,6 +58,7 @@ function ClientsPage() {
const userProfile = auth.user?.profile as Record<string, unknown> | undefined;
const role = resolveProfileRole(userProfile);
const tenantId = userProfile?.tenant_id as string | undefined;
const companyCode = userProfile?.companyCode as string | undefined;
const {
data,
@@ -82,6 +85,16 @@ function ClientsPage() {
queryFn: () => fetchDeveloperRequestStatus(tenantId),
enabled: hasAccessToken && role === "user",
});
const { data: tenants } = useQuery({
queryKey: ["myTenants"],
queryFn: fetchMyTenants,
enabled: hasAccessToken,
});
const { data: me } = useQuery({
queryKey: ["userMe"],
queryFn: fetchMe,
enabled: hasAccessToken,
});
const canCreateClient =
(role !== "user" && role !== "tenant_member") ||
@@ -117,6 +130,19 @@ function ClientsPage() {
const authFailures = statsData?.auth_failures_24h ?? 0;
const hasFilterResult = filteredClients.length > 0;
const isFilteredOut = clients.length > 0 && !hasFilterResult;
const currentTenant = tenants?.find(
(tenant) => tenant.id === tenantId || tenant.slug === companyCode,
);
const organizationName = currentTenant?.name || companyCode || "";
const profileName = me?.name || (userProfile?.name as string) || "";
const profileEmail = me?.email || (userProfile?.email as string) || "";
const profilePhone =
me?.phone ||
(userProfile?.phone as string | undefined) ||
(userProfile?.phone_number as string | undefined) ||
"";
const profileRole = me?.role || role;
const profileRoleLabel = t(`ui.admin.role.${profileRole}`, profileRole);
type StatTone = "up" | "down" | "stable";
type StatItem = {
@@ -644,8 +670,11 @@ function ClientsPage() {
setIsRequestModalOpen(false);
}}
tenantId={tenantId || ""}
initialName={(userProfile?.name as string) || ""}
initialOrg={(userProfile?.companyCode as string) || ""}
initialName={profileName}
initialOrg={organizationName}
initialEmail={profileEmail}
initialPhone={profilePhone}
initialRole={profileRoleLabel}
/>
</div>
);
@@ -658,6 +687,9 @@ interface RequestAccessModalProps {
tenantId: string;
initialName: string;
initialOrg: string;
initialEmail: string;
initialPhone: string;
initialRole: string;
}
function RequestAccessModal({
@@ -667,11 +699,20 @@ function RequestAccessModal({
tenantId,
initialName,
initialOrg,
initialEmail,
initialPhone,
initialRole,
}: RequestAccessModalProps) {
const [name, setName] = useState(initialName);
const [organization, setOrganization] = useState(initialOrg);
const [reason, setReason] = useState("");
useEffect(() => {
if (!isOpen) return;
setName(initialName);
setOrganization(initialOrg);
}, [initialName, initialOrg, isOpen]);
const mutation = useMutation({
mutationFn: requestDeveloperAccess,
onSuccess: () => {
@@ -725,7 +766,8 @@ function RequestAccessModal({
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
required
/>
</div>
@@ -736,13 +778,50 @@ function RequestAccessModal({
<Input
id="org"
value={organization}
onChange={(e) => setOrganization(e.target.value)}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
required
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="email">
{t("ui.dev.request.modal.email", "이메일")}
</Label>
<Input
id="email"
value={initialEmail}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="phone">
{t("ui.dev.request.modal.phone", "전화번호")}
</Label>
<Input
id="phone"
value={initialPhone}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="role">
{t("ui.dev.request.modal.role", "역할")}
</Label>
<Input
id="role"
value={initialRole}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="reason">
{t("ui.dev.request.modal.reason", "신청 사유")}
{t("ui.dev.request.modal.reason", "신청 사유")}{" "}
<span className="text-destructive">*</span>
</Label>
<Textarea
id="reason"
@@ -752,7 +831,7 @@ function RequestAccessModal({
"ui.dev.request.modal.reason_placeholder",
"예: 자체 서비스 연동 및 테스트용 OIDC 클라이언트 생성이 필요합니다.",
)}
className="min-h-[120px] resize-none"
className="min-h-[120px] resize-none border-primary/50 bg-background focus-visible:ring-primary/40"
required
/>
</div>

View File

@@ -7,7 +7,7 @@ import {
X,
XCircle,
} from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useAuth } from "react-oidc-context";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
@@ -32,11 +32,13 @@ import {
approveDeveloperRequest,
cancelDeveloperRequestApproval,
fetchDeveloperRequests,
fetchMyTenants,
rejectDeveloperRequest,
requestDeveloperAccess,
} from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { resolveProfileRole } from "../../lib/role";
import { fetchMe } from "../auth/authApi";
export default function DeveloperRequestPage() {
const auth = useAuth();
@@ -45,6 +47,7 @@ export default function DeveloperRequestPage() {
const role = resolveProfileRole(userProfile);
const isSuperAdmin = role === "super_admin";
const tenantId = userProfile?.tenant_id as string | undefined;
const companyCode = userProfile?.companyCode as string | undefined;
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
const [adminNotes, setAdminNotes] = useState<Record<number, string>>({});
@@ -54,6 +57,30 @@ export default function DeveloperRequestPage() {
queryFn: () => fetchDeveloperRequests(),
enabled: !!auth.user?.access_token,
});
const { data: tenants } = useQuery({
queryKey: ["myTenants"],
queryFn: fetchMyTenants,
enabled: !!auth.user?.access_token,
});
const { data: me } = useQuery({
queryKey: ["userMe"],
queryFn: fetchMe,
enabled: !!auth.user?.access_token,
});
const currentTenant = tenants?.find(
(tenant) => tenant.id === tenantId || tenant.slug === companyCode,
);
const organizationName = currentTenant?.name || companyCode || "";
const profileName = me?.name || (userProfile?.name as string) || "";
const profileEmail = me?.email || (userProfile?.email as string) || "";
const profilePhone =
me?.phone ||
(userProfile?.phone as string | undefined) ||
(userProfile?.phone_number as string | undefined) ||
"";
const profileRole = me?.role || role;
const profileRoleLabel = t(`ui.admin.role.${profileRole}`, profileRole);
const approveMutation = useMutation({
mutationFn: ({ id, adminNotes }: { id: number; adminNotes: string }) =>
@@ -189,8 +216,13 @@ export default function DeveloperRequestPage() {
<TableCell className="font-medium">
<div>{req.name}</div>
<div className="text-xs text-muted-foreground">
{req.userId}
{req.email || req.userId}
</div>
{(req.phone || req.role) && (
<div className="mt-1 text-xs text-muted-foreground">
{[req.phone, req.role].filter(Boolean).join(" / ")}
</div>
)}
</TableCell>
)}
<TableCell>{req.organization}</TableCell>
@@ -302,8 +334,11 @@ export default function DeveloperRequestPage() {
setIsRequestModalOpen(false);
}}
tenantId={tenantId || ""}
initialName={(userProfile?.name as string) || ""}
initialOrg={(userProfile?.companyCode as string) || ""}
initialName={profileName}
initialOrg={organizationName}
initialEmail={profileEmail}
initialPhone={profilePhone}
initialRole={profileRoleLabel}
/>
</div>
);
@@ -352,6 +387,9 @@ interface RequestAccessModalProps {
tenantId: string;
initialName: string;
initialOrg: string;
initialEmail: string;
initialPhone: string;
initialRole: string;
}
function RequestAccessModal({
@@ -361,11 +399,20 @@ function RequestAccessModal({
tenantId,
initialName,
initialOrg,
initialEmail,
initialPhone,
initialRole,
}: RequestAccessModalProps) {
const [name, setName] = useState(initialName);
const [organization, setOrganization] = useState(initialOrg);
const [reason, setReason] = useState("");
useEffect(() => {
if (!isOpen) return;
setName(initialName);
setOrganization(initialOrg);
}, [initialName, initialOrg, isOpen]);
const mutation = useMutation({
mutationFn: requestDeveloperAccess,
onSuccess: () => {
@@ -419,7 +466,8 @@ function RequestAccessModal({
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
required
/>
</div>
@@ -430,13 +478,50 @@ function RequestAccessModal({
<Input
id="org"
value={organization}
onChange={(e) => setOrganization(e.target.value)}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
required
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="email">
{t("ui.dev.request.modal.email", "이메일")}
</Label>
<Input
id="email"
value={initialEmail}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="phone">
{t("ui.dev.request.modal.phone", "전화번호")}
</Label>
<Input
id="phone"
value={initialPhone}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="role">
{t("ui.dev.request.modal.role", "역할")}
</Label>
<Input
id="role"
value={initialRole}
readOnly
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="reason">
{t("ui.dev.request.modal.reason", "신청 사유")}
{t("ui.dev.request.modal.reason", "신청 사유")}{" "}
<span className="text-destructive">*</span>
</Label>
<Textarea
id="reason"
@@ -446,7 +531,7 @@ function RequestAccessModal({
"ui.dev.request.modal.reason_placeholder",
"예: 자체 서비스 연동 및 테스트용 OIDC 클라이언트 생성이 필요합니다.",
)}
className="min-h-[120px] resize-none"
className="min-h-[120px] resize-none border-primary/50 bg-background focus-visible:ring-primary/40"
required
/>
</div>

View File

@@ -401,6 +401,9 @@ export type DeveloperRequest = {
tenantId: string;
name: string;
organization: string;
email?: string;
phone?: string;
role?: string;
reason: string;
status: DeveloperRequestStatus;
adminNotes?: string;