1
0
forked from baron/baron-sso

feat: integrate orgfront and expose internal ids

This commit is contained in:
2026-04-30 09:33:39 +09:00
parent 02375af08d
commit 9ce7a67f58
116 changed files with 22992 additions and 33 deletions

View File

@@ -0,0 +1,219 @@
import { useQuery } from "@tanstack/react-query";
import {
Briefcase,
Building2,
Fingerprint,
Mail,
Shield,
User,
} from "lucide-react";
import { useState } from "react";
import { useAuth } from "react-oidc-context";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../components/ui/card";
import { t } from "../../lib/i18n";
import { fetchMe } from "../auth/authApi";
import ProfileTenantSwitcher from "./ProfileTenantSwitcher";
function ProfilePage() {
const auth = useAuth();
const hasAccessToken = Boolean(auth.user?.access_token);
const {
data: profile,
isLoading,
error,
} = useQuery({
queryKey: ["userMe"],
queryFn: fetchMe,
enabled: hasAccessToken,
});
const [activeTab, setActiveTab] = useState<"basic" | "role">("basic");
if (isLoading) {
return (
<div className="p-8 text-center">
{t("ui.dev.profile.loading", "Loading profile...")}
</div>
);
}
if (error || !profile) {
return (
<div className="p-8 text-center text-red-500">
{t("ui.dev.profile.error", "Failed to load profile information.")}
</div>
);
}
// Fallback to token information if API data is incomplete
const displayTenant =
profile.tenant?.name ||
profile.tenantId ||
auth.user?.profile?.tenant_id?.toString() ||
"-";
const displayCompanyCode =
profile.companyCode || auth.user?.profile?.companyCode?.toString() || "-";
return (
<div className="space-y-6 max-w-4xl mx-auto">
<div>
<h1 className="text-3xl font-black tracking-tight">
{t("ui.dev.profile.title", "내 정보")}
</h1>
<p className="text-muted-foreground mt-2">
{t(
"ui.dev.profile.subtitle",
"사용자 상세 정보 및 할당된 역할(Role)을 확인합니다.",
)}
</p>
</div>
<div className="flex space-x-1 border-b border-border pb-px">
<button
type="button"
onClick={() => setActiveTab("basic")}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === "basic"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
}`}
>
{t("ui.dev.profile.tab.basic", "기본 정보")}
</button>
<button
type="button"
onClick={() => setActiveTab("role")}
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === "role"
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-foreground hover:border-border"
}`}
>
{t("ui.dev.profile.tab.role", "권한 및 역할")}
</button>
</div>
<div className="pt-4">
{activeTab === "basic" && (
<div className="grid gap-6 md:grid-cols-2">
<Card className="glass-panel">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<User className="h-5 w-5 text-primary" />
{t("ui.dev.profile.basic.title", "사용자 정보")}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Fingerprint className="h-4 w-4" />
{t("ui.dev.profile.basic.id", "User ID")}
</p>
<p className="text-sm break-all font-mono bg-muted/50 p-2 rounded-md">
{profile.id}
</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<User className="h-4 w-4" />
{t("ui.dev.profile.basic.name", "Name")}
</p>
<p className="text-sm">{profile.name}</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Mail className="h-4 w-4" />
{t("ui.dev.profile.basic.email", "Email")}
</p>
<p className="text-sm">{profile.email}</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Briefcase className="h-4 w-4" />
{t("ui.dev.profile.basic.phone", "Phone")}
</p>
<p className="text-sm">{profile.phone || "-"}</p>
</div>
</CardContent>
</Card>
<Card className="glass-panel">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Building2 className="h-5 w-5 text-primary" />
{t("ui.dev.profile.org.title", "조직 정보")}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">
{t("ui.dev.profile.org.tenant", "테넌트")}
</p>
<p className="text-sm">{displayTenant}</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium text-muted-foreground">
{t("ui.dev.profile.org.company_code", "회사 코드")}
</p>
<p className="text-sm">{displayCompanyCode}</p>
</div>
</CardContent>
</Card>
<ProfileTenantSwitcher />
</div>
)}
{activeTab === "role" && (
<Card className="glass-panel">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<Shield className="h-5 w-5 text-primary" />
{t("ui.dev.profile.role.title", "시스템 역할")}
</CardTitle>
<CardDescription>
{t(
"ui.dev.profile.role.description",
"현재 계정에 부여된 권한 등급입니다.",
)}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4 bg-muted/30 p-4 rounded-lg border border-border">
<div className="h-12 w-12 rounded-full bg-primary/20 flex items-center justify-center text-primary shrink-0">
<Briefcase className="h-6 w-6" />
</div>
<div className="flex flex-col gap-1 w-full">
<p className="text-sm text-muted-foreground font-medium uppercase tracking-wider">
{t("ui.dev.profile.role.current", "Current Role")}
</p>
<p className="text-xl font-bold mt-1">
{t(
`ui.common.role.${profile.role}`,
profile.role.toUpperCase(),
)}
</p>
<p className="mt-1 text-sm text-muted-foreground">
{t(
`ui.dev.profile.role.desc_${profile.role}`,
"시스템 역할에 대한 설명이 제공되지 않았습니다.",
)}
</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</div>
);
}
export default ProfilePage;

