feat(help): B02~B09 좌측 패널 [?] 설명서 모달 · 패널 안 설명 글을 파일로 옮김(54-2)

- [?] = 좌측 패널 아래 단추 묶음 맨 왼쪽(공용 한 벌 · B02 는 아래 단추 줄 · B08·B09 는 [?] 줄만)
- 모달 = 왼쪽 차례 · 오른쪽 글 · 고른 구조물 종류 · 연 상자 항목 먼저
- 글 = resources/help/<B0N>/*.md · 서버가 열 때마다 읽음(파일만 고치면 반영) · 설명서는 뼈대
- 옮김 = B03 안내 셋 · B05 구조물 저장 규칙 · 측구 주인 · 기슭막이 근거 · 칸 기본값 근거 · B06 표준 횡단면 두 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
2026-10-06 10:17:07 +09:00
co-authored by Claude Opus 5.5
parent bcc3cbb407
commit 73e8d4ea56
49 changed files with 622 additions and 242 deletions
+9
View File
@@ -0,0 +1,9 @@
# 프로젝트 등록 설명서
## 이 화면에서 하는 일
## 작업 순서
## 단추
## 주의할 점
+9
View File
@@ -0,0 +1,9 @@
# 파일입력 설명서
## 이 화면에서 하는 일
## 작업 순서
## 단추
## 주의할 점
@@ -0,0 +1,5 @@
# 필요한 파일
- 계획노선 — shapefile 한 벌(.shp·.shx·.dbf·.cpg·.prj) 5개.
- 지형 — 포인트클라우드 LAS/LAZ 1개와 좌표계 PRJ, 래스터 기준 TFW.
- 지형 TIF(DEM)는 선택 사항 — 없어도 지표면 분석이 진행됩니다.
@@ -0,0 +1,5 @@
# 고르는 방법
- 여러 개를 한 번에 끌어 놓으면 확장자로 카드에 자동 배정됩니다.
- 카드의 [선택]으로 한 칸씩 지정하거나 ✕로 되돌릴 수 있습니다.
- [임시 보관함]은 대시보드에 미리 올려 둔 자료를 가져옵니다.
+5
View File
@@ -0,0 +1,5 @@
# 알아 둘 것
- 이미 완료된 프로젝트에 다시 올리면 기존 분석 결과가 교체됩니다.
- 작업 좌표계는 라이다 PRJ가 기준 — 노선 좌표계와 다르면 업로드가 막힙니다.
- 라이다가 없으면 [LAS 없는 설계]를 켜고 도엽 등고선으로 진행합니다.
+9
View File
@@ -0,0 +1,9 @@
# 전처리 설명서
## 이 화면에서 하는 일
## 작업 순서
## 단추
## 주의할 점
+9
View File
@@ -0,0 +1,9 @@
# 종단설계 설명서
## 이 화면에서 하는 일
## 작업 순서
## 단추
## 주의할 점
@@ -0,0 +1,5 @@
# 구조물 · BOX암거
## 저장
- [저장]은 이 폼에 있는 칸만 바꿈 — 폼에 없는 칸(집계표·구조물도로 적은 값)은 그대로 둠 · 시설 종류를 바꾸면 새로 씀
@@ -0,0 +1,13 @@
# 구조물 · 기슭막이
## 저장
- [저장]은 이 폼에 있는 칸만 바꿈 — 폼에 없는 칸(집계표·구조물도로 적은 값)은 그대로 둠 · 시설 종류를 바꾸면 새로 씀
## 치수 근거
- 높이 — 계획홍수위 + 0.5~0.7m(사방기술교본 2-나:141 · 3-가:181) · 설계자 입력 · 비우면 미확정(금액에 안 들어감)
- 계획비탈 1:0.3~0.5(사방기술교본 2-나:129 · 3-가:151) — 제안값은 품셈 표준경사
- 둑마루 두께 0.3~0.5m(사방기술교본 2-나:143 · 3-가:201 · 3-가:219(콘크리트기슭막이) · 2-나:135(콘크리트 0.3m)) — 콘크리트 기준 · 돌쌓기는 뒷길이 + 0.30 식
- 뒷채움 조약돌 두께 — 사방기술교본 2-나:133(「사방품셈 규정에 의하여」) → 품셈 13-4-4 [주]⑨ 원문 L7164~7171
- ⚠ 사방(계류) 교본 기준 — 임도 배수관 유입·유출부 적용은 확인 남음
@@ -0,0 +1,6 @@
# 구조물 · 돌쌓기(메)
## 칸 기본값의 뜻
- **설치 측** — 「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식
- **버림 콘크리트**(횡단설계) — 사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜
@@ -0,0 +1,6 @@
# 구조물 · 돌쌓기(찰)
## 칸 기본값의 뜻
- **설치 측** — 「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식
- **버림 콘크리트**(횡단설계) — 사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜
@@ -0,0 +1,5 @@
# 구조물 · 물넘이포장
## 저장
- [저장]은 이 폼에 있는 칸만 바꿈 — 폼에 없는 칸(집계표·구조물도로 적은 값)은 그대로 둠 · 시설 종류를 바꾸면 새로 씀
+5
View File
@@ -0,0 +1,5 @@
# 구조물 · 배관
## 저장
- [저장]은 이 폼에 있는 칸만 바꿈 — 폼에 없는 칸(집계표·구조물도로 적은 값)은 그대로 둠 · 시설 종류를 바꾸면 새로 씀
@@ -0,0 +1,5 @@
# 구조물 · 비탈면 녹화
## 칸 기본값의 뜻
- **공법**(횡단설계) — 「초류종자 살포」는 여기 없음 — 토공집계에서 사면 전체로 섬(품셈 5-24 · 두 곳에 두면 이중계상 · 2026-09-15 브레인)
@@ -0,0 +1,5 @@
# 구조물 · 세월교
## 저장
- [저장]은 이 폼에 있는 칸만 바꿈 — 폼에 없는 칸(집계표·구조물도로 적은 값)은 그대로 둠 · 시설 종류를 바꾸면 새로 씀
+6
View File
@@ -0,0 +1,6 @@
# 구조물 · 옹벽
## 칸 기본값의 뜻
- **높이** — 기본값 · 소광리 도면 H=2.0 · 바꿀 수 있음
- **설치 측** — 「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식
@@ -0,0 +1,3 @@
# 구조물 · 측구(옆도랑)
- 횡단 설계에서 관리 — 여기서 넣어도 제원·수량은 그쪽 값을 씁니다.
@@ -0,0 +1,6 @@
# 구조물 · 큰돌쌓기
## 칸 기본값의 뜻
- **설치 측** — 「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식
- **버림 콘크리트**(횡단설계) — 사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜
@@ -0,0 +1,9 @@
# 구조물 · 흙막이
## 칸 기본값의 뜻
- **설치 측** — 「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식
- **단 수(다단)** — 1 = 다단 없음 — 도메인 수치가 아니라 「단이 하나」라는 표식(기슭막이와 같은 벌)
- **기준 올림(사면 위로)** — 0 = 자동 자리 그대로 — 올리지 않음을 뜻함(기슭막이와 같은 벌)
- **기준 좌우 이동** — 0 = 자동 자리 그대로 — 옮기지 않음을 뜻함(기슭막이와 같은 벌)
- **조달**(횡단설계) — 「채집」 — 별표2 「야면석 등은 가급적 현장에서 채취·사용」 권고. 구조물마다 바꿀 수 있음
+9
View File
@@ -0,0 +1,9 @@
# 횡단설계 설명서
## 이 화면에서 하는 일
## 작업 순서
## 단추
## 주의할 점
+9
View File
@@ -0,0 +1,9 @@
# 표준 횡단면 설정
## 변수 위치 안내
- 표준 편절편성 단면 모식도 — 각 설정값의 위치를 나타냅니다(실제 비율 아님).
## 표준횡단면 상세값
- L형 측구는 각 횡단면도에서 선택합니다.
+9
View File
@@ -0,0 +1,9 @@
# 상세설계 설명서
## 이 화면에서 하는 일
## 작업 순서
## 단추
## 주의할 점
+9
View File
@@ -0,0 +1,9 @@
# 수량산출 설명서
## 이 화면에서 하는 일
## 작업 순서
## 단추
## 주의할 점
+9
View File
@@ -0,0 +1,9 @@
# 원가계산 설명서
## 이 화면에서 하는 일
## 작업 순서
## 단추
## 주의할 점
+38
View File
@@ -0,0 +1,38 @@
"""54-2 [?] 설명서 — 서버가 resources/help/<페이지>/*.md 를 부를 때마다 읽음(고치면 바로 바뀜)."""
import tempfile
from pathlib import Path
import pytest
from fastapi import HTTPException
from common_util.common_util_help_router import HELP_ROOT, read_help
def test_every_page_has_manual_first():
for n in range(2, 10):
items = read_help(f"B0{n}")
assert items and items[0]["key"] == "00_설명서"
def test_title_order_and_live_edit():
tmp_path = Path(tempfile.mkdtemp())
page = tmp_path / "B05"
page.mkdir()
(page / "10_배관.md").write_text("# 배관\n\n본문", encoding="utf-8")
(page / "00_설명서.md").write_text("제목 없음", encoding="utf-8")
items = read_help("B05", tmp_path)
assert [i["key"] for i in items] == ["00_설명서", "10_배관"]
assert [i["title"] for i in items] == ["00_설명서", "배관"]
(page / "10_배관.md").write_text("# 배관\n\n고친 글", encoding="utf-8")
assert "고친 글" in read_help("B05", tmp_path)[1]["text"]
@pytest.mark.parametrize("page", ["B01", "B10", "../B05", "b05"])
def test_bad_page_refused(page):
with pytest.raises(HTTPException):
read_help(page)
def test_root_is_repo_resources():
assert HELP_ROOT.parts[-2:] == ("resources", "help")
@@ -0,0 +1,114 @@
/* 54-2 — [?] 설명서: B02~B09 좌측 패널 아래 단추 묶음 맨 왼쪽 · 모달 = 왼쪽 차례 · 오른쪽 파일 글 ·
* 구조물 종류를 고르면 그 항목 먼저 · 패널 안 설명 글 0
* 시작 화면: 아무 B 화면(로그인 · 환경 배정 storage 프로젝트가 현재 프로젝트)
* 부르는 법: orca eval --expression "$(cat 이 파일)" (async 식 · JSON 글을 돌려줌) · 1분 넘으면 끊기므로
* `window.__help542 = ["b02-proj-register", …];` 를 앞에 붙여 네 화면씩 두 번
* 쓰는 것: 없음(주소 이동 · 누르기 · 모달 닫기 · 끝나면 처음 주소로)
* 파일 고침 반영: 셸이 `resources/help/B04/99_임시.md` 를 만들고(글 A) 부름 → 글 B 로 고치고 다시 부름 →
* `temp` 가 A → B 로 바뀌면 통과 · 뒤에 파일 지움(없으면 temp = null)
* 기대 수치:
* - pages 8 · 줄마다 btn = 1 · first = true(묶음 맨 왼쪽) · inLeft = true(B02 는 아래 단추 줄) ·
* modal = true · toc[0] = 「… 설명서」 · text = 그 설명서 제목 글
* - B03 toc = 설명서 + 필요한 파일 · 고르는 방법 · 알아 둘 것 · B06 toc 에 「표준 횡단면 설정」
* - pipeTopic = 「구조물 · 배관」(B05 에서 배관을 고른 채 [?]) · leftovers = 0(옮긴 글이 패널에 없음)
*/
(async () => {
const w = (ms) => new Promise((r) => setTimeout(r, ms));
const until = async (fn, ms = 15000) => {
for (let t = 0; t < ms; t += 200) {
const v = fn();
if (v) return v;
await w(200);
}
return null;
};
const MOVED = [
"[저장]은 이 폼에 있는 칸만",
"에서 관리 — 여기서 넣어도",
"사방기술교본",
"표준 편절편성 단면 모식도",
"L형 측구는 각 횡단면도에서",
"끌어 놓으면 확장자로",
"shapefile 한 벌",
"작업 좌표계는 라이다 PRJ",
"칸이 생기기 전의 동작",
"소광리 도면 H=2.0",
];
const start = location.hash;
const vis = (n) => n && n.offsetParent !== null;
const leftText = () =>
document.querySelector(".ui-workflow-overlay__panel--title")?.innerText ?? "";
const openModal = async (btn) => {
btn.click();
const box = await until(() => document.querySelector(".ui-help .ui-help__text h2"));
const res = {
modal: !!box,
toc: [...document.querySelectorAll(".ui-help__link")].map((b) => b.innerText.trim()),
text: box?.innerText ?? null,
active: document.querySelector(".ui-help__text")?.dataset.helpKey ?? null,
};
const temp = [...document.querySelectorAll(".ui-help__link")].find((b) =>
b.dataset.helpKey?.startsWith("99_"),
);
if (temp) {
temp.click();
res.temp = document.querySelector(".ui-help__text p")?.innerText ?? null;
}
document.querySelector(".ui-help .ui-modal__top button")?.click();
await w(200);
res.closed = !document.querySelector(".ui-help");
return res;
};
const out = { pages: [] };
// orca eval 은 한 번에 1분쯤에서 끊김 — 앞에 `window.__help542 = ["b05-profile", …];` 를 붙이면 그 화면만
const routes = window.__help542 ?? [
"b02-proj-register",
"b03-file-input",
"b04-preprocess",
"b05-profile",
"b06-section",
"b07-design-detail",
"b08-quantity",
"b09-estimation",
];
let prev = null;
for (const route of routes) {
location.hash = `#/${route}`;
await w(3000); // 불러오는 동안의 임시 틀(B05 등)이 실제 화면으로 바뀔 때까지
// 앞 화면 단추가 빠지고 이 화면 단추가 설 때까지
const btn = await until(() =>
[...document.querySelectorAll(".ui-help-btn")].find((b) => vis(b) && b !== prev),
);
prev = btn;
await w(1500);
const row = btn?.parentElement;
const res = {
page: route.slice(0, 3).toUpperCase(),
btn: document.querySelectorAll(".ui-help-btn").length,
first: !!row && row.firstElementChild === btn,
inLeft: !!btn?.closest(".ui-sidebar-actions") || !!btn?.closest(".b02-proj__actions"),
leftovers: MOVED.filter((s) => leftText().includes(s)),
};
if (btn) Object.assign(res, await openModal(btn));
if (route === "b05-profile") {
// 배관 하나를 골라 [?] — 구조물 종류 항목이 먼저 열림(이미 골라져 있으면 그대로 · 끝나면 처음 상태로)
const picked = () => document.querySelector('[data-help-topic="구조물_배관"]');
const was = !!picked();
const item = await until(() =>
[...document.querySelectorAll(".b05-route__irregular-item")].find((li) =>
li.innerText.includes("배관"),
),
);
if (!was) item?.click();
await until(picked, 8000);
await w(800);
res.leftoversPipe = MOVED.filter((s) => leftText().includes(s));
const again = await openModal([...document.querySelectorAll(".ui-help-btn")].find(vis));
res.pipeTopic = again.text;
if (!was) item?.click(); // 고름 풂
}
out.pages.push(res);
}
location.hash = start;
return JSON.stringify(out);
})();