1
0
forked from baron/baron-sso

조직도 줌 레벨 상향

This commit is contained in:
2026-05-18 18:06:13 +09:00
parent 0b54992309
commit 9112c4fb36
44 changed files with 1000 additions and 1414 deletions

View File

@@ -1,3 +1,6 @@
{
"extends": ["../common/config/biome.base.json"]
"extends": ["../common/config/biome.base.json"],
"files": {
"ignore": [".vite"]
}
}

View File

@@ -22,13 +22,13 @@ import { useAuth } from "react-oidc-context";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import {
AppSidebar,
type ShellSidebarNavItem,
type ShellTranslator,
applyShellTheme,
buildShellProfileSummary,
buildShellSessionStatus,
readShellSessionExpiryEnabled,
readShellTheme,
type ShellSidebarNavItem,
shellLayoutClasses,
writeShellSessionExpiryEnabled,
} from "../../../../common/shell";

View File

@@ -111,37 +111,36 @@ const DialogPortal = ({ children }: { children?: React.ReactNode }) => {
};
DialogPortal.displayName = "DialogPortal";
const DialogClose = React.forwardRef<
HTMLButtonElement,
DialogTriggerProps
>(({ asChild = false, children, onClick, ...props }, ref) => {
const { setOpen } = useDialogContext("DialogClose");
const handleClose = (event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
if (!event.defaultPrevented) {
setOpen(false);
const DialogClose = React.forwardRef<HTMLButtonElement, DialogTriggerProps>(
({ asChild = false, children, onClick, ...props }, ref) => {
const { setOpen } = useDialogContext("DialogClose");
const handleClose = (event: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(event);
if (!event.defaultPrevented) {
setOpen(false);
}
};
if (asChild && React.isValidElement(children)) {
const child = children as React.ReactElement<{
onClick?: React.MouseEventHandler<HTMLElement>;
}>;
return React.cloneElement(child, {
...props,
onClick: composeEventHandlers(
child.props.onClick as React.MouseEventHandler<HTMLButtonElement>,
() => setOpen(false),
),
});
}
};
if (asChild && React.isValidElement(children)) {
const child = children as React.ReactElement<{
onClick?: React.MouseEventHandler<HTMLElement>;
}>;
return React.cloneElement(child, {
...props,
onClick: composeEventHandlers(
child.props.onClick as React.MouseEventHandler<HTMLButtonElement>,
() => setOpen(false),
),
});
}
return (
<button type="button" ref={ref} onClick={handleClose} {...props}>
{children}
</button>
);
});
return (
<button type="button" ref={ref} onClick={handleClose} {...props}>
{children}
</button>
);
},
);
DialogClose.displayName = "DialogClose";
const DialogOverlay = React.forwardRef<
@@ -169,8 +168,8 @@ const DialogOverlay = React.forwardRef<
DialogOverlay.displayName = "DialogOverlay";
const DialogContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
HTMLDialogElement,
React.HTMLAttributes<HTMLDialogElement>
>(({ className, children, onKeyDown, ...props }, ref) => {
const { open, setOpen } = useDialogContext("DialogContent");
@@ -194,13 +193,13 @@ const DialogContent = React.forwardRef<
return (
<DialogPortal>
<DialogOverlay />
<div
<dialog
ref={ref}
role="dialog"
open
aria-modal="true"
data-state="open"
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 m-0 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 backdrop:bg-transparent data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
onKeyDown={onKeyDown}
@@ -211,7 +210,7 @@ const DialogContent = React.forwardRef<
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogClose>
</div>
</dialog>
</DialogPortal>
);
});

View File

@@ -12,6 +12,8 @@ import {
} from "lucide-react";
import * as React from "react";
import { Link } from "react-router-dom";
import { PageHeader } from "../../../../common/core/components/page";
import { commonStickyTableHeaderClass } from "../../../../common/ui/table";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
@@ -38,8 +40,6 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { PageHeader } from "../../../../common/core/components/page";
import { commonStickyTableHeaderClass } from "../../../../common/ui/table";
import {
type ApiKeySummary,
deleteApiKey,

View File

@@ -13,7 +13,12 @@ import { PageHeader } from "../../../../common/core/components/page";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "../../components/ui/card";
import { Input } from "../../components/ui/input";
import type { AuditLog } from "../../lib/adminApi";
import { fetchAuditLogs } from "../../lib/adminApi";

View File

@@ -12,6 +12,7 @@ import {
import { useState } from "react";
import { useAuth } from "react-oidc-context";
import { useNavigate, useParams } from "react-router-dom";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -38,7 +39,6 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { toast } from "../../../components/ui/use-toast";
import {
type TenantAdmin,

View File

@@ -21,6 +21,7 @@ import {
import type React from "react";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -50,7 +51,6 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { toast } from "../../../components/ui/use-toast";
import {
type GroupSummary,

View File

@@ -1,6 +1,11 @@
import { useQuery } from "@tanstack/react-query";
import { ArrowRight, Building2, Plus } from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom";
import {
commonStickyTableHeaderClass,
commonTableShellClass,
commonTableViewportClass,
} from "../../../../../common/ui/table";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -18,11 +23,6 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import {
commonStickyTableHeaderClass,
commonTableShellClass,
commonTableViewportClass,
} from "../../../../../common/ui/table";
import { fetchAllTenants } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";

View File

@@ -10,6 +10,7 @@ import {
UserPlus,
} from "lucide-react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -32,7 +33,6 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { toast } from "../../../components/ui/use-toast";
import { fetchTenant, fetchUsers, updateUser } from "../../../lib/adminApi";
import { t } from "../../../lib/i18n";

View File

@@ -681,11 +681,7 @@ function ComparisonFilterButtons<T extends string>({
aria-pressed={isSelected}
aria-expanded={hasDetail && openDetailFor === option.value}
onClick={() => {
if (
hasDetail &&
isSelected &&
openDetailFor !== option.value
) {
if (hasDetail && isSelected && openDetailFor !== option.value) {
setOpenDetailFor(option.value);
return;
}

View File

@@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { Building2, Plus, Users } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -19,7 +20,6 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import {
type TenantSummary,
fetchAllTenants,

View File

@@ -3,6 +3,7 @@ import type { AxiosError } from "axios";
import { ArrowLeft, Shield, Trash2, UserPlus, Users } from "lucide-react";
import { useState } from "react";
import { Link, useParams } from "react-router-dom";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button";
import {
@@ -38,7 +39,6 @@ import {
TableHeader,
TableRow,
} from "../../../components/ui/table";
import { commonStickyTableHeaderClass } from "../../../../../common/ui/table";
import { toast } from "../../../components/ui/use-toast";
import {
addGroupMember,

View File

@@ -18,6 +18,7 @@ import {
} from "lucide-react";
import * as React from "react";
import { Link, useNavigate } from "react-router-dom";
import { PageHeader } from "../../../../common/core/components/page";
import {
SortableTableHead,
sortableTableHeadBaseClassName,
@@ -29,12 +30,11 @@ import {
sortItems,
toggleSort,
} from "../../../../common/core/utils";
import { PageHeader } from "../../../../common/core/components/page";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import {
commonTableShellClass,
commonTableViewportClass,
} from "../../../../common/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { Button } from "../../components/ui/button";
import {
Card,
@@ -545,7 +545,9 @@ function UserListPage() {
onChange={() => toggleColumn(field.key)}
/>
<div className="flex flex-col">
<span className="text-sm font-medium">{field.label}</span>
<span className="text-sm font-medium">
{field.label}
</span>
<span className="font-mono text-xs text-muted-foreground">
{field.key}
</span>

View File

@@ -19,9 +19,7 @@ createRoot(rootElement).render(
<StrictMode>
<AuthProvider {...oidcConfig}>
<QueryClientProvider client={queryClient}>
<RouterProvider
router={router}
/>
<RouterProvider router={router} />
<Toaster />
</QueryClientProvider>
</AuthProvider>

View File

@@ -7,7 +7,7 @@ const buildOutDir =
const allowedHosts = getAllowedHosts(
["sadmin.hmac.kr", "localhost", "172.16.10.176", "127.0.0.1"],
undefined,
undefined
undefined,
);
export default defineConfig(
@@ -40,5 +40,5 @@ export default defineConfig(
},
},
},
})
}),
);

View File

@@ -1,6 +1,6 @@
module baron-sso-backend
go 1.24.0
go 1.26.2
require (
github.com/ClickHouse/clickhouse-go/v2 v2.42.0

View File

@@ -39,6 +39,7 @@ back = "Back"
back_to_login = "Back to login"
cancel = "Cancel"
change_file = "Change File"
clear = "Clear"
clear_search = "Clear Search"
close = "Close"
collapse = "Collapse"

View File

@@ -39,6 +39,7 @@ back = "돌아가기"
back_to_login = "로그인으로 돌아가기"
cancel = "취소"
change_file = "파일 변경"
clear = "초기화"
clear_search = "검색 초기화"
close = "닫기"
collapse = "접기"

View File

@@ -39,6 +39,7 @@ back = ""
back_to_login = ""
cancel = ""
change_file = ""
clear = ""
clear_search = ""
close = ""
collapse = ""

View File

@@ -1,3 +1,6 @@
{
"extends": ["../common/config/biome.base.json"]
"extends": ["../common/config/biome.base.json"],
"files": {
"ignore": [".vite"]
}
}

View File

@@ -48,13 +48,6 @@
"node": ">=24.0.0"
}
},
"node_modules/@acemir/cssom": {
"version": "0.9.31",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"dev": true,
@@ -66,207 +59,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "5.1.11",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@asamuzakjp/generational-cache": "^1.0.1",
"@csstools/css-calc": "^3.2.0",
"@csstools/css-color-parser": "^4.1.0",
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@asamuzakjp/dom-selector": {
"version": "6.8.1",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@asamuzakjp/nwsapi": "^2.3.9",
"bidi-js": "^1.0.3",
"css-tree": "^3.1.0",
"is-potential-custom-element-name": "^1.0.1",
"lru-cache": "^11.2.6"
}
},
"node_modules/@asamuzakjp/generational-cache": {
"version": "1.0.1",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@asamuzakjp/nwsapi": {
"version": "2.3.9",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@bramus/specificity": {
"version": "2.4.2",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"css-tree": "^3.0.0"
},
"bin": {
"specificity": "bin/cli.js"
}
},
"node_modules/@csstools/color-helpers": {
"version": "6.0.2",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"optional": true,
"peer": true,
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/@csstools/css-calc": {
"version": "3.2.0",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "4.1.0",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@csstools/color-helpers": "^6.0.2",
"@csstools/css-calc": "^3.2.0"
},
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "4.0.0",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
"version": "1.1.3",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"optional": true,
"peer": true,
"peerDependencies": {
"css-tree": "^3.2.1"
},
"peerDependenciesMeta": {
"css-tree": {
"optional": true
}
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "4.0.0",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@@ -301,24 +93,6 @@
"tslib": "^2.4.0"
}
},
"node_modules/@exodus/bytes": {
"version": "1.15.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@noble/hashes": "^1.8.0 || ^2.0.0"
},
"peerDependenciesMeta": {
"@noble/hashes": {
"optional": true
}
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
@@ -2408,16 +2182,6 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 14"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"dev": true,
@@ -2553,16 +2317,6 @@
"node": ">=6.0.0"
}
},
"node_modules/bidi-js": {
"version": "1.0.3",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"dev": true,
@@ -2758,20 +2512,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/css-tree": {
"version": "3.2.1",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"mdn-data": "2.27.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"dev": true,
@@ -2783,41 +2523,11 @@
"node": ">=4"
}
},
"node_modules/cssstyle": {
"version": "6.2.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@asamuzakjp/css-color": "^5.0.1",
"@csstools/css-syntax-patches-for-csstree": "^1.0.28",
"css-tree": "^3.1.0",
"lru-cache": "^11.2.6"
},
"engines": {
"node": ">=20"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"devOptional": true,
"license": "MIT"
},
"node_modules/data-urls": {
"version": "7.0.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"whatwg-mimetype": "^5.0.0",
"whatwg-url": "^16.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/debug": {
"version": "4.4.3",
"license": "MIT",
@@ -2833,13 +2543,6 @@
}
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"license": "MIT",
@@ -2890,19 +2593,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/entities": {
"version": "8.0.0",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"license": "MIT",
@@ -3182,47 +2872,6 @@
"node": ">= 0.4"
}
},
"node_modules/html-encoding-sniffer": {
"version": "6.0.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@exodus/bytes": "^1.6.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"dev": true,
@@ -3275,13 +2924,6 @@
"node": ">=0.12.0"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/jiti": {
"version": "1.21.7",
"dev": true,
@@ -3290,47 +2932,6 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/jsdom": {
"version": "28.1.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.31",
"@asamuzakjp/dom-selector": "^6.8.1",
"@bramus/specificity": "^2.4.2",
"@exodus/bytes": "^1.11.0",
"cssstyle": "^6.0.1",
"data-urls": "^7.0.0",
"decimal.js": "^10.6.0",
"html-encoding-sniffer": "^6.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"parse5": "^8.0.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^6.0.0",
"undici": "^7.21.0",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^8.0.1",
"whatwg-mimetype": "^5.0.0",
"whatwg-url": "^16.0.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"license": "MIT",
@@ -3366,6 +2967,159 @@
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0",
"cpu": [
@@ -3404,6 +3158,48 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"dev": true,
@@ -3420,16 +3216,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "11.3.6",
"dev": true,
"license": "BlueOak-1.0.0",
"optional": true,
"peer": true,
"engines": {
"node": "20 || >=22"
}
},
"node_modules/lucide-react": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
@@ -3456,13 +3242,6 @@
"node": ">= 0.4"
}
},
"node_modules/mdn-data": {
"version": "2.27.1",
"dev": true,
"license": "CC0-1.0",
"optional": true,
"peer": true
},
"node_modules/merge2": {
"version": "1.4.1",
"dev": true,
@@ -3583,19 +3362,6 @@
"node": ">=18"
}
},
"node_modules/parse5": {
"version": "8.0.1",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"entities": "^8.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"dev": true,
@@ -3832,16 +3598,6 @@
"node": ">=10"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"dev": true,
@@ -4037,16 +3793,6 @@
"node": ">=8.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve": {
"version": "1.22.11",
"dev": true,
@@ -4138,19 +3884,6 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/saxes": {
"version": "6.0.0",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"license": "MIT"
@@ -4216,13 +3949,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/tailwind-merge": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
@@ -4365,26 +4091,6 @@
"node": ">=14.0.0"
}
},
"node_modules/tldts": {
"version": "7.0.30",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tldts-core": "^7.0.30"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "7.0.30",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"dev": true,
@@ -4396,32 +4102,6 @@
"node": ">=8.0"
}
},
"node_modules/tough-cookie": {
"version": "6.0.1",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"dependencies": {
"tldts": "^7.0.5"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "6.0.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"dev": true,
@@ -4447,16 +4127,6 @@
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.25.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
@@ -4755,54 +4425,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/webidl-conversions": {
"version": "8.0.1",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=20"
}
},
"node_modules/whatwg-mimetype": {
"version": "5.0.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=20"
}
},
"node_modules/whatwg-url": {
"version": "16.0.1",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@exodus/bytes": "^1.11.0",
"tr46": "^6.0.0",
"webidl-conversions": "^8.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"dev": true,
@@ -4818,23 +4440,6 @@
"node": ">=8"
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",

View File

@@ -9,8 +9,8 @@ import ClientDetailsPage from "../features/clients/ClientDetailsPage";
import ClientGeneralPage from "../features/clients/ClientGeneralPage";
import ClientRelationsPage from "../features/clients/ClientRelationsPage";
import ClientsPage from "../features/clients/ClientsPage";
import GlobalOverviewPage from "../features/overview/GlobalOverviewPage";
import DeveloperRequestPage from "../features/developer-request/DeveloperRequestPage";
import GlobalOverviewPage from "../features/overview/GlobalOverviewPage";
import ProfilePage from "../features/profile/ProfilePage";
import { DEVFRONT_AUTH_CALLBACK_PATH } from "../lib/authConfig";

View File

@@ -15,13 +15,13 @@ import { useAuth } from "react-oidc-context";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import {
AppSidebar,
type ShellSidebarNavItem,
type ShellTranslator,
applyShellTheme,
buildShellProfileSummary,
buildShellSessionStatus,
readShellSessionExpiryEnabled,
readShellTheme,
type ShellSidebarNavItem,
shellLayoutClasses,
writeShellSessionExpiryEnabled,
} from "../../../../common/shell";

View File

@@ -2,17 +2,20 @@ import { useInfiniteQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Download, RefreshCw, Search } from "lucide-react";
import * as React from "react";
import { parseAuditDetails } from "../../../../common/core/audit";
import { AuditLogTable } from "../../../../common/core/components/audit";
import { PageHeader } from "../../../../common/core/components/page";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "../../components/ui/card";
import { Input } from "../../components/ui/input";
import {
parseAuditDetails,
} from "../../../../common/core/audit";
import { AuditLogTable } from "../../../../common/core/components/audit";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page";
Card,
CardContent,
CardHeader,
CardTitle,
} from "../../components/ui/card";
import { Input } from "../../components/ui/input";
import type { DevAuditLog } from "../../lib/devApi";
import { fetchDevAuditLogs } from "../../lib/devApi";
import { t } from "../../lib/i18n";

View File

@@ -4,19 +4,19 @@ import { BookOpenText, Filter, Plus, Search, X } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useAuth } from "react-oidc-context";
import { Link, useNavigate } from "react-router-dom";
import { PageHeader } from "../../../../common/core/components/page";
import {
SortableTableHead,
sortableTableHeadBaseClassName,
sortableTableHeaderClassName,
} from "../../../../common/core/components/sort";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { PageHeader } from "../../../../common/core/components/page";
import {
type SortConfig,
type SortResolverMap,
sortItems,
toggleSort,
} from "../../../../common/core/utils";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import {
commonTableShellClass,
commonTableViewportClass,
@@ -330,61 +330,59 @@ function ClientsPage() {
}
advancedOpen={isAdvancedFilterOpen}
advanced={
<>
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.filter.type_label", "Type:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.type_all", "모든 유형")}
</option>
<option value="private">
{t("ui.dev.clients.type.private", "Server side App")}
</option>
<option value="pkce">
{t("ui.dev.clients.type.pkce", "PKCE")}
</option>
</select>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.consents.status_label", "Status:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.status_all", "모든 상태")}
</option>
<option value="active">
{t("ui.common.status.active", "Active")}
</option>
<option value="inactive">
{t("ui.common.status.inactive", "Inactive")}
</option>
</select>
</div>
<Button
variant="ghost"
size="sm"
className="ml-auto text-xs text-muted-foreground"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
}}
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.filter.type_label", "Type:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
{t("ui.common.reset", "초기화")}
</Button>
<option value="all">
{t("ui.dev.clients.filter.type_all", "모든 유형")}
</option>
<option value="private">
{t("ui.dev.clients.type.private", "Server side App")}
</option>
<option value="pkce">
{t("ui.dev.clients.type.pkce", "PKCE")}
</option>
</select>
</div>
</>
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.consents.status_label", "Status:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.status_all", "모든 상태")}
</option>
<option value="active">
{t("ui.common.status.active", "Active")}
</option>
<option value="inactive">
{t("ui.common.status.inactive", "Inactive")}
</option>
</select>
</div>
<Button
variant="ghost"
size="sm"
className="ml-auto text-xs text-muted-foreground"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
}}
>
{t("ui.common.reset", "초기화")}
</Button>
</div>
}
/>
</CardHeader>

