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:
@@ -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() }),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/* 접히는 제목 줄 — 단추 칸을 접힘 표시 바로 왼쪽으로 */
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user