Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

This commit is contained in:
2026-10-26 18:13:22 +09:00
3 changed files with 76 additions and 9 deletions
+59 -9
View File
@@ -12,12 +12,19 @@ import { icon, type IconName } from "@ui/ui_template_icons";
import { currentLanguageIndex, setLanguage } from "@ui/ui_template_locale";
import { readTouchMode, setTouchMode, type TouchMode } from "../A00_Common/device_context";
import { currentQuery, navigateTo, type MountedPage } from "../A00_Common/router";
import { fetchAllCompanies, type CompanyInfo, type DashboardUser } from "./B01_Dashboard_Api_Fetch";
import {
fetchAllCompanies,
fetchCompanyJoinRequests,
type CompanyInfo,
type DashboardUser,
} from "./B01_Dashboard_Api_Fetch";
import { fetchTempBatches } from "./B01_Dashboard_Api_Temp";
import { fetchAssignedPlans } from "./B01_Dashboard_UI_AssignedPlans";
import { roleLabel } from "./B01_Dashboard_UI_Common";
import { openCompanyPicker } from "./B01_Dashboard_UI_CompanyPick";
import { buildKnowledgeChat } from "./B01_Dashboard_UI_Knowledge_Chat";
import { ADMIN_COMPANY_KEY, pickAdminCompany } from "./B01_Dashboard_UI_Legacy";
import { buildNav, viewHref } from "./B01_Dashboard_UI_Nav";
import { buildNav, viewHref, type DashboardNav } from "./B01_Dashboard_UI_Nav";
import { roleOf, viewFor, VIEWS, type ViewCtx } from "./B01_Dashboard_UI_Views";
import "./B01_Dashboard_UI_Style_Shell.css";
@@ -116,14 +123,19 @@ function buildUserMenu(ctx: ViewCtx, roleText: string): HTMLElement {
menu.hidden = !open;
trigger.setAttribute("aria-expanded", String(open));
};
const dark = document.documentElement.getAttribute("data-theme") === "dark";
// 테마 줄 = 지금과 반대 화면 이름 · 바꾼 뒤 다시 씀
const themeItem = (): HTMLButtonElement => {
const dark = document.documentElement.getAttribute("data-theme") === "dark";
return menuItem(dark ? "sun" : "moon", dark ? "밝은 화면" : "어두운 화면", () => {
toggleTheme();
theme.replaceWith((theme = themeItem()));
});
};
let theme = themeItem();
menu.append(
who,
menuItem("userCircle", "내 정보", () => ctx.go("me")),
menuItem(dark ? "sun" : "moon", dark ? "밝은 화면" : "어두운 화면", () => {
toggleTheme();
setOpen(false);
}),
theme,
menuItem("world", currentLanguageIndex === 0 ? "English" : "한국어", () => {
const next = currentLanguageIndex === 0 ? "en" : "ko";
setLanguage(next);
@@ -183,6 +195,39 @@ function buildCompany(
return b;
}
/** 보관함 만료 임박 = 7 일 안 */
const EXPIRE_SOON_MS = 7 * 24 * 3600 * 1000;
/** 메뉴 배지 · 알림 수 — 가입 요청(회사 관리자 · 시스템 관리자) · 받은 예정노선 미연결 · 업데이트 ·
* 보관함 묶음 수(만료 임박은 처리할 일) · 처리할 일 = 빨강 배지 합 · 실패한 것은 0 */
async function refreshBadges(ctx: ViewCtx, nav: DashboardNav, bell: HTMLElement): Promise<void> {
const admin = ctx.role === "ADMIN" || ctx.role === "SYSTEM_ADMIN";
const [joins, plans, temp] = await Promise.all([
admin
? fetchCompanyJoinRequests(ctx.role === "SYSTEM_ADMIN" ? ctx.companyId : undefined).catch(
() => [],
)
: [],
ctx.role === "FOREST" ? [] : fetchAssignedPlans().catch(() => []),
fetchTempBatches()
.then((r) => r.batches)
.catch(() => []),
]);
const newPlans = plans.filter((p) => !p.linked_project_id || p.update_available).length;
const now = Date.now();
const expiring = temp.filter(
(b) => b.expires_at && Date.parse(b.expires_at) - now < EXPIRE_SOON_MS,
).length;
nav.setBadge("people", joins.length, true);
nav.setBadge("assigned", newPlans, true);
nav.setBadge("temp", temp.length);
const todo = joins.length + newPlans + expiring;
const badge = bell.querySelector<HTMLElement>(".b01-shell__badge")!;
badge.hidden = todo <= 0;
badge.textContent = String(todo);
bell.title = bell.ariaLabel = `처리할 일 ${todo} 건`;
}
export async function mountDashboardShell(
root: HTMLElement,
me: DashboardUser,
@@ -218,8 +263,12 @@ export async function mountDashboardShell(
spacer.className = "b01-shell__spacer";
const help = createHelpButton();
help.classList.add("b01-shell__help-btn");
const bell = iconButton("bell", "알림 (준비 중)");
bell.classList.add("b01-shell__bell");
// 알림 = 처리할 일 수 · 누르면 홈(처리할 일 목록 자리)
const bell = iconButton("bell", "처리할 일", () => ctx.go("home"));
const bellBadge = document.createElement("span");
bellBadge.className = "b01-shell__badge is-alert";
bellBadge.hidden = true;
bell.append(bellBadge);
header.append(
brand,
buildCompany(ctx, companies, (id) => {
@@ -252,6 +301,7 @@ export async function mountDashboardShell(
const mine = ++seq;
const view = viewFor(currentQuery().get("view"), role);
nav.setActive(view.group === "hidden" && view.key === "project" ? "projects" : view.key);
void refreshBadges(ctx, nav, bell);
const host = document.createElement("div");
host.className = "b01-shell__page";
host.dataset.view = view.key;
@@ -22,6 +22,10 @@
--color-border: var(--b01-line);
--color-primary: var(--b01-accent);
--color-primary-text: var(--b01-accent-text);
--color-text: var(--b01-text);
--color-text-body: var(--b01-text);
--color-text-secondary: var(--b01-muted);
--color-text-muted: var(--b01-muted);
min-height: 100dvh;
display: grid;
+13
View File
@@ -104,3 +104,16 @@ def test_셸_한_벌():
assert ':root[data-theme="dark"] .b01-shell {' in css
icons = _text(ROOT / "ui_template" / "ui_template_icons.ts")
assert "MIT License" in icons and "Paweł Kuna" in icons
def test_메뉴_배지_알림():
shell = _text(B01 / "B01_Dashboard_UI_Shell.ts")
badges = shell.split("async function refreshBadges")[1].split("\n}\n")[0]
assert 'nav.setBadge("people", joins.length, true);' in badges # 가입 요청 = 빨강
assert "!p.linked_project_id || p.update_available" in badges # 받은 예정노선 새로 · 업데이트
assert 'nav.setBadge("temp", temp.length);' in badges
assert "const todo = joins.length + newPlans + expiring;" in badges # 알림 = 처리할 일 합
assert "void refreshBadges(ctx, nav, bell);" in shell # 페이지 바뀔 때마다 다시 셈
css = _text(B01 / "B01_Dashboard_UI_Style_Shell.css")
assert "@media (max-width: 1024px)" in css # 작은 화면 = 아이콘 레일
assert "html:has(.b01-shell) .app-header" in css # 옛 ☰ 걷음