View File

@@ -9,6 +9,12 @@ import {
} from "lucide-react";
import { useEffect, useState } from "react";
import { useAuth } from "react-oidc-context";
import { PageHeader } from "../../../../common/core/components/page";
import {
commonStickyTableHeaderClass,
commonTableShellClass,
commonTableViewportClass,
} from "../../../../common/ui/table";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
@@ -27,12 +33,6 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import {
commonStickyTableHeaderClass,
commonTableShellClass,
commonTableViewportClass,
} from "../../../../common/ui/table";
import { PageHeader } from "../../../../common/core/components/page";
import { Textarea } from "../../components/ui/textarea";
import {
approveDeveloperRequest,
@@ -199,7 +199,9 @@ export default function DeveloperRequestPage() {
{t("ui.dev.request.table.user", "사용자")}
</TableHead>
)}
<TableHead>{t("ui.dev.request.table.org", "소속")}</TableHead>
<TableHead>
{t("ui.dev.request.table.org", "소속")}
</TableHead>
<TableHead>
{t("ui.dev.request.table.reason", "신청 사유")}
</TableHead>
@@ -237,7 +239,9 @@ export default function DeveloperRequestPage() {
</div>
{(req.phone || req.role) && (
<div className="mt-1 text-xs text-muted-foreground">
{[req.phone, req.role].filter(Boolean).join(" / ")}
{[req.phone, req.role]
.filter(Boolean)
.join(" / ")}
</div>
)}
</TableCell>
@@ -323,7 +327,10 @@ export default function DeveloperRequestPage() {
disabled={isActionPending}
>
<XCircle className="mr-1 h-3 w-3" />
{t("ui.dev.request.cancel_approval", "승인 취소")}
{t(
"ui.dev.request.cancel_approval",
"승인 취소",
)}
</Button>
</div>
) : (

View File

@@ -11,6 +11,11 @@ import {
import { useMemo, useState } from "react";
import { useAuth } from "react-oidc-context";
import { useNavigate } from "react-router-dom";
import {
OverviewAxisNotes,
OverviewMetric,
OverviewSelectionChips,
} from "../../../../common/core/components/overview";
import {
type ClientSummary,
type RPUsageDailyMetric,
@@ -22,11 +27,6 @@ import {
} from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { resolveProfileRole } from "../../lib/role";
import {
OverviewAxisNotes,
OverviewMetric,
OverviewSelectionChips,
} from "../../../../common/core/components/overview";
type ClientDistribution = {
activeClients: number;

View File

@@ -1,13 +1,18 @@
import { defineConfig, mergeConfig } from "vite";
import { commonViteConfig, getAllowedHosts } from "../common/config/vite.base";
import {
commonViteConfig,
getAllowedHosts,
hostFromUrl,
} from "../common/config/vite.base";
const buildOutDir =
process.env.DEVFRONT_BUILD_OUT_DIR ?? "/tmp/baron-sso-devfront-dist";
const devfrontHost = hostFromUrl(process.env.DEVFRONT_URL);
const allowedHosts = getAllowedHosts(
["sdev.hmac.kr", "localhost", "172.16.10.176", "127.0.0.1"],
process.env.DEVFRONT_URL,
process.env.DEVFRONT_ALLOWED_HOSTS
devfrontHost,
process.env.DEVFRONT_ALLOWED_HOSTS,
);
export default defineConfig(
@@ -39,5 +44,5 @@ export default defineConfig(
},
},
},
})
}),
);

