feat(B01): 회사 고르기를 알약으로 — 회사 따라 바뀌는 컨테이너마다(29-1 고침)

- 고르기 select 뺌 · 「시스템 설정」 · 프로젝트 · 사용자 관리 · 가입 요청 · 회사 관리 제목 오른쪽에 고른 회사 알약(공용 ui-tag 모양)
- 누르면 등록된 회사 작은 메뉴(공용 createMapContextMenu) · 어디서 골라도 함께 바뀜 · 알약 누름은 접힘으로 안 샘
- 회사 관리자 · 개인 화면은 알약 없음 · 임시 보관함 그대로
- 시험 test_b01_settings_cards 알약 판정으로 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z
This commit is contained in:
2026-09-30 17:35:59 +09:00
co-authored by Claude Opus 5.5
parent 59a4dd4eba
commit e7d8ea8002
4 changed files with 98 additions and 33 deletions
+44 -20
View File
@@ -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() }),
]),
);
+41 -6
View File
@@ -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);
}
/* 접히는 제목 줄 — 단추 칸을 접힘 표시 바로 왼쪽으로 */
+12 -6
View File
@@ -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
+1 -1
View File
@@ -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: [