diff --git a/README.md b/README.md
index a9559c83..5b62f029 100644
--- a/README.md
+++ b/README.md
@@ -541,11 +541,12 @@ KETO_WRITE_URL = "http://keto:4467"
```
## π i18n ꡬ쑰 (κ°λ΅)
-- **Source of Truth**: `locales/template.toml`μ΄ μ 체 ν€μ κΈ°μ€μ΄λ©° `locales/ko.toml`, `locales/en.toml`κ³Ό νμ λκΈ°νν©λλ€.
-- **React(Admin/Dev)**: `adminfront/src/lib/i18n.ts`, `devfront/src/lib/i18n.ts`μμ `t(key, fallback, vars)`λ‘ μ¬μ©νκ³ TOMLμ `?raw`λ‘ λ‘λν©λλ€.
+- **Root locales**: `locales/template.toml`, `locales/ko.toml`, `locales/en.toml`μ νμ¬ `userfront`μ μ μ i18n κ²μ¦ κΈ°μ€ λ¦¬μμ€μ
λλ€.
+- **Common locales**: `common/locales/template.toml`, `common/locales/ko.toml`, `common/locales/en.toml`μ `ui.common.*`, `msg.common.*` κ°μ React κ³΅ν΅ λ¬Έκ΅¬ λ μ΄μ΄μ
λλ€.
+- **React(Admin/Dev/Org)**: `adminfront/src/lib/i18n.ts`, `devfront/src/lib/i18n.ts`, `orgfront/src/lib/i18n.ts`μμ `t(key, fallback, vars)`λ₯Ό μ¬μ©νλ©° `common locale -> app locale override` μμλ‘ TOMLμ `?raw` λ‘λν©λλ€.
- **Flutter(User)**: `userfront/lib/i18n.dart`μμ `tr(key, fallback, params)` μ¬μ©. `locales/*.toml`μ `tools/i18n-scanner/gen-flutter-i18n.js`λ‘ `userfront/lib/i18n_data.dart`μ μ¬μ μμ±ν©λλ€.
- **UserFront λκΈ°ν κ·μΉ**: `locales/*.toml`μ μμ ν λ€μλ λ°λμ `./scripts/sync_userfront_locales.sh`λ₯Ό μ€νν΄ `userfront/assets/translations/*.toml`κ³Ό λ°νμ λ²μ 리μμ€λ₯Ό λκΈ°νν©λλ€.
-- **κ²μ¦**: `node tools/i18n-scanner/index.js`λ‘ μ½λ-ν€-λ‘μΌμΌ λκΈ°ν μνλ₯Ό μ κ²ν©λλ€.
+- **κ²μ¦**: `node tools/i18n-scanner/index.js`λ‘ `root locales`μ `common/locales`μ μ½λ-ν€-λ‘μΌμΌ λκΈ°ν μνλ₯Ό ν¨κ» μ κ²ν©λλ€.
## π§ͺ Code Check CI
μν¬νλ‘μ° νμΌ: `.gitea/workflows/code_check.yml`
diff --git a/adminfront/src/app/queryClient.ts b/adminfront/src/app/queryClient.ts
index 06c9afde..9064efe8 100644
--- a/adminfront/src/app/queryClient.ts
+++ b/adminfront/src/app/queryClient.ts
@@ -1,11 +1,7 @@
import { QueryClient } from "@tanstack/react-query";
+import { queryClientDefaultOptions } from "../../../common/core/query/queryClient";
+
export const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- staleTime: 30_000,
- refetchOnWindowFocus: false,
- retry: 1,
- },
- },
+ defaultOptions: queryClientDefaultOptions,
});
diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx
index 320361b7..76140dd5 100644
--- a/adminfront/src/components/layout/AppLayout.tsx
+++ b/adminfront/src/components/layout/AppLayout.tsx
@@ -26,6 +26,15 @@ import {
shouldAttemptSlidingSessionRenew,
shouldAttemptUnlimitedSessionRenew,
} from "../../lib/sessionSliding";
+import {
+ applyShellTheme,
+ buildShellProfileSummary,
+ buildShellSessionStatus,
+ readShellSessionExpiryEnabled,
+ readShellTheme,
+ shellLayoutClasses,
+ writeShellSessionExpiryEnabled,
+} from "../../../../common/shell";
import LanguageSelector from "../common/LanguageSelector";
import RoleSwitcher from "./RoleSwitcher";
@@ -62,15 +71,11 @@ function AppLayout() {
const mockRoleOverride = isMockRoleEnabled
? window.localStorage.getItem("X-Mock-Role")
: null;
- const [theme, setTheme] = useState<"light" | "dark">(() => {
- const stored = window.localStorage.getItem("admin_theme");
- return stored === "dark" ? "dark" : "light";
- });
+ const [theme, setTheme] = useState<"light" | "dark">(readShellTheme);
const [isProfileOpen, setIsProfileOpen] = useState(false);
- const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() => {
- const stored = window.localStorage.getItem("baron_session_expiry_enabled");
- return stored !== "false";
- });
+ const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(
+ readShellSessionExpiryEnabled,
+ );
const [nowMs, setNowMs] = useState(() => Date.now());
useEffect(() => {
@@ -214,14 +219,7 @@ function AppLayout() {
}, [auth.user]);
useEffect(() => {
- const root = document.documentElement;
- root.classList.remove("light", "dark");
- if (theme === "light") {
- root.classList.add("light");
- } else {
- root.classList.add("dark");
- }
- window.localStorage.setItem("admin_theme", theme);
+ applyShellTheme(theme);
}, [theme]);
useEffect(() => {
@@ -388,68 +386,26 @@ function AppLayout() {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
- const profileName =
- profile?.name?.trim() ||
- auth.user?.profile.name?.toString().trim() ||
- auth.user?.profile.preferred_username?.toString().trim() ||
- t("ui.dev.profile.unknown_name", "Unknown User");
- const profileEmail =
- profile?.email?.trim() ||
- auth.user?.profile.email?.toString().trim() ||
- t("ui.dev.profile.unknown_email", "unknown@example.com");
- const profileInitial = profileName.charAt(0).toUpperCase();
+ const profileSummary = buildShellProfileSummary({
+ profileName:
+ profile?.name ||
+ auth.user?.profile.name?.toString() ||
+ auth.user?.profile.preferred_username?.toString(),
+ profileEmail: profile?.email || auth.user?.profile.email?.toString(),
+ fallbackName: t("ui.dev.profile.unknown_name", "Unknown User"),
+ fallbackEmail: t("ui.dev.profile.unknown_email", "unknown@example.com"),
+ });
const profileRoleKey = mockRoleOverride || profile?.role || "user";
- const expiresAtSec = auth.user?.expires_at;
- const remainingMs =
- typeof expiresAtSec === "number" ? expiresAtSec * 1000 - nowMs : null;
- const remainingTotalSec =
- remainingMs !== null ? Math.max(0, Math.floor(remainingMs / 1000)) : null;
- const remainingMinutes =
- remainingTotalSec !== null ? Math.floor(remainingTotalSec / 60) : null;
- const remainingSeconds =
- remainingTotalSec !== null ? remainingTotalSec % 60 : null;
-
- let sessionToneClass =
- "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300";
- let sessionText = t("ui.dev.session.active", "μΈμ
νμ±");
-
- if (remainingMs === null) {
- sessionToneClass = "border-border bg-card text-muted-foreground";
- sessionText = t("ui.dev.session.unknown", "μ μ μμ");
- } else if (remainingMs <= 0) {
- sessionToneClass =
- "border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300";
- sessionText = t("ui.dev.session.expired", "μΈμ
λ§λ£");
- } else if (
- remainingMinutes !== null &&
- remainingSeconds !== null &&
- remainingMinutes <= 5
- ) {
- sessionToneClass =
- "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300";
- sessionText = t(
- "ui.dev.session.expiring",
- "λ§λ£ μλ°: {{minutes}}λΆ {{seconds}}μ΄ λ¨μ",
- {
- minutes: remainingMinutes,
- seconds: remainingSeconds,
- },
- );
- } else {
- sessionText = t(
- "ui.dev.session.remaining",
- "λ§λ£ μμ : {{minutes}}λΆ {{seconds}}μ΄ λ¨μ",
- {
- minutes: remainingMinutes ?? 0,
- seconds: remainingSeconds ?? 0,
- },
- );
- }
+ const sessionStatus = buildShellSessionStatus({
+ expiresAtSec: auth.user?.expires_at,
+ nowMs,
+ t,
+ });
const handleSessionExpiryToggle = () => {
setIsSessionExpiryEnabled((prev) => {
const next = !prev;
- window.localStorage.setItem("baron_session_expiry_enabled", String(next));
+ writeShellSessionExpiryEnabled(next);
return next;
});
};
@@ -463,11 +419,11 @@ function AppLayout() {
}
return (
-
-