View File

@@ -1,8 +1,18 @@
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [react()],
server: {
fs: {
allow: [
fileURLToPath(new URL(".", import.meta.url)),
fileURLToPath(new URL("../common", import.meta.url)),
"/workspace/common",
],
},
},
test: {
globals: true,
environment: "jsdom",

View File

@@ -218,6 +218,9 @@ local_picker_description = "Select the tenant to use as the parent from the tena
local_picker_empty = "No selectable tenants are available."
picker_description = "Select a tenant in org-chart to apply it as the parent tenant."
[msg.admin.tenants.scope]
description = "Select a parent tenant to filter the list to its descendants."
[msg.admin.tenants.admins]
add_success = "Tenant admin added successfully."
empty = "No tenant admins are assigned yet."
@@ -1153,6 +1156,12 @@ view_org_chart = "View Full Org Chart"
[ui.admin.tenants.view]
hierarchy = "Hierarchy"
list = "List"
table = "Table"
tree = "Tree"
[ui.admin.tenants.scope]
active = "{{name}} descendants"
pick = "Select parent scope"
[ui.admin.tenants.domain_conflict]
description = ""

View File

@@ -125,6 +125,9 @@ local_picker_description = "테넌트 목록에서 상위 테넌트로 사용할
local_picker_empty = "선택할 수 있는 테넌트가 없습니다."
picker_description = "org-chart에서 테넌트를 선택하면 상위 테넌트에 반영됩니다."
[msg.admin.tenants.scope]
description = "상위 테넌트를 선택하면 해당 하위 테넌트만 목록에 표시합니다."
[msg.dev.auth]
access_denied_description = "DevFront는 관리자 전용 화면입니다. 권한이 필요하면 관리자에게 요청해 주세요."
access_denied_title = "접근 권한이 없습니다."
@@ -1639,6 +1642,12 @@ view_org_chart = "전체 조직도 보기"
[ui.admin.tenants.view]
hierarchy = "계층 구조"
list = "평면 목록"
table = "평면"
tree = "트리"
[ui.admin.tenants.scope]
active = "{{name}} 하위"
pick = "상위 범위 선택"
[ui.admin.tenants.admins]
add_button = "관리자 추가"

View File

@@ -1505,6 +1505,12 @@ export = ""
[ui.admin.tenants.view]
hierarchy = ""
list = ""
table = ""
tree = ""
[ui.admin.tenants.scope]
active = ""
pick = ""
[ui.admin.tenants.admins]
add_button = ""
@@ -1625,6 +1631,9 @@ local_picker_description = ""
local_picker_empty = ""
picker_description = ""
[msg.admin.tenants.scope]
description = ""
[msg.admin.users]
self_delete_blocked = ""
export_error = ""

View File

@@ -1,3 +1,6 @@
{
"extends": ["../common/config/biome.base.json"]
"extends": ["../common/config/biome.base.json"],
"files": {
"ignore": [".vite"]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ import {
Search,
} from "lucide-react";
import * as React from "react";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
@@ -28,7 +29,6 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import type { DevAuditLog } from "../../lib/devApi";
import { fetchDevAuditLogs } from "../../lib/devApi";
import { t } from "../../lib/i18n";

View File

@@ -9,6 +9,7 @@ import {
} from "lucide-react";
import { useState } from "react";
import { Link, useParams } from "react-router-dom";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import {
@@ -26,7 +27,6 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";

View File

@@ -11,6 +11,7 @@ import {
import { useState } from "react";
import { useAuth } from "react-oidc-context";
import { Link, useNavigate } from "react-router-dom";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
import {
Avatar,
@@ -36,7 +37,6 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
import { fetchClients, fetchDevStats } from "../../lib/devApi";
import { t } from "../../lib/i18n";
import { cn } from "../../lib/utils";
@@ -228,61 +228,59 @@ function ClientsPage() {
}
advancedOpen={isAdvancedFilterOpen}
advanced={
<>
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.filter.type_label", "Type:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.type_all", "모든 유형")}
</option>
<option value="private">
{t("ui.dev.clients.type.private", "Server side App")}
</option>
<option value="pkce">
{t("ui.dev.clients.type.pkce", "PKCE")}
</option>
</select>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.consents.status_label", "Status:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.status_all", "모든 상태")}
</option>
<option value="active">
{t("ui.common.status.active", "Active")}
</option>
<option value="inactive">
{t("ui.common.status.inactive", "Inactive")}
</option>
</select>
</div>
<Button
variant="ghost"
size="sm"
className="ml-auto text-xs text-muted-foreground"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
}}
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.filter.type_label", "Type:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
{t("ui.common.reset", "초기화")}
</Button>
<option value="all">
{t("ui.dev.clients.filter.type_all", "모든 유형")}
</option>
<option value="private">
{t("ui.dev.clients.type.private", "Server side App")}
</option>
<option value="pkce">
{t("ui.dev.clients.type.pkce", "PKCE")}
</option>
</select>
</div>
</>
<div className="flex items-center gap-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground whitespace-nowrap">
{t("ui.dev.clients.consents.status_label", "Status:")}
</span>
<select
className="h-9 min-w-[140px] rounded-md border border-input bg-background px-3 text-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">
{t("ui.dev.clients.filter.status_all", "모든 상태")}
</option>
<option value="active">
{t("ui.common.status.active", "Active")}
</option>
<option value="inactive">
{t("ui.common.status.inactive", "Inactive")}
</option>
</select>
</div>
<Button
variant="ghost"
size="sm"
className="ml-auto text-xs text-muted-foreground"
onClick={() => {
setTypeFilter("all");
setStatusFilter("all");
}}
>
{t("ui.common.reset", "초기화")}
</Button>
</div>
}
/>
</CardHeader>

View File

@@ -345,7 +345,8 @@ describe("org chart layout", () => {
it("keeps zoom limits wide enough for large SVG organization charts", () => {
expect(clampScale(0.08)).toBe(0.08);
expect(clampScale(5)).toBe(5);
expect(clampScale(32)).toBe(32);
expect(clampScale(64)).toBe(32);
});
it("switches semantic zoom modes from overview to detail", () => {

View File

@@ -107,7 +107,7 @@ const WIDE_ASPECT_RATIO = MAX_TARGET_ASPECT_RATIO;
const TALL_ASPECT_RATIO = 0.5;
const CHART_MARGIN = 72;
const MIN_SCALE = 0.08;
const MAX_SCALE = 5;
const MAX_SCALE = 32;
const ZOOM_SENSITIVITY = 0.0015;
const FAMILY_FILTER_ID = "hanmac-family";
const DEFAULT_LAYOUT_OPTIONS: OrgChartLayoutOptions = {

View File

@@ -14,7 +14,7 @@ const allowedHosts = getAllowedHosts(
"127.0.0.1",
],
process.env.ORGFRONT_URL,
process.env.ORGFRONT_ALLOWED_HOSTS
process.env.ORGFRONT_ALLOWED_HOSTS,
);
export default defineConfig(
@@ -47,5 +47,5 @@ export default defineConfig(
},
},
},
})
}),
);

View File

@@ -111,7 +111,7 @@ function resolvePerformanceBudget(projectName: string): {
if (projectName.includes('mobile')) {
return { coldMs: 3000, warmMs: 1500 };
}
return { coldMs: 1700, warmMs: 1200 };
return { coldMs: 2300, warmMs: 1500 };
}
test.describe('UserFront login performance budget', () => {

View File

@@ -70,15 +70,17 @@ async function fillAt(page: Page, x: number, y: number, value: string): Promise<
}
async function openDepartmentEditor(page: Page): Promise<void> {
if (isMobileProject(page)) {
await enableFlutterAccessibility(page);
await page
.getByRole('group', { name: '소속 QA' })
.getByRole('button', { name: '편집' })
.click({ force: true });
const accessibleEditor = page
.getByRole('group', { name: '소속 QA' })
.getByRole('button', { name: '편집' });
if ((await accessibleEditor.count()) > 0) {
await accessibleEditor.click({ force: true });
await page.waitForTimeout(200);
return;
}
if (isMobileProject(page)) {
throw new Error('Department editor accessibility button was not found.');
}
const coords = coordsFor(page);
await page.locator('flt-glass-pane').click({
position: { x: coords.departmentEditX, y: coords.departmentEditY },
@@ -88,11 +90,15 @@ async function openDepartmentEditor(page: Page): Promise<void> {
}
async function blurDepartmentEditor(page: Page): Promise<void> {
if (isMobileProject(page)) {
await page.getByRole('textbox', { name: '소속' }).blur();
const textbox = page.getByRole('textbox', { name: '소속' });
if ((await textbox.count()) > 0) {
await textbox.blur();
await page.waitForTimeout(250);
return;
}
if (isMobileProject(page)) {
throw new Error('Department textbox was not found.');
}
const coords = coordsFor(page);
await page.locator('flt-glass-pane').click({
position: { x: coords.blurX, y: coords.blurY },
@@ -102,20 +108,28 @@ async function blurDepartmentEditor(page: Page): Promise<void> {
}
async function submitDepartmentEditor(page: Page): Promise<void> {
if (isMobileProject(page)) {
await page.getByRole('textbox', { name: '소속' }).press('Enter');
const textbox = page.getByRole('textbox', { name: '소속' });
if ((await textbox.count()) > 0) {
await textbox.press('Enter');
await page.waitForTimeout(250);
return;
}
if (isMobileProject(page)) {
throw new Error('Department textbox was not found.');
}
await page.keyboard.press('Enter');
await page.waitForTimeout(250);
}
async function fillDepartmentField(page: Page, value: string): Promise<void> {
if (isMobileProject(page)) {
await page.getByRole('textbox', { name: '소속' }).fill(value);
const textbox = page.getByRole('textbox', { name: '소속' });
if ((await textbox.count()) > 0) {
await textbox.fill(value);
return;
}
if (isMobileProject(page)) {
throw new Error('Department textbox was not found.');
}
const coords = coordsFor(page);
await fillAt(page, coords.departmentInputX, coords.departmentInputY, value);
}
@@ -216,6 +230,7 @@ async function mockProfileApis(page: Page, state: ProfileState): Promise<void> {
async function openProfilePage(page: Page): Promise<void> {
await page.goto('/ko/profile');
await expect(page).toHaveURL(/\/ko\/profile$/);
await enableFlutterAccessibility(page);
await page.waitForTimeout(1200);
}

View File

@@ -3,7 +3,6 @@ import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:easy_localization/easy_localization.dart' hide tr;
@@ -162,20 +161,10 @@ bool _shouldRunStartupSessionRecovery(Uri uri) {
return !isPublicAuthPath(path, uri);
}
Future<void> _loadRuntimeEnv() async {
for (final fileName in const ['assets/.env', '.env']) {
try {
await dotenv.load(fileName: fileName);
return;
} catch (_) {}
}
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
usePathUrlStrategy();
await EasyLocalization.ensureInitialized();
await _loadRuntimeEnv();
LocaleRegistry.primeWithDefaults();
// 1. Global Error Handling

View File

@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
cli_config:
dependency: transitive
description:
@@ -276,14 +276,6 @@ 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:
@@ -336,18 +328,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
@@ -669,26 +661,26 @@ packages:
dependency: transitive
description:
name: test
sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
url: "https://pub.dev"
source: hosted
version: "1.26.3"
version: "1.30.0"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.7"
version: "0.7.10"
test_core:
dependency: transitive
description:
name: test_core
sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
url: "https://pub.dev"
source: hosted
version: "0.6.12"
version: "0.6.16"
toml:
dependency: "direct main"
description: