feat(M02): 서고 구분 · 상세구분 · 연결 화면(51-12)

- 서고 목록표에 구분 · 상세구분 칸(키 뒤)
- 단가승격 상세 정보 칸 = 구분 · 상세구분 · 연결(원 로직 키 · 출처) 읽기만
- 사전조합 상세 정보 칸 = 구분 · 상세구분 고르기(로직 구분 목록 logic-lib/subs · 구분 바꾸면 상세구분 = 그 첫 줄)
- 새 사전조합 처음 값 = 로직 구분 목록 첫 줄
- 로직 고르기 창 구분 · 상세구분 거름에 승격 · 사전조합 줄도 걸림
- 화면 시나리오 51-12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
2026-10-05 20:49:43 +09:00
co-authored by Claude Opus 5.5
parent c7a84a0ed9
commit dda5f6233f
6 changed files with 275 additions and 17 deletions
@@ -0,0 +1,142 @@
/* 51-12 — 서고(단가승격 · 사전조합) 구분 · 상세구분 · 연결
* 시작 화면: M02 마스터 템플릿 · 마스터 층(#/m02-master-template?open=일위대가조합&root=master) · 시스템 관리자
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌)
* 쓰기 없음 — GET · calc 밖 요청은 가짜 400(새로 만들기 POST 본문만 잡음) · 마스터 SP-000001 · SC-000001 읽기만
* 기대 수치:
* - heads = ["", "키", "구분", "상세구분", "이름", ...] · listSP = SP-000001 줄 구분 · 상세구분 빈 것 아님
* - promote = { 구분, 상세구분, 연결 } 셋 다 글 있음 · readOnly = true
* - precombo = { selects: 2, 구분 = 문서 값 · subOptions ≥ 1 } · switched.상세구분 = 바꾼 구분의 첫 상세구분 · back = 처음 값
* - created = { 구분: 목록 첫 구분, 상세구분: 그 첫 상세구분 }
* - picker = 구분(SP-000001 것) 고르면 SP-000001 줄 보임 · 다른 구분이면 안 보임
*/
(async () => {
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 6000) => {
for (let t = 0; t < ms; t += 100) {
const v = fn();
if (v) return v;
await wait(100);
}
return null;
};
const json = (body, status = 200) =>
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
const real = window.fetch;
let posted = null;
window.fetch = async (input, init = {}) => {
const url = String(input?.url ?? input);
const method = (init.method ?? "GET").toUpperCase();
if (method === "POST" && url.includes("/library/precombo")) {
posted = JSON.parse(init.body);
return json({ detail: "시험 — 쓰기 막음" }, 400);
}
if (method !== "GET" && !url.includes("/calc"))
return json({ detail: "시험 — 쓰기 막음" }, 400);
return real(input, init);
};
const sec = [...document.querySelectorAll(".m02-side__lists > section")].find(
(s) => s.querySelector(".ui-collapsible__title")?.textContent?.trim() === "일위대가 조합",
);
const items = [...sec.querySelectorAll("[data-precombo]")];
const list = (kind) => document.querySelector(`[data-lib-list="${kind}"]`);
const edit = () => document.querySelector(".m02-main__host [data-labor]");
const field = (name) => edit()?.querySelector(`[data-labor-field="${name}"]`);
const btn = (root, text) =>
[...(root?.querySelectorAll("button") ?? [])].find((b) => b.textContent === text);
const subs = await (
await real("/api/m02/logic-lib/subs", { credentials: "include" })
)
.json()
.then((d) => d.subs);
try {
// ① 승격 목록 · 상세
items[0].click();
const spRow = await until(() => list("promote")?.querySelector('[data-lib-key="SP-000001"]'));
const heads = [...list("promote").querySelectorAll("thead th")].map((th) => th.textContent);
const cells = [...spRow.querySelectorAll("td")].map((td) => td.textContent);
const listSP = { 구분: cells[2], 상세구분: cells[3] };
spRow.querySelector("td:nth-child(5)").click();
await until(() => field("연결"));
const val = (name) => field(name)?.querySelector("input")?.value ?? null;
const promote = {
구분: val("구분"),
상세구분: val("상세구분"),
연결: val("연결"),
readOnly: ["구분", "상세구분", "연결"].every(
(n) => field(n)?.querySelector("input")?.readOnly,
),
};
// ② 사전조합 상세 — 고르기 · 바꿔 보고 되돌림
items[1].click();
(
await until(() =>
list("precombo")?.querySelector('[data-lib-key="SC-000001"] td:nth-child(5)'),
)
).click();
const subSel = await until(() => field("구분")?.querySelector("select"));
await until(() => subSel.options.length > 1, 4000);
const detSel = field("상세구분").querySelector("select");
const start = { 구분: subSel.value, 상세구분: detSel.value };
const precombo = {
selects: edit().querySelectorAll(".m01lab__info select").length,
...start,
subOptions: subSel.options.length,
};
const other = subs.find((s) => s.name !== start.구분 && s.details.length);
subSel.value = other.name;
subSel.dispatchEvent(new Event("change"));
const switched = {
구분: field("구분").querySelector("select").value,
상세구분: field("상세구분").querySelector("select").value,
want: other.details[0],
};
const s2 = field("구분").querySelector("select");
s2.value = start.구분;
s2.dispatchEvent(new Event("change"));
const d2 = field("상세구분").querySelector("select");
d2.value = start.상세구분;
d2.dispatchEvent(new Event("change"));
const back = {
구분: field("구분").querySelector("select").value,
상세구분: field("상세구분").querySelector("select").value,
};
// ③ 고르기 창 — 구분 거름에 승격 줄
btn(edit(), "로직 추가").click();
const pick = await until(() => document.querySelector(".m01-logic__pick"));
await until(() => pick.querySelector('[data-pick="SP-000001"]'), 20000);
const [fSub, fDet] = pick.querySelectorAll(".ui-find__filters select");
const pickerWith = async (s, d) => {
fSub.value = s;
fSub.dispatchEvent(new Event("change"));
if (d != null) {
fDet.value = d;
fDet.dispatchEvent(new Event("change"));
}
await wait(100);
return !!pick.querySelector('[data-pick="SP-000001"]');
};
const picker = {
same: await pickerWith(promote.구분, promote.상세구분),
other: await pickerWith(subs.find((s) => s.name !== promote.구분)?.name ?? "", null),
};
pick.closest(".m01-logic__backdrop")?.remove();
// ④ 새 사전조합 처음 값(POST 본문만 잡음)
items[1].click();
await until(() => list("precombo"));
btn(document.querySelector(".m02-main__slot"), "생성").click();
const box = await until(() => document.querySelector(".m02-modal"));
box.querySelector("input").value = "시험 51-12";
btn(box, "생성").click();
await until(() => posted);
btn(document.querySelector(".m02-modal"), "닫기")?.click();
document.querySelector(".m02-modal")?.closest(".ui-modal__backdrop, .ui-modal")?.remove();
const created = {
구분: posted?.문서?.구분,
상세구분: posted?.문서?.상세구분,
want: [subs[0].name, subs[0].details[0] ?? null],
};
return JSON.stringify({ heads, listSP, promote, precombo, switched, back, picker, created });
} finally {
window.fetch = real;
}
})();