Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_6

This commit is contained in:
2026-10-26 09:19:48 +09:00
65 changed files with 1548 additions and 233 deletions
+30 -6
View File
@@ -33,6 +33,8 @@
단계 = 아래 열쇠 중 하나 + 덧붙임
{"click": "<css>"} 누름 · {"click": …, "confirm": true} = 확인창만 여는 위험 단추(아래)
{"fill": "<css>", "value": "글"} 칸 채움
{"select": "<css>", "value": "값"} 고르기 칸(<select>) 값 고름(option value 또는 글)
{"scroll": "<css>"} 그 요소가 화면에 들어오게 스크롤
{"hover": "<css>"} 마우스 올림(툴팁)
{"contextmenu": "<css>"} 오른쪽 누름(누름 메뉴 열기)
{"press": "Escape"} 키 · {"press": "Enter", "sel": "<css>"} = 그 요소에서
@@ -49,6 +51,9 @@
- 확인창을 찍을 때만 `"confirm": true` 로 그 단추를 누름 — 그 장면은 after 에 누름 단계가 있어야 함([취소])
- 페이지의 /api 쓰기 요청(PUT · PATCH · DELETE · allow_post 밖 POST)은 모두 끊음 · 끊은 요청은 표에 적음
- 브라우저 확인창(window.confirm)은 늘 취소 · 새 컨텍스트라 사용자 브라우저 · 저장소와 안 섞임
- 장면마다 새 탭 · sessionStorage 비우고 시작(앞 장면 조작이 안 남음) · localStorage 는 프로젝트 · 테마 ·
말만 세움
- clip · callouts 대상은 찍기 전 화면 안으로 스크롤(clip 먼저 · 번호 차례) 뒤 자리를 잼
- `_manual.json`(관리자가 손으로 바꾼 그림 · 이름 목록 또는 이름 열쇠 사전) 이름은 건너뜀(--force 면 찍음)
3D(WebGL) · 지도(카카오) · CAD(iframe)
@@ -80,7 +85,9 @@ STEP_TIMEOUT = 15000
SETTLE_MS = 500
CLIP_PAD = 16
PROJECT_KEYS = ("road", "stab", "plan", "none")
STEP_KINDS = ("click", "contextmenu", "fill", "hover", "press", "eval", "wait", "wait_ms")
STEP_KINDS = (
"click", "contextmenu", "fill", "select", "hover", "press", "eval", "wait", "wait_ms", "scroll",
) # fmt: skip
SCENE_KEYS = {
"name", "route", "project", "viewport", "before", "settle_ms", "clip", "clip_pad",
"callouts", "after", "allow_post",
@@ -96,6 +103,7 @@ DANGER_RE = re.compile(
SAFE_RE = re.compile(r"취소|닫기|\b(cancel|close)", re.I)
THEME_KEY, LANG_KEY, PROJECT_KEY = "frd_theme", "frd_lang", "frd_current_project_id"
CALLOUT_CLASS = "__guide_callout"
SEEDED = "__guide_seeded" # 탭 이름(window.name)에 남겨 첫 적재를 가림
YELLOW = "#FFD400"
RING_PAD, DOT = 4, 28 # 테두리 바깥 여백 · 번호 원 지름(CSS px)
@@ -142,8 +150,8 @@ def _check_steps(steps: Any, where: str) -> tuple[list[str], bool]:
if not isinstance(val, str) or not val.strip():
errs.append(f"{at} {kind} 값이 빈 글")
continue
if kind == "fill" and not isinstance(st.get("value"), str):
errs.append(f"{at} fill 은 value(글) 필요")
if kind in ("fill", "select") and not isinstance(st.get("value"), str):
errs.append(f"{at} {kind} 은 value(글) 필요")
if st.get("confirm"):
if kind != "click":
errs.append(f"{at} confirm 은 click 에만")
@@ -356,6 +364,8 @@ def run_steps(page: Any, steps: list[dict[str, Any]]) -> None:
page.wait_for_timeout(st["wait_ms"])
elif "wait" in st:
_locate(page, st["wait"], frame).wait_for(state="visible", timeout=timeout)
elif "scroll" in st:
_locate(page, st["scroll"], frame).scroll_into_view_if_needed(timeout=timeout)
elif "eval" in st:
target = page.frame_locator(frame).locator(":root") if frame else page
target.evaluate(f"() => ({st['eval']})")
@@ -365,7 +375,7 @@ def run_steps(page: Any, steps: list[dict[str, Any]]) -> None:
else:
page.keyboard.press(st["press"])
else:
kind = next(k for k in ("click", "contextmenu", "fill", "hover") if k in st)
kind = next(k for k in ("click", "contextmenu", "fill", "select", "hover") if k in st)
loc = _locate(page, st[kind], frame)
loc.wait_for(state="visible", timeout=timeout)
if kind == "click":
@@ -377,6 +387,8 @@ def run_steps(page: Any, steps: list[dict[str, Any]]) -> None:
loc.click(button="right", timeout=timeout)
elif kind == "fill":
loc.fill(st["value"], timeout=timeout)
elif kind == "select":
loc.select_option(st["value"], timeout=timeout)
else:
loc.hover(timeout=timeout)
@@ -396,7 +408,7 @@ def _box(page: Any, sel: str, frame: str | None, what: str) -> dict[str, float]:
def shoot_scene(
context: Any, base: str, scene: dict[str, Any], pid: str | None
) -> tuple[bytes, list[str]]:
"""장면 하나 → (PNG 2배, 끊은 쓰기 요청). 새 탭마다 sessionStorage 새로."""
"""장면 하나 → (PNG 2배, 끊은 쓰기 요청). 새 탭 · 첫 적재 때 sessionStorage 비움."""
vw, vh = scene.get("viewport") or VIEWPORT
allow = scene.get("allow_post") or []
blocked: list[str] = []
@@ -406,7 +418,10 @@ def shoot_scene(
seed = json.dumps([THEME_KEY, LANG_KEY, PROJECT_KEY, pid])
page.add_init_script(
f"(([t, l, k, pid]) => {{ localStorage.setItem(t, 'light'); localStorage.setItem(l, 'ko');"
f" pid ? localStorage.setItem(k, pid) : localStorage.removeItem(k); }})({seed});"
f" pid ? localStorage.setItem(k, pid) : localStorage.removeItem(k);"
# 첫 적재만 비움 — 장면 안 새로고침 · 이동은 그 장면 조작을 지킴
f" if (!window.name.includes('{SEEDED}')) {{ sessionStorage.clear();"
f" window.name += '{SEEDED}'; }} }})({seed});"
)
page.on("dialog", lambda d: d.dismiss())
@@ -431,6 +446,15 @@ def shoot_scene(
run_steps(page, scene.get("before") or [])
page.wait_for_timeout(scene.get("settle_ms", SETTLE_MS))
# 찍을 대상을 화면 안으로 — clip 먼저 · 번호 차례(어긋난 번호 · 잘린 그림 막음)
targets = [(c["sel"], c.get("frame")) for c in scene.get("callouts") or []]
if scene.get("clip"):
targets.insert(0, (scene["clip"], None))
for sel, frame in targets:
try:
_locate(page, sel, frame).scroll_into_view_if_needed(timeout=5000)
except Exception:
pass # 안 보이면 아래 _box 가 까닭을 냄
items, boxes = [], []
for c in scene.get("callouts") or []:
ring, dot = callout_boxes(_box(page, c["sel"], c.get("frame"), f"번호 {c['n']}"),
+22 -49
View File
@@ -1,55 +1,28 @@
# 안정성평가 만들기 설명서
# 안정성평가 만들기
## 이 화면에서 하는 일
횡단 설계까지 끝난 임도 프로젝트 하나를 골라, 그 시점 자료로 새 안정성평가 프로젝트를 만드는 화면.
- 횡단 설계(B06)까지 끝난 임도 프로젝트 하나를 골라 새 안정성평가 프로젝트를 만듦.
- 만드는 순간의 원본 자료를 안정성평가 쪽에 따로 복사해 둠(동결 복사). 그 뒤 해석은 모두 이 복사본으로 함.
- 대시보드 [+] 새 프로젝트 → 「안정성평가」를 누르면 이 화면이 열림.
## 여는 길
- 대시보드 → [+] 새 프로젝트 → 「안정성평가」 카드를 누름.
- 안정성평가를 새로 만들 때만 거치는 화면 — 만든 뒤에는 대시보드의 프로젝트 목록에서 그 안정성평가를 열어 작업함.
![화면 한눈에](E01_00_overview.webp)
## 화면 구역
| 번호 | 구역 | 하는 일 |
| --- | --- | --- |
| 1 | 제목 · 안내 글 | 화면 이름과 한 줄 안내 |
| 2 | 대상 임도 프로젝트 | 평가할 임도를 고르는 칸 — [고르기](01_대상_임도_고르기) |
| 3 | 프로젝트명 | 새 안정성평가의 이름 — [이름과 등록](02_이름과_등록) |
| 4 | 단추 줄 | [?] 설명서 · [취소] · [등록] |
## 작업 순서
1. **대상 임도 프로젝트** 칸에서 원본 임도를 고름. 목록 글 = 프로젝트 이름 · 지역 · 사업 연도(최근에 고친 프로젝트가 위).
2. **프로젝트명** 칸을 확인함. 손대지 않으면 「원본 이름 안정성평가」로 자동으로 채워짐 — 원하는 이름으로 바꿔도 됨.
3. [등록]을 누름. 복사하는 동안 화면 전체가 잠기고, 끝나면 「프로젝트를 만들었습니다.」 알림이 뜸.
4. 새 프로젝트가 현재 프로젝트가 되고 첫 단계 「지반 조건」(E02) 화면으로 넘어감.
1. **대상 임도 프로젝트** 칸에서 평가할 임도를 고름.
2. **프로젝트명** 칸을 확인함. 바꾸고 싶으면 원하는 이름으로 고쳐 씀.
3. [등록]을 누름.
4. 복사가 끝나면 첫 단계 「지반 조건」 화면이 열림 — [지반 조건](E02/00_설명서)으로 이어짐.
## 단추
| 단추 | 하는 일 |
| --- | --- |
| [?] | 이 설명서를 엶 |
| [취소] | 만들지 않고 대시보드로 돌아감 |
| [등록] | 고른 임도를 복사해 안정성평가 프로젝트를 만듦. 고를 임도가 없으면 흐리게 꺼져 있음 |
## 목록에 나오는 임도
- 우리 회사 임도 설계 프로젝트 중 **횡단 설계(B06) 단계가 끝난 것만** 나옴.
- 고를 임도가 하나도 없으면 칸 아래에 「횡단 설계(B06)까지 끝난 임도 프로젝트가 없습니다.」가 보임 — 먼저 임도 프로젝트에서 횡단 설계를 끝낼 것.
- 지역 · 도로 종류 · 사업 연도는 원본 값을 그대로 물려받음.
## 복사하는 것 · 안 하는 것
해석에 쓰는 자료만 복사함(보통 수십 MB 안쪽).
| 복사함 | 안 함 |
| --- | --- |
| 원본 프로젝트 정보 · 단계 상태 · 지표면 확정 값 | 원본 점군(LAS) 파일 |
| 최신 노선과 측점 · 종단 · 횡단 자료 | 상세 설계(B07) · 수량(B08) · 내역(B09) |
| 노선 폴더(구조물 배치 포함) · 종횡단 파일 | 쓰이지 않은 다른 지형 격자 |
| 원본 설계에 쓰인 지형 격자(DTM) 하나 | |
| 최신 노선 3D 면 · 배수 편집분 | |
## 결과 읽는 법
- [등록]이 끝나면 지반 조건(E02) 화면이 열리고, 지반물성치 · 기준안전율 등은 기본값으로, 측점별 토층 두께는 복사한 B06 값으로 채워져 있음.
- 원본 횡단 파일이 없는 측점이 있으면 E02 · E03 화면 위에 **「횡단 없는 측점 N 개 — 해석에서 빠짐」** 띠가 보임. 띠를 누르면 빠진 측점의 누가거리(m)가 펼쳐짐.
- 빠진 측점은 해석 목록에 나오지 않음. 나머지 측점 해석에는 영향 없음.
## 알아 둘 것
- **업데이트 없음**: 만든 뒤 원본 임도를 고쳐도 이 안정성평가는 바뀌지 않음(만든 순간의 자료로 고정).
- 원본의 바뀐 설계로 다시 평가하려면 이 화면에서 **안정성평가를 새로 만들 것**.
- 복사에 실패하면 「원본 자료 복사에 실패했습니다.」 알림이 뜨고, 만들다 만 프로젝트는 자동으로 지워짐 — 다시 [등록]하면 됨.
- 원본 임도 프로젝트에는 아무것도 쓰지 않음(읽기만 함).
- 안정성평가의 지반 값은 **문헌 추정치 · 현장 시험 없음**이 기본임 — 다음 단계(E02)에서 현장 여건에 맞게 확인 · 수정할 것.
- 횡단 없는 측점을 살리려면 원본 임도에서 그 측점 횡단을 다시 만든 뒤 안정성평가를 새로 만들 것.
> 참고: 만드는 순간의 임도 자료를 복사해 쓰므로, 만든 뒤에 임도를 고쳐도 이 안정성평가는 바뀌지 않음. [복사하는 자료](03_복사하는_자료)에서 자세히 설명.
@@ -0,0 +1,24 @@
# 대상 임도 고르기
평가할 임도 프로젝트를 목록에서 하나 고르는 단계.
## 여는 길
- 안정성평가 만들기 화면 왼쪽 위 **대상 임도 프로젝트** 칸을 누름.
![대상 임도 프로젝트 칸](E01_01_source.webp)
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | **대상 임도 프로젝트** 칸을 눌러 목록을 엶. |
| 2 | 목록에서 평가할 임도를 고름. 줄마다 「프로젝트 이름 · 지역 · 사업 연도」가 적혀 있고, 최근에 고친 프로젝트가 위에 있음. |
| 3 | 고르면 오른쪽 **프로젝트명** 칸이 「고른 이름 안정성평가」로 자동으로 바뀜. |
## 보이는 것
- 목록에는 우리 회사 임도 중 **횡단 설계(B06)까지 끝난 프로젝트만** 나옴.
- 목록이 비어 있으면 칸 아래에 「횡단 설계(B06)까지 끝난 임도 프로젝트가 없습니다.」가 보이고 [등록]이 흐리게 꺼짐.
> 주의: 횡단 설계가 끝나지 않은 임도는 목록에 나오지 않음. 먼저 [횡단 설계](B06/00_설명서)를 끝낼 것.
@@ -0,0 +1,36 @@
# 이름과 등록
새 안정성평가의 이름을 정하고 [등록]으로 만드는 단계.
## 여는 길
- 안정성평가 만들기 화면 오른쪽 **프로젝트명** 칸과 아래 단추 줄.
![이름을 직접 쓴 모습](E01_02_name_typed.webp)
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | **프로젝트명** 칸을 확인함. 손대지 않으면 「원본 이름 안정성평가」가 그대로 쓰임. |
| 2 | 다른 이름을 쓰고 싶으면 칸을 눌러 고쳐 씀. 한 번 직접 쓰면 임도를 다시 골라도 이름이 바뀌지 않음. |
| 3 | 아래 단추 줄에서 [등록]을 누름. |
| 4 | 복사하는 동안 화면 전체가 잠기고, 끝나면 「프로젝트를 만들었습니다.」 알림이 뜸. |
| 5 | 새 프로젝트가 현재 프로젝트가 되고 「지반 조건」 화면으로 넘어감. |
![단추 줄](E01_03_buttons.webp)
| 번호 | 단추 | 하는 일 |
| --- | --- | --- |
| 1 | [?] | 이 설명서를 엶 |
| 2 | [취소] | 만들지 않고 대시보드로 돌아감 |
| 3 | [등록] | 고른 임도를 복사해 안정성평가를 만듦. 고를 임도가 없으면 흐리게 꺼짐 |
![설명서 창](E01_04_help_modal.webp)
## 보이는 것
- [?]를 누르면 왼쪽 차례 + 오른쪽 글로 된 설명서 창이 열림.
- 복사에 실패하면 「원본 자료 복사에 실패했습니다.」가 뜨고 만들다 만 프로젝트는 자동으로 지워짐 — 다시 [등록]하면 됨.
> 참고: 이름은 나중에 구분하기 쉬운 것으로 정할 것(예: 「용화 안정성평가 1차」).
@@ -0,0 +1,30 @@
# 복사하는 자료
[등록]할 때 임도에서 무엇을 가져오고 무엇을 두고 오는지 정리한 글.
## 여는 길
- 별도로 여는 화면 없음 — [등록] 뒤 안정성평가 프로젝트에 이 자료가 들어 있음.
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | [이름과 등록](02_이름과_등록)에서 [등록]을 누름. |
| 2 | 아래 표의 「복사함」 자료가 안정성평가 쪽에 따로 복사됨. 보통 수십 MB 안쪽. |
## 보이는 것
| 복사함 | 안 함 |
| --- | --- |
| 원본 프로젝트 정보 · 단계 상태 · 지표면 확정 값 | 원본 점군(LAS) 파일 |
| 최신 노선과 측점 · 종단 · 횡단 자료 | 상세 설계(B07) · 수량(B08) · 내역(B09) |
| 노선 폴더(구조물 배치 포함) · 종횡단 파일 | 쓰이지 않은 다른 지형 격자 |
| 원본 설계에 쓰인 지형 격자 하나 | |
| 최신 노선 3D 면 · 배수 편집분 | |
- 횡단 파일이 없는 측점이 있으면 다음 화면 위에 **「횡단 없는 측점 N 개 — 해석에서 빠짐」** 띠가 보임. 띠를 누르면 빠진 측점의 누가거리가 펼쳐짐.
> 주의: 만든 뒤 원본 임도를 고쳐도 안정성평가는 바뀌지 않음. 고친 설계로 다시 평가하려면 이 화면에서 새로 만들 것.
> 참고: 원본 임도에는 아무것도 쓰지 않음(읽기만 함). 지반 값은 문헌 추정치가 기본이므로 [지반 조건](E02/00_설명서)에서 현장 값으로 확인 · 수정할 것.
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

+7
View File
@@ -0,0 +1,7 @@
[
{"name": "E01_00_overview", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"wait_ms": 800}], "clip": null, "callouts": [{"n": 1, "sel": ".ui-general-layout__title"}, {"n": 2, "sel": ".b02-proj__grid > :nth-child(1)"}, {"n": 3, "sel": ".b02-proj__grid > :nth-child(2)"}, {"n": 4, "sel": ".b02-proj__actions"}]},
{"name": "E01_01_source", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"wait_ms": 800}], "clip": ".b02-proj__grid > :nth-child(1)", "callouts": []},
{"name": "E01_02_name_typed", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"wait_ms": 800}, {"fill": ".b02-proj__grid input", "value": "용화 안정성평가 1차"}], "clip": ".b02-proj__grid", "callouts": [{"n": 1, "sel": ".b02-proj__grid input"}]},
{"name": "E01_03_buttons", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"wait_ms": 800}], "clip": ".b02-proj__actions", "callouts": [{"n": 1, "sel": ".b02-proj__actions .ui-help-btn"}, {"n": 2, "sel": ".b02-proj__actions .ui-btn--ghost:not(.ui-help-btn)"}, {"n": 3, "sel": ".b02-proj__actions .ui-btn--filled"}]},
{"name": "E01_04_help_modal", "route": "#/e01-stab-register", "project": "none", "before": [{"wait": ".b02-proj__grid select"}, {"click": ".b02-proj__actions .ui-help-btn"}, {"wait": ".ui-help__grid"}, {"wait_ms": 600}], "clip": null, "callouts": []}
]
+27 -43
View File
@@ -1,50 +1,34 @@
# 지반 조건 설명서
# 지반 조건
## 이 화면에서 하는 일
2D · 3D 해석에 쓸 지반 조건(지층 물성 · 기준안전율 · 노면 하중 · 지진 · 지하수 · 층 두께)을 확인하고 정하는 화면.
- 2D 해석(E03)에 들어갈 지반 조건을 정함 — 지반물성치 · 기준안전율 · 노면 하중 · 지진 · 지하수 · 층 두께.
- 처음 열면 기본값(문헌 추정치)과 복사한 B06 측점 값으로 채워져 있음. 칸마다 바꿀 수 있음.
- 좌측 = 안내 글 · 상태 글 · [초기화] [저장].
- 가운데 = 카드 다섯 — 「지반물성치」 · 「기준안전율」 · 「노면 하중 · 지진」 · 「지하수」 · 「층 두께 · 구간 덮어쓰기」.
## 여는 길
- [안정성평가 만들기](E01/00_설명서)에서 [등록]을 누르면 첫 화면으로 열림.
- 이미 만든 안정성평가는 대시보드에서 프로젝트를 열거나, 다른 단계 화면 왼쪽 아래 [<] [>] 단추로 옮겨 옴.
![화면 한눈에](E02_00_overview.webp)
## 화면 구역
| 번호 | 구역 | 하는 일 |
| --- | --- | --- |
| 1 | 왼쪽 안내 글 · 상태 글 | 「문헌 추정치 · 현장 시험 없음」 안내와 「저장 안 된 변경이 있습니다.」 등 상태 |
| 2 | 지반물성치 카드 | 지층별 단위중량 · 점착력 · 내부마찰각 — [지반물성치](01_지반물성치) |
| 3 | 기준안전율 카드 | 조건(건기 · 우기 · 지진 시 · 단기)별 기준 — [기준안전율](02_기준안전율) |
| 4 | 노면 하중 · 지진 카드 | 노면 하중 · 지진 검토 켬/끔 — [노면 하중 · 지진](03_노면하중_지진) |
| 5 | 지하수 카드 | 건기 지하수 · 우기 규칙 — [지하수](04_지하수) |
| 6 | 층 두께 · 구간 덮어쓰기 카드 | 구간별 지층 · 두께 바꾸기 + 측점별 적용 지층 표 — [층 두께 · 구간 덮어쓰기](05_층두께_구간덮어쓰기) |
| 7 | 왼쪽 아래 단추 줄 | [?] · [<] · [>] · [초기화] · [저장] — [저장과 초기화](06_저장과_초기화) |
## 작업 순서
1. **지반물성치** 카드에서 지층별 γt · γsat · c · φ 를 확인함. 현장 값이 있으면 고쳐 넣고, 필요하면 [지층 추가].
2. 같은 카드 아래 **B06 지반 → 지층** 줄에서 B06 의 토사 · 리핑암 · 발파암 · 성토체가 어느 지층으로 해석될지 고름.
3. **기준안전율** 카드에서 조건(건기 · 우기 · 지진 시 · 단기)별 기준을 확인함.
4. **노면 하중 · 지진** 카드에서 노면 하중(kPa)과 지진 검토 켬/끔 · 수평지진계수 kh 를 정함.
5. **지하수** 카드에서 건기 지하수와 우기 규칙(넷 중 하나)을 고름.
6. **층 두께 · 구간 덮어쓰기** 카드에서 특정 구간만 지층 · 토층 두께를 바꿀 것이 있으면 [구간 추가]. 아래 **측점별 적용 지층** 표로 결과를 확인함.
7. 좌측 [저장]을 누름. 저장해야 2D 해석(E03)에 쓰임.
1. **지반물성치** 카드에서 지층별 값을 확인하고, 현장 값이 있으면 고쳐 넣음.
2. 같은 카드 아래 「B06 지반 → 지층」 줄에서 횡단 설계의 토사 · 리핑암 · 발파암 · 성토체가 어느 지층으로 계산될지 고름.
3. **기준안전율** · **노면 하중 · 지진** · **지하수** 카드를 차례로 확인함.
4. 특정 구간만 지층이나 두께를 바꿀 것이 있으면 **층 두께 · 구간 덮어쓰기** 카드에서 [구간 추가].
5. 왼쪽 아래 [저장]을 누름 — 저장한 값만 다음 단계 [2D 해석](E03/00_설명서)에 쓰임.
## 단추
> 주의: 지반물성치 기본값은 문헌 추정치이고 현장 시험 값이 아님. 현장 조사 값이 있으면 꼭 바꿔 넣을 것.
| 단추 | 자리 | 하는 일 |
| --- | --- | --- |
| [저장] | 좌측 아래 | 화면 값을 검사한 뒤 작업본으로 저장함. 틀린 칸이 있으면 저장하지 않고 알림 |
| [초기화] | 좌측 아래 | 확인 창 뒤 초기값(만들 때 기본값 + B06 측점)으로 되돌림. 저장한 값이 사라짐 |
| [?] [<] [>] | 좌측 아래 | 설명서 · 이전 단계 · 다음 단계(2D 해석) |
| [지층 추가] | 지반물성치 카드 | 「새 지층 n」 줄을 덧붙임(풍화토 기본값으로 시작) |
| [지움] | 표 줄 끝 | 그 줄을 지움. 쓰이는 지층은 지울 수 없음 |
| [기본값] | 카드마다 | 그 카드 값만 기본값으로 되돌림(다른 카드는 그대로) |
| [구간 추가] | 층 두께 카드 | 덮어쓰기 구간 한 줄(첫 측점 ~ 끝 측점)을 덧붙임 |
| [수위선 구간 추가] · [점 추가] | 지하수 카드 | 우기 규칙이 「사용자 수위선」일 때만 보임 |
## 결과 읽는 법
- 칸에 마우스를 올리면 **기본값과 근거**(지식DB 07 · KDS 11 70 05 등)가 보임. 사용자가 더한 지층은 「사용자 지층 — 출처 없음」.
- 틀린 값은 칸 테두리가 **빨갛게** 되고, 마우스를 올리면 까닭(「범위 밖 (0 ~ 60)」 등)이 보임. 좌측에 「입력값을 확인하세요 — 틀린 칸 N」.
- 바꾸고 저장하지 않으면 좌측에 「저장 안 된 변경이 있습니다.」가 보임.
- **측점별 적용 지층** 표 = 측점마다 실제로 해석에 들어갈 위 지층 · 아래 지층 · 토층 두께. 구간 덮어쓰기가 걸린 줄은 바탕색이 다름. 아래 지층이 없으면 「전부 위 지층」.
- 공유받은 회사(보기만)는 칸과 단추가 꺼지고 「보기만 허용된 프로젝트입니다.」가 보임.
## 알아 둘 것
- **문헌 추정치 · 현장 시험 없음**: 지반물성치 기본값은 국내 설계 편람 · 학술 자료의 대표값임. 현장 시추 · 시험 값이 아니므로 결과는 참고용 — 현장 조사 값이 있으면 꼭 바꿔 넣을 것.
- 자동저장 없음. 바꾼 값은 이 브라우저 탭에만 임시로 남고, [저장]해야 E03 [해석]에 쓰임.
- 화면 위 「횡단 없는 측점 N 개 — 해석에서 빠짐」 띠 = 원본에 횡단 파일이 없어 해석에서 빠지는 측점. 누르면 목록이 펼쳐짐.
- 측점의 B06 지반 · B06 토층 두께는 복사본 값이라 화면에서 고칠 수 없음 — 바꾸려면 구간 덮어쓰기를 쓸 것.
- 저장 뒤 E03 에서 [해석]을 다시 누르면 값이 바뀐 측점만 다시 계산함.
- 기준안전율 표의 「단기」 줄 · 「쌓기(연약지반)」 칸은 값만 보관함 — 지금 2D 해석 판정은 건기 · 우기 · 지진 시의 깎기 · 쌓기 칸만 씀.
- 지하수 규칙(건기 · 우기 갈래 넷)은 E03 안전율 계산과 횡단 그림의 수위선에 함께 반영됨. 규칙을 바꾸면 E03 에서 다시 [해석].
- 칸 뜻 · 범위는 「칸 뜻」, 근거 · 수식은 「쓰인 방법과 근거」, 확정 안 된 값 목록은 「사용자 확정 전 값」 항목에 있음.
> 참고: 값을 바꾸면 이 브라우저 탭에만 임시로 남음. [저장]을 눌러야 해석에 쓰임.
@@ -0,0 +1,52 @@
# 지반물성치
지층마다 흙의 무게 · 점착력 · 내부마찰각을 정하는 카드.
## 여는 길
- 지반 조건 화면 맨 위 **지반물성치** 카드.
![지반물성치 카드](E02_01_materials.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 표 머리 | 지층 · γt · γsat · c · φ · 지하수 무리 — 칸 뜻은 [칸 뜻](10_칸_뜻) |
| 2 | 지층 한 줄 | 풍화토 · 풍화암 · 연암 · 경암 · 성토 · 붕적토 6줄이 기본 |
| 3 | [지층 추가] | 새 줄을 더함 |
| 4 | [기본값] | 이 카드 값만 기본값으로 되돌림 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 칸에 마우스를 올려 기본값과 근거를 확인함. |
| 2 | 현장 값이 있으면 γt · γsat · c · φ 칸을 고쳐 씀. |
| 3 | 새 지층이 필요하면 [지층 추가]를 누름. |
| 4 | 필요 없는 지층은 줄 끝 [지움]으로 지움. |
| 5 | 줄마다 「지하수 무리」를 「토사 · 풍화암」 또는 「암반」으로 고름. |
![지층 추가 뒤](E02_02_material_added.webp)
- [지층 추가]를 누르면 맨 아래에 「새 지층 1」이 풍화토 값으로 생김. 이름과 값을 고쳐 씀.
![마우스를 올린 모습](E02_12_hover_source.webp)
## 보이는 것
- 칸 위에 마우스를 올리면 **기본값과 근거**(지식DB · KDS 11 70 05 등)가 뜸. 사용자가 더한 지층은 「사용자 지층 — 출처 없음」.
- 틀린 값은 칸 테두리가 빨갛게 되고 마우스를 올리면 까닭이 뜸 — [저장과 초기화](06_저장과_초기화).
### B06 지반 → 지층
![B06 지반 → 지층 줄](E02_03_b06_map.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 토사 (경계 위) | 횡단의 암 경계 위 흙 |
| 2 | 리핑암 (경계 아래) | 리핑암 측점의 암 경계 아래 |
| 3 | 발파암 (경계 아래) | 발파암 측점의 암 경계 아래 |
| 4 | 성토체 | 쌓기로 새로 올린 흙 |
> 주의: 다른 칸에서 쓰이는 지층은 지울 수 없음. 같은 이름의 지층도 만들 수 없음.
> 참고: 문헌 추정치는 현장 시험 값이 아님 — 결과는 참고용으로 볼 것.
@@ -0,0 +1,30 @@
# 기준안전율
해석 결과 O.K · N.G를 가르는 기준을 정하는 카드.
## 여는 길
- 지반 조건 화면 **기준안전율** 카드(왼쪽 가운데).
![기준안전율 카드](E02_04_safety.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 표 머리 | 깎기 · 쌓기 · 쌓기(연약지반) |
| 2 | 건기 줄 | 평상시 기준 |
| 3 | 단기 줄 | 값만 보관하는 줄 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 줄(건기 · 우기 · 지진 시 · 단기)과 칸(깎기 · 쌓기 · 쌓기(연약지반))이 만나는 칸의 값을 확인함. |
| 2 | 바꿀 값이 있으면 1.0 ~ 3.0 사이로 고쳐 씀. |
| 3 | 처음 값으로 돌리려면 카드 위 [기본값]을 누름. |
## 보이는 것
- 해석 판정 = 최소 안전율이 기준 이상이면 O.K, 작으면 N.G.
- 우기 깎기 기본 1.2는 KDS 11 70 05 표 4.3-3의 우기 규칙과 짝 — 지하수 규칙을 바꾸면 이 값도 함께 살필 것([지하수](04_지하수)).
> 주의: 지금 2D 해석 판정은 건기 · 우기 · 지진 시 줄의 깎기 · 쌓기 칸만 씀. 「단기」 줄과 「쌓기(연약지반)」 칸은 값만 보관함.
@@ -0,0 +1,31 @@
# 노면 하중 · 지진
노면 위 차량 하중과 지진 검토 여부를 정하는 카드.
## 여는 길
- 지반 조건 화면 **노면 하중 · 지진** 카드(오른쪽 가운데).
![노면 하중 · 지진 카드](E02_05_loads.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 노면 하중 (kPa) | 노면 양 끝 사이에 고르게 얹는 차량 · 통행 하중 (기본 10) |
| 2 | 지진 검토 | 켜면 2D 해석에 「지진」 조건이 생김 (기본 끔) |
| 3 | 수평지진계수 kh | 지진 검토를 켰을 때만 넣음 (0 ~ 0.5) |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 노면 하중 값을 확인하고 필요하면 고쳐 씀 (0 ~ 100). |
| 2 | 지진을 검토하려면 **지진 검토** 칸을 체크함. |
| 3 | 수평지진계수 kh 를 넣음. |
![지진 검토를 켠 모습](E02_06_seismic_on.webp)
## 보이는 것
- 지진 검토를 켜면 [2D 해석](E03/00_설명서)의 조건 목록에 「지진」이 더해짐.
> 참고: 노면 하중 · 지진 값은 [칸 뜻](10_칸_뜻)에서 범위 확인.
+51
View File
@@ -0,0 +1,51 @@
# 지하수
건기 · 우기에 지하수위를 어떻게 볼지 정하는 카드.
## 여는 길
- 지반 조건 화면 **지하수** 카드(왼쪽 아래).
![지하수 카드](E02_07_water.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 건기 지하수 | 건기 규칙 (기본 「없음」) |
| 2 | 우기 규칙 | 「KDS 기본」 · 지표 아래 깊이 = 비탈 높이 × 비율 · 간극수압비 r_u · 사용자 수위선 |
| 3 | 우기 · 토사 · 풍화암 | 「KDS 기본」일 때 토사 · 풍화암의 수위 규칙 (기본 「지표 포화」) |
| 4 | 우기 · 암반 | 「KDS 기본」일 때 암반의 수위 규칙 (기본 「비탈 높이 ½ 포화」) |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | **건기 지하수**를 고름. |
| 2 | **우기 규칙**을 고름. 기본 「KDS 기본」은 KDS 11 70 05 표 4.3-3 을 따름. |
| 3 | 「KDS 기본」이면 토사 · 풍화암 · 암반 규칙을 각각 고름. |
| 4 | 다른 규칙을 골랐으면 기준안전율도 함께 확인함([기준안전율](02_기준안전율)). |
![깊이 비율을 고른 모습](E02_09_water_ratio.webp)
- 「지표 아래 깊이 = 비탈 높이 × 비율」을 고르면 **깊이 비율** 칸이 생김.
- 「간극수압비 r_u」를 고르면 **r_u** 칸이 생김.
### 사용자 수위선
![사용자 수위선](E02_08_water_line.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | [수위선 구간 추가] | 구간 한 줄을 더함 |
| 2 | 구간 줄 | 시작 · 끝(m) + 점(offset · 깊이) 목록. [점 추가]로 점을 더하고 [지움]으로 지움 |
- 점 = 노면 가운데 기준 좌우 거리(m, 왼쪽 +)와 공사 뒤 지표 아래 깊이(m).
- 덮지 않은 측점은 KDS 규칙을 따름.
## 보이는 것
- 규칙은 2D 해석의 안전율 계산과 횡단 그림의 지하수위 점선에 함께 반영됨.
- r_u를 고르면 수위선 없이 횡단 그림 아래에 「지하수 = 간극수압비 r_u …」 글만 보임.
> 주의: 규칙을 바꾸고 [저장]한 뒤에는 [2D 해석](E03/00_설명서)에서 [해석]을 다시 누를 것.
> 참고: 3D 해석은 우기를 「지표까지 포화」 한 가지로 셈 — 이 카드의 규칙은 3D에 쓰이지 않음.
@@ -0,0 +1,37 @@
# 층 두께 · 구간 덮어쓰기
특정 구간의 측점만 지층이나 토층 두께를 바꾸고, 측점마다 최종 적용 값을 확인하는 카드.
## 여는 길
- 지반 조건 화면 맨 아래 **층 두께 · 구간 덮어쓰기** 카드.
![층 두께 · 구간 덮어쓰기 카드](E02_10_zones.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | [구간 추가] | 덮어쓸 구간 한 줄을 더함 |
| 2 | 측점별 적용 지층 | 측점마다 실제로 해석에 들어갈 지층 · 두께 (읽기만 가능) |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | [구간 추가]를 누름. 처음에는 첫 측점 ~ 끝 측점 전체 구간이 잡힘. |
| 2 | **시작 (m)** · **끝 (m)** 을 바꿔 덮어쓸 누가거리 구간을 정함. 끝은 시작보다 커야 함. |
| 3 | **위 지층** · **아래 지층**을 고름. 「B06 그대로」는 바꾸지 않음. |
| 4 | **토층 두께 (m)** 를 넣음. 빈 칸은 B06 값 그대로. |
| 5 | 아래 **측점별 적용 지층** 표에서 결과를 확인함. |
![구간을 더한 모습](E02_11_zone_added.webp)
## 보이는 것
- 구간 안 측점은 양 끝을 포함해 표에서 바탕색이 달라짐.
- 줄이 여럿 겹치면 **뒤 줄이 이김**.
- 아래 지층이 없는 측점은 표에 「전부 위 지층」으로 보임.
- 표의 「B06 지반」 · 「B06 토층 두께」는 복사본 값이라 고칠 수 없음 — 바꾸려면 구간 덮어쓰기를 씀.
> 주의: 토사 측점은 B06 토층 두께가 없어 「전부 위 지층」임. 토사 측점에 아래 지층을 넣으려면 아래 지층과 함께 토층 두께도 넣을 것.
> 참고: 원본에 횡단 파일이 없는 측점은 화면 위 「횡단 없는 측점 N 개 — 해석에서 빠짐」 띠에 나옴. 누르면 목록이 펼쳐짐.
@@ -0,0 +1,40 @@
# 저장과 초기화
바꾼 값을 작업본으로 저장하거나 처음 값으로 되돌리는 왼쪽 아래 단추 줄.
## 여는 길
- 지반 조건 화면 왼쪽 패널 맨 아래.
![단추 줄](E02_14_buttons.webp)
| 번호 | 단추 | 하는 일 |
| --- | --- | --- |
| 1 | [?] | 이 설명서를 엶 |
| 2 | [<] | 이전 단계로 이동 |
| 3 | [>] | 다음 단계(2D 해석)로 이동 |
| 4 | [초기화] | 처음 값(만들 때 기본값 + B06 측점)으로 되돌림 |
| 5 | [저장] | 화면 값을 검사한 뒤 저장함 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 값을 고침. 칸을 고치면 왼쪽에 「저장 안 된 변경이 있습니다.」가 보임. |
| 2 | 틀린 칸이 있으면 테두리가 빨갛게 되고 왼쪽에 「입력값을 확인하세요 — 틀린 칸 N」이 뜸. 마우스를 올려 까닭을 읽고 고침. |
| 3 | [저장]을 누름. 틀린 칸이 없으면 「저장했습니다.」 알림이 뜸. |
| 4 | 처음으로 되돌리려면 [초기화]를 누르고 확인 창에서 [초기화]를 한 번 더 누름. 취소하려면 [취소]. |
![틀린 값을 쓴 모습](E02_13_invalid.webp)
![초기화 확인 창](E02_15_reset_confirm.webp)
## 보이는 것
- 틀린 값이 있으면 저장되지 않고 알림만 뜸.
- [초기화]를 확인하면 저장한 값이 사라지고 「초기값으로 되돌렸습니다.」가 뜸.
- 공유받은 회사(보기만)는 칸과 단추가 꺼지고 「보기만 허용된 프로젝트입니다.」가 보임.
> 주의: [초기화]는 저장한 값까지 되돌림. 되돌리기 전에 꼭 필요한 값은 따로 적어 둘 것.
> 참고: 저장 뒤 [2D 해석](E03/00_설명서)에서 [해석]을 다시 누르면 값이 바뀐 측점만 다시 계산함.
+6
View File
@@ -2,6 +2,8 @@
## 지반물성치 표
![지반물성치 카드](E02_01_materials.webp)
| 칸 | 뜻 | 넣을 수 있는 범위 |
| --- | --- | --- |
| 지층 | 지층 이름 | 1~30 자 · 같은 이름 안 됨 |
@@ -15,6 +17,8 @@
## B06 지반 → 지층
![B06 지반 → 지층 줄](E02_03_b06_map.webp)
| 칸 | 뜻 | 기본 |
| --- | --- | --- |
| 토사 (경계 위) | B06 횡단의 암 경계 위 흙(모든 측점) | 풍화토 |
@@ -24,6 +28,8 @@
## 층 두께 · 구간 덮어쓰기
![층 두께 · 구간 덮어쓰기 카드](E02_10_zones.webp)
| 칸 | 뜻 |
| --- | --- |
| 시작 (m) · 끝 (m) | 덮어쓸 누가거리 구간. 양 끝 측점 포함 · 끝은 시작보다 커야 함(0 ~ 100,000) |
Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+18
View File
@@ -0,0 +1,18 @@
[
{"name": "E02_00_overview", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}], "clip": null, "callouts": [{"n": 1, "sel": ".ui-workflow-overlay__panel .e02-hint"}, {"n": 2, "sel": ".e02-cards > .e02-card:nth-child(1)"}, {"n": 3, "sel": ".e02-cards > .e02-card:nth-child(2)"}, {"n": 4, "sel": ".e02-cards > .e02-card:nth-child(3)"}, {"n": 5, "sel": ".e02-cards > .e02-card:nth-child(4)"}, {"n": 6, "sel": ".e02-cards > .e02-card:nth-child(5)"}, {"n": 7, "sel": ".ui-workflow-overlay__panel .ui-sidebar-actions"}], "viewport": [1600, 1400]},
{"name": "E02_01_materials", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}], "clip": ".e02-cards > .e02-card:nth-child(1)", "callouts": [{"n": 1, "sel": ".e02-cards > .e02-card:nth-child(1) .e02-table thead"}, {"n": 2, "sel": ".e02-cards > .e02-card:nth-child(1) .e02-table tbody tr:nth-child(1)"}, {"n": 3, "sel": ".e02-cards > .e02-card:nth-child(1) .e02-card__actions button:nth-child(1)"}, {"n": 4, "sel": ".e02-cards > .e02-card:nth-child(1) .e02-card__actions button:nth-child(2)"}]},
{"name": "E02_02_material_added", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}, {"click": ".e02-cards > .e02-card:nth-child(1) .e02-card__actions button:nth-child(1)"}, {"wait_ms": 400}], "clip": ".e02-cards > .e02-card:nth-child(1)", "callouts": [{"n": 1, "sel": ".e02-cards > .e02-card:nth-child(1) .e02-table tbody tr:last-child"}]},
{"name": "E02_03_b06_map", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}], "clip": ".e02-map", "callouts": [{"n": 1, "sel": "[data-path='label_map.soil']"}, {"n": 2, "sel": "[data-path='label_map.ripping_rock']"}, {"n": 3, "sel": "[data-path='label_map.blasting_rock']"}, {"n": 4, "sel": "[data-path='label_map.fill']"}]},
{"name": "E02_04_safety", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}], "clip": ".e02-cards > .e02-card:nth-child(2)", "callouts": [{"n": 1, "sel": ".e02-cards > .e02-card:nth-child(2) .e02-table thead"}, {"n": 2, "sel": ".e02-cards > .e02-card:nth-child(2) .e02-table tbody tr:nth-child(1)"}, {"n": 3, "sel": ".e02-cards > .e02-card:nth-child(2) .e02-table tbody tr:nth-child(4)"}]},
{"name": "E02_05_loads", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}], "clip": ".e02-cards > .e02-card:nth-child(3)", "callouts": [{"n": 1, "sel": "[data-path='road_surcharge_kpa']"}, {"n": 2, "sel": "[data-path='seismic.enabled']"}, {"n": 3, "sel": "[data-path='seismic.kh']"}]},
{"name": "E02_06_seismic_on", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}, {"click": "[data-path='seismic.enabled']"}, {"hover": ".e02-cards > .e02-card:nth-child(3) h3"}, {"wait_ms": 300}], "clip": ".e02-cards > .e02-card:nth-child(3)", "callouts": [{"n": 1, "sel": "[data-path='seismic.kh']"}]},
{"name": "E02_07_water", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}], "clip": ".e02-cards > .e02-card:nth-child(4)", "callouts": [{"n": 1, "sel": "[data-path='groundwater.dry']"}, {"n": 2, "sel": "[data-path='groundwater.wet_mode']"}, {"n": 3, "sel": "[data-path='groundwater.wet_soil']"}, {"n": 4, "sel": "[data-path='groundwater.wet_rock']"}]},
{"name": "E02_08_water_line", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}, {"select": "[data-path='groundwater.wet_mode']", "value": "line"}, {"wait_ms": 400}, {"click": ".e02-cards > .e02-card:nth-child(4) .e02-card__actions button:nth-child(1)"}, {"wait_ms": 400}], "clip": ".e02-cards > .e02-card:nth-child(4)", "callouts": [{"n": 1, "sel": ".e02-cards > .e02-card:nth-child(4) .e02-card__actions button:nth-child(1)"}, {"n": 2, "sel": ".e02-cards > .e02-card:nth-child(4) .e02-table"}]},
{"name": "E02_09_water_ratio", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}, {"select": "[data-path='groundwater.wet_mode']", "value": "depth_ratio"}, {"wait_ms": 400}], "clip": ".e02-cards > .e02-card:nth-child(4)", "callouts": []},
{"name": "E02_10_zones", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}], "clip": ".e02-cards > .e02-card:nth-child(5)", "callouts": [{"n": 1, "sel": ".e02-cards > .e02-card:nth-child(5) .e02-card__actions button:nth-child(1)"}, {"n": 2, "sel": ".e02-cards > .e02-card:nth-child(5) .e02-stations"}]},
{"name": "E02_11_zone_added", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}, {"click": ".e02-cards > .e02-card:nth-child(5) .e02-card__actions button:nth-child(1)"}, {"wait_ms": 400}], "clip": ".e02-cards > .e02-card:nth-child(5)", "callouts": [{"n": 1, "sel": ".e02-cards > .e02-card:nth-child(5) .e02-table:not(.e02-table--read) tbody tr:last-child"}]},
{"name": "E02_12_hover_source", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}, {"hover": "[data-path='materials.0.c']"}, {"wait_ms": 600}], "clip": null, "callouts": []},
{"name": "E02_13_invalid", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}, {"fill": "[data-path='materials.0.phi']", "value": "99"}, {"eval": "document.querySelector(\"[data-path='materials.0.phi']\").dispatchEvent(new Event('input',{bubbles:true}))"}, {"hover": "[data-path='materials.0.phi']"}, {"wait_ms": 600}], "clip": null, "callouts": [{"n": 1, "sel": "[data-path='materials.0.phi']"}, {"n": 2, "sel": ".e02-status"}]},
{"name": "E02_14_buttons", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}], "clip": ".ui-workflow-overlay__panel .ui-sidebar-actions", "callouts": [{"n": 1, "sel": ".ui-workflow-overlay__panel .ui-help-btn"}, {"n": 2, "sel": ".ui-workflow-overlay__panel .ui-step-nav-btn[data-step-nav='-1']"}, {"n": 3, "sel": ".ui-workflow-overlay__panel .ui-step-nav-btn[data-step-nav='1']"}, {"n": 4, "sel": ".ui-workflow-overlay__panel .ui-btn--danger-ghost"}, {"n": 5, "sel": ".ui-workflow-overlay__panel .ui-btn--filled"}]},
{"name": "E02_15_reset_confirm", "route": "#/e02-stab-ground", "project": "stab", "before": [{"wait": ".e02-cards"}, {"click": ".ui-workflow-overlay__panel .ui-btn--danger-ghost", "confirm": true}, {"wait": ".ui-confirm"}], "clip": ".ui-confirm__panel", "callouts": [], "after": [{"click": ".ui-confirm__panel button:has-text(\"취소\")"}]}
]
+30 -62
View File
@@ -1,69 +1,37 @@
# 2D 해석 설명서
# 2D 해석
## 이 화면에서 하는 일
모든 측점 횡단에서 비탈이 미끄러질 위험을 2D 한계평형해석으로 셈하고, 측점 · 좌우 비탈 · 조건마다 최소 안전율과 O.K · N.G 판정을 보여 주는 화면.
- 모든 측점 횡단에서 비탈이 미끄러질 위험을 2D 한계평형해석으로 셈 — 측점 · 좌우 비탈 · 조건(건기 · 우기 · 지진)마다 최소 안전율과 판정(O.K · N.G).
- 깎기 비탈 · 쌓기 비탈을 따로 셈. 옹벽 · 석축 등 벽이 있는 측점은 벽을 넣어 셈.
- 「보강 검토」 탭에서 비탈 완화 · 소단 가정을 다시 해석하고, 벽의 외적 안정(활동 · 전도 · 지지력)을 검토함.
- 화면 = 좌 측점 목록 · 위 평면 위험 띠 · 가운데 횡단 · 우 결과.
## 여는 길
- 지반 조건 화면 왼쪽 아래 [>] 단추 — 또는 대시보드에서 안정성평가 프로젝트를 열고 「2D 해석」 단계를 고름.
- 먼저 [지반 조건](E02/00_설명서)에서 값을 확인하고 [저장]해 둘 것. 저장한 값으로만 계산함.
![화면 한눈에](E03_00_overview.webp)
## 화면 구역
| 번호 | 구역 | 하는 일 |
| --- | --- | --- |
| 1 | 조건 · 정렬 | 보는 조건(건기 · 우기)과 목록 순서 — [측점 목록](02_측점_목록) |
| 2 | N.G 만 | 판정이 N.G 인 측점만 보임 |
| 3 | 측점 목록 | 측점별 최소 안전율 · 판정 · 대표 별 |
| 4 | 단추 줄 | [대표 단면 저장] · [3D 보기] · [해석] — [해석 돌리기](01_해석_돌리기) |
| 5 | 평면 위험 띠 | 노선을 판정 색으로 칠한 평면 — [평면 위험 띠](03_평면_위험_띠) |
| 6 | 횡단 머리줄 | 측점 이름 · 비탈 · 보기 칸 · 겹쳐 그리기 표시 칸 |
| 7 | 횡단 그림 | 임계 활동면과 안전율 지도 — [횡단 그림](04_횡단_그림) |
| 8 | 범례 | 안전율 색 · 지층 색 |
| 9 | 결과 | 최소 안전율 · 판정 · 절편표 · 그래프 — [결과 읽기](05_결과_읽기) |
## 작업 순서
### 해석
1. 지반 조건을 저장한 뒤, 왼쪽 아래 [해석]을 누름.
2. 끝나면 목록 · 평면 띠 · 횡단이 채워짐. 처음 보이는 측점은 안전율이 가장 낮은 곳.
3. **조건**에서 건기 · 우기를 바꿔 가며 N.G 측점을 찾음.
4. 측점을 눌러 횡단 · 결과를 확인함.
5. 필요하면 「보강 검토」 · 「확률 · 민감도」로 더 살핌 — [보강 검토](06_보강_검토) · [확률 · 민감도](07_확률_민감도).
6. 보고서에 넣을 대표 단면을 정하고 [대표 단면 저장] — [대표 단면과 3D 보기](08_대표_단면과_3D_보기).
1. 지반 조건(E02)에서 값을 확인하고 [저장]해 둠 — 저장한 값으로만 계산함.
2. 좌측 아래 [해석]을 누름. 화면이 잠기고 「해석 중 n% · 측점 k / N」 진행률이 보임. 측점 수십 개면 몇 분 걸림.
3. 끝나면 「해석이 끝났습니다.」 알림 뒤 목록 · 평면 띠 · 횡단이 채워짐. 처음 보이는 측점 = 지금 조건에서 안전율이 가장 낮은 측점.
4. 좌측 위 **조건**에서 건기 · 우기(지진 검토를 켰으면 지진도)를 고름. 목록 · 평면 띠 · 횡단이 그 조건으로 바뀜.
5. 목록 줄이나 평면 띠의 측점 근처를 눌러 측점을 고름. 가운데 위 비탈 칸에서 「좌 비탈 · 깎기」 등 비탈을 고름(처음 = 안전율 낮은 쪽).
6. 횡단의 절편이나 우측 절편표 줄을 누르면 그 절편의 힘이 자유물체도 · 그래프에 보임.
7. 보고서(E05)에 넣을 대표 단면은 목록 「대표」 칸 별(★ · ☆)을 눌러 켜고 끈 뒤 [대표 단면 저장].
8. [3D 보기]로 노선 3D 면 위 안전율 색을 봄.
> 주의: 지반 값이 문헌 추정치(현장 시험 없음)라 안전율은 설계 검토용 참고값임. 현장 값으로 바꿔 다시 해석할 것.
### 보강 검토
1. 측점을 고르고 가운데 위 보기 칸에서 「보강 검토」를 고름. 우측이 보강 검토 판으로 바뀜.
2. **비탈 완화 · 소단 가정**: 비탈 · 기울기 1:n · 소단 폭 · 소단 높이를 넣고 [가정 해석]. 또는 [최소 완화 찾기]로 지금 조건 기준안전율을 맞추는 가장 급한 기울기를 찾음(수십 초 ~ 1 분).
3. **벽 외적 안정**: 벽 있는 측점에서만 보임. 보강 검토 탭을 열면 Coulomb · 배면 수위 0 으로 한 번 자동 검토함. 토압 · 배면 수위를 바꿔 [외적 안정 검토].
4. 네일 · 앵커 보강 가정은 준비 중.
## 단추
| 단추 · 칸 | 자리 | 하는 일 |
| --- | --- | --- |
| 조건 · 정렬 | 좌측 위 | 조건 고름 · 「측점 순」 또는 「안전율 낮은 순」 |
| N.G 만 | 좌측 위 | 지금 조건에서 N.G 인 측점만 보임 |
| [해석] | 좌측 아래 | 전 측점 해석. 입력이 같으면 「입력이 같아 다시 셀 것이 없습니다.」 · 바뀐 측점만 다시 셈 |
| [3D 보기] | 좌측 아래 | 노선 3D 면 색칠 창을 엶(해석 뒤에만 켜짐) · [닫기]로 닫음 |
| [대표 단면 저장] | 좌측 아래 | 별 켬/끔을 저장함(바꾼 것이 있을 때만 켜짐) |
| 비탈 칸 | 가운데 위 | 볼 비탈(좌 · 우 · 깎기 · 쌓기) |
| 보기 칸 | 가운데 위 | 「해석 결과」 ↔ 「보강 검토」 |
| 등안전율 지도 · 원호 중심 격자 · 다른 후보 | 가운데 위 | 횡단 그림에 켜고 끔(기본 = 지도 켬 · 격자 끔 · 후보 켬) |
| [가정 해석] · [최소 완화 찾기] | 보강 검토 | 비탈 가정 재해석 |
| [외적 안정 검토] | 보강 검토 | 벽 활동 · 전도 · 지지력 검토 |
- 횡단 그림: 휠 = 확대 · 축소 · 가운데 단추 끌기 = 옮기기 · 두 번 누름 = 처음 배율. 마우스 아래 「거리 · EL」(offset · 표고) 표시.
## 결과 읽는 법
- **O.K** = 최소 안전율 ≥ 기준안전율(E02) · **N.G** = 그보다 작음 · 「-」 = 해석 없음 · 「오류」 = 그 측점 계산 실패(줄에 마우스를 올리면 까닭).
- 판정에 쓰는 안전율 = Bishop 간편법 최소값(원호 · 비원호 중 작은 쪽). 측점 판정 = 그 조건 좌 · 우 비탈 중 하나라도 N.G 면 N.G.
- **좌측 목록**: 측점 · 좌 · 우(깎 · 쌓) · 조건별 최소 안전율 · 판정 · 대표. ▦ = 벽 있는 측점.
- **평면 위험 띠**: 노선을 지금 조건 판정 색으로 칠함(초록 O.K · 빨강 N.G · 회색 해석 없음). 오른쪽 위에 판정별 측점 수. 고른 측점은 동그라미.
- **횡단 색(등안전율)**: 원호 중심 자리마다 최소 안전율을 기준안전율 대비 일곱 띠로 칠함(밝은 화면 기준) — 진한 빨강(기준의 0.8 배 미만) · 빨강(0.8 ~ 0.9) · 분홍(0.9 ~ 1.0) · 연회색(1.0 ~ 1.1) · 연파랑(1.1 ~ 1.25) · 파랑(1.25 ~ 1.5) · 진파랑(1.5 배 이상). 범례에는 실제 안전율 값으로 적힘.
- **횡단 선**: 굵은 선 = 임계 활동면(Fs 값 · 원호 중심 Xc · Zc · 반경 R) · 옅은 선 = 다른 방법 · 다른 꼴 후보 · 점선 = 지하수위 · 노면 위 눈금 = 노면 하중 · 회색 빗금 = 벽.
- **우측 결과**: 최소 안전율 · 판정 · 기준안전율 · 방법 · 수렴 · 비탈 높이 · 원호 중심 · 반경 · 방법별 최소 안전율 표(Fellenius · Bishop · Janbu 보정 · Spencer) · 절편표 · 절편 자유물체도 · 절편력 그래프. 칸 뜻은 「칸 뜻」 항목.
- **3D 보기**: 노선 면을 가장 가까운 측점의 (최소 안전율 ÷ 기준안전율) 띠 색으로 칠함 · 해석 없는 측점은 회색. 표시만 함.
- **대표 단면 별**: 채운 별 ★ = 켬 · 밑줄 = 사용자가 바꿈. 자동으로 켜지는 측점 = 쌓기 높이 10 m 이상 · 깎기 높이 15 m 이상 · 최소 안전율 하위 5 곳.
- 좌측 상태 글: 「해석 시각」 · 「지반 조건이 바뀌었습니다 — [해석]을 다시 누르세요」(보이는 결과는 옛 결과) · 「대표 단면 바뀜 — 저장 안 됨」.
## 알아 둘 것
- **문헌 추정치 · 현장 시험 없음**: 지반 값이 문헌 대표값이라 안전율은 설계 검토용 참고값임. 현장 조사 값으로 바꿔 다시 해석할 것.
- 해석은 서버가 함 · 화면은 결과만 보임. 같은 입력이면 다시 셈하지 않음.
- 지하수 = E02 에서 고른 규칙(건기 · 우기 갈래 넷)을 안전율 계산과 지하수위 점선에 같이 씀. r_u 를 골랐으면 수위선 없이 그림 아래에 「지하수 = 간극수압비 r_u …」 글만 보임.
- 탐색 범위 = 노면 반대쪽 끝 ~ 비탈 끝 바깥 (5 m 와 비탈 높이 2 배 중 큰 값)까지 — 설계 비탈을 지나는 면만 봄. 노선 밖 자연 비탈 전체 붕괴는 광역 위험(E07) 몫.
- 깊이 0.3 m 미만으로 겉만 긁는 면은 붕괴로 보지 않음.
- 벽은 아주 단단한 물체로 넣어 활동면이 벽을 지나지 않음 — 벽 자체 안정은 보강 검토의 「벽 외적 안정」으로 봄.
- 보강 가정은 따로 저장되는 검토용 결과임 — 설계 · 원본 해석 결과 · 임도 프로젝트는 바뀌지 않음. 먼저 지금 입력으로 [해석]이 끝나 있어야 함. 벽이 선 비탈은 가정 해석 대신 벽 외적 안정으로 검토할 것.
- 공유받은 회사(보기만)는 [해석] · 별 · 보강 단추가 꺼짐.
> 참고: 해석은 서버가 하고 화면은 결과만 보임. 같은 입력이면 다시 계산하지 않음.
@@ -0,0 +1,29 @@
# 해석 돌리기
[해석] 단추로 모든 측점의 2D 해석을 한꺼번에 계산하는 단계.
## 여는 길
- 2D 해석 화면 왼쪽 아래 [해석] 단추.
![해석 단추를 누른 모습](E03_01_analyze_fresh.webp)
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | [지반 조건](E02/00_설명서)에서 [저장]을 했는지 확인함. |
| 2 | 왼쪽 아래 [해석]을 누름. |
| 3 | 화면이 잠기고 「해석 중 n% · 측점 k / N」 진행률이 보임. 측점이 수십 개면 몇 분 걸림. |
| 4 | 끝나면 「해석이 끝났습니다.」 알림이 뜨고 목록 · 평면 띠 · 횡단이 채워짐. |
## 보이는 것
- 입력이 같으면 위 그림처럼 「입력이 같아 다시 셀 것이 없습니다.」 알림만 뜸.
- 지반 조건을 바꾼 뒤에는 왼쪽에 「지반 조건이 바뀌었습니다 — [해석]을 다시 누르세요」가 뜨고, 다시 [해석]하면 바뀐 측점만 다시 계산함.
- 아직 해석하지 않았으면 가운데에 「아직 해석하지 않았습니다 — [해석]을 누르세요.」가 보임.
- 왼쪽 상태 글에 「해석 시각」이 적힘.
> 주의: 해석 중에는 화면이 잠김. 끝날 때까지 기다릴 것.
> 참고: 공유받은 회사(보기만)는 [해석]이 꺼져 있음.
@@ -0,0 +1,34 @@
# 측점 목록
왼쪽 패널에서 측점별 안전율과 판정을 보고, 볼 측점을 고르는 단계.
## 여는 길
- 2D 해석 화면 왼쪽 패널 — [해석] 뒤에 채워짐.
![측점 목록](E03_02_list.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 표 머리 | 측점 · 좌 · 우(깎 · 쌓) · 조건별 최소 안전율 · 판정 · 대표 |
| 2 | N.G 줄 | 판정이 N.G 인 측점 — 빨간 마름모 |
| 3 | ▦ | 옹벽 · 석축 등 벽이 있는 측점 |
| 4 | ★ | 보고서에 넣을 대표 단면 (☆ = 아님) |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 위 **조건** 칸에서 건기 · 우기(지진 검토를 켰으면 지진도)를 고름. 표 · 평면 띠 · 횡단이 그 조건으로 바뀜. |
| 2 | **정렬** 칸에서 「측점 순」 또는 「안전율 낮은 순」을 고름. |
| 3 | **N.G 만**을 체크하면 N.G 측점만 남음. |
| 4 | 줄을 눌러 측점을 고름. 고른 줄은 진한 테두리로 보임. |
![우기 · 안전율 낮은 순 · N.G 만](E03_03_wet_ng_only.webp)
## 보이는 것
- **O.K** = 최소 안전율 ≥ 기준안전율, **N.G** = 그보다 작음, 「-」 = 해석 없음, 「오류」 = 그 측점 계산 실패(줄에 마우스를 올리면 까닭).
- 측점 판정 = 그 조건에서 좌 · 우 비탈 중 하나라도 N.G 면 N.G.
> 참고: 기준안전율은 [기준안전율](E02/02_기준안전율)에서 정한 값.
@@ -0,0 +1,28 @@
# 평면 위험 띠
노선 전체를 위에서 본 모양으로 그리고, 지금 조건의 판정을 색으로 칠한 띠.
## 여는 길
- 2D 해석 화면 가운데 맨 위 **평면 위험 띠** 카드.
![평면 위험 띠](E03_04_plan.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 노선 그림 | 초록 = O.K · 빨강 = N.G · 회색 = 해석 없음 · 고른 측점은 동그라미 |
| 2 | 범례 | 판정별 측점 수 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 왼쪽 **조건**에서 건기 · 우기를 바꿔 색이 어떻게 달라지는지 봄. |
| 2 | 띠에서 보고 싶은 곳 근처를 누름 — 가장 가까운 측점이 골라짐. |
## 보이는 것
- 위가 북쪽이고 모든 곳이 같은 축척.
- 빨간 구간이 몰린 곳이 위험한 구간.
> 참고: 3D로 보려면 [대표 단면과 3D 보기](08_대표_단면과_3D_보기).
@@ -0,0 +1,38 @@
# 횡단 그림
고른 측점의 횡단 · 가장 위험한 활동면 · 안전율 지도를 보는 가운데 영역.
## 여는 길
- 목록이나 평면 띠에서 측점을 고르면 가운데에 나타남.
![횡단 그림 카드](E03_05_section.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 제목 | 「STA.측점 · 조건」 |
| 2 | 비탈 칸 | 「좌 비탈 · 쌓기」 · 「우 비탈 · 깎기」 중 볼 비탈. 처음 = 안전율 낮은 쪽 |
| 3 | 보기 칸 | 「해석 결과」 · 「보강 검토」 · 「확률 · 민감도」 |
| 4 | 겹쳐 그리기 | 등안전율 지도 · 원호 중심 격자 · 다른 후보 |
| 5 | 횡단 | 지층 · 지표 · 활동면 |
| 6 | 범례 | 안전율 색 · 지층 색 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | **비탈** 칸에서 볼 비탈을 고름. |
| 2 | **등안전율 지도** · **원호 중심 격자** · **다른 후보** 체크로 겹쳐 그릴 것을 켜고 끔. |
| 3 | 휠로 확대 · 축소, 가운데 단추를 끌어 옮기고, 두 번 누르면 처음 배율로 돌아옴. |
| 4 | 절편을 누르면 그 절편의 힘이 오른쪽에 보임 — [결과 읽기](05_결과_읽기). |
![원호 중심 격자를 켠 모습](E03_06_centers_on.webp)
## 보이는 것
- **굵은 선** = 가장 위험한 활동면(안전율 · 원호 중심 · 반경 글 포함). **옅은 선** = 다른 방법 · 다른 꼴 후보. **점선** = 지하수위. 노면 위 눈금 = 노면 하중. 회색 빗금 = 벽.
- **등안전율 지도** = 원호 중심 자리마다 최소 안전율을 기준 대비 일곱 띠로 칠함. 범례에는 실제 안전율 값이 적힘.
- 지하수 규칙이 r_u 면 수위선 없이 그림 아래에 「지하수 = 간극수압비 r_u …」 글만 보임.
- 벽 있는 측점은 아래에 「옹벽 · 석축(회색 빗금)은 강한 물체로 넣어 활동면이 지나지 않습니다.」가 보임.
> 참고: 좌 · 우 비탈이 깎기 · 쌓기로 따로 계산됨. 보통 N.G 가 난 쪽을 먼저 봄.
@@ -0,0 +1,40 @@
# 결과 읽기
고른 측점 · 비탈의 계산 결과를 오른쪽 결과 영역에서 읽는 단계.
## 여는 길
- 보기 칸이 「해석 결과」일 때 가운데 오른쪽에 보임.
![최소 안전율 · 방법별 표](E03_07_kpi.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 결과 위쪽 | 최소 안전율 · 판정 · 기준안전율 · 방법 · 수렴 · 비탈 높이 · 원호 중심 · 반경 |
| 2 | 방법별 표 | Fellenius · Bishop · Janbu 보정 · Spencer 값 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 위쪽에서 최소 안전율과 판정(O.K · N.G)을 확인함. 판정에는 Bishop 간편법 최소값(원호 · 비원호 중 작은 쪽)을 씀. |
| 2 | 방법별 표로 다른 방법의 값과 견줌. 「수렴 안 됨」은 그 방법이 답을 못 찾았다는 뜻. |
| 3 | 아래 **절편표**에서 절편마다 폭 · 높이 · 각도 · 무게 · 간극수압 · 힘을 봄. |
| 4 | 절편표 줄이나 횡단의 절편을 누르면 **절편 자유물체도**에 그 절편의 힘이 그려짐. |
| 5 | **절편력 그래프**에서 유효 수직응력 · 간극수압 · 동원 전단응력이 활동면 따라 어떻게 변하는지 봄. |
![절편표](E03_08_slice_table.webp)
![절편을 누른 모습 — 자유물체도와 그래프](E03_09_slice_pick.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 고른 절편 줄 | 바탕색이 달라짐 |
| 2 | 절편 자유물체도 | 그 절편에 걸리는 힘 화살표 (힘 kN/m · 길이는 힘에 비례) |
| 3 | 절편력 그래프 | 활동면 따라 σ' · u · τ 변화 |
## 보이는 것
- 칸 뜻은 [칸 뜻](10_칸_뜻), 쓰인 식과 근거는 [쓰인 방법과 근거](20_쓰인_방법과_근거).
> 주의: 안전율은 문헌 추정치로 계산한 참고값임.
@@ -0,0 +1,50 @@
# 보강 검토
비탈을 완화하거나 벽 · 네일 · 앵커로 보강했다고 가정하고 안전율이 어떻게 달라지는지 보는 단계.
## 여는 길
- 횡단 머리줄 **보기** 칸에서 「보강 검토」를 고름. 오른쪽 영역이 보강 검토 판으로 바뀜.
- 먼저 지금 입력으로 [해석]이 끝나 있어야 함.
![비탈 완화 · 소단 가정](E03_10_reinforce_slope.webp)
## 단계
### 비탈 완화 · 소단 가정
| 단계 | 할 일 |
| --- | --- |
| 1 | **비탈**을 고름. |
| 2 | **기울기 1:n** · **소단 폭 m** · **소단 높이 m 마다**를 넣음. 기울기 칸에는 지금 값이 흐리게 보임. |
| 3 | [가정 해석]을 누름. 형상만 바꿔 다시 계산함. |
| 4 | 기준안전율을 맞추는 가장 급한 기울기가 궁금하면 [최소 완화 찾기]를 누름(수십 초 ~ 1 분). |
### 벽 외적 안정 (벽 있는 측점만)
![벽 외적 안정](E03_11_reinforce_wall.webp)
| 단계 | 할 일 |
| --- | --- |
| 1 | 벽 있는 측점(목록에 ▦)을 고르고 보강 검토를 엶. 처음 열면 Coulomb · 배면 수위 0 으로 한 번 자동 검토함. |
| 2 | **토압** 방법과 **배면 수위 ÷ 벽 높이**를 바꿈. |
| 3 | [외적 안정 검토]를 누름. |
| 4 | 표에서 활동 · 전도 · 지지력의 안전율과 O.K · N.G 를 확인함. |
### 네일 · 앵커 가정
![네일 · 앵커 가정](E03_12_support.webp)
| 단계 | 할 일 |
| --- | --- |
| 1 | **비탈** · **종류**(네일 · 앵커)를 고름. |
| 2 | 행 수 · 첫 행 높이 · 수직 간격 · 수평 간격 · 길이 · 각도 · 인장 한도 · 부착 · 정착장 · 천공 지름 · 선인장 값을 넣음. 기본값은 사용자 확정 전 후보임. |
| 3 | [보강 해석] 또는 기준안전율을 맞추는 가장 짧은 길이를 찾는 [최소 길이 찾기]를 누름. |
## 보이는 것
- 전 · 후 안전율과 판정이 표로 나오고 가운데 횡단이 가정한 형상으로 바뀜.
- 가정 결과는 검토용으로 따로 저장됨 — 설계 · 원본 해석 결과는 바뀌지 않음.
- 벽이 선 비탈은 가정 해석 대신 벽 외적 안정으로 검토할 것.
> 주의: 네일 · 앵커 기본값은 아직 사용자 확정 전인 후보값임.
@@ -0,0 +1,32 @@
# 확률 · 민감도
지반 값이 평균에서 흩어질 때 안전율이 어떻게 변하는지, 파괴확률은 얼마인지 보는 단계.
## 여는 길
- 횡단 머리줄 **보기** 칸에서 「확률 · 민감도」를 고름.
- 대표 단면(★)만 해석함 — 목록에서 별을 켜고 [대표 단면 저장]한 뒤 볼 것.
![확률 · 민감도 입력](E03_13_prob.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 변수 표 | 지층별 c · φ · γ 의 쓰기 · 평균 · 분포 · 변동계수 · 절단 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 표에서 확률로 다룰 변수만 **쓰기**에 체크함. 평균은 지반 조건 값이라 여기서 바꾸지 않음. |
| 2 | **분포** · **변동계수** · **절단 ±σ 배**를 확인함. 기본값은 후보임. |
| 3 | 아래에서 **임계면**(고정 · 매회 탐색) · **방법** · **표본 방법** · **표본 수** · **시드** · **c–φ 상관 ρ**를 정함. |
| 4 | [확률 해석]을 누름. 카드 안에 진행률이 보임. |
| 5 | 끝나면 결과 영역에 결정론 안전율 · 평균 · 표준편차 · 파괴확률 · 신뢰도 지수가 나옴. |
## 보이는 것
- 결과 = 안전율 히스토그램 · 누적분포 · 민감도 · 토네이도 그림(범위 끝 값에서의 안전율).
- 아래 「기본값 후보 · 사용자 확정 전 — 근거」를 펼치면 기본값의 근거가 보임.
- [기본값]은 입력을 처음 값으로 되돌림.
> 주의: 분포와 변동계수 기본값은 사용자 확정 전 후보임.
@@ -0,0 +1,35 @@
# 대표 단면과 3D 보기
보고서에 넣을 대표 단면을 정하고, 노선 3D 면 위에 안전율 색을 보는 단계.
## 여는 길
- 목록의 ★ 열, 왼쪽 아래 [대표 단면 저장] · [3D 보기].
![별을 눌러 바꾼 모습](E03_14_rep_star.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | ★ | 눌러 켜고 끔 |
| 2 | 상태 글 | 「대표 단면 바뀜 — 저장 안 됨」 |
| 3 | [대표 단면 저장] | 바꾼 별을 저장 (바꾼 것이 있을 때만 켜짐) |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 목록 「대표」 열의 별을 눌러 켜거나 끔. 채운 별(★)이 켜진 것, 밑줄은 사용자가 바꾼 것. |
| 2 | [대표 단면 저장]을 누름. 「대표 단면을 저장했습니다.」 알림이 뜸. |
| 3 | 노선 3D 면을 보려면 [3D 보기]를 누름(해석 뒤에만 켜짐). |
![3D 보기](E03_15_view3d.webp)
## 보이는 것
- 자동으로 켜지는 측점 = 쌓기 높이 10 m 이상 · 깎기 높이 15 m 이상 · 최소 안전율 하위 5 곳.
- 3D 보기 = 노선 면을 가장 가까운 측점의 (최소 안전율 ÷ 기준안전율) 띠 색으로 칠함. 해석 없는 측점은 회색. 표시만 하고 [닫기]로 닫음.
- 별을 바꾸고 저장하지 않으면 왼쪽에 「대표 단면 바뀜 — 저장 안 됨」이 계속 보임.
> 참고: 대표 단면은 보고서 화면([보고서](E05/00_설명서))과 「확률 · 민감도」에 쓰임.
> 주의: 노선 3D 면이 복사본에 없으면 「복사본에 노선면이 없습니다.」 알림만 뜨고 3D 창이 열리지 않음.
+18
View File
@@ -0,0 +1,18 @@
[
{"name": "E03_00_overview", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"wait_ms": 800}], "clip": null, "callouts": [{"n": 1, "sel": ".ui-workflow-overlay__panel .e03-panel__row"}, {"n": 2, "sel": ".ui-workflow-overlay__panel .e03-check"}, {"n": 3, "sel": "[data-role=station-list]"}, {"n": 4, "sel": ".ui-workflow-overlay__panel .ui-sidebar-actions"}, {"n": 5, "sel": ".e03-plan"}, {"n": 6, "sel": ".e03-body > .e03-card .e03-card__head"}, {"n": 7, "sel": ".e03-section"}, {"n": 8, "sel": ".e03-body > .e03-card > div:last-child"}, {"n": 9, "sel": ".e03-result"}], "viewport": [1600, 1500]},
{"name": "E03_01_analyze_fresh", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"click": ".ui-workflow-overlay__panel .ui-btn--filled"}, {"wait": ".ui-toast"}, {"wait_ms": 300}], "clip": null, "callouts": [{"n": 1, "sel": ".ui-workflow-overlay__panel .ui-btn--filled"}]},
{"name": "E03_02_list", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"wait_ms": 800}], "clip": "[data-role=station-list]", "callouts": [{"n": 1, "sel": "[data-role=station-list] thead"}, {"n": 2, "sel": "[data-role=station-list] tbody tr[data-verdict=ng]"}, {"n": 3, "sel": "[data-role=station-list] tr[data-key='351.00'] .e03-wall-tag"}, {"n": 4, "sel": "[data-role=station-list] tbody tr[data-key='240.00'] .e03-star"}]},
{"name": "E03_03_wet_ng_only", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"select": ".ui-workflow-overlay__panel select:nth-of-type(1)", "value": "wet"}, {"select": ".ui-workflow-overlay__panel .e03-panel__row > :nth-child(2) select", "value": "fs"}, {"click": ".ui-workflow-overlay__panel .e03-check input"}, {"wait_ms": 500}], "clip": ".ui-workflow-overlay__panel", "callouts": [{"n": 1, "sel": ".ui-workflow-overlay__panel .e03-panel__row > :nth-child(1)"}, {"n": 2, "sel": ".ui-workflow-overlay__panel .e03-panel__row > :nth-child(2)"}, {"n": 3, "sel": ".ui-workflow-overlay__panel .e03-check"}]},
{"name": "E03_04_plan", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"wait_ms": 800}], "clip": ".e03-plan", "callouts": [{"n": 1, "sel": "[data-role=plan]"}, {"n": 2, "sel": "[data-role=plan-legend]"}]},
{"name": "E03_05_section", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"wait_ms": 800}], "clip": ".e03-body > .e03-card", "callouts": [{"n": 1, "sel": ".e03-body > .e03-card .e03-card__head h3"}, {"n": 2, "sel": ".e03-body > .e03-card .e03-card__head select:not([data-role=mode])"}, {"n": 3, "sel": ".e03-body > .e03-card .e03-card__head select[data-role=mode]"}, {"n": 4, "sel": ".e03-body > .e03-card .e03-card__head .e03-panel__row"}, {"n": 5, "sel": ".e03-section"}, {"n": 6, "sel": ".e03-body > .e03-card > div:last-child"}]},
{"name": "E03_06_centers_on", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"click": ".e03-body > .e03-card .e03-card__head .e03-panel__row label:nth-child(2) input"}, {"wait_ms": 600}], "clip": ".e03-body > .e03-card", "callouts": [{"n": 1, "sel": ".e03-body > .e03-card .e03-card__head .e03-panel__row label:nth-child(2)"}]},
{"name": "E03_07_kpi", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"wait_ms": 800}], "clip": ".e03-result", "callouts": [{"n": 1, "sel": ".e03-result .e03-kpi"}, {"n": 2, "sel": "[data-role=methods]"}]},
{"name": "E03_08_slice_table", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"scroll": "[data-role=slice-table]"}], "clip": ".e03-result", "callouts": [{"n": 1, "sel": "[data-role=slice-table]"}]},
{"name": "E03_09_slice_pick", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"click": "[data-role=slice-table] tbody tr:nth-child(3)"}, {"wait": "[data-role=fbd]"}, {"scroll": "[data-role=fbd]"}, {"wait_ms": 500}], "clip": null, "callouts": [{"n": 1, "sel": "[data-role=slice-table] tbody tr.is-picked"}, {"n": 2, "sel": "[data-role=fbd]"}, {"n": 3, "sel": "[data-role=stress-graph]"}]},
{"name": "E03_10_reinforce_slope", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"select": "select[data-role=mode]", "value": "reinforce"}, {"wait": "[data-role=reinforce]"}, {"wait_ms": 600}], "clip": "[data-role=reinforce]", "callouts": []},
{"name": "E03_11_reinforce_wall", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"click": "[data-role=station-list] tr[data-key='351.00']"}, {"wait_ms": 800}, {"select": "select[data-role=mode]", "value": "reinforce"}, {"wait": "[data-role=wall-table]", "timeout": 30000}, {"wait_ms": 600}], "clip": "[data-role=reinforce]", "callouts": [{"n": 1, "sel": "[data-role=wall-table]"}]},
{"name": "E03_12_support", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"select": "select[data-role=mode]", "value": "reinforce"}, {"wait": "[data-role=support]"}, {"wait_ms": 600}], "clip": "[data-role=support]", "callouts": []},
{"name": "E03_13_prob", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"select": "select[data-role=mode]", "value": "prob"}, {"wait": "[data-role=prob]"}, {"wait_ms": 600}], "clip": "[data-role=prob]", "callouts": [{"n": 1, "sel": "[data-role=prob-vars]"}]},
{"name": "E03_14_rep_star", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"click": "[data-role=station-list] tbody tr[data-key='100.00'] .e03-star"}, {"wait_ms": 500}], "clip": ".ui-workflow-overlay__panel", "callouts": [{"n": 1, "sel": "[data-role=station-list] tbody tr[data-key='100.00'] .e03-star"}, {"n": 2, "sel": ".ui-workflow-overlay__panel .e03-muted[data-role=status]"}, {"n": 3, "sel": ".ui-workflow-overlay__panel .ui-sidebar-actions .ui-btn--ghost:nth-child(1)"}]},
{"name": "E03_15_view3d", "route": "#/e03-stab-section", "project": "stab", "before": [{"wait": "[data-role=station-list] tbody tr"}, {"wait": ".e03-section svg"}, {"click": ".ui-workflow-overlay__panel .ui-sidebar-actions .ui-btn--ghost:nth-child(2)"}, {"wait": "[data-role=view3d] canvas"}, {"wait_ms": 4000}], "clip": null, "callouts": []}
]
+29 -60
View File
@@ -1,68 +1,37 @@
# 3D 해석 설명서
# 3D 해석
## 이 화면에서 하는 일
노선 한 구간(보통 약 100 m)을 골라 지형 전체를 입체로 보고, 가장 위험한 3차원 활동면(흙이 미끄러져 내리는 바닥면)을 찾는 화면.
- 노선 한 구간(보통 약 100 m)을 골라 지형 전체를 입체로 보고 가장 위험한 3차원 활동면(흙이 미끄러져 내리는 바닥면)을 찾음.
- 땅을 1 m 기둥 격자로 나눠 셀마다 「그 셀을 지나는 활동면 중 가장 낮은 안전율」을 색 지도로 보여 줌.
- 가장 위험한 활동면의 안전율 · 무너질 흙의 양(임계 체적) · 같은 구간 2D(E03) 결과와의 비를 오른쪽에 보여 줌.
- 계산은 모두 서버가 함. 화면은 결과를 그리기만 함.
## 여는 길
- 2D 해석 화면 왼쪽 아래 [>] 단추 — 또는 대시보드에서 안정성평가 프로젝트를 열고 「3D 해석」 단계를 고름.
- 먼저 [지반 조건](E02/00_설명서)에서 값을 [저장]해 둘 것. 3D 해석은 저장한 지반 조건을 씀.
![화면 한눈에](E04_00_overview.webp)
## 화면 구역
| 번호 | 구역 | 하는 일 |
| --- | --- | --- |
| 1 | 안내 글 | 탐색 · 해석 방식 한 줄 설명 |
| 2 | 구간 카드 | 해석할 구간을 고르거나 직접 정함 — [구간 고르기](01_구간_고르기) |
| 3 | 탐색 설정 카드 | 조건 · 활동면 모양 · 타원체 길이비 · 탐색 폭 — [탐색 설정](02_탐색_설정) |
| 4 | 고급 설정 카드 | 기둥 크기 · 중심 간격 · 부피 범위 등 — [탐색 설정](02_탐색_설정) |
| 5 | 단추 줄 | [다시 해석] · [해석] — [해석 돌리기](03_해석_돌리기) |
| 6 | 보기 단추 | 평면 지도 ↔ 3D 보기 · 최소 안전율 ↔ 임계 체적 |
| 7 | 가운데 지도 | 셀별 결과 색 지도 — [평면 지도](04_평면_지도) |
| 8 | 범례 | 색 단계 |
| 9 | 구간 결과 | 3D 최소 안전율 · 판정 · 임계 체적 — [구간 결과](06_구간_결과) |
## 작업 순서
1. 왼쪽 **구간** 카드 목록에서 구간 하나를 누름(예: `0 – 100 m`). 직접 정하려면 아래 **시작 (m)** · **끝 (m)** 칸에 숫자를 넣음.
1. 왼쪽 **구간** 카드에서 구간 하나를 누름.
2. **탐색 설정** 카드에서 **조건**(건기 · 우기)과 **활동면 모양**을 고름. 처음에는 기본값 그대로 써도 됨.
3. 필요하면 **고급 설정** 카드를 펼쳐 기둥 크기 · 구 중심 격자 · 부피 범위를 바꿈(칸 뜻은 「칸 뜻」 문서).
4. 왼쪽 아래 **[해석]** 을 누름. 진행 막대에 「해석 중 n% · 합성 지형 → 거친 탐색 → 고운 탐색 → 타원체」 차례가 보임.
5. 끝나면 「3D 해석이 끝났습니다.」 알림과 함께 가운데 지도에 색이 칠해지고 오른쪽 **구간 결과** 가 채워짐.
6. 가운데 위 **[3D 보기]** 로 바꿔 입체 지형 위의 임계 활동면을 확인함.
7. 다른 구간도 같은 방법으로 해석함. 해석한 구간은 목록에 색 점과 `Fs` 값이 붙음.
8. 보고서에 넣으려면 E05 보고서 화면에서 **[보고서 갱신]** 을 누름(3D요약 시트에 구간마다 한 줄).
3. 왼쪽 아래 [해석]을 누름.
4. 끝나면 가운데 지도에 색이 칠해지고 오른쪽 **구간 결과**가 채워짐.
5. 가운데 위 [3D 보기]로 바꿔 입체 지형 위의 임계 활동면을 확인함 — [3D 보기](05_3D_보기).
6. 다른 구간도 같은 방법으로 해석함.
## 단추
> 주의: 지반 물성치는 문헌 추정치(현장 시험 없음)라 안전율은 비교 · 검토용으로 볼 것.
| 자리 | 단추 | 하는 일 |
| --- | --- | --- |
| 왼쪽 아래 | **[해석]** | 고른 구간을 해석함. 입력(지반 조건 · 탐색 설정 · 구간)이 저장된 결과와 같으면 다시 셈하지 않고 「입력이 같아 저장된 결과를 엽니다.」 |
| 왼쪽 아래 | **[다시 해석]** | 입력이 같아도 처음부터 다시 셈 |
| 진행 막대 옆 | **[끊기]** | 돌고 있는 해석을 멈춤. 전에 저장된 결과는 그대로 남음 |
| 고급 설정 카드 | **[기본값]** | 탐색 설정 칸을 기본값으로 되돌림(구간 시작 · 끝은 그대로 · 조건은 건기) |
| 가운데 위 | **[평면 지도]** · **[3D 보기]** | 위에서 본 색 지도와 입체 보기를 바꿈 |
| 가운데 위 | **[최소 안전율]** · **[임계 체적]** | 지도 · 3D 색칠 기준을 바꿈 |
| 3D 보기 위 | **임계 활동면** 체크 | 켬 = 활동체를 걷어낸 지표 + 붉은 활동면 · 끔 = 원래 지표만 |
- 평면 지도: 마우스 휠 = 확대 · 축소, 끌기 = 옮김, 두 번 누름 = 화면 맞춤, 셀 위에 올리면 그 셀의 최소 안전율 · 임계 체적 · 가까운 측점이 뜸.
- 3D 보기: 끌기 = 돌림, 휠 = 확대 · 축소. 오른쪽 위 나침반이 방위를 알려 줌.
## 결과 읽는 법
- **평면 지도**: 셀 색 = 그 셀을 지나는 활동면 중 가장 낮은 안전율. 붉을수록 위험. 점선 윤곽 = 구간 전체에서 가장 위험한 활동면이 땅을 자르는 범위, 동그라미와 `Fs` 글 = 그 활동면 자리. 굵은 선 = 고른 구간.
- **안전율 색 단계**: 1.0 미만 · 1.0~1.2 · 1.2~1.3 · 1.3~1.5 · 1.5~2.0 · 2.0~3.0 · 3.0 이상 · 회색 = 값 없음(활동면이 닿지 않은 셀). 1.3(우기) · 1.5(건기)가 기준 경계.
- **임계 체적 색 단계**: 10 m³ 미만 · 10~100 · 100~1,000 · 1,000~10,000 · 10,000 m³ 이상 — 짙을수록 큰 붕괴.
- **3D 보기**: 지표 색 = 지금 고른 층(안전율 또는 체적) · 파란 관 = 노선 중심선 · 짙은 붉은 면 = 임계 활동면(지표 아래 부분만).
- **오른쪽 구간 결과**:
| 줄 | 뜻 |
| --- | --- |
| 3D 최소 안전율 | 구간에서 찾은 가장 낮은 3D 안전율 |
| 판정 | 「만족」 또는 「불만족」 · 괄호 안 = 조건과 기준(예: 건기 ≥ 1.50) |
| 2D 최소 안전율 (같은 구간) | E03 측점 결과 중 이 구간 안 가장 낮은 값 · 없으면 「2D 결과 없음」 |
| 3D / 2D 비 | 3D 최소 ÷ 2D 최소 · 1 보다 크면 2D 가 안전 쪽(보수적) |
| 임계 체적 | 가장 위험한 활동면 위 흙의 부피(m³) |
| 다스린 방법 | 판정에 쓴 방법(3D Bishop 또는 3D Janbu 보정) |
| 3D Bishop · 3D Janbu 보정 · Hovland (비교) | 같은 활동면을 세 방법으로 셈한 값 |
| 위치 (가까운 측점) | 가장 위험한 활동면에 가장 가까운 측점 |
| 셀 수 · 시험 활동면 수 · 계산 시간 | 계산 규모 |
## 알아 둘 것
- **문헌 추정치 · 현장 시험 없음** — 지반 물성치는 문헌 · 기준값을 적용한 추정치이며 현장 조사 · 시험 성과를 반영하지 않았음. 안전율은 비교 · 검토용으로 볼 것.
- 결과는 서버에 구간마다 자동 보관됨(따로 [저장] 없음). 탐색 설정 칸 값은 이 브라우저 탭에만 기억됨.
- E02 지반 조건을 바꾸면 그 구간 결과 아래 「입력(지반 조건 · 탐색 설정 · 엔진)이 바뀌었습니다 — 다시 해석하세요.」 경고가 뜨고 목록에 「옛 결과」 가 붙음. **[해석]** 을 다시 누를 것.
- 3D 는 E02 에서 **[저장]** 한 지반 조건을 씀. 해석 전 E02 에서 [저장] 을 한 번 눌러 둘 것.
- 한 번에 해석할 수 있는 구간은 500 m 이하. 구간 안 측점이 둘 이상이어야 함.
- 해석 하나에 수십 초 ~ 수 분 걸림(구간 길이 · 탐색 폭 · 기둥 크기에 따라). 한 프로젝트에서 한 번에 하나만 돎.
- 「조건에 맞는 활동면이 없습니다 — 부피 · 높이 범위를 넓히세요.」 가 뜨면 고급 설정의 최소 · 최대 부피나 중심 높이 범위를 넓힐 것.
- 우기는 「지표까지 포화」 한 가지로 셈 — E02 의 지하수 규칙(깊이 비율 · r_u · 수위선)은 3D 에 쓰지 않음. 지진 조건 · 옹벽 · 석축은 3D 에 넣지 않음.
- 2D 대조 값은 E03 측점마다 기준 대비 가장 낮은 조건 · 비탈의 안전율이라 조건을 맞추지 않음. 같은 조건끼리 견준 표는 E05 보고서 「3D요약」 시트에 있음.
- 보기만 허용된 프로젝트에서는 「보기만 허용된 프로젝트입니다.」 가 뜨고 [해석] 단추가 꺼짐.
- 쓰인 방법 · 근거 · 한계는 「쓰인 방법과 근거」 문서, 아직 사용자 확정 전인 값은 「사용자 확정 전 값」 문서 참고.
> 참고: 결과는 구간마다 서버에 자동 보관되므로 따로 저장 단추가 없음.
@@ -0,0 +1,32 @@
# 구간 고르기
3D로 해석할 노선 구간을 정하는 단계.
## 여는 길
- 3D 해석 화면 왼쪽 맨 위 **구간** 카드.
![구간 카드](E04_01_sections.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 구간 목록 | 100 m 단위로 나눈 구간 |
| 2 | 해석한 구간 | 색 점과 `Fs` 값이 붙음 |
| 3 | 시작 (m) · 끝 (m) | 직접 정하는 구간 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 목록에서 구간 하나를 누름. 시작 · 끝 칸이 그 값으로 채워지고 가운데 지도에 굵은 선으로 표시됨. |
| 2 | 직접 정하려면 **시작 (m)** · **끝 (m)** 칸에 누가거리를 넣음. |
![직접 구간을 넣은 모습](E04_02_custom_range.webp)
## 보이는 것
- 해석한 구간은 목록에 색 점과 `Fs` 값이 붙음.
- E02 지반 조건 등을 바꾼 뒤에는 그 구간에 「옛 결과」가 붙음 — [해석 돌리기](03_해석_돌리기).
- 아직 해석하지 않은 구간은 오른쪽에 「이 구간 결과가 없습니다 — [해석]을 누르세요.」가 보임.
> 주의: 한 번에 해석할 수 있는 구간은 500 m 이하이고, 구간 안에 측점이 둘 이상이어야 함.
@@ -0,0 +1,33 @@
# 탐색 설정
가장 위험한 활동면을 어떤 조건 · 모양 · 범위로 찾을지 정하는 단계.
## 여는 길
- 3D 해석 화면 왼쪽 **탐색 설정** 카드와 **고급 설정** 카드.
![탐색 설정 카드](E04_03_search.webp)
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | **조건**을 고름 — 「건기」 또는 「우기 (지표까지 포화)」. |
| 2 | **활동면 모양**을 고름 — 「구」 또는 「구 + 타원체」. |
| 3 | 「구 + 타원체」면 **타원체 길이비**를 넣음. |
| 4 | **탐색 폭 (중심선 ±m)** 을 넣음 — 노선 중심선에서 좌우로 얼마나 넓게 찾을지. |
| 5 | 더 바꿀 것이 있으면 **고급 설정** 카드 제목을 눌러 펼침. |
![고급 설정](E04_04_advanced.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 기둥 크기 · 구 중심 간격 · 중심 높이 · 반지름 증분 · 최대 깊이 · 최소 · 최대 부피 | 탐색 격자와 부피 범위 — 칸 뜻은 [칸 뜻](10_칸_뜻) |
| 2 | [기본값] | 탐색 설정 칸을 기본값으로 되돌림(구간 시작 · 끝은 그대로 · 조건은 건기) |
## 보이는 것
- 탐색 설정 값은 이 브라우저 탭에만 기억됨.
- 우기는 「지표까지 포화」 한 가지로 셈 — [지반 조건](E02/04_지하수)의 우기 규칙은 3D 에 쓰이지 않음. 지진 조건 · 옹벽 · 석축도 3D 에 넣지 않음.
> 주의: 「조건에 맞는 활동면이 없습니다 — 부피 · 높이 범위를 넓히세요.」가 뜨면 고급 설정의 최소 · 최대 부피나 중심 높이 범위를 넓힐 것.
@@ -0,0 +1,34 @@
# 해석 돌리기
고른 구간을 3D 로 계산하는 단계.
## 여는 길
- 3D 해석 화면 왼쪽 아래 단추 줄.
![단추 줄](E04_05_buttons.webp)
| 번호 | 단추 | 하는 일 |
| --- | --- | --- |
| 1 | [다시 해석] | 입력이 같아도 처음부터 다시 셈 |
| 2 | [해석] | 고른 구간을 해석함. 입력이 저장된 결과와 같으면 다시 셈하지 않고 저장된 결과를 엶 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | [구간](01_구간_고르기)과 [탐색 설정](02_탐색_설정)을 확인함. |
| 2 | [해석]을 누름. |
| 3 | 진행 막대에 「해석 중 n% · 합성 지형 → 거친 탐색 → 고운 탐색 → 타원체」 차례가 보임. 막대 옆 [끊기]로 멈출 수 있음. |
| 4 | 끝나면 「3D 해석이 끝났습니다.」 알림과 함께 지도에 색이 칠해지고 오른쪽 결과가 채워짐. |
## 보이는 것
- 입력이 같으면 「입력이 같아 저장된 결과를 엽니다.」 알림이 뜸.
- [끊기]를 누르면 「해석을 끊었습니다.」가 뜨고 전에 저장된 결과는 그대로 남음.
- 한 번 해석에 수십 초 ~ 수 분 걸림. 한 프로젝트에서 한 번에 하나만 돎.
- 지반 조건을 바꾸면 그 구간에 「옛 결과」가 붙음 — [해석]을 다시 누를 것.
> 주의: 해석 전에 [지반 조건](E02/06_저장과_초기화)에서 [저장]을 한 번 눌러 둘 것.
> 참고: 공유받은 회사(보기만)는 「보기만 허용된 프로젝트입니다.」가 뜨고 [해석]이 꺼짐.
@@ -0,0 +1,36 @@
# 평면 지도
위에서 내려다본 셀별 색 지도로 해석 결과를 보는 단계.
## 여는 길
- 3D 해석 화면 가운데 — 맨 위 **[평면 지도]** 단추(처음 상태).
![평면 지도](E04_06_plan_map.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 보기 단추 | [평면 지도] · [3D 보기] |
| 2 | 층 단추 | [최소 안전율] · [임계 체적] |
| 3 | 지도 | 셀별 색 |
| 4 | 범례 | 색 단계 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | 휠로 확대 · 축소하고, 끌어서 옮김. 두 번 누르면 화면에 맞춤. |
| 2 | [임계 체적]을 눌러 색칠 기준을 바꿈. |
| 3 | 셀 위에 마우스를 올리면 그 셀의 최소 안전율 · 임계 체적 · 가까운 측점이 뜸. |
![임계 체적으로 바꾼 모습](E04_07_layer_volume.webp)
![셀 위에 마우스를 올린 모습](E04_08_map_tip.webp)
## 보이는 것
- **셀 색** = 그 셀을 지나는 활동면 중 가장 낮은 안전율. 붉을수록 위험. 점선 윤곽 = 구간 전체에서 가장 위험한 활동면이 땅을 자르는 범위, 동그라미와 `Fs` 글 = 그 활동면 자리. 굵은 선 = 고른 구간.
- **안전율 색 단계**: 1.0 미만 · 1.0~1.2 · 1.2~1.3 · 1.3~1.5 · 1.5~2.0 · 2.0~3.0 · 3.0 이상 · 회색 = 값 없음(활동면이 닿지 않은 셀).
- **임계 체적 색 단계**: 10 m³ 미만 · 10~100 · 100~1,000 · 1,000~10,000 · 10,000 m³ 이상 — 짙을수록 큰 붕괴.
> 참고: 1.3(우기) · 1.5(건기)가 기준 경계임.
+30
View File
@@ -0,0 +1,30 @@
# 3D 보기
입체 지형 위에서 임계 활동면을 확인하는 단계.
## 여는 길
- 3D 해석 화면 가운데 위 **[3D 보기]** 단추.
![3D 보기](E04_09_view3d.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 나침반 | 방위 |
| 2 | 임계 활동면 체크 | 켬 = 활동체를 걷어낸 지표 + 붉은 활동면 · 끔 = 원래 지표만 |
## 단계
| 단계 | 할 일 |
| --- | --- |
| 1 | [3D 보기]를 누름. |
| 2 | 끌어서 돌리고 휠로 확대 · 축소함. |
| 3 | **임계 활동면**을 켜고 꺼 가며 지표와 활동면을 견줌. |
| 4 | [최소 안전율] · [임계 체적]으로 지표 색 기준을 바꿈. |
## 보이는 것
- 지표 색 = 지금 고른 층(안전율 또는 체적).
- 파란 관 = 노선 중심선. 짙은 붉은 면 = 임계 활동면(지표 아래 부분만).
> 참고: 3D 그림은 컴퓨터에 따라 처음 그릴 때 몇 초 걸릴 수 있음.
@@ -0,0 +1,38 @@
# 구간 결과
고른 구간의 3D 계산 결과를 오른쪽 **구간 결과** 패널에서 읽는 단계.
## 여는 길
- 3D 해석 화면 오른쪽 **구간 결과** 패널. 접혀 있으면 오른쪽 가장자리의 「구간 결과」 탭을 누름.
![구간 결과](E04_10_result.webp)
| 번호 | 칸 | 뜻 |
| --- | --- | --- |
| 1 | 3D 최소 안전율 | 구간에서 찾은 가장 낮은 3D 안전율 |
| 2 | 경고 글 | 「입력(지반 조건 · 탐색 설정 · 엔진)이 바뀌었습니다 — 다시 해석하세요.」 |
## 단계
| 줄 | 뜻 |
| --- | --- |
| 3D 최소 안전율 | 구간에서 찾은 가장 낮은 값 |
| 판정 | 「만족」 또는 「불만족」 — 괄호 안은 조건과 기준(예: 건기 ≥ 1.50) |
| 2D 최소 안전율 (같은 구간) | [2D 해석](E03/00_설명서) 측점 결과 중 이 구간 안 가장 낮은 값. 없으면 「2D 결과 없음」 |
| 3D / 2D 비 | 3D 최소 ÷ 2D 최소. 1 보다 크면 2D 가 안전 쪽(보수적) |
| 임계 체적 | 가장 위험한 활동면 위 흙의 부피(m³) |
| 다스린 방법 | 판정에 쓴 방법(3D Bishop 또는 3D Janbu 보정) |
| 3D Bishop · 3D Janbu 보정 · Hovland (비교) | 같은 활동면을 세 방법으로 셈한 값 |
| 위치 (가까운 측점) | 가장 위험한 활동면에 가장 가까운 측점 |
| 셀 수 · 시험 활동면 수 · 계산 시간 | 계산 규모 |
## 보이는 것
- 해석하지 않은 구간은 아래 그림처럼 「이 구간 결과가 없습니다 — [해석]을 누르세요.」가 보임.
![결과가 없는 구간](E04_11_no_result.webp)
> 주의: 2D 대조 값은 측점마다 기준 대비 가장 낮은 조건 · 비탈의 값이라 조건을 맞추지 않음. 같은 조건끼리 견준 표는 [보고서](E05/00_설명서)의 「3D요약」 시트에 있음.
> 참고: 쓰인 방법과 한계는 [쓰인 방법과 근거](20_쓰인_방법과_근거), 확정 전 값은 [사용자 확정 전 값](30_사용자_확정_전_값).
+14
View File
@@ -0,0 +1,14 @@
[
{"name": "E04_00_overview", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}], "clip": null, "callouts": [{"n": 1, "sel": ".e04-panel .e04-hint"}, {"n": 2, "sel": ".e04-panel > section.ui-collapsible:nth-of-type(1)"}, {"n": 3, "sel": ".e04-panel > section.ui-collapsible:nth-of-type(2)"}, {"n": 4, "sel": ".e04-panel > section.ui-collapsible:nth-of-type(3)"}, {"n": 5, "sel": ".ui-workflow-overlay__panel .ui-sidebar-actions"}, {"n": 6, "sel": ".e04-toolbar"}, {"n": 7, "sel": ".e04-map"}, {"n": 8, "sel": ".e04-legend-slot"}, {"n": 9, "sel": ".ui-side-panels__body"}]},
{"name": "E04_01_sections", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}], "clip": ".e04-panel > section.ui-collapsible:nth-of-type(1)", "callouts": [{"n": 1, "sel": ".e04-sections"}, {"n": 2, "sel": "button.e04-section[data-key='0.00_40.00']"}, {"n": 3, "sel": ".e04-range"}]},
{"name": "E04_02_custom_range", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}, {"eval": "(()=>{const i=document.querySelector(\"[data-key='start_m']\");i.value='100';i.dispatchEvent(new Event('change',{bubbles:true}))})()"}, {"eval": "(()=>{const i=document.querySelector(\"[data-key='end_m']\");i.value='200';i.dispatchEvent(new Event('change',{bubbles:true}))})()"}, {"wait_ms": 1500}], "clip": ".ui-workflow-overlay__panel", "callouts": [{"n": 1, "sel": ".e04-range"}]},
{"name": "E04_03_search", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"wait_ms": 1500}], "clip": ".e04-panel > section.ui-collapsible:nth-of-type(2)", "callouts": []},
{"name": "E04_04_advanced", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}, {"click": ".e04-panel > section.ui-collapsible:nth-of-type(3) .ui-collapsible__title"}, {"wait_ms": 500}], "clip": ".e04-panel > section.ui-collapsible:nth-of-type(3)", "callouts": [{"n": 1, "sel": "[data-key='cell_m']"}, {"n": 2, "sel": ".e04-panel > section.ui-collapsible:nth-of-type(3) .ui-btn"}]},
{"name": "E04_05_buttons", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"wait_ms": 1500}], "clip": ".ui-workflow-overlay__panel .ui-sidebar-actions", "callouts": [{"n": 1, "sel": ".ui-workflow-overlay__panel .ui-sidebar-actions .ui-btn--ghost"}, {"n": 2, "sel": ".ui-workflow-overlay__panel .ui-sidebar-actions .ui-btn--filled"}]},
{"name": "E04_06_plan_map", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}], "clip": ".e04-center", "callouts": [{"n": 1, "sel": ".e04-toggle:nth-child(1)"}, {"n": 2, "sel": ".e04-toggle:nth-child(2)"}, {"n": 3, "sel": ".e04-map__canvas"}, {"n": 4, "sel": ".e04-legend"}]},
{"name": "E04_07_layer_volume", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}, {"click": ".e04-toggle button[data-value=volume]"}, {"wait_ms": 800}], "clip": ".e04-center", "callouts": []},
{"name": "E04_08_map_tip", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}, {"hover": ".e04-map__canvas"}, {"wait_ms": 600}], "clip": ".e04-center", "callouts": []},
{"name": "E04_09_view3d", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}, {"click": ".e04-toggle button[data-value='3d']"}, {"wait": "canvas.e04-view3d__canvas"}, {"wait_ms": 6000}], "clip": ".e04-center", "callouts": [{"n": 1, "sel": ".e04-view3d__compass"}, {"n": 2, "sel": ".e04-view3d__toggle"}]},
{"name": "E04_10_result", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}], "clip": ".ui-side-panels__body", "callouts": [{"n": 1, "sel": ".e04-result__list dd"}, {"n": 2, "sel": ".e04-warn"}]},
{"name": "E04_11_no_result", "route": "#/e04-stab-3d", "project": "stab", "before": [{"wait": ".e04-section"}, {"wait": ".e04-map__canvas"}, {"click": "button.e04-section[data-key='0.00_40.00']"}, {"wait_ms": 2500}, {"eval": "(()=>{const i=document.querySelector(\"[data-key='start_m']\");i.value='100';i.dispatchEvent(new Event('change',{bubbles:true}))})()"}, {"eval": "(()=>{const i=document.querySelector(\"[data-key='end_m']\");i.value='200';i.dispatchEvent(new Event('change',{bubbles:true}))})()"}, {"wait_ms": 1500}], "clip": ".ui-side-panels__body", "callouts": []}
]
+5
View File
@@ -0,0 +1,5 @@
{
"road": "c2390e1c-ee66-42ef-8cd7-253ec00bf904",
"stab": "850175bf-e740-477b-b0f5-486d0fb3a6ab",
"plan": "04f77c23-9ae3-4d22-9eb5-a4fa212ba47a"
}
@@ -11,6 +11,7 @@ B07 화면과 같은 TS 를 Node 로(`B09_Draft_Print_Drawings_Node.ts` · CLAUD
from __future__ import annotations
import asyncio
import json
import logging
import subprocess
from pathlib import Path
@@ -76,9 +77,12 @@ async def b07_drawing(project_id: UUID, drawing_id: str) -> dict[str, Any]:
from B07_DesignDetail.B07_DesignDetail_Router import get_design_drawing
response = await get_design_drawing(project_id, drawing_id)
if not hasattr(response, "drawing"):
raise ValueError(f"B07 도면 「{drawing_id}」 을 읽지 못했습니다.")
return response.model_dump()
if hasattr(response, "drawing"):
return response.model_dump()
# 계획평면도 파생 저장본(508-3c) = 그린 응답 JSON 그대로(Response) — 오류 JSONResponse 와 구별
if getattr(response, "status_code", None) == 200 and response.media_type == "application/json":
return json.loads(response.body)
raise ValueError(f"B07 도면 「{drawing_id}」 을 읽지 못했습니다.")
async def section_detail(project_id: UUID, route_id: int) -> dict[str, Any] | None:
+109 -2
View File
@@ -5,19 +5,30 @@
`_` 로 시작하는 파일 · 그림은 항목 아님.
판(version) = 파일 sha256 앞 16자 · 페이지 제목 = `00_설명서` 의 제목.
그림 = 페이지 폴더의 `이름.webp|png|jpg` · 판 = 고친 시각 · 내려주기는 로그인만 · `no-cache`.
바로 고침(538-3) = 시스템 관리자만 · 판이 같을 때만 그 줄 범위를 갈아 원자 쓰기 · 다르면 409.
그림 바꾸기(538-4) = 시스템 관리자만 · png · jpg · webp · 5 MB · Pillow 확인 · 폭 1800 px 로 줄임 →
WebP 로 그 이름에 덮음 · `_manual.json`(손으로 바꾼 그림 이름 — 자동 캡처가 안 덮음)에 더함.
"""
from __future__ import annotations
import codecs
import hashlib
import io
import json
import re
import threading
from pathlib import Path
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import FileResponse
from PIL import Image, ImageOps
from pydantic import BaseModel, Field
from common_util.common_util_auth import verify_session
from common_util.common_util_atomic import atomic_write_bytes
from common_util.common_util_auth import require_system_admin, verify_session
router = APIRouter(prefix="/api/help", tags=["help"])
@@ -105,3 +116,99 @@ def get_image(page: str, name: str) -> FileResponse:
with path.open("rb") as f: # 올린 그림은 이름과 달리 WebP — 머리 글자로 가름
media = MEDIA.get(f.read(4), "image/jpeg")
return FileResponse(path, media_type=media, headers={"Cache-Control": "no-cache"})
class HelpEdit(BaseModel):
version: str = Field(max_length=64)
start: int = Field(ge=0)
end: int = Field(ge=0)
text: str = Field(max_length=200_000)
# ponytail: 서버 한 벌 안 잠금 — 판 확인 ~ 쓰기 사이 경합만 막음 · 서버 여러 벌이면 파일 잠금
_write_lock = threading.Lock()
def item_path(page: str, key: str, root: Path = HELP_ROOT) -> Path:
folder = page_folder(page, root)
bad = not key or key.startswith((".", "_")) or any(c in key for c in "/\\")
path = folder / f"{key}.md"
if bad or not path.is_file():
raise HTTPException(status_code=404, detail="없는 항목")
return path
def replace_lines(page: str, key: str, edit: HelpEdit, root: Path = HELP_ROOT) -> dict[str, str]:
"""판이 같을 때만 start~end 줄(0부터 · 끝 포함)을 글로 갈아 끼움 — 빈 글 = 그 줄 지움."""
path = item_path(page, key, root)
with _write_lock:
blob = path.read_bytes()
if file_version(blob) != edit.version:
raise HTTPException(status_code=409, detail="다른 관리자가 먼저 고침")
text = blob.decode("utf-8-sig")
newline = "\r\n" if "\r\n" in text else "\n"
lines = text.split(newline)
if edit.start > edit.end or edit.end >= len(lines):
raise HTTPException(status_code=400, detail="줄 범위 틀림")
new = edit.text.replace("\r\n", "\n").split("\n") if edit.text else []
lines[edit.start : edit.end + 1] = new
out = newline.join(lines).encode("utf-8")
atomic_write_bytes(path, codecs.BOM_UTF8 + out if blob.startswith(codecs.BOM_UTF8) else out)
return next(item for item in read_help(page, root) if item["key"] == key)
@router.put("/{page}/{key}", dependencies=[Depends(require_system_admin)])
def put_item(page: str, key: str, edit: HelpEdit) -> dict[str, dict[str, str]]:
return {"item": replace_lines(page, key, edit, HELP_ROOT)}
MAX_IMG_BYTES = 5 * 1024 * 1024
MAX_IMG_WIDTH = 1800
UPLOAD_SUFFIXES = {".png", ".jpg", ".jpeg", ".webp"}
def to_webp(blob: bytes) -> bytes:
if not blob or len(blob) > MAX_IMG_BYTES:
raise HTTPException(status_code=400, detail="그림은 5 MB 이하")
try:
with Image.open(io.BytesIO(blob)) as raw:
if raw.format not in {"PNG", "JPEG", "MPO", "WEBP"}: # MPO = 사진기 JPEG
raise HTTPException(status_code=400, detail="png · jpg · webp 그림만")
img = ImageOps.exif_transpose(raw)
if img.width > MAX_IMG_WIDTH:
img = img.resize((MAX_IMG_WIDTH, round(img.height * MAX_IMG_WIDTH / img.width)))
if img.mode not in ("RGB", "RGBA"):
img = img.convert(
"RGBA" if "transparency" in img.info or "A" in img.mode else "RGB"
)
out = io.BytesIO()
img.save(out, "WEBP", quality=90)
except (OSError, ValueError, Image.DecompressionBombError) as error:
raise HTTPException(status_code=400, detail="그림 파일 아님") from error
return out.getvalue()
def save_image(page: str, name: str, blob: bytes, root: Path = HELP_ROOT) -> str:
path = image_path(page, name, root)
if not path.parent.is_dir():
raise HTTPException(status_code=404, detail="없는 페이지")
webp = to_webp(blob)
manual = path.parent / "_manual.json"
with _write_lock:
atomic_write_bytes(path, webp)
names = set(json.loads(manual.read_text(encoding="utf-8"))) if manual.is_file() else set()
names.add(name)
text = json.dumps(sorted(names), ensure_ascii=False, indent=2) + "\n"
atomic_write_bytes(manual, text.encode("utf-8"))
return image_version(path)
@router.post("/{page}/img", dependencies=[Depends(require_system_admin)])
async def post_image(
page: str, name: str = Form(...), file: UploadFile = File(...)
) -> dict[str, str]:
if Path(file.filename or "").suffix.lower() not in UPLOAD_SUFFIXES:
raise HTTPException(status_code=400, detail="png · jpg · webp 그림만")
blob = await file.read(MAX_IMG_BYTES + 1)
version = await run_in_threadpool(save_image, page, name, blob, HELP_ROOT)
return {"name": name, "version": version}
@@ -219,11 +219,12 @@ class _FakePool:
async def __aexit__(self, *_):
return False
async def execute(self, *_):
return None
async def execute(self, sql, *_):
self.sql = sql
async def fetchone(self):
return pool.row
# 예정노선 지정(plan_route_assignees) 없음 — 회사 대조만 시험.
return None if "plan_route_assignees" in self.sql else pool.row
class _Connection:
async def __aenter__(self):
+142 -1
View File
@@ -7,6 +7,7 @@
from __future__ import annotations
import io
import json
import os
import shutil
@@ -88,7 +89,7 @@ def test_html_은_먼저_막음(html) -> None:
def root(monkeypatch):
path = Path(tempfile.mkdtemp())
(path / "B05").mkdir()
(path / "B05" / "00_설명서.md").write_text("# 종단\n\n첫 문단\n", encoding="utf-8")
(path / "B05" / "00_설명서.md").write_bytes("# 종단\n\n첫 문단\n".encode())
monkeypatch.setattr(help_router, "HELP_ROOT", path)
yield path
shutil.rmtree(path, ignore_errors=True)
@@ -130,3 +131,143 @@ def test_그림_내려주기_no_cache(root) -> None:
def test_그림_404(root, url) -> None:
(root / "B05" / "full.webp").write_bytes(b"RIFF0000WEBP")
assert client().get(url).status_code == 404
# ── 538-3 글 바로 고침 ────────────────────────────────────────────────────────
def put(c: TestClient, version: str, start: int, end: int, text: str, key: str = "00_설명서"):
body = {"version": version, "start": start, "end": end, "text": text}
return c.put(f"/api/help/B05/{key}", json=body)
def version_of(root: Path) -> str:
return help_router.file_version((root / "B05" / "00_설명서.md").read_bytes())
def test_그_줄만_갈고_새_판(root) -> None:
old = version_of(root)
res = put(client(), old, 2, 2, "고친 문단\n둘째 줄")
assert res.status_code == 200
item = res.json()["item"]
text = (root / "B05" / "00_설명서.md").read_bytes().decode()
assert text == "# 종단\n\n고친 문단\n둘째 줄\n" == item["text"]
assert item["version"] == version_of(root) != old
assert put(client(), item["version"], 2, 3, "").status_code == 200 # 빈 글 = 그 줄 지움
assert (root / "B05" / "00_설명서.md").read_bytes().decode() == "# 종단\n\n"
def test_crlf_bom_그대로(root) -> None:
path = root / "B05" / "00_설명서.md"
path.write_bytes(b"\xef\xbb\xbf# A\r\n\r\nB\r\n")
assert put(client(), version_of(root), 2, 2, "C").status_code == 200
assert path.read_bytes() == b"\xef\xbb\xbf# A\r\n\r\nC\r\n"
def test_판_다르면_409_파일_그대로(root) -> None:
before = (root / "B05" / "00_설명서.md").read_bytes()
assert put(client(), "0" * 16, 2, 2, "남의 글").status_code == 409
assert (root / "B05" / "00_설명서.md").read_bytes() == before
def test_시스템_관리자만_403(root) -> None:
before = (root / "B05" / "00_설명서.md").read_bytes()
for role in ["USER", "COMPANY_ADMIN"]:
assert put(client(role), version_of(root), 2, 2, "x").status_code == 403
assert (root / "B05" / "00_설명서.md").read_bytes() == before
@pytest.mark.parametrize(
("start", "end", "key", "code"),
[
(3, 2, "00_설명서", 400),
(0, 9, "00_설명서", 400),
(0, 0, "없음", 404),
(0, 0, "_manual", 404),
(0, 0, "..%2F..%2FCLAUDE", 404),
],
)
def test_나쁜_범위_항목(root, start, end, key, code) -> None:
assert put(client(), version_of(root), start, end, "x", key).status_code == code
def test_원자_쓰기_실패해도_원본_그대로(root, monkeypatch) -> None:
before = (root / "B05" / "00_설명서.md").read_bytes()
def boom(*_args):
raise OSError("쓰기 실패")
monkeypatch.setattr("common_util.common_util_atomic.os.replace", boom)
with pytest.raises(OSError):
put(client(), version_of(root), 2, 2, "반쯤")
assert (root / "B05" / "00_설명서.md").read_bytes() == before
assert sorted(p.name for p in (root / "B05").iterdir()) == [
"00_설명서.md"
] # 임시 파일 남지 않음
# ── 538-4 그림 바꾸기 ─────────────────────────────────────────────────────────
def png(width: int = 40, height: int = 20) -> bytes:
from PIL import Image
out = io.BytesIO()
Image.new("RGB", (width, height), (200, 30, 30)).save(out, "PNG")
return out.getvalue()
def upload(c: TestClient, name: str, blob: bytes, filename: str = "고른.png", page: str = "B05"):
return c.post(
f"/api/help/{page}/img",
data={"name": name},
files={"file": (filename, blob, "application/octet-stream")},
)
def test_그림_webp_로_덮고_manual_에_적음(root) -> None:
from PIL import Image
(root / "B05" / "full.webp").write_bytes(b"old")
res = upload(client(), "full.webp", png(3000, 1500))
assert res.status_code == 200
body = res.json()
with Image.open(root / "B05" / "full.webp") as img:
assert (img.format, img.size) == ("WEBP", (1800, 900)) # 폭 1800 넘으면 줄임
assert body == {
"name": "full.webp",
"version": help_router.image_version(root / "B05" / "full.webp"),
}
assert upload(client(), "new_one.webp", png(), "a.jpg").status_code == 200 # 새 이름도 받음
manual = json.loads((root / "B05" / "_manual.json").read_text(encoding="utf-8"))
assert manual == ["full.webp", "new_one.webp"]
assert [i["key"] for i in client().get("/api/help").json()["pages"][0]["items"]] == [
"00_설명서"
]
@pytest.mark.parametrize(
("name", "blob", "filename", "code"),
[
("full.webp", png(), "고른.gif", 400), # 확장자
("full.webp", b"not an image at all", "고른.png", 400), # 그림 아님
("full.webp", b"", "고른.png", 400),
("../full.webp", png(), "고른.png", 404), # 경로 탈출
("full.gif", png(), "고른.png", 404),
("한글.webp", png(), "고른.png", 404),
],
)
def test_그림_거절(root, name, blob, filename, code) -> None:
assert upload(client(), name, blob, filename).status_code == code
assert sorted(p.name for p in (root / "B05").iterdir()) == ["00_설명서.md"]
def test_그림_5MB_넘으면_거절(root, monkeypatch) -> None:
monkeypatch.setattr(help_router, "MAX_IMG_BYTES", 1000)
assert upload(client(), "full.webp", png(400, 400) + b"\0" * 2000).status_code == 400
def test_그림_시스템_관리자만_403(root) -> None:
assert upload(client("USER"), "full.webp", png()).status_code == 403
assert upload(client(), "full.webp", png(), page="..%2FB05").status_code == 404
assert not (root / "B05" / "full.webp").exists()
+10 -1
View File
@@ -28,7 +28,14 @@ def _folder(scenes, manual=None) -> Path:
def test_good_scene_passes():
sc = _scene(
before=[{"click": ".card"}, {"wait": ".modal"}, {"wait_ms": 300}, {"hover": ".tip"}],
before=[
{"click": ".card"},
{"wait": ".modal"},
{"wait_ms": 300},
{"hover": ".tip"},
{"select": "#kind", "value": "plan"},
{"scroll": ".far"},
],
clip=".modal",
callouts=[{"n": 1, "sel": ".a"}, {"n": 2, "sel": ".b", "pos": "br"}],
after=[{"press": "Escape"}],
@@ -57,6 +64,8 @@ def test_name_rule(name):
{"callouts": [{"n": 1, "sel": ".a", "pos": "mid"}]},
{"before": [{"click": ".a", "wait": ".b"}]},
{"before": [{"fill": ".a"}]},
{"before": [{"select": ".a"}]},
{"before": [{"scroll": ""}]},
{"before": [{"jump": ".a"}]},
{"allow_post": ["projects"]},
{"extra": 1},
@@ -0,0 +1,35 @@
"""B09 인쇄 도면 — B07 계획평면도 파생 저장본(Response JSON 그대로)도 읽음 · 오류 응답은 400 글."""
from __future__ import annotations
import asyncio
import json
from uuid import uuid4
import pytest
from fastapi import Response
from fastapi.responses import JSONResponse
from B07_DesignDetail import B07_DesignDetail_Router as b07
from B09_Estimation.draft import B09_Draft_Print_Drawings_Source as source
DRAWING = {"entities": [{"type": "Line"}], "layers": []}
def _read(monkeypatch, response):
async def fake(project_id, drawing_id):
return response
monkeypatch.setattr(b07, "get_design_drawing", fake)
return asyncio.run(source.b07_drawing(uuid4(), "plan_terrain"))
def test_저장본_Response_JSON_을_읽음(monkeypatch):
body = json.dumps({"id": "plan_terrain", "drawing": DRAWING, "confirmed": False})
got = _read(monkeypatch, Response(content=body.encode(), media_type="application/json"))
assert got["drawing"] == DRAWING and got["confirmed"] is False
def test_오류_응답은_읽지_못함(monkeypatch):
with pytest.raises(ValueError, match="plan_terrain"):
_read(monkeypatch, JSONResponse(status_code=404, content={"message": "없음"}))
+27
View File
@@ -185,3 +185,30 @@
.ui-help__text ol {
padding-left: 1.4em;
}
/* 바로 고침(538-3) — 시스템 관리자만 · 덩어리 둘레 점선 · 원문 글 상자 */
.ui-help__text.is-editable [data-src]:hover {
outline: 1px dashed var(--color-border);
outline-offset: 2px;
}
.ui-help__edit {
display: block;
box-sizing: border-box;
width: 100%;
margin: 0 0 var(--spacing-8);
padding: var(--spacing-8);
border: 1px solid var(--color-accent);
border-radius: var(--radius-sm);
background: var(--color-surface);
color: var(--color-text-body);
font: inherit;
line-height: 1.6;
resize: vertical;
}
.ui-help__mine {
margin: 0 0 var(--spacing-8);
color: var(--color-warning);
font-size: 0.9em;
}
+9 -2
View File
@@ -14,6 +14,7 @@
import "./ui_template_help.css";
import { API_BASE_URL, PLAN_STEP_ROUTES, ROUTES, STAB_STEP_ROUTES } from "@config/config_frontend";
import { createButton, el } from "./ui_template_elements";
import { attachHelpEditing } from "./ui_template_help_edit";
import { openModal } from "./ui_template_modal";
export interface HelpItem {
@@ -240,6 +241,7 @@ async function openHelp(): Promise<void> {
const pages = GROUPS.flatMap((g) => guide.pages.filter((p) => p.page.startsWith(g.prefix)));
const start = firstAnchor(pages);
let flush = async (): Promise<boolean> => true;
let jump = (_anchor: string): void => undefined;
openModal({
title: TEXT.title,
closeLabel: TEXT.close,
@@ -306,11 +308,16 @@ async function openHelp(): Promise<void> {
ev.preventDefault();
goTo(goto.dataset.helpGoto ?? "");
});
if (guide.admin)
flush = attachHelpEditing(article, pages, (page, item) =>
renderHelpItem(page, item, true),
).flush;
body.append(el("div", { className: "ui-help__grid", children: [toc, article] }));
// 모달이 붙은 뒤 이동 — mount 는 붙기 전
requestAnimationFrame(() => goTo(start));
jump = goTo;
},
});
// 모달이 붙은 뒤 이동 — mount 는 붙기 전 · 숨은 탭에서도 되게 rAF 안 씀
jump(start);
}
export function createHelpButton(): HTMLButtonElement {
+174
View File
@@ -0,0 +1,174 @@
/* =============================================================================
* ui_template_help_edit.ts
* [?] 사용자 가이드 바로 고침(538-3) — 시스템 관리자만(서버도 시스템 관리자만 받음).
* 덩어리(제목 · 문단 · 목록 · 표 · 주의 상자 · 그림 줄) 더블클릭 → 그 덩어리 원문 마크다운 textarea 로 바꿔 끼움.
* 바깥 누름 · Ctrl+Enter = 저장(안 바뀌었으면 요청 안 함) · Esc = 취소 · 한글 조합 중 Enter · Esc 무시 ·
* 고침이 열린 채 모달을 닫으면 저장(`flush`).
* 저장 = `PUT /api/help/{page}/{key}` {version, start, end, text} → 그 항목만 다시 그림 ·
* 409 = 「다른 관리자가 먼저 고침」 → 다시 불러옴 + 내 글은 그 절 머리에 보여 둠.
* 그림 바꾸기(538-4) = 그림(또는 빈 그림 자리) 더블클릭 → 파일 고르기 → `POST /api/help/{page}/img`
* (그 그림 이름에 WebP 로 덮음) → 새 판으로 그 항목만 다시 그림. 그림 한 번 누름 = 아무 일 없음.
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { el, showToast } from "./ui_template_elements";
import type { HelpGuide, HelpItem, HelpPage } from "./ui_template_help";
const TEXT = {
conflict: "다른 관리자가 먼저 고침 — 다시 불러옴",
failed: "저장 못 함 — 다시 불러옴",
mine: "내가 쓴 글 — 저장 안 됨 · 복사해 다시 넣음",
imgFailed: "그림 못 바꿈",
};
type Render = (page: HelpPage, item: HelpItem) => string;
export function attachHelpEditing(
article: HTMLElement,
pages: HelpPage[],
render: Render,
): { flush: () => Promise<boolean> } {
let open: (() => Promise<void>) | null = null;
let pending: Promise<void> = Promise.resolve();
article.classList.add("is-editable");
const find = (section: HTMLElement): { page: HelpPage; item: HelpItem } | null => {
const page = pages.find((p) => p.page === section.dataset.page);
const item = page?.items.find((i) => i.key === section.dataset.key);
return page && item ? { page, item } : null;
};
/** 서버의 지금 글로 그 항목 · 그림 판을 바꿔 둠 */
const reload = async (page: HelpPage, item: HelpItem): Promise<void> => {
const response = await fetch(`${API_BASE_URL}/help`, {
credentials: "include",
cache: "no-store",
});
if (!response.ok) return;
const fresh = ((await response.json()) as HelpGuide).pages.find((p) => p.page === page.page);
const freshItem = fresh?.items.find((i) => i.key === item.key);
if (fresh) page.images = fresh.images;
if (freshItem) Object.assign(item, freshItem);
};
const keepMine = (section: HTMLElement, text: string): void => {
const area = el("textarea", { className: "ui-help__edit", attrs: { readonly: "" } });
area.value = text;
section.prepend(el("div", { className: "ui-help__mine", children: [TEXT.mine, area] }));
};
const edit = (section: HTMLElement, block: HTMLElement): void => {
const found = find(section);
if (!found) return;
const { page, item } = found;
const [start, end] = (block.dataset.src ?? "").split("-").map(Number);
const before = item.text
.split(/\r?\n/)
.slice(start, end + 1)
.join("\n");
const area = el("textarea", { className: "ui-help__edit" });
area.value = before;
const fit = (): void => {
area.style.height = "auto";
area.style.height = `${area.scrollHeight + 2}px`;
};
let done = false;
const finish = async (keep: boolean): Promise<void> => {
if (done) return;
done = true;
open = null;
const text = area.value;
if (!keep || text === before) return void area.replaceWith(block);
area.disabled = true;
const response = await fetch(
`${API_BASE_URL}/help/${page.page}/${encodeURIComponent(item.key)}`,
{
method: "PUT",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ version: item.version, start, end, text }),
},
).catch(() => null);
if (response?.ok) {
Object.assign(item, ((await response.json()) as { item: HelpItem }).item);
section.innerHTML = render(page, item);
return;
}
showToast(response?.status === 409 ? TEXT.conflict : TEXT.failed, "warning");
await reload(page, item).catch(() => undefined);
section.innerHTML = render(page, item);
keepMine(section, text);
};
area.addEventListener("input", fit);
area.addEventListener("keydown", (ev) => {
if (ev.isComposing || ev.keyCode === 229) return;
if (ev.key === "Escape") {
ev.preventDefault(); // 모달 Esc 닫기도 막음
ev.stopPropagation();
void finish(false);
} else if (ev.key === "Enter" && (ev.ctrlKey || ev.metaKey)) {
ev.preventDefault();
pending = finish(true);
}
});
area.addEventListener("blur", () => (pending = finish(true)));
open = () => (pending = finish(true));
block.replaceWith(area);
fit();
area.focus();
};
// 그림 고르기 — 하나를 돌려 씀 · 고른 뒤 값 비움(같은 파일 다시 골라도 change)
const picker = el("input", {
attrs: { type: "file", accept: "image/png,image/jpeg,image/webp", hidden: "" },
});
article.append(picker);
let pickFor: { section: HTMLElement; name: string } | null = null;
picker.addEventListener("change", async () => {
const file = picker.files?.[0];
picker.value = "";
const found = pickFor && find(pickFor.section);
if (!file || !pickFor || !found) return;
const { section, name } = pickFor;
const form = new FormData();
form.append("name", name);
form.append("file", file);
const response = await fetch(`${API_BASE_URL}/help/${found.page.page}/img`, {
method: "POST",
credentials: "include",
body: form,
}).catch(() => null);
const result = (await response?.json().catch(() => null)) as {
version?: string;
detail?: string;
} | null;
if (!response?.ok || !result?.version)
return void showToast(result?.detail ?? TEXT.imgFailed, "error");
found.page.images = { ...found.page.images, [name]: result.version };
section.innerHTML = render(found.page, found.item);
});
article.addEventListener("dblclick", (ev) => {
const target = ev.target as HTMLElement;
const section = target.closest<HTMLElement>(".ui-help__item");
if (!section || open || target.closest("textarea, .ui-help__mine")) return;
const figure = target.closest<HTMLElement>("figure[data-img]");
if (figure && !target.closest("figcaption")) {
pickFor = { section, name: figure.dataset.img ?? "" };
return void picker.click();
}
const block = target.closest<HTMLElement>("[data-src]");
if (!block || !section.contains(block)) return;
ev.preventDefault();
window.getSelection()?.removeAllRanges();
edit(section, block);
});
return {
flush: async () => {
if (open) await open();
await pending;
return true;
},
};
}
+5 -1
View File
@@ -55,7 +55,11 @@ export function openModal(opts: ModalOptions): { close: () => void } {
backdrop.addEventListener("pointerdown", (ev) => (outside = ev.target === backdrop));
backdrop.addEventListener("pointerup", (ev) => (outside &&= ev.target === backdrop));
backdrop.addEventListener("click", (ev) => ev.target === backdrop && outside && void ask());
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && void ask());
// 몸이 막은 Esc(글 고침 취소) · 한글 조합 중 Esc 는 안 닫음
backdrop.addEventListener(
"keydown",
(ev) => ev.key === "Escape" && !ev.defaultPrevented && !ev.isComposing && void ask(),
);
opts.mount(body, close, actions);
// 단추가 있을 때만 머리에 — [닫기] 는 머리 직속 그대로(B05 추천의 `.ui-modal__top > button` 갈아 끼우기)
if (actions.childElementCount) closeButton.before(actions);