feat(M02): 가져올 곳 탭 readableSources · 회사 맡음 manages — 복사 생성 · 복사해 가져오기 탭 목록(39-6 E5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
2026-10-04 17:21:17 +09:00
co-authored by Claude Opus 5.5
parent ddd73349f2
commit eee8f9a459
3 changed files with 123 additions and 0 deletions
@@ -93,6 +93,78 @@ export function accessFor(
return { canWrite: layer === "personal" || companyAdmin, needCompany: false };
}
/** 회사 C 를 맡음 — 시스템 관리자 · 또는 C 의 회사 관리자(서버 `M02_Layer_Access.manages` 짝 · 39-6) */
export function manages(company: string | null, user: SessionUser | null): boolean {
if (!company || !user) return false;
if (user.role === "SYSTEM_ADMIN") return true;
const companyAdmin = user.role === "ADMIN" || !!user.is_master;
return companyAdmin && company === ownOwner("company", user);
}
/** 가져올 곳 탭 하나 — `people` = 다른 사람(주인은 부르는 쪽이 `fetchOwners` 로 고름) */
export interface Source {
id: "master" | "company" | "personal" | "people" | "practice";
layer: ManageLayer;
/** 층 주인 — 자기 층 · 마스터 · 실무문서 · 다른 사람 = null */
owner: string | null;
label: string;
}
/** 복사 생성 · 복사해 가져오기의 탭(39-6 · 답 2) — 읽을 수 있는 곳 + 기본 탭.
* 기본 = 시스템 관리자 마스터 · 그 밖 내 개인(회사 없으면 마스터) · `practice` = 일위대가 고를 때만 */
export function readableSources(
user: SessionUser | null,
company: string | null,
opt: { practice?: boolean } = {},
): { tabs: Source[]; first: Source["id"] } {
const isSystem = user?.role === "SYSTEM_ADMIN";
const own = ownOwner("personal", user);
const tabs: Source[] = [
{ id: "master", layer: "system", owner: null, label: L("M02_PickerSystem") },
...(company
? [
{
id: "company" as const,
layer: "company" as const,
owner: company === ownOwner("company", user) ? null : company,
label: L("M02_PickerCompany"),
},
]
: []),
...(own
? [
{
id: "personal" as const,
layer: "personal" as const,
owner: null,
label: L("M02_PickerPersonal"),
},
]
: []),
...(manages(company, user)
? [
{
id: "people" as const,
layer: "personal" as const,
owner: null,
label: L("M02_SourcePeople"),
},
]
: []),
...(opt.practice && isSystem
? [
{
id: "practice" as const,
layer: "system" as const,
owner: null,
label: L("M02_SourcePractice"),
},
]
: []),
];
return { tabs, first: isSystem || !own ? "master" : "personal" };
}
/** 회사 없음 — 개인 · 회사 층은 회사 폴더 아래라 회사부터 */
export function buildNeedCompany(): HTMLElement {
const go = createButton({ label: L("M02_NeedCompanyGo"), variant: "filled" });
+49
View File
@@ -87,6 +87,55 @@ def test_주소_인자를_라우트에서_뗌():
assert 'open: "구조물조합", root: "personal"' in picker and "layer=personal" not in picker
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_가져올_곳_탭():
"""39-6 · 답 2 — 탭 = 마스터 · 볼 회사 · 내 개인 · 다른 사람(맡은 회사) · 실무문서(시스템 관리자 ·
일위대가) · 기본 = 시스템 관리자 마스터 · 그 밖 내 개인(회사 없으면 마스터)."""
text = _text(LAYER)
parts = [
re.search(r"^export const ownOwner[\s\S]*?;\n", text, re.M),
re.search(r"^export function manages[\s\S]*?^}", text, re.M),
re.search(r"^export interface Source[\s\S]*?^}", text, re.M),
re.search(r"^export function readableSources[\s\S]*?^}", text, re.M),
]
assert all(parts), "ownOwner · manages · Source · readableSources 를 못 찾음"
script = (
'type ManageLayer = "system" | "company" | "personal";\n'
"interface SessionUser { id: string; role?: string; company_id?: number | null;"
" is_master?: boolean }\n"
"const L = (key: string): string => key;\n"
+ "\n".join(p.group(0).replace("export ", "") for p in parts if p)
+ "\nconst cases = JSON.parse(process.argv[1]);"
+ "\nconsole.log(JSON.stringify(cases.map(([u, c, practice]) => {"
+ "\n const got = readableSources(u, c, { practice });"
+ "\n return [got.first, got.tabs.map((t) => t.id + (t.owner ? ':' + t.owner : ''))];"
+ "\n})));"
)
sys1 = {"id": "1", "role": "SYSTEM_ADMIN", "company_id": 1}
admin = {"id": "5", "role": "ADMIN", "company_id": 3}
master = {"id": "6", "role": "USER", "company_id": 3, "is_master": True}
user = {"id": "7", "role": "USER", "company_id": 3}
orphan = {"id": "8", "role": "USER", "company_id": None}
cases = [
[sys1, "3", True], [sys1, "1", False], [admin, "3", True], [master, "3", False],
[user, "3", True], [orphan, None, False],
] # fmt: skip
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script, json.dumps(cases)],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == [
["master", ["master", "company:3", "personal", "people", "practice"]], # 고객 회사 봄
["master", ["master", "company", "personal", "people"]], # 자기 회사 · 일위대가 아님
["personal", ["master", "company", "personal", "people"]], # 회사 관리자 · 실무문서 없음
["personal", ["master", "company", "personal", "people"]], # is_master = 회사 관리자
["personal", ["master", "company", "personal"]], # 일반 = 다른 사람 없음
["master", ["master"]], # 회사 없음
] # fmt: skip
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_볼_회사는_시스템_관리자만_고름():
"""29-2 — 시스템 관리자 = 해시 company · sessionStorage aislo.adminCompany · 자기 회사 차례 ·
+2
View File
@@ -103,6 +103,8 @@ export const ui_locales_m2 = {
M02_RootShared: ["회사 공용", "Company shared"],
M02_RootReadOnly: ["읽기", "read-only"],
M02_RootNone: ["볼 수 있는 층이 없음", "Nothing to show"],
M02_SourcePeople: ["다른 사람", "Others"],
M02_SourcePractice: ["실무문서", "Practice documents"],
M02_TreeSearch: [
"찾기 — 이름 · 키 · 폴더 · 단위 (낱말 여럿 = 모두 담은 것)",
"Find — name, key, folder, unit (all words)",