View File

@@ -0,0 +1,92 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Building2, Save } from "lucide-react";
import { useState } from "react";
import { Button } from "../../components/ui/button";
import { toast } from "../../components/ui/use-toast";
import { fetchMyTenants } from "../../lib/devApi";
import { t } from "../../lib/i18n";
export default function ProfileTenantSwitcher() {
const queryClient = useQueryClient();
const { data: tenants, isLoading } = useQuery({
queryKey: ["myTenants"],
queryFn: fetchMyTenants,
});
const [selectedTenantId, setSelectedTenantId] = useState<string>(() => {
return window.localStorage.getItem("dev_tenant_id") || "";
});
const handleSave = () => {
window.localStorage.setItem("dev_tenant_id", selectedTenantId);
// Invalidate queries to refresh data with new tenant context
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] !== "userMe" && query.queryKey[0] !== "myTenants",
});
toast(t("ui.dev.tenant.switch_success", "테넌트 전환 완료"), "success");
};
if (isLoading || !tenants || tenants.length === 0) {
return null;
}
// If there's only one tenant, the user doesn't need to switch.
// Still show it as read-only or hidden. Let's just show it as disabled.
const isSingleTenant = tenants.length <= 1;
return (
<div className="flex flex-col gap-4 mt-6 p-4 rounded-lg border border-border bg-card">
<div className="flex items-center gap-2 mb-2">
<Building2 className="h-5 w-5 text-primary" />
<h3 className="font-semibold">
{t("ui.dev.tenant.workspace", "작업 테넌트 (컨텍스트)")}
</h3>
</div>
<p className="text-sm text-muted-foreground -mt-2 mb-2">
{t(
"ui.dev.tenant.workspace_desc",
"현재 작업 중인 테넌트를 선택하고 저장하여 API 요청 컨텍스트를 변경합니다.",
)}
</p>
<div className="flex items-center gap-3">
<select
aria-label={t("ui.dev.tenant.workspace", "작업 테넌트 (컨텍스트)")}
value={selectedTenantId}
onChange={(e) => setSelectedTenantId(e.target.value)}
disabled={isSingleTenant}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
{tenants.map((tenant) => (
<option key={tenant.id} value={tenant.id}>
{tenant.name}
</option>
))}
</select>
<Button
type="button"
onClick={handleSave}
disabled={isSingleTenant}
className="gap-2"
>
<Save size={16} />
{t("ui.common.save", "저장")}
</Button>
</div>
{isSingleTenant && (
<p className="text-xs text-muted-foreground mt-1">
{t(
"ui.dev.tenant.single_notice",
"단일 테넌트에 소속되어 전환할 필요가 없습니다.",
)}
</p>
)}
</div>
);
}