From e7d8ea80020d0d0246596edfba44fba4b1194309 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 17:35:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(B01):=20=ED=9A=8C=EC=82=AC=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=EB=A5=BC=20=EC=95=8C=EC=95=BD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=E2=80=94=20=ED=9A=8C=EC=82=AC=20=EB=94=B0=EB=9D=BC?= =?UTF-8?q?=20=EB=B0=94=EB=80=8C=EB=8A=94=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=EB=A7=88=EB=8B=A4(29-1=20=EA=B3=A0=EC=B9=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 고르기 select 뺌 · 「시스템 설정」 · 프로젝트 · 사용자 관리 · 가입 요청 · 회사 관리 제목 오른쪽에 고른 회사 알약(공용 ui-tag 모양) - 누르면 등록된 회사 작은 메뉴(공용 createMapContextMenu) · 어디서 골라도 함께 바뀜 · 알약 누름은 접힘으로 안 샘 - 회사 관리자 · 개인 화면은 알약 없음 · 임시 보관함 그대로 - 시험 test_b01_settings_cards 알약 판정으로 고침 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- B01_Dashboard/B01_Dashboard_UI_Page.ts | 64 ++++++++++++++------- B01_Dashboard/B01_Dashboard_UI_Style.css | 47 +++++++++++++-- resources/tester/test_b01_settings_cards.py | 18 ++++-- ui_template/ui_template_locale_b3.ts | 2 +- 4 files changed, 98 insertions(+), 33 deletions(-) diff --git a/B01_Dashboard/B01_Dashboard_UI_Page.ts b/B01_Dashboard/B01_Dashboard_UI_Page.ts index 243720e8c..3d0c9971a 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Page.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Page.ts @@ -2,12 +2,12 @@ import { ROUTES } from "@config/config_frontend"; import { attachCollapsible } from "@ui/ui_template_collapsible"; import { createButton, - createSelectField, createTag, hideLoadingOverlay, showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; +import { createMapContextMenu, type MapContextMenu } from "@ui/ui_template_context_menu"; import { section } from "@ui/ui_template_general_blocks"; import { createGeneralLayout } from "@ui/ui_template_general_layout"; import { navigateTo } from "../A00_Common/router"; @@ -200,30 +200,46 @@ function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null) return wrap; } -/** 회사 고르기(시스템 관리자만) — 고르면 저장 뒤 대시보드를 다시 읽음 */ -function buildCompanyPicker(state: DashboardState): HTMLElement { - const field = createSelectField({ - label: L("B01_Dashboard_PickCompany"), - options: state.allCompanies.map((c) => ({ value: String(c.id), text: c.name })), - value: String(state.adminCompanyId ?? ""), - compact: true, - onChange: (value) => { - sessionStorage.setItem(ADMIN_COMPANY_KEY, value); - window.dispatchEvent(new HashChangeEvent("hashchange")); - }, +/** 회사 메뉴 — 공용 작은 메뉴 하나를 알약 모두가 같이 씀 */ +let companyMenu: MapContextMenu | null = null; + +/** 회사 알약(시스템 관리자만) — 누르면 등록된 회사 메뉴 · 고르면 저장 뒤 대시보드를 다시 읽음 */ +function companyPill(state: DashboardState): HTMLElement { + const name = state.allCompanies.find((c) => c.id === state.adminCompanyId)?.name ?? "-"; + const pill = document.createElement("button"); + pill.type = "button"; + pill.className = "ui-tag ui-tag--accent b01-dashboard__company-pill"; + pill.title = L("B01_Dashboard_PickCompany"); + pill.textContent = `${name} ▾`; + pill.addEventListener("click", (event) => { + // 제목 줄 누름 = 접힘 — 알약 누름은 올려 보내지 않음 + event.stopPropagation(); + if (!companyMenu?.element.isConnected) { + companyMenu = createMapContextMenu("b01-dashboard"); + document.body.append(companyMenu.element); + } + const box = pill.getBoundingClientRect(); + companyMenu.open( + box.left, + box.bottom + 4, + state.allCompanies.map((c): [string, () => void] => [ + c.name, + () => { + sessionStorage.setItem(ADMIN_COMPANY_KEY, String(c.id)); + window.dispatchEvent(new HashChangeEvent("hashchange")); + }, + ]), + ); }); - field.root.classList.add("b01-dashboard__company-pick"); - // 제목 줄 누름 = 접힘 — 고르기 누름은 올려 보내지 않음 - field.root.addEventListener("click", (event) => event.stopPropagation()); - return field.root; + return pill; } /** 시스템 설정 컨테이너 — 모든 역할 · 한 줄 통째 · 제목 눌러 통째로 접힘 */ function buildSystemSettingsSection(state: DashboardState): HTMLElement { const { user } = state; - const picker = user.role === "SYSTEM_ADMIN" ? [buildCompanyPicker(state)] : []; + const pill = user.role === "SYSTEM_ADMIN" ? [companyPill(state)] : []; const panel = buildSystemSettingsPanel(user, state.adminCompanyId); - const card = section(L("B01_Dashboard_SystemSettings"), panel, true, picker); + const card = section(L("B01_Dashboard_SystemSettings"), panel, true, pill); card.querySelector(".ui-card__body")?.classList.add("ui-collapsible"); card.querySelector(".b01-dashboard__section-header")?.classList.add("ui-collapsible__title"); attachCollapsible(card); @@ -238,23 +254,31 @@ function buildPage(state: DashboardState): HTMLElement { grid.className = "b01-dashboard__grid"; if (state.user.role === "SYSTEM_ADMIN") { - // 시스템 설정은 리소스 현황 아래(29-1) · 그 아래 컨테이너는 고른 회사 것 + // 시스템 설정은 리소스 현황 아래(29-1) · 회사 알약이 붙은 컨테이너는 고른 회사 것 const picked = state.allCompanies.filter((c) => c.id === state.adminCompanyId); grid.append( section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true), buildSystemSettingsSection(state), section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [ + companyPill(state), createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }), ]), buildTempUploadSection(), section(L("B01_Dashboard_Users"), userTable(state.allUsers, state.user), true, [ + companyPill(state), createButton({ label: "+", onClick: () => openAddMemberModal(undefined, state.adminCompanyId), }), ]), - section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.allJoinRequests, true), true), + section( + L("B01_Dashboard_JoinRequests"), + joinRequestTable(state.allJoinRequests, true), + true, + [companyPill(state)], + ), section(L("B01_Dashboard_Companies"), companyTable(picked, state.user), true, [ + companyPill(state), createButton({ label: "+", onClick: () => openCreateCompanyModal() }), ]), ); diff --git a/B01_Dashboard/B01_Dashboard_UI_Style.css b/B01_Dashboard/B01_Dashboard_UI_Style.css index 0e082f38a..f2c2a0a3b 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Style.css +++ b/B01_Dashboard/B01_Dashboard_UI_Style.css @@ -68,12 +68,47 @@ font-size: var(--text-caption); } -/* 회사 고르기 — 시스템 설정 제목 줄 오른쪽 · 라벨과 고르기를 한 줄로 */ -.ui-field.b01-dashboard__company-pick { - flex-direction: row; - align-items: center; - gap: var(--spacing-8); - margin: 0; +/* 회사 알약 — 회사를 따라 바뀌는 컨테이너 제목 오른쪽 · 누르면 회사 메뉴 */ +.b01-dashboard__company-pill { + border: 0; + font: inherit; + font-size: var(--text-caption); + cursor: pointer; +} + +.b01-dashboard__context-menu { + position: fixed; + z-index: var(--z-modal); + display: flex; + flex-direction: column; + min-width: 160px; + max-height: 60vh; + overflow-y: auto; + padding: var(--spacing-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-cards); + background: var(--color-surface-raised); + box-shadow: var(--shadow-lg); +} + +.b01-dashboard__context-menu[hidden] { + display: none; +} + +.b01-dashboard__context-menu-item { + padding: var(--spacing-8) var(--spacing-12); + border: 0; + border-radius: var(--radius-cards); + background: transparent; + color: inherit; + font: inherit; + font-size: var(--text-body-sm); + text-align: left; + cursor: pointer; +} + +.b01-dashboard__context-menu-item:hover { + background: var(--color-surface-sunken); } /* 접히는 제목 줄 — 단추 칸을 접힘 표시 바로 왼쪽으로 */ diff --git a/resources/tester/test_b01_settings_cards.py b/resources/tester/test_b01_settings_cards.py index b36822cc2..f51ccef1c 100644 --- a/resources/tester/test_b01_settings_cards.py +++ b/resources/tester/test_b01_settings_cards.py @@ -113,26 +113,32 @@ def test_모든_역할_한_줄_통째로_접힘(): assert "StructureManage" not in src and "buildStructurePanel" not in src assert "top-row" not in src sect = _fn(src, "buildSystemSettingsSection") - assert 'L("B01_Dashboard_SystemSettings"), panel, true, picker)' in sect # 한 줄 통째 + assert 'L("B01_Dashboard_SystemSettings"), panel, true, pill)' in sect # 한 줄 통째 assert '"ui-collapsible"' in sect and '"ui-collapsible__title"' in sect assert "attachCollapsible(card)" in sect config = _text(ROOT / "config" / "config_frontend.ts") assert re.search(r"PROTECTED_ROUTES[\s\S]*?ROUTES\.M02_MASTER_TEMPLATE,[\s\S]*?\];", config) -def test_회사_고르기_시스템_관리자만(): +def test_회사_알약_시스템_관리자만(): src = _text(PAGE) sect = _fn(src, "buildSystemSettingsSection") - assert 'user.role === "SYSTEM_ADMIN" ? [buildCompanyPicker(state)] : []' in sect - picker = _fn(src, "buildCompanyPicker") - assert "sessionStorage.setItem(ADMIN_COMPANY_KEY, value)" in picker - assert "event.stopPropagation()" in picker # 고르기 누름이 접힘으로 새지 않음 + assert 'user.role === "SYSTEM_ADMIN" ? [companyPill(state)] : []' in sect + pill = _fn(src, "companyPill") + assert "sessionStorage.setItem(ADMIN_COMPANY_KEY, String(c.id))" in pill + assert "event.stopPropagation()" in pill # 알약 누름이 접힘으로 새지 않음 + assert 'createMapContextMenu("b01-dashboard")' in pill # 공용 작은 메뉴 + assert '"ui-tag ui-tag--accent b01-dashboard__company-pill"' in pill # 공용 알약 모양 assert 'const ADMIN_COMPANY_KEY = "aislo.adminCompany";' in src # M02 와 같은 키 load = src.split("async function loadRoleData")[1].split("\n}\n")[0] for call in ("fetchAllUsers(companyId)", "fetchCompanyJoinRequests(companyId)", "fetchCompanyProjects(companyId)"): # fmt: skip assert call in load, call build = _fn(src, "buildPage") + # 회사 따라 바뀌는 컨테이너 넷(프로젝트 · 사용자 관리 · 가입 요청 · 회사 관리)마다 알약 + sys_part = build.split('} else if (state.user.role === "ADMIN")')[0] + assert sys_part.count("companyPill(state)") == 4 + assert "companyPill" not in build.split('} else if (state.user.role === "ADMIN")')[1] assert "companyTable(picked, state.user)" in build assert "openAddMemberModal(undefined, state.adminCompanyId)" in build diff --git a/ui_template/ui_template_locale_b3.ts b/ui_template/ui_template_locale_b3.ts index 63b1d25d9..48ad7110d 100644 --- a/ui_template/ui_template_locale_b3.ts +++ b/ui_template/ui_template_locale_b3.ts @@ -145,7 +145,7 @@ export const ui_locales_b3 = { B01_Dashboard_PermCompanyRead: ["회사 읽기", "Company read"], B01_Dashboard_PermCompanyWrite: ["회사 수정", "Company edit"], B01_Dashboard_PermPersonalWrite: ["개인 수정", "Personal edit"], - B01_Dashboard_PickCompany: ["회사", "Company"], + B01_Dashboard_PickCompany: ["회사 고르기", "Pick company"], /* --- B08·B09 빈 화면 (PLAN 7-3) --- */ B08_Quantity_Rebuilding: [