From 2c5eed17742301a425d58296617997502c82e054 Mon Sep 17 00:00:00 2001
From: kyy
Date: Tue, 2 Jun 2026 11:46:40 +0900
Subject: [PATCH] =?UTF-8?q?75f192fb24=20=EA=B8=B0=EC=A4=80=20=EB=B3=91?=
=?UTF-8?q?=ED=95=A9=20code-check=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Makefile | 17 +-
devfront/playwright.config.ts | 4 +-
.../src/features/audit/AuditLogsPage.test.tsx | 8 +-
.../src/features/clients/ClientsPage.test.tsx | 38 +++--
devfront/src/features/clients/ClientsPage.tsx | 19 +--
.../clients/components/ClientLogo.test.tsx | 2 +-
.../routes/ClientFederationPage.test.tsx | 28 ++--
.../DeveloperRequestPage.test.tsx | 6 +-
.../features/overview/GlobalOverviewPage.tsx | 103 ++++++------
.../overview/recentClientChanges.test.ts | 148 ++++++++++++------
.../features/overview/recentClientChanges.ts | 4 +-
devfront/src/lib/apiClient.test.ts | 14 +-
devfront/src/locales/en.toml | 1 +
devfront/src/locales/ko.toml | 1 +
devfront/src/locales/template.toml | 1 +
devfront/tests/clients.spec.ts | 38 +++--
userfront/pubspec.lock | 32 ++--
17 files changed, 276 insertions(+), 188 deletions(-)
diff --git a/Makefile b/Makefile
index 941b14b2..25346795 100644
--- a/Makefile
+++ b/Makefile
@@ -280,7 +280,11 @@ code-check-front-lint:
cd adminfront && npx biome format .
@echo "==> devfront biome lint/format check"
rm -rf devfront/playwright-report devfront/test-results
- cd devfront && npm ci --ignore-scripts
+ @if [ -d devfront/node_modules ]; then \
+ echo "devfront/node_modules already present; skipping npm install."; \
+ else \
+ cd devfront && npm ci --ignore-scripts; \
+ fi
cd devfront && npx biome lint .
cd devfront && npx biome format .
@echo "==> orgfront biome lint/format check"
@@ -324,7 +328,14 @@ code-check-devfront-tests:
@mkdir -p reports/devfront
@rm -rf reports/devfront/playwright-report reports/devfront/test-results
@status=0; \
- (cd devfront && npm ci --ignore-scripts) || status=$$?; \
+ preview_pattern='[v]ite preview --host 127.0.0.1 --strictPort --port 4174'; \
+ pkill -f "$$preview_pattern" >/dev/null 2>&1 || true; \
+ trap 'pkill -f "$$preview_pattern" >/dev/null 2>&1 || true' EXIT INT TERM; \
+ if [ -d devfront/node_modules ]; then \
+ echo "devfront/node_modules already present; skipping npm install."; \
+ else \
+ (cd devfront && npm ci --ignore-scripts) || status=$$?; \
+ fi; \
if [ $$status -eq 0 ]; then \
(cd devfront && $(PLAYWRIGHT_INSTALL_ALL)) || status=$$?; \
fi; \
@@ -388,7 +399,7 @@ code-check-userfront-e2e-tests:
(cd "$$tmp_dir/userfront" && flutter build web --wasm --release) || status=$$?; \
fi; \
if [ $$status -eq 0 ]; then \
- (cd "$$tmp_dir/userfront-e2e" && $(PLAYWRIGHT_INSTALL_CHROMIUM)) || status=$$?; \
+ (cd "$$tmp_dir/userfront-e2e" && $(PLAYWRIGHT_INSTALL_ALL)) || status=$$?; \
fi; \
if [ $$status -eq 0 ]; then \
port="$$(node -e "const net=require('node:net'); const s=net.createServer(); s.listen(0,'127.0.0.1',()=>{console.log(s.address().port); s.close();});")"; \
diff --git a/devfront/playwright.config.ts b/devfront/playwright.config.ts
index a792b3f5..cfb1eb55 100644
--- a/devfront/playwright.config.ts
+++ b/devfront/playwright.config.ts
@@ -13,7 +13,7 @@ const configuredWorkers = process.env.PLAYWRIGHT_WORKERS
const skipWebServer =
process.env.PLAYWRIGHT_SKIP_WEBSERVER === "1" ||
process.env.PLAYWRIGHT_SKIP_WEBSERVER === "true";
-const baseURL = process.env.PLAYWRIGHT_BASE_URL || "http://127.0.0.1:5176";
+const baseURL = process.env.PLAYWRIGHT_BASE_URL || "http://127.0.0.1:4174";
/**
* Read environment variables from file.
@@ -74,7 +74,7 @@ export default defineConfig({
? undefined
: {
command:
- "VITE_OIDC_AUTHORITY=http://localhost:5000/oidc ./node_modules/.bin/vite build && ./node_modules/.bin/vite preview --host 127.0.0.1 --strictPort --port 5176",
+ "VITE_OIDC_AUTHORITY=http://localhost:5000/oidc ./node_modules/.bin/vite build && ./node_modules/.bin/vite preview --host 127.0.0.1 --strictPort --port 4174",
url: baseURL,
reuseExistingServer: false,
},
diff --git a/devfront/src/features/audit/AuditLogsPage.test.tsx b/devfront/src/features/audit/AuditLogsPage.test.tsx
index 8e7f3c70..021bbd51 100644
--- a/devfront/src/features/audit/AuditLogsPage.test.tsx
+++ b/devfront/src/features/audit/AuditLogsPage.test.tsx
@@ -158,7 +158,9 @@ describe("AuditLogsPage", () => {
};
const container = await renderPage();
- expect(container.textContent).toContain("감사 로그는 개발자 권한이 있어야 볼 수 있습니다.");
+ expect(container.textContent).toContain(
+ "감사 로그는 개발자 권한이 있어야 볼 수 있습니다.",
+ );
const button = Array.from(container.querySelectorAll("button")).find(
(item) => item.textContent?.includes("개발자 권한 신청"),
@@ -177,7 +179,9 @@ describe("AuditLogsPage", () => {
.spyOn(URL, "createObjectURL")
.mockReturnValue("blob:csv");
const revokeObjectURL = vi.spyOn(URL, "revokeObjectURL").mockReturnValue();
- const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});
+ const clickSpy = vi
+ .spyOn(HTMLAnchorElement.prototype, "click")
+ .mockImplementation(() => {});
const container = await renderPage();
expect(container.textContent).toContain("table:1");
diff --git a/devfront/src/features/clients/ClientsPage.test.tsx b/devfront/src/features/clients/ClientsPage.test.tsx
index eba48ea8..6c99d8f3 100644
--- a/devfront/src/features/clients/ClientsPage.test.tsx
+++ b/devfront/src/features/clients/ClientsPage.test.tsx
@@ -31,9 +31,10 @@ vi.mock("react-oidc-context", () => ({
}));
vi.mock("react-router-dom", async () => {
- const actual = await vi.importActual(
- "react-router-dom",
- );
+ const actual =
+ await vi.importActual(
+ "react-router-dom",
+ );
return {
...actual,
useNavigate: () => navigateMock,
@@ -175,7 +176,9 @@ describe("ClientsPage", () => {
});
const container = await renderPage();
- expect(container.textContent).toContain("총 6개의 애플리케이션이 등록되어 있습니다.");
+ expect(container.textContent).toContain(
+ "총 6개의 애플리케이션이 등록되어 있습니다.",
+ );
expect(container.textContent).toContain("App 6");
expect(container.textContent).toContain("App 2");
expect(container.textContent).not.toContain("App 1");
@@ -192,26 +195,27 @@ describe("ClientsPage", () => {
expect(container.textContent).toContain("App 6");
expect(container.textContent).toContain("접기");
- const advancedButton = Array.from(container.querySelectorAll("button")).find(
- (button) => button.textContent === "Advanced Filters",
- );
+ const advancedButton = Array.from(
+ container.querySelectorAll("button"),
+ ).find((button) => button.textContent === "Advanced Filters");
expect(advancedButton).toBeTruthy();
await act(async () => {
- advancedButton?.dispatchEvent(
- new MouseEvent("click", { bubbles: true }),
- );
+ advancedButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
- const searchInput = Array.from(
- container.querySelectorAll("input"),
- ).find((input) =>
- input.getAttribute("placeholder")?.includes("클라이언트 이름/ID로 검색"),
+ const searchInput = Array.from(container.querySelectorAll("input")).find(
+ (input) =>
+ input
+ .getAttribute("placeholder")
+ ?.includes("클라이언트 이름/ID로 검색"),
) as HTMLInputElement | undefined;
- expect(searchInput).toBeTruthy();
+ if (!searchInput) {
+ throw new Error("Expected search input to be rendered");
+ }
await act(async () => {
- await setInputValue(searchInput!, "missing-client");
+ await setInputValue(searchInput, "missing-client");
});
expect(container.textContent).toContain("조건에 맞는 연동 앱이 없습니다.");
@@ -226,7 +230,7 @@ describe("ClientsPage", () => {
});
await act(async () => {
- await setInputValue(searchInput!, "");
+ await setInputValue(searchInput, "");
});
expect(container.textContent).toContain("App 1");
diff --git a/devfront/src/features/clients/ClientsPage.tsx b/devfront/src/features/clients/ClientsPage.tsx
index 2057783e..c82680f6 100644
--- a/devfront/src/features/clients/ClientsPage.tsx
+++ b/devfront/src/features/clients/ClientsPage.tsx
@@ -532,10 +532,12 @@ function ClientsPage() {
t("ui.dev.clients.untitled", "Untitled")}
- {t(
- "ui.dev.clients.tenant_scoped",
- "Tenant-scoped",
- )}
+
+ {t(
+ "ui.dev.clients.tenant_scoped",
+ "Tenant-scoped",
+ )}
+
@@ -615,14 +617,9 @@ function ClientsPage() {
"ui.dev.clients.list.collapse_aria",
"연동 앱 목록 접기",
)
- : t(
- "ui.dev.clients.list.more_aria",
- "연동 앱 목록 더보기",
- )
- }
- onClick={() =>
- setIsClientListExpanded((current) => !current)
+ : t("ui.dev.clients.list.more_aria", "연동 앱 목록 더보기")
}
+ onClick={() => setIsClientListExpanded((current) => !current)}
>
{isClientListExpanded
? t("ui.common.collapse", "접기")
diff --git a/devfront/src/features/clients/components/ClientLogo.test.tsx b/devfront/src/features/clients/components/ClientLogo.test.tsx
index 353c9f27..1fbe8fde 100644
--- a/devfront/src/features/clients/components/ClientLogo.test.tsx
+++ b/devfront/src/features/clients/components/ClientLogo.test.tsx
@@ -8,7 +8,7 @@ vi.mock("../../../components/ui/avatar", () => ({
Avatar: ({ children }: { children: ReactNode }) => (
{children}
),
- AvatarImage: (props: ComponentProps<"img">) =>
,
+ AvatarImage: (props: ComponentProps<"img">) =>
,
AvatarFallback: ({ children }: { children: ReactNode }) => (
{children}
),
diff --git a/devfront/src/features/clients/routes/ClientFederationPage.test.tsx b/devfront/src/features/clients/routes/ClientFederationPage.test.tsx
index d1dace77..f97300cd 100644
--- a/devfront/src/features/clients/routes/ClientFederationPage.test.tsx
+++ b/devfront/src/features/clients/routes/ClientFederationPage.test.tsx
@@ -9,9 +9,10 @@ const listIdpConfigsMock = vi.fn();
const createIdpConfigMock = vi.fn();
vi.mock("react-router-dom", async () => {
- const actual = await vi.importActual(
- "react-router-dom",
- );
+ const actual =
+ await vi.importActual(
+ "react-router-dom",
+ );
return {
...actual,
useParams: () => params,
@@ -19,10 +20,8 @@ vi.mock("react-router-dom", async () => {
});
vi.mock("../../../lib/devApi", () => ({
- listIdpConfigsForClient: (clientId: string) =>
- listIdpConfigsMock(clientId),
- createIdpConfigForClient: (payload: unknown) =>
- createIdpConfigMock(payload),
+ listIdpConfigsForClient: (clientId: string) => listIdpConfigsMock(clientId),
+ createIdpConfigForClient: (payload: unknown) => createIdpConfigMock(payload),
}));
vi.mock("../../../lib/i18n", () => ({
@@ -146,16 +145,15 @@ describe("ClientFederationPage", () => {
'input[name="oidc_client_secret"]',
) as HTMLInputElement | null;
- expect(displayName).toBeTruthy();
- expect(issuerUrl).toBeTruthy();
- expect(clientId).toBeTruthy();
- expect(clientSecret).toBeTruthy();
+ if (!displayName || !issuerUrl || !clientId || !clientSecret) {
+ throw new Error("Expected federation form inputs to be rendered");
+ }
await act(async () => {
- await setInputValue(displayName!, "New Provider");
- await setInputValue(issuerUrl!, "https://login.example");
- await setInputValue(clientId!, "client-oidc");
- await setInputValue(clientSecret!, "secret-value");
+ await setInputValue(displayName, "New Provider");
+ await setInputValue(issuerUrl, "https://login.example");
+ await setInputValue(clientId, "client-oidc");
+ await setInputValue(clientSecret, "secret-value");
});
const submitButton = Array.from(container.querySelectorAll("button")).find(
diff --git a/devfront/src/features/developer-request/DeveloperRequestPage.test.tsx b/devfront/src/features/developer-request/DeveloperRequestPage.test.tsx
index 7616e525..a357c1ec 100644
--- a/devfront/src/features/developer-request/DeveloperRequestPage.test.tsx
+++ b/devfront/src/features/developer-request/DeveloperRequestPage.test.tsx
@@ -159,10 +159,12 @@ describe("DeveloperRequestPage", () => {
const reasonField = container.querySelector(
"textarea",
) as HTMLTextAreaElement | null;
- expect(reasonField).toBeTruthy();
+ if (!reasonField) {
+ throw new Error("Expected reason textarea to be rendered");
+ }
await act(async () => {
- await setTextAreaValue(reasonField!, "Need RP access");
+ await setTextAreaValue(reasonField, "Need RP access");
});
const submitButton = Array.from(container.querySelectorAll("button")).find(
diff --git a/devfront/src/features/overview/GlobalOverviewPage.tsx b/devfront/src/features/overview/GlobalOverviewPage.tsx
index 4ab57bc1..77bae531 100644
--- a/devfront/src/features/overview/GlobalOverviewPage.tsx
+++ b/devfront/src/features/overview/GlobalOverviewPage.tsx
@@ -119,9 +119,7 @@ function resolveAppLocale(): AppLocale {
return pathLocale;
}
- return window.navigator.language.toLowerCase().startsWith("ko")
- ? "ko"
- : "en";
+ return window.navigator.language.toLowerCase().startsWith("ko") ? "ko" : "en";
}
function formatRecentChangeTimestamp(value: string) {
@@ -390,7 +388,10 @@ function summarizeRecentChanges(
items: RecentClientChange[],
period: RPUsagePeriod,
): RecentChangePoint[] {
- const byDate = new Map }>();
+ const byDate = new Map<
+ string,
+ { changeCount: number; actors: Set }
+ >();
for (const item of items) {
const bucket = toPeriodBucket(item.timestamp.slice(0, 10), period);
const current = byDate.get(bucket) ?? {
@@ -447,7 +448,9 @@ function buildRecentChangeSeries(
items: RecentClientChange[],
period: RPUsagePeriod,
): RecentChangeSeries[] {
- const dates = summarizeRecentChanges(items, period).map((point) => point.date);
+ const dates = summarizeRecentChanges(items, period).map(
+ (point) => point.date,
+ );
const byClient = new Map<
string,
{
@@ -937,7 +940,7 @@ function GlobalOverviewPage() {
const [visibleRecentClientChangesCount, setVisibleRecentClientChangesCount] =
useState(6);
const [isRecentChangesDetailOpen, setIsRecentChangesDetailOpen] =
- useState(false);
+ useState(true);
const usageDays = period === "day" ? 14 : period === "week" ? 84 : 90;
const statsQuery = useQuery({
queryKey: ["dev-dashboard-stats"],
@@ -1112,40 +1115,34 @@ function GlobalOverviewPage() {
),
[currentClientIdSet, recentClientChangesWithActors],
);
- const recentChangeFilterOptions = useMemo(
- () => {
- const activeOptions = Array.from(
- new Map(
- recentClientChangesWithActors
- .filter((item) => currentClientIdSet.has(item.clientId))
- .map((item) => [
- item.clientId,
- { id: item.clientId, label: item.clientName },
- ]),
- ).values(),
- ).sort((left, right) => left.label.localeCompare(right.label));
+ const recentChangeFilterOptions = useMemo(() => {
+ const activeOptions = Array.from(
+ new Map(
+ recentClientChangesWithActors
+ .filter((item) => currentClientIdSet.has(item.clientId))
+ .map((item) => [
+ item.clientId,
+ { id: item.clientId, label: item.clientName },
+ ]),
+ ).values(),
+ ).sort((left, right) => left.label.localeCompare(right.label));
- if (deletedRecentChangeClientIds.length === 0) {
- return activeOptions;
- }
+ if (deletedRecentChangeClientIds.length === 0) {
+ return activeOptions;
+ }
- return [
- ...activeOptions,
- {
- id: deletedRecentChangeFilterId,
- label: t(
- "ui.dev.dashboard.recent_changes.deleted_group",
- "삭제된 앱",
- ),
- },
- ];
- },
- [
- currentClientIdSet,
- deletedRecentChangeClientIds.length,
- recentClientChangesWithActors,
- ],
- );
+ return [
+ ...activeOptions,
+ {
+ id: deletedRecentChangeFilterId,
+ label: t("ui.dev.dashboard.recent_changes.deleted_group", "삭제된 앱"),
+ },
+ ];
+ }, [
+ currentClientIdSet,
+ deletedRecentChangeClientIds.length,
+ recentClientChangesWithActors,
+ ]);
const filteredRecentClientChanges = useMemo(() => {
if (selectedRecentChangeClientIds.length === 0) {
return recentClientChangesWithActors;
@@ -1155,7 +1152,8 @@ function GlobalOverviewPage() {
return recentClientChangesWithActors.filter(
(item) =>
selectedSet.has(item.clientId) ||
- (includeDeletedGroup && deletedRecentChangeClientIds.includes(item.clientId)),
+ (includeDeletedGroup &&
+ deletedRecentChangeClientIds.includes(item.clientId)),
);
}, [
deletedRecentChangeClientIds,
@@ -1163,7 +1161,8 @@ function GlobalOverviewPage() {
selectedRecentChangeClientIds,
]);
const selectedRecentChangeSeries = useMemo(
- () => buildRecentChangeSeries(filteredRecentClientChanges, recentChangesPeriod),
+ () =>
+ buildRecentChangeSeries(filteredRecentClientChanges, recentChangesPeriod),
[filteredRecentClientChanges, recentChangesPeriod],
);
const recentChangedClientCount = useMemo(
@@ -1180,7 +1179,9 @@ function GlobalOverviewPage() {
new Set(
filteredRecentClientChanges
.map((item) => item.clientId)
- .filter((clientId) => deletedRecentChangeClientIds.includes(clientId)),
+ .filter((clientId) =>
+ deletedRecentChangeClientIds.includes(clientId),
+ ),
).size,
[deletedRecentChangeClientIds, filteredRecentClientChanges],
);
@@ -1251,10 +1252,10 @@ function GlobalOverviewPage() {
};
useEffect(() => {
- setVisibleRecentClientChangesCount((current) =>
- Math.min(Math.max(6, current), filteredRecentClientChanges.length),
- );
- }, [filteredRecentClientChanges.length, selectedRecentChangeClientIds]);
+ setVisibleRecentClientChangesCount((current) =>
+ Math.min(Math.max(6, current), filteredRecentClientChanges.length),
+ );
+ }, [filteredRecentClientChanges.length]);
if (isLoadingDeveloperAccessGate) {
return (
@@ -1466,9 +1467,9 @@ function GlobalOverviewPage() {
}
label={t(
- "ui.dev.dashboard.recent_changes.summary.deleted_clients",
- "삭제된 앱 수",
- )}
+ "ui.dev.dashboard.recent_changes.summary.deleted_clients",
+ "삭제된 앱 수",
+ )}
value={deletedRecentChangedClientCount.toLocaleString()}
/>
) : (
visibleRecentClientChanges.map((item) => {
- const { date, time } =
- formatRecentChangeTimestamp(item.timestamp);
+ const { date, time } = formatRecentChangeTimestamp(
+ item.timestamp,
+ );
return (
) : null}
-
);
}
diff --git a/devfront/src/features/overview/recentClientChanges.test.ts b/devfront/src/features/overview/recentClientChanges.test.ts
index 8163f10e..b69c9a28 100644
--- a/devfront/src/features/overview/recentClientChanges.test.ts
+++ b/devfront/src/features/overview/recentClientChanges.test.ts
@@ -56,7 +56,9 @@ describe("recent client changes", () => {
mockLocale("en");
expect(getRecentClientActionLabel("CREATE_CLIENT")).toBe("App creation");
- expect(getRecentClientActionLabel("UPDATE_CLIENT")).toBe("Settings changes");
+ expect(getRecentClientActionLabel("UPDATE_CLIENT")).toBe(
+ "Settings changes",
+ );
expect(getRecentClientActionLabel("UPDATE_CLIENT_STATUS")).toBe(
"Status changes",
);
@@ -64,7 +66,9 @@ describe("recent client changes", () => {
"Client secret rotation",
);
expect(getRecentClientActionLabel("ADD_RELATION")).toBe("Add Relationship");
- expect(getRecentClientActionLabel("REMOVE_RELATION")).toBe("Remove");
+ expect(getRecentClientActionLabel("REMOVE_RELATION")).toBe(
+ "Remove Relationship",
+ );
expect(getRecentClientActionLabel("DELETE_CLIENT")).toBe("App deletion");
expect(getRecentClientActionLabel("OTHER_ACTION")).toBe("OTHER_ACTION");
@@ -90,59 +94,107 @@ describe("recent client changes", () => {
it("builds recent client changes with sorting, filtering, and detail slicing", () => {
mockLocale("ko");
- const clients = [makeClient("client-a", "Alpha"), makeClient("client-b", "")];
+ const clients = [
+ makeClient("client-a", "Alpha"),
+ makeClient("client-b", ""),
+ ];
const auditLogs = [
- makeAuditLog("evt-1", "2026-05-27T07:00:00.000Z", "CREATE_CLIENT", "client-a", {
- after: { name: "Alpha", type: "private", status: "active" },
- }),
- makeAuditLog("evt-2", "2026-05-27T08:00:00.000Z", "UPDATE_CLIENT", "client-a", {
- before: {
- name: "Alpha old",
- status: "inactive",
- sameField: "same",
- oldField: "old-value",
+ makeAuditLog(
+ "evt-1",
+ "2026-05-27T07:00:00.000Z",
+ "CREATE_CLIENT",
+ "client-a",
+ {
+ after: { name: "Alpha", type: "private", status: "active" },
},
- after: {
- name: "Alpha new",
- status: "active",
- sameField: "same",
- newField: "new-value",
+ ),
+ makeAuditLog(
+ "evt-2",
+ "2026-05-27T08:00:00.000Z",
+ "UPDATE_CLIENT",
+ "client-a",
+ {
+ before: {
+ name: "Alpha old",
+ status: "inactive",
+ sameField: "same",
+ oldField: "old-value",
+ },
+ after: {
+ name: "Alpha new",
+ status: "active",
+ sameField: "same",
+ newField: "new-value",
+ },
},
- }),
- makeAuditLog("evt-3", "2026-05-27T09:00:00.000Z", "UPDATE_CLIENT_STATUS", "client-a", {
- before: { status: "inactive" },
- after: { status: "active" },
- }),
- makeAuditLog("evt-4", "2026-05-27T10:00:00.000Z", "ADD_RELATION", "client-b", {
- after: {
- relation: "audit_viewer",
- subject: "User:89692983-f512-4d96-845d-ac6123d08b95",
+ ),
+ makeAuditLog(
+ "evt-3",
+ "2026-05-27T09:00:00.000Z",
+ "UPDATE_CLIENT_STATUS",
+ "client-a",
+ {
+ before: { status: "inactive" },
+ after: { status: "active" },
},
- }),
- makeAuditLog("evt-5", "2026-05-27T11:00:00.000Z", "REMOVE_RELATION", "client-b", {
- before: {
- relation: "admins",
- subject: "User:89692983-f512-4d96-845d-ac6123d08b95",
+ ),
+ makeAuditLog(
+ "evt-4",
+ "2026-05-27T10:00:00.000Z",
+ "ADD_RELATION",
+ "client-b",
+ {
+ after: {
+ relation: "audit_viewer",
+ subject: "User:89692983-f512-4d96-845d-ac6123d08b95",
+ },
},
- }),
- makeAuditLog("evt-6", "2026-05-27T12:00:00.000Z", "ROTATE_SECRET", "client-a", {
- after: {},
- }),
- makeAuditLog("evt-7", "2026-05-27T13:00:00.000Z", "DELETE_CLIENT", "client-a", {
- before: {
- name: "Alpha",
- status: "inactive",
+ ),
+ makeAuditLog(
+ "evt-5",
+ "2026-05-27T11:00:00.000Z",
+ "REMOVE_RELATION",
+ "client-b",
+ {
+ before: {
+ relation: "admins",
+ subject: "User:89692983-f512-4d96-845d-ac6123d08b95",
+ },
},
- }),
- makeAuditLog("evt-8", "2026-05-27T14:00:00.000Z", "UNSUPPORTED_ACTION", "client-a", {
- after: { name: "Ignored" },
- }),
+ ),
+ makeAuditLog(
+ "evt-6",
+ "2026-05-27T12:00:00.000Z",
+ "ROTATE_SECRET",
+ "client-a",
+ {
+ after: {},
+ },
+ ),
+ makeAuditLog(
+ "evt-7",
+ "2026-05-27T13:00:00.000Z",
+ "DELETE_CLIENT",
+ "client-a",
+ {
+ before: {
+ name: "Alpha",
+ status: "inactive",
+ },
+ },
+ ),
+ makeAuditLog(
+ "evt-8",
+ "2026-05-27T14:00:00.000Z",
+ "UNSUPPORTED_ACTION",
+ "client-a",
+ {
+ after: { name: "Ignored" },
+ },
+ ),
];
- const changes = buildRecentClientChanges(
- auditLogs,
- clients,
- );
+ const changes = buildRecentClientChanges(auditLogs, clients);
expect(changes).toHaveLength(7);
expect(changes[0]).toMatchObject({
@@ -164,7 +216,7 @@ describe("recent client changes", () => {
expect(changes[2]).toMatchObject({
eventId: "evt-5",
clientName: "client-b",
- actionLabel: "제외",
+ actionLabel: "관계 삭제",
detailLabels: [
{ label: "관계", value: "admins" },
{
diff --git a/devfront/src/features/overview/recentClientChanges.ts b/devfront/src/features/overview/recentClientChanges.ts
index 534cb86a..2084a226 100644
--- a/devfront/src/features/overview/recentClientChanges.ts
+++ b/devfront/src/features/overview/recentClientChanges.ts
@@ -49,7 +49,7 @@ export function getRecentClientActionLabel(action: string) {
case "ADD_RELATION":
return t("ui.dev.clients.relationships.add_title", "관계 추가");
case "REMOVE_RELATION":
- return t("ui.common.remove", "Remove");
+ return t("ui.dev.clients.relationships.remove_title", "관계 삭제");
case "DELETE_CLIENT":
return t("ui.dev.clients.recent_changes.guide.delete", "앱 삭제");
default:
@@ -68,7 +68,7 @@ function getRecentClientFieldLabel(key: string) {
case "relation":
return t("ui.dev.clients.relationships.relation", "관계");
case "subject":
- return t("ui.dev.clients.relationships.subject", "대상");
+ return t("ui.dev.clients.relationships.subject", "주체");
case "client_secret":
return t(
"ui.dev.clients.details.credentials.client_secret",
diff --git a/devfront/src/lib/apiClient.test.ts b/devfront/src/lib/apiClient.test.ts
index 9458a557..7ac620b9 100644
--- a/devfront/src/lib/apiClient.test.ts
+++ b/devfront/src/lib/apiClient.test.ts
@@ -32,8 +32,9 @@ describe("apiClient", () => {
beforeEach(() => {
vi.resetModules();
vi.stubEnv("MODE", "test");
- (window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })._IS_TEST_MODE =
- true;
+ (
+ window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
+ )._IS_TEST_MODE = true;
window.localStorage.clear();
getUserMock.mockResolvedValue(null);
findPersistedOidcUserMock.mockReturnValue(undefined);
@@ -47,7 +48,8 @@ describe("apiClient", () => {
window.localStorage.setItem("dev_tenant_id", "tenant-1");
const { default: apiClient } = await import("./apiClient");
- const requestHandler = apiClient.interceptors.request.handlers[0]?.fulfilled;
+ const requestHandler =
+ apiClient.interceptors.request.handlers[0]?.fulfilled;
const result = await requestHandler?.({ headers: {} });
@@ -58,7 +60,8 @@ describe("apiClient", () => {
it("rejects non-auth response errors without redirecting", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { default: apiClient } = await import("./apiClient");
- const responseHandler = apiClient.interceptors.response.handlers[0]?.rejected;
+ const responseHandler =
+ apiClient.interceptors.response.handlers[0]?.rejected;
const error = { response: { status: 500, data: { error: "boom" } } };
await expect(responseHandler?.(error)).rejects.toBe(error);
@@ -69,7 +72,8 @@ describe("apiClient", () => {
it("warns and rejects auth failures in test mode", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { default: apiClient } = await import("./apiClient");
- const responseHandler = apiClient.interceptors.response.handlers[0]?.rejected;
+ const responseHandler =
+ apiClient.interceptors.response.handlers[0]?.rejected;
const error = {
response: {
status: 403,
diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml
index 460fb853..92eb923b 100644
--- a/devfront/src/locales/en.toml
+++ b/devfront/src/locales/en.toml
@@ -1601,6 +1601,7 @@ revoke_cache = "Revoke Cache"
[ui.dev.clients.relationships]
title = "Client Relationships"
add_title = "Add Relationship"
+remove_title = "Remove Relationship"
relation = "Relation"
user_id = "User ID"
user_id_placeholder = "kratos user id"
diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml
index 444c776e..2e1963b6 100644
--- a/devfront/src/locales/ko.toml
+++ b/devfront/src/locales/ko.toml
@@ -1600,6 +1600,7 @@ revoke_cache = "캐시 삭제"
[ui.dev.clients.relationships]
title = "클라이언트 관계"
add_title = "관계 추가"
+remove_title = "관계 삭제"
relation = "관계"
user_id = "사용자 ID"
user_id_placeholder = "kratos 사용자 id"
diff --git a/devfront/src/locales/template.toml b/devfront/src/locales/template.toml
index 141eacba..4f3295de 100644
--- a/devfront/src/locales/template.toml
+++ b/devfront/src/locales/template.toml
@@ -1655,6 +1655,7 @@ revoke_cache = ""
[ui.dev.clients.relationships]
title = ""
add_title = ""
+remove_title = ""
relation = ""
user_id = ""
user_id_placeholder = ""
diff --git a/devfront/tests/clients.spec.ts b/devfront/tests/clients.spec.ts
index aa6e1946..af5b5491 100644
--- a/devfront/tests/clients.spec.ts
+++ b/devfront/tests/clients.spec.ts
@@ -37,7 +37,9 @@ test("clients page loads correctly", async ({ page }) => {
// 페이지 내 주요 텍스트 확인
await expect(page.getByText("연동 앱 목록")).toBeVisible();
- await expect(page.getByText("Total Applications", { exact: true })).toHaveCount(0);
+ await expect(
+ page.getByText("Total Applications", { exact: true }),
+ ).toHaveCount(0);
// 테이블 헤더 확인
await expect(
@@ -108,9 +110,7 @@ test("clients page shows only five apps by default and expands with more button"
const clients = Array.from({ length: 6 }, (_, index) =>
makeClient(`client-${index + 1}`, {
name: `Preview App ${index + 1}`,
- createdAt: new Date(
- Date.UTC(2026, 2, 3, 9, 10 - index, 0),
- ).toISOString(),
+ createdAt: new Date(Date.UTC(2026, 2, 3, 9, 10 - index, 0)).toISOString(),
}),
);
@@ -126,9 +126,13 @@ test("clients page shows only five apps by default and expands with more button"
page.getByRole("heading", { name: "연동 앱 목록" }),
).toBeVisible();
await expect(
- page.locator("table").first().locator("tbody tr").filter({
- hasText: /Preview App \d/,
- }),
+ page
+ .locator("table")
+ .first()
+ .locator("tbody tr")
+ .filter({
+ hasText: /Preview App \d/,
+ }),
).toHaveCount(5);
await expect(
page.getByText("Preview App 6", { exact: true }),
@@ -142,13 +146,15 @@ test("clients page shows only five apps by default and expands with more button"
await moreButton.click();
await expect(
- page.locator("table").first().locator("tbody tr").filter({
- hasText: /Preview App \d/,
- }),
+ page
+ .locator("table")
+ .first()
+ .locator("tbody tr")
+ .filter({
+ hasText: /Preview App \d/,
+ }),
).toHaveCount(6);
- await expect(
- page.getByText("Preview App 6", { exact: true }),
- ).toBeVisible();
+ await expect(page.getByText("Preview App 6", { exact: true })).toBeVisible();
await expect(
page.getByRole("button", { name: "연동 앱 목록 더보기" }),
).toHaveCount(0);
@@ -205,15 +211,13 @@ test("overview page shows user-delete relation cleanup in recent changes", async
).toBeVisible();
await expect(page.getByText("관계 삭제", { exact: true })).toBeVisible();
await expect(page.getByText(/관계:\s*config_editor/)).toBeVisible();
- await expect(page.getByText(/대상:\s*User:deleted-user/)).toBeVisible();
+ await expect(page.getByText(/주체:\s*User:deleted-user/)).toBeVisible();
await expect(
page.getByText("cleanup-actor", { exact: true }).first(),
).toBeVisible();
});
-test("clients page no longer shows recent changes card", async ({
- page,
-}) => {
+test("clients page no longer shows recent changes card", async ({ page }) => {
await seedAuth(page, "super_admin");
const clients = Array.from({ length: 6 }, (_, index) =>
makeClient(`client-${index + 1}`, {
diff --git a/userfront/pubspec.lock b/userfront/pubspec.lock
index b23d80a9..8b6fff8c 100644
--- a/userfront/pubspec.lock
+++ b/userfront/pubspec.lock
@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
+ sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
- version: "1.4.1"
+ version: "1.4.0"
cli_config:
dependency: transitive
description:
@@ -268,6 +268,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.2"
leak_tracker:
dependency: transitive
description:
@@ -320,18 +328,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
+ sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
- version: "0.12.19"
+ version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
- version: "0.13.0"
+ version: "0.11.1"
meta:
dependency: transitive
description:
@@ -653,26 +661,26 @@ packages:
dependency: transitive
description:
name: test
- sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
+ sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
url: "https://pub.dev"
source: hosted
- version: "1.30.0"
+ version: "1.26.3"
test_api:
dependency: transitive
description:
name: test_api
- sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
+ sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
- version: "0.7.10"
+ version: "0.7.7"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
+ sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
url: "https://pub.dev"
source: hosted
- version: "0.6.16"
+ version: "0.6.12"
toml:
dependency: "direct main"
description: