From a6b236f2b2a3b5d8a25a691bf1f90ed39a2b1b04 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 14:02:33 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(837-1):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EC=B8=B5=20?= =?UTF-8?q?=EA=B0=80=EB=A5=B4=EA=B8=B0=20=EA=B1=B7=EC=9D=8C=20=E2=80=94=20?= =?UTF-8?q?=EB=88=84=EA=B0=80=20=ED=95=98=EB=93=A0=20=EC=9E=90=EA=B8=B0=20?= =?UTF-8?q?=EA=B0=9C=EC=9D=B8=20=EC=B8=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면: 왼쪽 층 고름(개인·회사·마스터) · 머리 층 배지 · 작업 중 줄의 층 글 걷음(Hopyo_Side · Hopyo_Import) - 서버: 불러오기 길(Hopyo_Router)은 경로 층 · 주인 · 몸 층을 무시하고 개인 층 고정 · 단산 블록 → 가져온 식(from-hopyo · preview)도 개인 층 - 시험: 새 지킴 시험 test_837_1 · 낡은 시험 고침(785_4 층 고름 → 층 가르기 없음 · dirty 글 / 705_2 남의 층 403 → 경로 층 무시 200) Co-Authored-By: Claude Sonnet 5.5 --- M02_MasterTemplete/M02_Hopyo_Router.py | 10 ++- .../M02_MasterTemplete_Hopyo_Import.ts | 30 ++------ .../M02_MasterTemplete_Hopyo_Side.ts | 70 ++++--------------- M02_MasterTemplete/M02_Sangeun_Router.py | 10 +-- resources/tester/test_705_2_hopyo_paste.py | 5 +- resources/tester/test_785_4_import_screen.py | 20 +++--- .../tester/test_837_1_import_personal_only.py | 46 ++++++++++++ 7 files changed, 92 insertions(+), 99 deletions(-) create mode 100644 resources/tester/test_837_1_import_personal_only.py diff --git a/M02_MasterTemplete/M02_Hopyo_Router.py b/M02_MasterTemplete/M02_Hopyo_Router.py index 90abb00a5..9ff588aea 100644 --- a/M02_MasterTemplete/M02_Hopyo_Router.py +++ b/M02_MasterTemplete/M02_Hopyo_Router.py @@ -7,7 +7,7 @@ 그대로 쓰는 길(목록 · 올림 · 엑셀 보기 머리 · 격자 · 지우기 · 모두 고정 · 붙여넣기 · 다시 잇기 · 구조물 조합 · 칠 = B09 · M01 이 씀) · 옛 맞춤 길(호표 하나 맞춤 · 미리 셈 · 실무 호표 · 확인 만들기 · 안 이은 줄 · 단가산출 만들기)은 785-7 에서 지움. -층 권한 = `Router_Items._root` · ⚠ 등록은 `main.py`(로그인만). +층 = 늘 개인(요구 855 · 위 `_root` 가 고정) · ⚠ 등록은 `main.py`(로그인만). """ from __future__ import annotations @@ -33,11 +33,17 @@ from M02_MasterTemplete import M02_Hopyo_View as hopyo_view_ from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_San_MatPick as mat_pick from M02_MasterTemplete import M02_Sangeun_FromHopyo as from_hopyo -from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root +from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call +from M02_MasterTemplete.M02_MasterTemplete_Router_Items import _root as _layer_root router = APIRouter(prefix="/api/m02", tags=["M02 Hopyo"]) +def _root(session: Session, layer: str, owner: str | None = None, write: bool = False) -> Path: + """불러오기 자리 = 늘 자기 개인 층(요구 855) — 경로 층 · 주인 글 · 몸 층은 받되 무시(회사 · 마스터 · 남의 층 불가).""" + return _layer_root(session, "personal", None, write=write) + + # ── [수정] 보기(809-1) — 경쟁사 로직 인정 · 요소만 연결 · 읽기만 ───────────────── diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Import.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Import.ts index c3fe645d7..64ef55d08 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Import.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Import.ts @@ -1,8 +1,8 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_Import.ts * 일위대가 불러오기 본 칸 보기(PLAN 785-4 · 요구 642-11 — 마법사 `Hopyo_Wizard` 갈음 · 가운데 상세 페이지 없음) — - * 왼쪽 [불러오기] → 층 · 파일 고름 → 올림(`Hopyo_Side`) → 바로 이 보기: - * · 머리 막대 = 엑셀 보기 몸(`Hopyo_Sheet` — 시트 · 호표 · 검산 · 연결)의 머리에 층 · [끝내기]를 덧붙임 + * 왼쪽 [불러오기] → 파일 고름 → 올림(`Hopyo_Side` · 층 가르기 없음 — 누가 하든 자기 개인 층 · 요구 855) → 바로 이 보기: + * · 머리 막대 = 엑셀 보기 몸(`Hopyo_Sheet` — 시트 · 호표 · 검산 · 연결)의 머리에 [끝내기]를 덧붙임 * · 아래 = 엑셀(탭 셋) + 오른쪽 호표 · 단가산출 목록(`Hopyo_Sheet` · `Hopyo_SheetSk`) · 만들기 = 호표 줄 [내 일위대가로](807-1) * · [끝내기] = 물음(안 만든 단가산출 알림) → 서버가 이번 HP · 격자 저장본 지움 · * 만든 일위대가 · 단가산출(엑셀 사본 걷고 출처만) · 별칭 장부 남음 · 옛 「안 이은 줄」 띠는 785-7 에서 지움(판정 「고를 것」) @@ -10,34 +10,19 @@ * 하루 지난 것은 서버가 지움 · 엑셀 자료는 보관하지 않음(필요하면 다시 올림) * ========================================================================== */ -import { createBadge } from "@ui/ui_template_badge"; import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createText } from "@ui/ui_template_text"; -import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { deleteHopyo, type HopyoFile, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; import type { HopyoSheetBody } from "./M02_MasterTemplete_Hopyo_Sheet"; -import { accessFor, type ManageLayer } from "./M02_MasterTemplete_Layer"; import "./M02_MasterTemplete_Hopyo.css"; -export const LAYERS: { layer: ManageLayer; text: string }[] = [ - { layer: "personal", text: "개인" }, - { layer: "company", text: "회사" }, - { layer: "system", text: "마스터" }, -]; -export const layerText = (layer: string): string => - LAYERS.find((x) => x.layer === layer)?.text ?? layer; +/** 불러오기 자리 = 늘 자기 개인 층(요구 855 — 층 가르기 없음) */ +export const MINE: HopyoScope = { layer: "personal", owner: null }; const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); -/** 쓸 수 있는 자기 층(개인 · 회사 · 마스터 차례) */ -export async function writableLayers(): Promise { - const user = await fetchSessionUser().catch(() => null); - return LAYERS.map((x) => x.layer).filter((layer) => accessFor(layer, null, user).canWrite); -} - export interface HopyoImportArgs { - layer: ManageLayer; file: HopyoFile; /** [끝내기] 로 올린 파일을 지웠음 */ onEnded: () => void; @@ -52,8 +37,8 @@ export interface HopyoImport { } export function buildHopyoImport(a: HopyoImportArgs): HopyoImport { - const { layer, file } = a; - const scope: HopyoScope = { layer, owner: null }; + const { file } = a; + const scope = MINE; let held = true; let body: HopyoSheetBody | null = null; @@ -74,7 +59,6 @@ export function buildHopyoImport(a: HopyoImportArgs): HopyoImport { // ── 머리 막대에 덧붙일 것 ── const finish = createButton({ label: "끝내기", variant: "filled", size: 28, onClick: () => void end() }); // prettier-ignore finish.dataset.hopyoFinish = ""; - const layerNote = createBadge(`${layerText(layer)} 층`, "neutral"); async function end(): Promise { finish.disabled = true; @@ -111,7 +95,7 @@ export function buildHopyoImport(a: HopyoImportArgs): HopyoImport { 파일: file.파일, target: scope, canWrite: true, - headExtra: [layerNote, finish], + headExtra: [finish], }); body = view; root.replaceChildren(...view.parts); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts index 7ede0c750..563c14f1d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts @@ -3,8 +3,8 @@ * 좌측 「일위대가 불러오기」 컨테이너(PLAN 324-4 · 718-1 한시 · 785-4) — * · [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) = 바로 파일 하나 고르기 창(엑셀 · 경쟁사 .estx · .STC · 814-1) → 올림 → 본 칸 불러오기 보기 * (`M02_MasterTemplete_Hopyo_Import` · 엑셀 + 오른쪽 호표 목록 · 가운데 상세 페이지 없음 · 누름 하나) - * · 결과 층 고름 = 작은 고름(쓸 수 있는 층 둘 이상일 때만 · 일반 = 개인 하나라 안 보임) - * · 「작업 중」 줄 = 층마다 서버 목록(같은 엑셀 작업 중 이어서) · 누르면 본 칸 불러오기 보기 + * · 층 가르기 없음 — 누가 하든 올린 것 · 만든 것 = 자기 개인 층(요구 855 · 공유는 만든 뒤 본인이) + * · 「작업 중」 줄 = 자기 개인 층 서버 목록(같은 엑셀 작업 중 이어서) · 누르면 본 칸 불러오기 보기 * · 다른 엑셀 [불러오기] · 다른 화면으로 = 지금 보기 떠남(올린 파일 지움 · 743-1) — 고르고 안 만든 확인 캐시가 있으면 물음 한 번 * 옛 [별칭 장부](338-3)는 걷음(815-2) — 별칭은 M01 요소 「별칭」 칸 · M01 별칭 모아 보기. * ========================================================================== */ @@ -12,22 +12,9 @@ import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { createEmpty } from "@ui/ui_template_empty"; import { createListRow } from "@ui/ui_template_list_row"; -import { createSegment } from "@ui/ui_template_segment"; import { t as L } from "@ui/ui_template_locale"; -import { - listHopyo, - uploadHopyo, - type HopyoFile, - type HopyoScope, -} from "./M02_MasterTemplete_Hopyo_Api"; -import { - buildHopyoImport, - LAYERS, - layerText, - writableLayers, - type HopyoImport, -} from "./M02_MasterTemplete_Hopyo_Import"; -import type { ManageLayer } from "./M02_MasterTemplete_Layer"; +import { listHopyo, uploadHopyo, type HopyoFile } from "./M02_MasterTemplete_Hopyo_Api"; +import { buildHopyoImport, MINE, type HopyoImport } from "./M02_MasterTemplete_Hopyo_Import"; export interface HopyoSectionOptions { /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ @@ -46,12 +33,9 @@ export interface HopyoSection { } const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); -const scopeOf = (layer: ManageLayer): HopyoScope => ({ layer, owner: null }); export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { const title = L("M02_HopyoImport"); - let layers: ManageLayer[] = []; - let pick: ManageLayer = "personal"; const input = el("input", { attrs: { type: "file", accept: ".xls,.xlsx,.estx,.stc", "data-hopyo-file": "" }, }) as HTMLInputElement; @@ -65,50 +49,25 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { start.title = "파일 하나 — 경쟁사 엑셀(.xls · .xlsx) 또는 경쟁사 파일(.estx · .STC) → 우리 정보로 다시 만듦(한시 · 끝내거나 떠나면 올린 파일 지움)"; start.dataset.hopyoWizardOpen = ""; - const seg = el("div", { className: "m02-side__row" }); - seg.hidden = true; const jobs = el("div", { className: "m02-hopyo-side", attrs: { "data-hopyo-jobs": "" } }); const body = el("div", { className: "m02-hopyo-side", - children: [el("div", { className: "m02-side__row", children: [start, input] }), seg, jobs], + children: [el("div", { className: "m02-side__row", children: [start, input] }), jobs], }); let open: { key: string; view: HTMLElement; imp?: HopyoImport } | null = null; - /** 층 고름 · 작업 중 줄 — 쓸 수 있는 층을 처음 한 번 읽음 */ + /** 작업 중 줄 — 자기 개인 층 서버 목록 */ async function refresh(): Promise { - if (!layers.length) { - layers = await writableLayers(); - if (!layers.includes(pick)) pick = layers[0] ?? "personal"; - if (layers.length > 1) { - const s = createSegment({ - options: LAYERS.filter((x) => layers.includes(x.layer)).map((x) => ({ value: x.layer, label: x.text })), // prettier-ignore - value: pick, - size: 24, - onChange: (v) => (pick = v as ManageLayer), - }); - s.root.dataset.hopyoLayer = ""; - seg.replaceChildren(s.root); - seg.hidden = false; - } - } - const rows = ( - await Promise.all( - layers.map((layer) => - listHopyo(scopeOf(layer)) - .then((list) => list.map((f) => ({ layer, f }))) - .catch(() => []), - ), - ) - ).flat(); + const rows = await listHopyo(MINE).catch(() => []); jobs.replaceChildren( ...(rows.length - ? rows.map(({ layer, f }) => { + ? rows.map((f) => { const row = createListRow({ label: f.파일, - sub: `호표 ${f.호표수} · ${layerText(layer)} — 이어서`, + sub: `호표 ${f.호표수} — 이어서`, size: 28, active: open?.imp !== undefined && open.key === f.키, - onClick: () => void show(layer, f), + onClick: () => void show(f), }); row.dataset.hopyoKey = f.키; return row; @@ -119,8 +78,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { /** [불러오기] — 지금 보기를 떠날 수 있는지 묻고 바로 파일 고르기 창 */ async function choose(): Promise { - if (!layers.length) await refresh().catch(() => {}); - if (!layers.length) return void showToast("쓸 수 있는 층 없음", "warning"); if (!(await leaveOk())) return; input.click(); } @@ -129,14 +86,14 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { input.value = ""; if (!file) return; start.disabled = true; - uploadHopyo(scopeOf(pick), file) + uploadHopyo(MINE, file) .then((got: HopyoFile & { 이어서?: boolean; 원파일?: boolean }) => { const tool = /\.estx$/i.test(file.name) ? "EST" : "STmate"; // 810-3 원파일 올림 안내 if (got.이어서) showToast(`같은 파일 작업 중 — ${got.키} 이어서`, "info"); else if (got.원파일) showToast(`${tool} 파일 — 단가산출표 · 산출근거`, "success"); else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success"); if (got.읽기불가) showToast(`가져온 식으로 열 수 없음 — ${got.읽기불가}`, "warning"); - return show(pick, got, true); + return show(got, true); }) .catch((error) => showToast(why(error), "error")) .finally(() => (start.disabled = false)); @@ -153,12 +110,11 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { } /** 본 칸에 불러오기 보기 — 같은 파일이면 그대로 */ - async function show(layer: ManageLayer, file: HopyoFile, checked = false): Promise { + async function show(file: HopyoFile, checked = false): Promise { if (open?.imp && open.key === file.키 && open.view.isConnected) return; if (!checked && !(await leaveOk())) return; open?.imp?.leave(); // 불러오기 떠남 = 올린 파일 지움(743-1) const imp = buildHopyoImport({ - layer, file, onEnded: () => { open = null; diff --git a/M02_MasterTemplete/M02_Sangeun_Router.py b/M02_MasterTemplete/M02_Sangeun_Router.py index 98b4ec47e..056aace19 100644 --- a/M02_MasterTemplete/M02_Sangeun_Router.py +++ b/M02_MasterTemplete/M02_Sangeun_Router.py @@ -5,7 +5,7 @@ 가져온 식(793-2 · `M02_Sangeun_Imported`) `…/sangeun/imported`(만들기 · 층 쓰기) · `/price` · `/link`(화면 캐시 문서 고침 · 저장 없음 · 층 읽기 · 기초로 바꾸기 없음 · 809-1) · 불러오기 단산 블록 → 가져온 식(793-3b · `M02_Sangeun_FromHopyo`) `…/sangeun/from-hopyo` -(만들 층 쓰기 · 몸 `문서` = 미리 보기에서 고친 문서) · 미리 보기 `…/from-hopyo/preview`(층 읽기 · 안 씀 · 835-1). +(늘 개인 층 쓰기 · 몸 `문서` = 미리 보기에서 고친 문서) · 미리 보기 `…/from-hopyo/preview`(층 읽기 · 안 씀 · 835-1). 로직 사본(795-1 · `M02_Sangeun_Copy`) `…/sangeun/copy/price` [단가 갱신] · `/regen` [재생성] (화면 캐시 문서에 할일 얹음 · 저장 없음 · 층 읽기). 수식 탭(796-2b · `M02_Sangeun_Equation`) `…/sangeun/equation`(층 읽기) · `/preview`(고침 미리 셈 · @@ -176,8 +176,8 @@ def sangeun_from_hopyo( ) -> dict: """불러오기 단산 블록 → 가져온 식 SK(793-3b) — 경로 층 = 만들 층 · 몸 층 = 파일 층 · 답 = {이름, 검산, 단가기준, 다시씀}(같은 내용 = 그 SK).""" - folder = _root(session, layer, owner, write=True) - source = _root(session, body.층, body.주인) + folder = _root(session, "personal", None, write=True) # 불러오기 = 늘 개인 층(요구 855) + source = _root(session, "personal", None) mine = _root(session, "personal", None, write=body.고름 is not None) fp, name = _call(source, mat_pick.fingerprint, source, body.키) # 827-3 그 파일 재료 고름 if body.고름 is not None and fp: @@ -200,8 +200,8 @@ def sangeun_from_hopyo_preview( ) -> dict: """[가져온 식으로 열기] 미리 보기(835-1 · 요구 852-1) — 층에 안 씀(재료 고름도 메모리) · 답 = {문서, 검산, 같은것(같은 내용 SK 키 | None)} · [저장] = `from-hopyo` 몸 `문서`.""" - folder = _root(session, layer, owner) - source = _root(session, body.층, body.주인) + folder = _root(session, "personal", None) # 불러오기 = 늘 개인 층(요구 855) + source = _root(session, "personal", None) mine = _root(session, "personal", None) fp, _ = _call(source, mat_pick.fingerprint, source, body.키) picks = mat_pick.merge(mat_pick.read(mine, fp)["고름"], body.고름) if fp else {} diff --git a/resources/tester/test_705_2_hopyo_paste.py b/resources/tester/test_705_2_hopyo_paste.py index b567d7af9..257ad1276 100644 --- a/resources/tester/test_705_2_hopyo_paste.py +++ b/resources/tester/test_705_2_hopyo_paste.py @@ -352,10 +352,11 @@ def test_구조물_조합으로_규격_묶음_울진_돌기슭막이(client): assert master_hash() == before -def test_구조물_조합으로_남의_층_403(client): +def test_구조물_조합으로_경로_층_무시_늘_개인(client): # 837-1 · 요구 855 — 옛 「남의 층 403」 갈음 key = _upload(client, "대흥") body = {"층": "personal", "키": key, "번": 17} - assert client.post("/api/m02/layers/company/hopyo-item", json=body).status_code == 403 + assert client.post("/api/m02/layers/company/hopyo-item", json=body).status_code == 200 + assert list((layers_dir() / "labor").glob("*.json")) # 쓴 곳 = 개인 층(`layers_dir`) def test_구조물_조합으로_같은_내용_409_씀_새로(client): diff --git a/resources/tester/test_785_4_import_screen.py b/resources/tester/test_785_4_import_screen.py index 24b212d9b..75a2a7188 100644 --- a/resources/tester/test_785_4_import_screen.py +++ b/resources/tester/test_785_4_import_screen.py @@ -27,20 +27,20 @@ def test_누름_하나_바로_엑셀(): choose = side.split("async function choose()")[1].split("input.addEventListener")[0] assert "input.click();" in choose # [불러오기] = 바로 파일 고르기 창 change = side.split('input.addEventListener("change"')[1].split("async function leaveOk")[0] - assert ( - "uploadHopyo(scopeOf(pick), file)" in change and "return show(pick, got, true);" in change - ) + assert "uploadHopyo(MINE, file)" in change and "return show(got, true);" in change show = side.split("async function show(")[1].split("async function showLedger")[0] assert "buildHopyoImport({" in show and "opt.show(" in show # 올림 뒤 본 칸 바로 imp = _src("Import") - assert "m.buildHopyoSheet({" in imp and "headExtra: [layerNote, finish]" in imp + assert "m.buildHopyoSheet({" in imp and "headExtra: [finish]" in imp -def test_결과_층_고름_둘_이상일_때만(): - side = _src("Side") - assert "layers.length > 1" in side and "seg.hidden = false;" in side - assert "seg.hidden = true;" in side # 처음 숨김 — 일반(개인 하나)은 안 보임 - assert 'let pick: ManageLayer = "personal";' in side +def test_층_가르기_없음_늘_개인(): # 837-1 · 요구 855 — 예전 「결과 층 고름」 걷음 + side, imp = _src("Side"), _src("Import") + for gone in ("createSegment", "writableLayers", "LAYERS", "layerText", "seg.hidden", "pick"): + assert gone not in side, gone + for gone in ("LAYERS", "layerText", "writableLayers", "layerNote", "createBadge"): + assert gone not in imp, gone + assert 'export const MINE: HopyoScope = { layer: "personal", owner: null };' in imp def test_끝내기_머리_막대_끝냄_뒤_빈_본_칸(): @@ -71,5 +71,5 @@ def test_부품_상자만_700줄_늘리지_않음(): def test_해시_캐시_열쇠_없음(): # 809-1 — 판정 고름 캐시(`Hopyo_Picks`) 걷음 · 옮기기 = [내 일위대가로] assert not (M02 / "logic" / "M02_MasterTemplete_Hopyo_Picks.ts").exists() - assert "draftKey" not in _src("Import") and "dirty: () => false" in _src("Import") + assert "draftKey" not in _src("Import") and "dirty: () => body?.matDirty()" in _src("Import") assert "location.hash" not in _src("Import") + _src("Side") # 새 해시 열쇠 없음 diff --git a/resources/tester/test_837_1_import_personal_only.py b/resources/tester/test_837_1_import_personal_only.py new file mode 100644 index 000000000..96d23093d --- /dev/null +++ b/resources/tester/test_837_1_import_personal_only.py @@ -0,0 +1,46 @@ +"""837-1 일위대가 불러오기 = 늘 자기 개인 층(요구 855) — 서버 · 화면 없이 돎. +불러오기 길(`M02_Hopyo_Router` · `Sangeun_Router` from-hopyo)은 경로 층 · 주인 · 몸 층을 무시하고 개인 층만 씀.""" + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from M02_MasterTemplete import M02_Hopyo_Router as hopyo # noqa: E402 + + +def test_불러오기_길_층_고정(monkeypatch): + seen: list[tuple] = [] + monkeypatch.setattr( + hopyo, + "_layer_root", + lambda s, layer, owner=None, write=False: seen.append((layer, owner, write)), + ) + for layer, owner in (("company", "3"), ("system", None), ("personal", "3/9")): + hopyo._root({}, layer, owner, write=True) + hopyo._root({}, layer, owner) + assert seen == [("personal", None, True), ("personal", None, False)] * 3 + + +def test_라우터_루트_모두_고정_함수(): + src = (ROOT / "M02_MasterTemplete" / "M02_Hopyo_Router.py").read_text(encoding="utf-8") + assert "import _root as _layer_root" in src + assert "_layer_root(" in src.split("def _root(")[1].split("\n\n")[0] + assert src.count("_layer_root(") == 1 # 층 고르는 길은 이 한 곳뿐 + + +def test_단산_블록_만들기_개인_층만(): + src = (ROOT / "M02_MasterTemplete" / "M02_Sangeun_Router.py").read_text(encoding="utf-8") + for fn in ("def sangeun_from_hopyo(", "def sangeun_from_hopyo_preview("): + body = src.split(fn)[1].split("\n@router")[0] + assert "body.층" not in body and "_root(session, layer" not in body, fn + assert '_root(session, "personal", None' in body, fn + + +def test_화면_층_고름_없음(): + m02 = ROOT / "M02_MasterTemplete" + side = (m02 / "M02_MasterTemplete_Hopyo_Side.ts").read_text(encoding="utf-8") + imp = (m02 / "M02_MasterTemplete_Hopyo_Import.ts").read_text(encoding="utf-8") + assert "createSegment" not in side and "writableLayers" not in side + imp + assert '"회사"' not in side + imp and '"마스터"' not in side + imp From 41d2b1101484396cca4266b2fc2746d8c05dfb5b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 14:17:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?test(837-1):=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EC=B8=B5=20=EA=B3=A0=EC=A0=95=EC=97=90=20=EA=B1=B8?= =?UTF-8?q?=EB=A6=B0=20=EB=82=A1=EC=9D=80=20=EC=8B=9C=ED=97=98=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8=20=E2=80=94=20=EC=B8=B5=20=EA=B6=8C=ED=95=9C=20403=20?= =?UTF-8?q?=E2=86=92=20=EA=B2=BD=EB=A1=9C=20=EC=B8=B5=20=EB=AC=B4=EC=8B=9C?= =?UTF-8?q?=20200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_m02_hopyo_excel: 역할 표 · 마스터 폴더 시험을 「늘 개인 층 · 마스터 폴더 증가 0」 로 갈음 - test_m02_hopyo_grid: 남의 개인 층 403 → 주인 글 무시 200 - test_m02_hopyo_screen: 층 고름 글 대조 → 층 고름 없음 Co-Authored-By: Claude Sonnet 5.5 --- resources/tester/test_m02_hopyo_excel.py | 34 +++++++++++------------ resources/tester/test_m02_hopyo_grid.py | 2 +- resources/tester/test_m02_hopyo_screen.py | 5 ++-- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/resources/tester/test_m02_hopyo_excel.py b/resources/tester/test_m02_hopyo_excel.py index 3d84889a9..e2a59a820 100644 --- a/resources/tester/test_m02_hopyo_excel.py +++ b/resources/tester/test_m02_hopyo_excel.py @@ -191,32 +191,32 @@ def test_엉터리_시트없음_큰_파일_400(client, monkeypatch): @pytest.mark.parametrize( - ("role", "layer", "owner", "want"), + ("role", "layer", "owner"), [ - ({"role": "SYSTEM_ADMIN"}, "system", None, 200), - ({"role": "USER"}, "system", None, 403), - ({"role": "ADMIN"}, "company", None, 200), - ({"role": "USER"}, "company", None, 403), - ({"role": "USER"}, "personal", None, 200), - ({"role": "USER"}, "personal", "7/43", 403), - ({"role": "USER"}, "personal", "8/50", 403), + ({"role": "SYSTEM_ADMIN"}, "system", None), + ({"role": "USER"}, "system", None), + ({"role": "ADMIN"}, "company", None), + ({"role": "USER"}, "company", None), + ({"role": "USER"}, "personal", None), + ({"role": "USER"}, "personal", "7/43"), + ({"role": "USER"}, "personal", "8/50"), ], ) -def test_역할_표(client, role, layer, owner, want): +def test_역할_표_늘_개인_층(client, role, layer, owner): + """837-1 · 요구 855 — 누가 어느 경로 층 · 주인으로 올려도 자기 개인 층(옛 「층 권한 403」 갈음).""" client.who.update(role) params = {"owner": owner} if owner else {} - assert _post(client, layer, **params).status_code == want - if want == 200: - assert client.get(URL.format(layer)).json()["줄"][0]["호표수"] == 68 + assert _post(client, layer, **params).status_code == 200 + assert client.get(URL.format("personal")).json()["줄"][0]["호표수"] == 68 + assert client.get(URL.format(layer), params=params).json()["줄"][0]["호표수"] == 68 # 같은 자리 -def test_마스터_층은_마스터_폴더(client): +def test_마스터_경로로_올려도_마스터_폴더에_안_씀(client): client.who.update(role="SYSTEM_ADMIN") + before = master_hash() assert _post(client, "system").status_code == 200 - assert (client.system / hs.DIRNAME / "HP-000001.json").is_file() - client.who.update(role="USER") - assert client.get(URL.format("system")).json()["줄"][0]["키"] == "HP-000001" # 읽기 = 누구나 - assert client.delete(URL.format("system") + "/HP-000001").status_code == 403 + assert not (client.system / hs.DIRNAME).exists() # 837-1 — 마스터 폴더 증가 0 + assert master_hash() == before def _linked_xlsx() -> bytes: diff --git a/resources/tester/test_m02_hopyo_grid.py b/resources/tester/test_m02_hopyo_grid.py index 59aba9f0d..1e1e4ee04 100644 --- a/resources/tester/test_m02_hopyo_grid.py +++ b/resources/tester/test_m02_hopyo_grid.py @@ -100,7 +100,7 @@ def test_올리기_격자_200_404_지우기(client): ("role", "layer", "owner", "want"), [ ({"role": "USER"}, "personal", None, 200), - ({"role": "USER"}, "personal", "7/43", 403), + ({"role": "USER"}, "personal", "7/43", 200), # 837-1 — 주인 글 무시 · 늘 자기 개인 층 ({"role": "USER"}, "company", None, 200), ({"role": "USER"}, "system", None, 200), ], diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 36d536b5b..eed03d6c1 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -39,9 +39,8 @@ def test_좌측_판_자리와_권한(): assert "input.hidden = true;" in hopyo_side choose = hopyo_side.split("async function choose()")[1].split("input.addEventListener")[0] assert "input.click();" in choose # 누름 하나 — 바로 파일 고르기 창 - assert "accessFor(layer, null, user).canWrite" in imp # 결과 층 = 쓸 수 있는 층만 - assert 'let pick: ManageLayer = "personal";' in hopyo_side # 기본 개인 - assert "layers.length > 1" in hopyo_side # 층 고름 = 둘 이상일 때만(일반 = 개인 하나) + assert "writableLayers" not in imp + hopyo_side # 837-1 층 고름 걷음 — 늘 개인 층 + assert "uploadHopyo(MINE, file)" in hopyo_side for gone in ("wantSangeun", "STEPS", "second(", "steps.set", "data-hopyo-sangeun"): assert gone not in imp + hopyo_side, gone # 중간 걸음 · 단가산출 체크 없음