feat(B01): 대시보드 셸 마무리 — 메뉴 배지 · 알림 수 · 사용자 메뉴 테마 줄 (546-2)
- 메뉴 배지 = 가입 요청(빨강) · 받은 예정노선 미연결 · 업데이트(빨강) · 임시 보관함 묶음 수 — 페이지 바뀔 때마다 다시 셈 - 알림 종 = 처리할 일 수(가입 요청 + 새 예정노선 + 보관함 만료 7 일 안) · 누르면 홈 - 사용자 메뉴 테마 줄 = 바꾼 뒤 반대 화면 이름으로 다시 씀 - 셸 안 글 색 변수(--color-text 등)도 셸 색으로 — 어두운 화면 제목 보라 빠짐 - 시험 test_546_b01_shell 배지 · 레일 · ☰ 걷음 단정 더함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,12 +12,19 @@ import { icon, type IconName } from "@ui/ui_template_icons";
|
|||||||
import { currentLanguageIndex, setLanguage } from "@ui/ui_template_locale";
|
import { currentLanguageIndex, setLanguage } from "@ui/ui_template_locale";
|
||||||
import { readTouchMode, setTouchMode, type TouchMode } from "../A00_Common/device_context";
|
import { readTouchMode, setTouchMode, type TouchMode } from "../A00_Common/device_context";
|
||||||
import { currentQuery, navigateTo, type MountedPage } from "../A00_Common/router";
|
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 { roleLabel } from "./B01_Dashboard_UI_Common";
|
||||||
import { openCompanyPicker } from "./B01_Dashboard_UI_CompanyPick";
|
import { openCompanyPicker } from "./B01_Dashboard_UI_CompanyPick";
|
||||||
import { buildKnowledgeChat } from "./B01_Dashboard_UI_Knowledge_Chat";
|
import { buildKnowledgeChat } from "./B01_Dashboard_UI_Knowledge_Chat";
|
||||||
import { ADMIN_COMPANY_KEY, pickAdminCompany } from "./B01_Dashboard_UI_Legacy";
|
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 { roleOf, viewFor, VIEWS, type ViewCtx } from "./B01_Dashboard_UI_Views";
|
||||||
import "./B01_Dashboard_UI_Style_Shell.css";
|
import "./B01_Dashboard_UI_Style_Shell.css";
|
||||||
|
|
||||||
@@ -116,14 +123,19 @@ function buildUserMenu(ctx: ViewCtx, roleText: string): HTMLElement {
|
|||||||
menu.hidden = !open;
|
menu.hidden = !open;
|
||||||
trigger.setAttribute("aria-expanded", String(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(
|
menu.append(
|
||||||
who,
|
who,
|
||||||
menuItem("userCircle", "내 정보", () => ctx.go("me")),
|
menuItem("userCircle", "내 정보", () => ctx.go("me")),
|
||||||
menuItem(dark ? "sun" : "moon", dark ? "밝은 화면" : "어두운 화면", () => {
|
theme,
|
||||||
toggleTheme();
|
|
||||||
setOpen(false);
|
|
||||||
}),
|
|
||||||
menuItem("world", currentLanguageIndex === 0 ? "English" : "한국어", () => {
|
menuItem("world", currentLanguageIndex === 0 ? "English" : "한국어", () => {
|
||||||
const next = currentLanguageIndex === 0 ? "en" : "ko";
|
const next = currentLanguageIndex === 0 ? "en" : "ko";
|
||||||
setLanguage(next);
|
setLanguage(next);
|
||||||
@@ -183,6 +195,39 @@ function buildCompany(
|
|||||||
return b;
|
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(
|
export async function mountDashboardShell(
|
||||||
root: HTMLElement,
|
root: HTMLElement,
|
||||||
me: DashboardUser,
|
me: DashboardUser,
|
||||||
@@ -218,8 +263,12 @@ export async function mountDashboardShell(
|
|||||||
spacer.className = "b01-shell__spacer";
|
spacer.className = "b01-shell__spacer";
|
||||||
const help = createHelpButton();
|
const help = createHelpButton();
|
||||||
help.classList.add("b01-shell__help-btn");
|
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(
|
header.append(
|
||||||
brand,
|
brand,
|
||||||
buildCompany(ctx, companies, (id) => {
|
buildCompany(ctx, companies, (id) => {
|
||||||
@@ -252,6 +301,7 @@ export async function mountDashboardShell(
|
|||||||
const mine = ++seq;
|
const mine = ++seq;
|
||||||
const view = viewFor(currentQuery().get("view"), role);
|
const view = viewFor(currentQuery().get("view"), role);
|
||||||
nav.setActive(view.group === "hidden" && view.key === "project" ? "projects" : view.key);
|
nav.setActive(view.group === "hidden" && view.key === "project" ? "projects" : view.key);
|
||||||
|
void refreshBadges(ctx, nav, bell);
|
||||||
const host = document.createElement("div");
|
const host = document.createElement("div");
|
||||||
host.className = "b01-shell__page";
|
host.className = "b01-shell__page";
|
||||||
host.dataset.view = view.key;
|
host.dataset.view = view.key;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
--color-border: var(--b01-line);
|
--color-border: var(--b01-line);
|
||||||
--color-primary: var(--b01-accent);
|
--color-primary: var(--b01-accent);
|
||||||
--color-primary-text: var(--b01-accent-text);
|
--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;
|
min-height: 100dvh;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -104,3 +104,16 @@ def test_셸_한_벌():
|
|||||||
assert ':root[data-theme="dark"] .b01-shell {' in css
|
assert ':root[data-theme="dark"] .b01-shell {' in css
|
||||||
icons = _text(ROOT / "ui_template" / "ui_template_icons.ts")
|
icons = _text(ROOT / "ui_template" / "ui_template_icons.ts")
|
||||||
assert "MIT License" in icons and "Paweł Kuna" in icons
|
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 # 옛 ☰ 걷음
|
||||||
|
|||||||
Reference in New Issue
Block a user