forked from baron/baron-sso
개발자 등록 신청 입력 안내 및 역할 표기 개선
This commit is contained in:
@@ -18,6 +18,9 @@ type DeveloperRequest struct {
|
|||||||
TenantID string `gorm:"index;not null" json:"tenantId"`
|
TenantID string `gorm:"index;not null" json:"tenantId"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Name string `gorm:"not null" json:"name"`
|
||||||
Organization string `json:"organization"`
|
Organization string `json:"organization"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Role string `json:"role"`
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
Status string `gorm:"default:'pending';not null" json:"status"` // pending, approved, rejected, cancelled
|
Status string `gorm:"default:'pending';not null" json:"status"` // pending, approved, rejected, cancelled
|
||||||
AdminNotes string `json:"adminNotes"`
|
AdminNotes string `json:"adminNotes"`
|
||||||
|
|||||||
@@ -2800,7 +2800,17 @@ func (h *DevHandler) ListMyTenants(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
role := normalizeUserRole(profile.Role)
|
role := normalizeUserRole(profile.Role)
|
||||||
if role == domain.RoleUser {
|
if role == domain.RoleUser {
|
||||||
return errorJSON(c, fiber.StatusForbidden, "access denied")
|
if profile.TenantID == nil || strings.TrimSpace(*profile.TenantID) == "" {
|
||||||
|
return c.JSON([]domain.Tenant{})
|
||||||
|
}
|
||||||
|
tenant, err := h.TenantSvc.GetTenant(c.Context(), *profile.TenantID)
|
||||||
|
if err != nil {
|
||||||
|
return errorJSON(c, fiber.StatusInternalServerError, "failed to get tenant")
|
||||||
|
}
|
||||||
|
if tenant == nil {
|
||||||
|
return c.JSON([]domain.Tenant{})
|
||||||
|
}
|
||||||
|
return c.JSON([]domain.Tenant{*tenant})
|
||||||
}
|
}
|
||||||
|
|
||||||
if role == domain.RoleSuperAdmin {
|
if role == domain.RoleSuperAdmin {
|
||||||
@@ -2839,6 +2849,12 @@ func (h *DevHandler) RequestDeveloperAccess(c *fiber.Ctx) error {
|
|||||||
if profile == nil {
|
if profile == nil {
|
||||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
|
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized")
|
||||||
}
|
}
|
||||||
|
if h.Auth != nil {
|
||||||
|
if enriched, err := h.Auth.GetEnrichedProfile(c); err == nil && enriched != nil {
|
||||||
|
profile = enriched
|
||||||
|
c.Locals("user_profile", enriched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -2857,11 +2873,25 @@ func (h *DevHandler) RequestDeveloperAccess(c *fiber.Ctx) error {
|
|||||||
return errorJSON(c, fiber.StatusBadRequest, "tenantId is required")
|
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 tenant, err := h.TenantSvc.GetTenant(c.Context(), req.TenantID); err == nil && tenant != nil && strings.TrimSpace(tenant.Name) != "" {
|
||||||
|
organization = strings.TrimSpace(tenant.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
devReq := domain.DeveloperRequest{
|
devReq := domain.DeveloperRequest{
|
||||||
UserID: profile.ID,
|
UserID: profile.ID,
|
||||||
TenantID: req.TenantID,
|
TenantID: req.TenantID,
|
||||||
Name: req.Name,
|
Name: name,
|
||||||
Organization: req.Organization,
|
Organization: organization,
|
||||||
|
Email: profile.Email,
|
||||||
|
Phone: profile.Phone,
|
||||||
|
Role: normalizeUserRole(profile.Role),
|
||||||
Reason: req.Reason,
|
Reason: req.Reason,
|
||||||
Status: domain.DeveloperRequestStatusPending,
|
Status: domain.DeveloperRequestStatusPending,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ShieldHalf,
|
ShieldHalf,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||||
@@ -43,11 +43,13 @@ import {
|
|||||||
fetchClients,
|
fetchClients,
|
||||||
fetchDevStats,
|
fetchDevStats,
|
||||||
fetchDeveloperRequestStatus,
|
fetchDeveloperRequestStatus,
|
||||||
|
fetchMyTenants,
|
||||||
requestDeveloperAccess,
|
requestDeveloperAccess,
|
||||||
} from "../../lib/devApi";
|
} from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { resolveProfileRole } from "../../lib/role";
|
import { resolveProfileRole } from "../../lib/role";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
|
import { fetchMe } from "../auth/authApi";
|
||||||
|
|
||||||
function ClientsPage() {
|
function ClientsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -56,6 +58,7 @@ function ClientsPage() {
|
|||||||
const userProfile = auth.user?.profile as Record<string, unknown> | undefined;
|
const userProfile = auth.user?.profile as Record<string, unknown> | undefined;
|
||||||
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 {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -82,6 +85,16 @@ function ClientsPage() {
|
|||||||
queryFn: () => fetchDeveloperRequestStatus(tenantId),
|
queryFn: () => fetchDeveloperRequestStatus(tenantId),
|
||||||
enabled: hasAccessToken && role === "user",
|
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 =
|
const canCreateClient =
|
||||||
(role !== "user" && role !== "tenant_member") ||
|
(role !== "user" && role !== "tenant_member") ||
|
||||||
@@ -117,6 +130,19 @@ function ClientsPage() {
|
|||||||
const authFailures = statsData?.auth_failures_24h ?? 0;
|
const authFailures = statsData?.auth_failures_24h ?? 0;
|
||||||
const hasFilterResult = filteredClients.length > 0;
|
const hasFilterResult = filteredClients.length > 0;
|
||||||
const isFilteredOut = clients.length > 0 && !hasFilterResult;
|
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 StatTone = "up" | "down" | "stable";
|
||||||
type StatItem = {
|
type StatItem = {
|
||||||
@@ -644,8 +670,11 @@ function ClientsPage() {
|
|||||||
setIsRequestModalOpen(false);
|
setIsRequestModalOpen(false);
|
||||||
}}
|
}}
|
||||||
tenantId={tenantId || ""}
|
tenantId={tenantId || ""}
|
||||||
initialName={(userProfile?.name as string) || ""}
|
initialName={profileName}
|
||||||
initialOrg={(userProfile?.companyCode as string) || ""}
|
initialOrg={organizationName}
|
||||||
|
initialEmail={profileEmail}
|
||||||
|
initialPhone={profilePhone}
|
||||||
|
initialRole={profileRoleLabel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -658,6 +687,9 @@ interface RequestAccessModalProps {
|
|||||||
tenantId: string;
|
tenantId: string;
|
||||||
initialName: string;
|
initialName: string;
|
||||||
initialOrg: string;
|
initialOrg: string;
|
||||||
|
initialEmail: string;
|
||||||
|
initialPhone: string;
|
||||||
|
initialRole: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestAccessModal({
|
function RequestAccessModal({
|
||||||
@@ -667,11 +699,20 @@ function RequestAccessModal({
|
|||||||
tenantId,
|
tenantId,
|
||||||
initialName,
|
initialName,
|
||||||
initialOrg,
|
initialOrg,
|
||||||
|
initialEmail,
|
||||||
|
initialPhone,
|
||||||
|
initialRole,
|
||||||
}: RequestAccessModalProps) {
|
}: RequestAccessModalProps) {
|
||||||
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("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
setName(initialName);
|
||||||
|
setOrganization(initialOrg);
|
||||||
|
}, [initialName, initialOrg, isOpen]);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: requestDeveloperAccess,
|
mutationFn: requestDeveloperAccess,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -725,7 +766,8 @@ function RequestAccessModal({
|
|||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -736,13 +778,50 @@ function RequestAccessModal({
|
|||||||
<Input
|
<Input
|
||||||
id="org"
|
id="org"
|
||||||
value={organization}
|
value={organization}
|
||||||
onChange={(e) => setOrganization(e.target.value)}
|
readOnly
|
||||||
|
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="reason">
|
<Label htmlFor="reason">
|
||||||
{t("ui.dev.request.modal.reason", "신청 사유")}
|
{t("ui.dev.request.modal.reason", "신청 사유")}{" "}
|
||||||
|
<span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="reason"
|
id="reason"
|
||||||
@@ -752,7 +831,7 @@ function RequestAccessModal({
|
|||||||
"ui.dev.request.modal.reason_placeholder",
|
"ui.dev.request.modal.reason_placeholder",
|
||||||
"예: 자체 서비스 연동 및 테스트용 OIDC 클라이언트 생성이 필요합니다.",
|
"예: 자체 서비스 연동 및 테스트용 OIDC 클라이언트 생성이 필요합니다.",
|
||||||
)}
|
)}
|
||||||
className="min-h-[120px] resize-none"
|
className="min-h-[120px] resize-none border-primary/50 bg-background focus-visible:ring-primary/40"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useAuth } from "react-oidc-context";
|
import { useAuth } from "react-oidc-context";
|
||||||
import { Badge } from "../../components/ui/badge";
|
import { Badge } from "../../components/ui/badge";
|
||||||
import { Button } from "../../components/ui/button";
|
import { Button } from "../../components/ui/button";
|
||||||
@@ -32,11 +32,13 @@ import {
|
|||||||
approveDeveloperRequest,
|
approveDeveloperRequest,
|
||||||
cancelDeveloperRequestApproval,
|
cancelDeveloperRequestApproval,
|
||||||
fetchDeveloperRequests,
|
fetchDeveloperRequests,
|
||||||
|
fetchMyTenants,
|
||||||
rejectDeveloperRequest,
|
rejectDeveloperRequest,
|
||||||
requestDeveloperAccess,
|
requestDeveloperAccess,
|
||||||
} from "../../lib/devApi";
|
} from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { resolveProfileRole } from "../../lib/role";
|
import { resolveProfileRole } from "../../lib/role";
|
||||||
|
import { fetchMe } from "../auth/authApi";
|
||||||
|
|
||||||
export default function DeveloperRequestPage() {
|
export default function DeveloperRequestPage() {
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
@@ -45,6 +47,7 @@ export default function DeveloperRequestPage() {
|
|||||||
const role = resolveProfileRole(userProfile);
|
const role = resolveProfileRole(userProfile);
|
||||||
const isSuperAdmin = role === "super_admin";
|
const isSuperAdmin = role === "super_admin";
|
||||||
const tenantId = userProfile?.tenant_id as string | undefined;
|
const tenantId = userProfile?.tenant_id as string | undefined;
|
||||||
|
const companyCode = userProfile?.companyCode as string | undefined;
|
||||||
|
|
||||||
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
const [isRequestModalOpen, setIsRequestModalOpen] = useState(false);
|
||||||
const [adminNotes, setAdminNotes] = useState<Record<number, string>>({});
|
const [adminNotes, setAdminNotes] = useState<Record<number, string>>({});
|
||||||
@@ -54,6 +57,30 @@ export default function DeveloperRequestPage() {
|
|||||||
queryFn: () => fetchDeveloperRequests(),
|
queryFn: () => fetchDeveloperRequests(),
|
||||||
enabled: !!auth.user?.access_token,
|
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({
|
const approveMutation = useMutation({
|
||||||
mutationFn: ({ id, adminNotes }: { id: number; adminNotes: string }) =>
|
mutationFn: ({ id, adminNotes }: { id: number; adminNotes: string }) =>
|
||||||
@@ -189,8 +216,13 @@ export default function DeveloperRequestPage() {
|
|||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
<div>{req.name}</div>
|
<div>{req.name}</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{req.userId}
|
{req.email || req.userId}
|
||||||
</div>
|
</div>
|
||||||
|
{(req.phone || req.role) && (
|
||||||
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
|
{[req.phone, req.role].filter(Boolean).join(" / ")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
<TableCell>{req.organization}</TableCell>
|
<TableCell>{req.organization}</TableCell>
|
||||||
@@ -302,8 +334,11 @@ export default function DeveloperRequestPage() {
|
|||||||
setIsRequestModalOpen(false);
|
setIsRequestModalOpen(false);
|
||||||
}}
|
}}
|
||||||
tenantId={tenantId || ""}
|
tenantId={tenantId || ""}
|
||||||
initialName={(userProfile?.name as string) || ""}
|
initialName={profileName}
|
||||||
initialOrg={(userProfile?.companyCode as string) || ""}
|
initialOrg={organizationName}
|
||||||
|
initialEmail={profileEmail}
|
||||||
|
initialPhone={profilePhone}
|
||||||
|
initialRole={profileRoleLabel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -352,6 +387,9 @@ interface RequestAccessModalProps {
|
|||||||
tenantId: string;
|
tenantId: string;
|
||||||
initialName: string;
|
initialName: string;
|
||||||
initialOrg: string;
|
initialOrg: string;
|
||||||
|
initialEmail: string;
|
||||||
|
initialPhone: string;
|
||||||
|
initialRole: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestAccessModal({
|
function RequestAccessModal({
|
||||||
@@ -361,11 +399,20 @@ function RequestAccessModal({
|
|||||||
tenantId,
|
tenantId,
|
||||||
initialName,
|
initialName,
|
||||||
initialOrg,
|
initialOrg,
|
||||||
|
initialEmail,
|
||||||
|
initialPhone,
|
||||||
|
initialRole,
|
||||||
}: RequestAccessModalProps) {
|
}: RequestAccessModalProps) {
|
||||||
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("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
|
setName(initialName);
|
||||||
|
setOrganization(initialOrg);
|
||||||
|
}, [initialName, initialOrg, isOpen]);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: requestDeveloperAccess,
|
mutationFn: requestDeveloperAccess,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -419,7 +466,8 @@ function RequestAccessModal({
|
|||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -430,13 +478,50 @@ function RequestAccessModal({
|
|||||||
<Input
|
<Input
|
||||||
id="org"
|
id="org"
|
||||||
value={organization}
|
value={organization}
|
||||||
onChange={(e) => setOrganization(e.target.value)}
|
readOnly
|
||||||
|
className="focus-visible:ring-0 focus-visible:ring-offset-0 focus-visible:border-input"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="reason">
|
<Label htmlFor="reason">
|
||||||
{t("ui.dev.request.modal.reason", "신청 사유")}
|
{t("ui.dev.request.modal.reason", "신청 사유")}{" "}
|
||||||
|
<span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="reason"
|
id="reason"
|
||||||
@@ -446,7 +531,7 @@ function RequestAccessModal({
|
|||||||
"ui.dev.request.modal.reason_placeholder",
|
"ui.dev.request.modal.reason_placeholder",
|
||||||
"예: 자체 서비스 연동 및 테스트용 OIDC 클라이언트 생성이 필요합니다.",
|
"예: 자체 서비스 연동 및 테스트용 OIDC 클라이언트 생성이 필요합니다.",
|
||||||
)}
|
)}
|
||||||
className="min-h-[120px] resize-none"
|
className="min-h-[120px] resize-none border-primary/50 bg-background focus-visible:ring-primary/40"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -401,6 +401,9 @@ export type DeveloperRequest = {
|
|||||||
tenantId: string;
|
tenantId: string;
|
||||||
name: string;
|
name: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
role?: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
status: DeveloperRequestStatus;
|
status: DeveloperRequestStatus;
|
||||||
adminNotes?: string;
|
adminNotes?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user