feat(785-6): 엑셀 보기에 줄 판 · 블록 판 · [옮기기] 이음

- Hopyo_Sheet.ts: [수정] = 시트 아래 줄 판(createJudgeRows · 그 호표 자리 뜀) · 오른쪽 판 아래 [옮기기](createJudgeMove) · 고름 캐시 한 벌 · 옮긴 뒤 목록 · 칠 · 단가산출 목록 다시 읽음
- Hopyo_SheetSk.ts: 단가산출 목록 = 새 블록 판정(hopyo-judge 목록 · 판정 · 고를 것 n · 표지 · 부름 · 만든 SK · 캐시 확인) · 줄 누름 = 그 자리 + 블록 판(createJudgeBlock)
- Hopyo_JudgeApi.ts: 목록 블록 줄 판정 · 고를수 · Block.ts 타입 흠 둘
- 낡은 시험 고침(까닭 = 785-6 이 옛 마법사 단가산출 칸 · 확인 창을 새 판으로 갈음): test_627_sheet_sk(판 바꿈 · 강조 · 뛰기를 새 목록으로 · helper_778_5 sheetSk 대역 새 꼴) · test_785_4 줄 판 자리 손잡이 · test_m02_hopyo_screen [수정] = 줄 판
- test_642_6: 엑셀 보기 이음 · 가운데 페이지 없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 23:16:43 +09:00
co-authored by Claude Opus 5.5
parent 8fde93b910
commit dcecc0b326
10 changed files with 325 additions and 70 deletions
+100 -9
View File
@@ -18,10 +18,63 @@ const fakeEl = () => ({
this.kids.push(x);
},
});
const list = []; // 그린 목록 줄 벌
const reads = []; // 목록 읽음
const BRIEFS = [
{
번: 8,
이름: "거푸집",
단위: "㎡",
부름: [1],
표지: null,
만든: null,
판정: "고를 것",
고를수: 2,
},
{
번: 9,
이름: "잡석",
단위: "㎥",
부름: [],
표지: "준용",
만든: null,
판정: "줄 조합",
고를수: 0,
},
{
번: 10,
이름: "운반",
단위: "㎡",
부름: [2],
표지: null,
만든: "SK-000001",
판정: "기초로 풂",
고를수: 0,
},
];
const STUBS = {
"@ui/ui_template_locale": { t: (k) => (k === "M02_JudgeBlock" ? "단산{n}" : k) },
"@ui/ui_template_badge": { createBadge: (text, tone) => `${tone}:${text}` },
"@ui/ui_template_text": { createText: (text) => text },
"@ui/ui_template_list_row": { createListRow: (opts) => ({ opts, dataset: {} }) },
"./M02_MasterTemplete_Hopyo_JudgeApi": {
readJudge: async (scope, key, to) => {
reads.push({ key, to });
return { 키: key, 파일: "a.xls", 호표: [], 블록: BRIEFS };
},
},
"./M02_MasterTemplete_Hopyo_Block": { createJudgeBlock: (o) => ({ ...o, destroy() {} }) },
"@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} },
"@ui/ui_template_elements": {
el: (tag, o = {}) => ({ ...fakeEl(), tag, className: o.className }),
el: (tag, o = {}) => ({
...fakeEl(),
tag,
className: o.className,
replaceChildren(...kids) {
this.kids = kids;
if (kids.length && kids[0]?.opts) list.push(kids);
},
}),
},
"./M02_MasterTemplete_Hopyo_WizardSk": {
sangeunStep: (scope, key, file, onChange, o) => {
@@ -49,7 +102,7 @@ global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: ()
(async () => {
const input = JSON.parse(fs.readFileSync(0, "utf8"));
if (input.sheetSk) return process.stdout.write(JSON.stringify(sheetSk(input.sheetSk)));
if (input.sheetSk) return process.stdout.write(JSON.stringify(await sheetSk(input.sheetSk)));
const { blocks } = input;
const calls = [];
global.fetch = async (url, init = {}) => {
@@ -135,8 +188,8 @@ global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: ()
process.exit(1);
});
/** 780-2 판 바꾸기 — 탭(역할) 차례로 고름 · 판 숨김 · 칸 지음 한 번 · 강조 · 줄 누름 뛰기 */
function sheetSk({ same, canWrite, picks }) {
/** 780-2 · 785-6 판 바꾸기 — 탭(역할) 차례로 고름 · 판 숨김 · 목록 읽음 한 번 · 강조 · 줄 누름 = 뛰기 + 블록 판 */
async function sheetSk({ same, canWrite, picks }) {
const M02 = path.join(ROOT, "M02_MasterTemplete");
const { createSheetSk } = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_SheetSk.ts"));
const roles = { s1: "일위대가표", s2: "단가산출표", s3: "단가산출상세" };
@@ -156,13 +209,51 @@ function sheetSk({ same, canWrite, picks }) {
};
const scope = { layer: "personal", owner: null };
const target = same ? scope : { layer: "company", owner: null };
const p = createSheetSk({ scope, key: "HP-000001", 파일: "a.xls", target, canWrite, nav, side });
const state = {
get: () => ({ 호표: {}, 블록: { 8: { 고른: [], 확인: true } } }),
on: () => () => {},
};
const opened = [];
const p = createSheetSk({
scope,
key: "HP-000001",
target,
canWrite,
nav,
side,
state,
openLines: (make) => opened.push(make("host")),
closeLines: () => {},
});
const seen = [];
const rows = () => list.at(-1) ?? [];
for (const [sheet, cell] of picks) {
p.at({ 시트: sheet, 칸: cell });
seen.push({ 호표판숨김: side.hidden, 단가판숨김: p.root.hidden, 지음: made.length });
if (sheet === "s2" && made[0]) made[0].onRow(8); // 목록 줄 누름(단가산출표 탭)
if (sheet === "s3" && made[0]) made[0].onRow(8); // 상세 탭
await new Promise((r) => setTimeout(r, 0));
const active = rows().find((r) => r.opts.active)?.opts.label ?? null;
seen.push({
호표판숨김: side.hidden,
단가판숨김: p.root.hidden,
읽음: reads.length,
강조: active,
});
if (sheet !== "s1")
rows()
.find((r) => r.opts.label.startsWith("단산8 "))
?.opts.onClick();
}
return { seen, toggled, made: made.map((m) => ({ args: m.args, marks: m.marks })), jumps };
return {
seen,
toggled,
jumps,
reads,
blocks: opened.map((o) => ({ no: o.no, readOnly: o.readOnly, onSheet: !!o.onSheet })),
rows: rows().map((r) => ({
label: r.opts.label,
sub: r.opts.sub,
stripe: r.opts.stripe,
tail: r.opts.trailing,
})),
count: p.count(),
};
}
+27 -16
View File
@@ -35,29 +35,40 @@ PICKS = [["s1", "B5"], ["s2", "B12"], ["s1", "C3"], ["s3", "A85"], ["s3", "A2"]]
def test_판_바꿈_목록_지음_강조_뛰기():
got = _node({"sheetSk": {"same": True, "canWrite": True, "picks": PICKS}})
assert got["seen"] == [
{"호표판숨김": False, "단가판숨김": True, "지음": 0}, # 일위대가표 = 호표 목록 · 칸 안 지음
{"호표판숨김": True, "단가판숨김": False, "지음": 1}, # 단가산출표 = 단가산출 목록
{"호표판숨김": False, "단가판숨김": True, "지음": 1},
{"호표판숨김": True, "단가판숨김": False, "지음": 1}, # 다시 와도 한 번만 지음
{"호표판숨김": True, "단가판숨김": False, "지음": 1},
# 785-6 — 단가산출 목록 = 새 블록 판정(hopyo-judge 목록) · 처음 단가산출 탭에 올 때 한 번 읽음
hide = [(s["호표판숨김"], s["단가판숨김"], s["읽음"]) for s in got["seen"]]
assert hide == [
(False, True, 0),
(True, False, 1),
(False, True, 1),
(True, False, 1),
(True, False, 1),
]
assert [on for _, on in got["toggled"]] == [False, True, False, True, True]
(step,) = got["made"]
assert step["args"]["o"] == {
"onRow": True,
"readOnly": False,
} # `lines` 자리는 이 대역에서 없음
assert step["args"]["key"] == "HP-000001"
assert step["marks"] == [8, 8, 0] # 목록 행 12 → 단산8 · 상세 85 행 → 단산8 · 2 행 → 블록 0
# 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위)
assert got["reads"] == [{"key": "HP-000001", "to": {"layer": "personal", "owner": None}}]
# 목록 행 12 → 단산8 강조 · 상세 2 행 → 블록 0(없음) — 목록 줄 누름 뒤 단산8
assert [s["강조"] for s in got["seen"]] == [
None,
"단산8 거푸집",
"단산8 거푸집",
"단산8 거푸집",
None,
]
# 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위) + 시트 아래 블록 판
assert got["jumps"] == [["s2", [8, 9]], ["s3", [8, 9]], ["s3", [8, 9]]]
assert got["blocks"] == [{"no": 8, "readOnly": False, "onSheet": True}] * 3
rows = {r["label"]: r for r in got["rows"]}
assert rows["단산8 거푸집"]["sub"] == "㎡ · 고를 것 2 · M02_JudgeCalls 1"
assert rows["단산8 거푸집"]["tail"] == "accent:M02_JudgeConfirmed" # 캐시 확인
assert rows["단산9 잡석"]["sub"] == "㎥ · 줄 조합 · 준용 · M02_JudgeOutside"
assert rows["단산10 운반"]["tail"] == "success:M02_JudgeMade SK-000001"
assert got["count"] == {"블록": 2, "밖": 1, "줄": 2} # 만든 SK 뺌
def test_다른_층_권한_없음_보기만():
for same, can in ((False, True), (True, False)):
got = _node({"sheetSk": {"same": same, "canWrite": can, "picks": [["s2", "B5"]]}})
assert got["made"][0]["args"]["o"]["readOnly"] is True
assert got["blocks"][0]["readOnly"] is True
def test_거르기_줄0_막음_끝내기_글():
@@ -81,7 +92,7 @@ def test_캐시_공유_알림_칠_다시():
sheet = _src("Sheet")
assert "createSheetSk(" in sheet and "sk?.at(s);" in sheet
side = _src("SheetSk")
assert "sangeunStep(opt.scope, opt.key" in side # 마법사와 같은 파일 층 · 키
assert "readJudge(opt.scope, opt.key, opt.target)" in side # 785-6 새 블록 판정 목록
nav = _src("SheetNav")
assert "blockAt(sheet: string, row: number)" in nav and "spotOf(no: number, role: Role)" in nav
wizard = _src("Import")
+11
View File
@@ -267,3 +267,14 @@ def test_블록칠_새판정(monkeypatch):
monkeypatch.setitem(sys.modules, "M02_MasterTemplete.M02_Hopyo_Make", Make)
monkeypatch.setattr(sys.modules["M02_MasterTemplete"], "M02_Hopyo_Make", Make, raising=False)
assert mk._judged(Path("."), "x") == {10: {"판정": "기초로 풂", "고를수": 0}}
def test_엑셀보기_이음_가운데페이지없음():
"""줄 판 = 시트 아래 `[data-hopyo-lines]` · [옮기기] = 오른쪽 판 아래 · 단가산출 목록 = 새 블록 판정."""
assert not (M02 / "M02_MasterTemplete_Hopyo_Wizard.ts").exists()
sheet = (M02 / "M02_MasterTemplete_Hopyo_Sheet.ts").read_text(encoding="utf-8")
assert "createJudgeRows({" in sheet and "createJudgeMove({" in sheet
right = sheet.split('className: "m02-hopyo-sheet__side"')[1].split("});")[0]
assert "move.root" in right
sk = (M02 / "M02_MasterTemplete_Hopyo_SheetSk.ts").read_text(encoding="utf-8")
assert "createJudgeBlock({" in sk and "sangeunStep" not in sk
+2 -2
View File
@@ -57,8 +57,8 @@ def test_흠_셋_고침():
assert "skLineTable({" in sk.split("function drawLines()")[1]
assert "is-sub" not in sk and "o.narrow" not in sk
sheet = _src("Sheet")
assert 'attrs: { "data-hopyo-lines": "" }' in sheet and "lines," in sheet
assert "lines: HTMLElement;" in _src("SheetSk")
assert 'attrs: { "data-hopyo-lines": "" }' in sheet and "openLines," in sheet
assert "openLines: (make: (host: HTMLElement)" in _src("SheetSk") # 785-6 줄 판 자리 한 손잡이
def test_끝내기_머리_막대_끝냄_뒤_빈_본_칸():
+3 -1
View File
@@ -328,7 +328,9 @@ def test_옛_즉시_확정과_바로_만들기_없음():
) # 하위 먼저 · 함께(777-1)
sheet = texts["Hopyo_Sheet"] # 목록 · [수정] = 엑셀 보기 몸
edit = sheet.split("const editButton =")[1].split("function paintList(")[0]
assert '"수정"' in edit and "e.stopPropagation();" in edit and "openHopyoCheck({" in edit
assert (
'"수정"' in edit and "e.stopPropagation();" in edit and "createJudgeRows({" in edit
) # 785-6 줄 판
row = sheet.split("function paintList(")[1]
assert row.index("m02-hopyo-sheet__no") < row.index("editButton(h)") < row.index("__name")
assert 'linked.classList.toggle("is-linked", allLinked(h));' in row