diff --git a/devfront/src/features/clients/ClientConsentsPage.tsx b/devfront/src/features/clients/ClientConsentsPage.tsx
index de941fd7..c2498d7d 100644
--- a/devfront/src/features/clients/ClientConsentsPage.tsx
+++ b/devfront/src/features/clients/ClientConsentsPage.tsx
@@ -29,6 +29,7 @@ import {
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";
+import { ClientDetailTabs } from "./ClientDetailTabs";
function ClientConsentsPage() {
const params = useParams();
@@ -214,29 +215,7 @@ function ClientConsentsPage() {
-
-
- {t("ui.dev.clients.details.tab.connection", "Federation")}
-
-
- {t("ui.dev.clients.details.tab.consents", "Consent & Users")}
-
-
- {t("ui.dev.clients.details.tab.settings", "Settings")}
-
-
- {t("ui.dev.clients.details.tab.relationships", "Relationships")}
-
-
+
diff --git a/devfront/src/features/clients/ClientDetailTabs.tsx b/devfront/src/features/clients/ClientDetailTabs.tsx
new file mode 100644
index 00000000..8b5372c1
--- /dev/null
+++ b/devfront/src/features/clients/ClientDetailTabs.tsx
@@ -0,0 +1,54 @@
+import { Link } from "react-router-dom";
+import { t } from "../../lib/i18n";
+import { cn } from "../../lib/utils";
+
+type ClientDetailTab = "connection" | "consents" | "settings" | "relationships";
+
+interface ClientDetailTabsProps {
+ activeTab: ClientDetailTab;
+ clientId: string;
+}
+
+const tabOrder: Array<{
+ key: ClientDetailTab;
+ href: (clientId: string) => string;
+}> = [
+ { key: "connection", href: (clientId) => `/clients/${clientId}` },
+ { key: "consents", href: (clientId) => `/clients/${clientId}/consents` },
+ { key: "settings", href: (clientId) => `/clients/${clientId}/settings` },
+ {
+ key: "relationships",
+ href: (clientId) => `/clients/${clientId}/relationships`,
+ },
+];
+
+export function ClientDetailTabs({
+ activeTab,
+ clientId,
+}: ClientDetailTabsProps) {
+ return (
+
+ {tabOrder.map((tab) => {
+ const isActive = tab.key === activeTab;
+ return isActive ? (
+
+ {t(`ui.dev.clients.details.tab.${tab.key}`)}
+
+ ) : (
+
+ {t(`ui.dev.clients.details.tab.${tab.key}`)}
+
+ );
+ })}
+
+ );
+}
diff --git a/devfront/src/features/clients/ClientDetailsPage.tsx b/devfront/src/features/clients/ClientDetailsPage.tsx
index eabdfc1c..7d6825fb 100644
--- a/devfront/src/features/clients/ClientDetailsPage.tsx
+++ b/devfront/src/features/clients/ClientDetailsPage.tsx
@@ -38,6 +38,7 @@ import {
} from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";
+import { ClientDetailTabs } from "./ClientDetailTabs";
function ClientDetailsPage() {
const params = useParams();
@@ -253,32 +254,7 @@ function ClientDetailsPage() {
: t("msg.common.loading", "Loading...")}
-
-
- {t("ui.dev.clients.details.tab.connection", "Federation")}
-
-
- {t("ui.dev.clients.details.tab.consents", "Consent & Users")}
-
-
- {t("ui.dev.clients.details.tab.settings", "Settings")}
-
-
- {t("ui.dev.clients.details.tab.relationships", "Relationships")}
-
-
+
diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx
index 2da52255..dedff139 100644
--- a/devfront/src/features/clients/ClientGeneralPage.tsx
+++ b/devfront/src/features/clients/ClientGeneralPage.tsx
@@ -43,6 +43,7 @@ import type {
} from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";
+import { ClientDetailTabs } from "./ClientDetailTabs";
interface ScopeItem {
id: string;
@@ -665,33 +666,9 @@ function ClientGeneralPage() {
)}
-
- {!isCreate && (
- <>
-
- {t("ui.dev.clients.details.tab.connection", "Federation")}
-
-
- {t("ui.dev.clients.details.tab.consents", "Consent & Users")}
-
-
- {t("ui.dev.clients.details.tab.relationships", "Relationships")}
-
-
- {t("ui.dev.clients.details.tab.settings", "Settings")}
-
- >
- )}
-
+ {!isCreate && (
+
+ )}
{/* 1. Application Identity */}
diff --git a/devfront/src/features/clients/ClientRelationsPage.tsx b/devfront/src/features/clients/ClientRelationsPage.tsx
index bb0ee916..af1120f7 100644
--- a/devfront/src/features/clients/ClientRelationsPage.tsx
+++ b/devfront/src/features/clients/ClientRelationsPage.tsx
@@ -30,6 +30,7 @@ import {
removeClientRelation,
} from "../../lib/devApi";
import { t } from "../../lib/i18n";
+import { ClientDetailTabs } from "./ClientDetailTabs";
const relationOptions = [
"admins",
@@ -48,9 +49,8 @@ function ClientRelationsPage() {
const params = useParams();
const queryClient = useQueryClient();
const clientId = params.id ?? "";
- const [relation, setRelation] = useState<(typeof relationOptions)[number]>(
- "config_editor",
- );
+ const [relation, setRelation] =
+ useState<(typeof relationOptions)[number]>("config_editor");
const [userId, setUserId] = useState("");
const { data: clientData } = useQuery({
@@ -86,7 +86,9 @@ function ClientRelationsPage() {
userId: userId.trim(),
}),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["client-relations", clientId] });
+ queryClient.invalidateQueries({
+ queryKey: ["client-relations", clientId],
+ });
setUserId("");
toast(
t(
@@ -115,7 +117,9 @@ function ClientRelationsPage() {
mutationFn: (payload: { relation: string; subject: string }) =>
removeClientRelation(clientId, payload.relation, payload.subject),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["client-relations", clientId] });
+ queryClient.invalidateQueries({
+ queryKey: ["client-relations", clientId],
+ });
toast(
t(
"msg.dev.clients.relationships.removed",
@@ -191,10 +195,7 @@ function ClientRelationsPage() {
{clientData?.client?.name || clientId}
/
- {t(
- "ui.dev.clients.details.tab.relationships",
- "Relationships",
- )}
+ {t("ui.dev.clients.details.tab.relationships", "Relationships")}
@@ -231,29 +232,7 @@ function ClientRelationsPage() {
-
-
- {t("ui.dev.clients.details.tab.connection", "Federation")}
-
-
- {t("ui.dev.clients.details.tab.consents", "Consent & Users")}
-
-
- {t("ui.dev.clients.details.tab.settings", "Settings")}
-
-
- {t("ui.dev.clients.details.tab.relationships", "Relationships")}
-
-
+
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml
index 87256821..e9e51705 100644
--- a/devfront/src/locales/en.toml
+++ b/devfront/src/locales/en.toml
@@ -1363,6 +1363,7 @@ title = "Security Note"
connection = "Federation"
consents = "Consent & Users"
settings = "Settings"
+relationships = "Relationships"
[ui.dev.clients.general]
create = "Create Application"
diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml
index 2ac3c74a..00ec0f3d 100644
--- a/devfront/src/locales/ko.toml
+++ b/devfront/src/locales/ko.toml
@@ -1362,6 +1362,7 @@ title = "보안 메모"
connection = "연동 설정"
consents = "동의 및 사용자"
settings = "설정"
+relationships = "관계"
[ui.dev.clients.general]
create = "앱 생성"
diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml
index 7bff5ee6..d6252524 100644
--- a/devfront/src/locales/template.toml
+++ b/devfront/src/locales/template.toml
@@ -1363,6 +1363,7 @@ title = ""
connection = ""
consents = ""
settings = ""
+relationships = ""
[ui.dev.clients.general]
create = ""
diff --git a/devfront/tests/devfront-client-tabs.spec.ts b/devfront/tests/devfront-client-tabs.spec.ts
new file mode 100644
index 00000000..69d0b5a0
--- /dev/null
+++ b/devfront/tests/devfront-client-tabs.spec.ts
@@ -0,0 +1,68 @@
+import { type Page, expect, test } from "@playwright/test";
+import {
+ type ClientRelation,
+ type Consent,
+ installDevApiMock,
+ makeClient,
+ seedAuth,
+} from "./helpers/devfront-fixtures";
+
+function expectClientTabsOrder(pagePath: string, expectedActive: RegExp) {
+ return async ({ page }: { page: Page }) => {
+ const state = {
+ clients: [makeClient("client-tabs", { name: "탭 테스트 앱" })],
+ consents: [] as Consent[],
+ relations: {
+ "client-tabs": [
+ {
+ relation: "config_editor",
+ subject: "User:user-1",
+ subjectType: "User",
+ subjectId: "user-1",
+ },
+ ] satisfies ClientRelation[],
+ },
+ auditLogsByCursor: undefined,
+ };
+ await installDevApiMock(page, state);
+
+ await page.goto(pagePath);
+
+ const header = page
+ .locator("header")
+ .filter({ hasText: "탭 테스트 앱" })
+ .first();
+ const tabs = header.locator(
+ "div.border-b.border-border .whitespace-nowrap",
+ );
+
+ await expect(tabs).toHaveText([
+ "연동 설정",
+ "동의 및 사용자",
+ "설정",
+ "관계",
+ ]);
+
+ await expect(
+ header
+ .locator("div.border-b.border-border .text-primary")
+ .filter({ hasText: expectedActive }),
+ ).toHaveCount(1);
+ };
+}
+
+test.describe("DevFront client detail tabs", () => {
+ test.beforeEach(async ({ page }) => {
+ await seedAuth(page, "rp_admin");
+ });
+
+ test(
+ "settings page keeps tab order and uses localized relationships label",
+ expectClientTabsOrder("/clients/client-tabs/settings", /^설정$/),
+ );
+
+ test(
+ "relationships page keeps tab order and uses localized relationships label",
+ expectClientTabsOrder("/clients/client-tabs/relationships", /^관계$/),
+ );
+});