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", - )} +

@@ -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: