From 7e8396db1c1478e8ef1db9aa68cc8655f6ea568f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 15:17:49 +0900 Subject: [PATCH 1/7] fix(chat): exclude technical info --- B01_Dashboard/B01_Dashboard_Knowledge_Chat.py | 2 +- resources/tester/test_b01_knowledge_chat.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py b/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py index 26be22a0e..42179624c 100644 --- a/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py +++ b/B01_Dashboard/B01_Dashboard_Knowledge_Chat.py @@ -13,7 +13,7 @@ from common_util.common_util_auth import verify_session router = APIRouter(prefix="/knowledge-chat", tags=["B01_KnowledgeChat"]) KNOWLEDGE = Path(__file__).resolve().parents[1] / "resources" / "knowledge" -EXCLUDED = ("실무문서", "stmate", "다산소프트") +EXCLUDED = ("실무문서", "stmate", "다산소프트", "technical_info") MODEL = "gemini-3.8-flash" diff --git a/resources/tester/test_b01_knowledge_chat.py b/resources/tester/test_b01_knowledge_chat.py index 237e79bb1..bf7667c07 100644 --- a/resources/tester/test_b01_knowledge_chat.py +++ b/resources/tester/test_b01_knowledge_chat.py @@ -49,9 +49,10 @@ def test_chat_searches_all_knowledge_markdown_except_excluded_folders(monkeypatc result = asyncio.run(chat.ask_knowledge_chat(chat.ChatRequest(question="임도 기준"), {})) assert result["answer"] == "설명 [1]" paths = {source["path"] for source in result["sources"]} - assert paths == {"original/법률/현행.md", "technical_info/_검토.md"} + assert paths == {"original/법률/현행.md"} assert "실무문서" not in sent["prompt"] assert "비공개 사례" not in sent["prompt"] assert "STmate" not in sent["prompt"] assert "다산소프트" not in sent["prompt"] + assert "기술정보 안내" not in sent["prompt"] chat._sections.cache_clear() From 6d3a1be428dfc5e6a503be4e9d2cb53ab9606931 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 15:27:18 +0900 Subject: [PATCH 2/7] feat(chat): add conversation UI and icon --- .../B01_Dashboard_UI_Knowledge_Chat.ts | 40 +++++++-- B01_Dashboard/B01_Dashboard_UI_Style.css | 86 +++++++++++++++++-- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/B01_Dashboard/B01_Dashboard_UI_Knowledge_Chat.ts b/B01_Dashboard/B01_Dashboard_UI_Knowledge_Chat.ts index 519ba5bf9..b718490a0 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Knowledge_Chat.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Knowledge_Chat.ts @@ -2,6 +2,7 @@ import { API_BASE_URL } from "@config/config_frontend"; type Source = { path: string; excerpt: string }; type ChatAnswer = { answer: string; sources: Source[]; detail?: string }; +const chatIcon = ``; export function buildKnowledgeChat(): HTMLElement { const wrap = document.createElement("div"); @@ -9,25 +10,44 @@ export function buildKnowledgeChat(): HTMLElement { const toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "b01-knowledge-chat__toggle"; - toggle.textContent = "원문 안내"; + toggle.innerHTML = chatIcon; + toggle.setAttribute("aria-label", "챗봇 열기"); + toggle.title = "챗봇"; toggle.setAttribute("aria-expanded", "false"); const panel = document.createElement("section"); panel.className = "b01-knowledge-chat__panel"; panel.hidden = true; - panel.setAttribute("aria-label", "지식DB 원문 안내 채팅"); + panel.id = "b01-knowledge-chat-panel"; + panel.setAttribute("aria-label", "챗봇"); + toggle.setAttribute("aria-controls", panel.id); const header = document.createElement("header"); - header.textContent = "지식DB 원문 안내"; + const avatar = document.createElement("span"); + avatar.className = "b01-knowledge-chat__avatar"; + avatar.innerHTML = chatIcon; + const title = document.createElement("strong"); + title.textContent = "챗봇"; + const close = document.createElement("button"); + close.type = "button"; + close.className = "b01-knowledge-chat__close"; + close.textContent = "×"; + close.setAttribute("aria-label", "챗봇 닫기"); + header.append(avatar, title, close); const messages = document.createElement("div"); messages.className = "b01-knowledge-chat__messages"; messages.setAttribute("role", "log"); messages.setAttribute("aria-live", "polite"); + const welcome = document.createElement("p"); + welcome.className = "b01-knowledge-chat__reply"; + welcome.textContent = + "안녕하세요! 법령이나 지침에 대해 질문해 주세요. 근거 문서도 함께 보여드릴게요."; + messages.append(welcome); const form = document.createElement("form"); form.className = "b01-knowledge-chat__form"; const input = document.createElement("input"); input.type = "text"; input.required = true; input.maxLength = 500; - input.placeholder = "법령·지침을 질문하세요"; + input.placeholder = "메시지를 입력하세요"; input.setAttribute("aria-label", "질문"); const submit = document.createElement("button"); submit.type = "submit"; @@ -36,11 +56,15 @@ export function buildKnowledgeChat(): HTMLElement { panel.append(header, messages, form); wrap.append(toggle, panel); - toggle.addEventListener("click", () => { - panel.hidden = !panel.hidden; + const setOpen = (open: boolean): void => { + panel.hidden = !open; toggle.setAttribute("aria-expanded", String(!panel.hidden)); - if (!panel.hidden) input.focus(); - }); + toggle.setAttribute("aria-label", open ? "챗봇 닫기" : "챗봇 열기"); + if (open) input.focus(); + else toggle.focus(); + }; + toggle.addEventListener("click", () => setOpen(Boolean(panel.hidden))); + close.addEventListener("click", () => setOpen(false)); form.addEventListener("submit", async (event) => { event.preventDefault(); diff --git a/B01_Dashboard/B01_Dashboard_UI_Style.css b/B01_Dashboard/B01_Dashboard_UI_Style.css index 93d70fa3c..16efe22a4 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Style.css +++ b/B01_Dashboard/B01_Dashboard_UI_Style.css @@ -535,16 +535,30 @@ .b01-knowledge-chat__toggle, .b01-knowledge-chat__form button { - padding: 10px 16px; border: 0; - border-radius: 8px; background: var(--color-royal-amethyst, #6843a7); color: #fff; font: inherit; cursor: pointer; } +.b01-knowledge-chat__toggle { + display: grid; + width: 58px; + height: 58px; + place-items: center; + border-radius: 50%; + box-shadow: 0 4px 16px #0004; +} + +.b01-knowledge-chat__toggle svg { + width: 30px; + height: 30px; +} + .b01-knowledge-chat__panel { + display: flex; + flex-direction: column; width: min(380px, calc(100vw - 32px)); height: min(540px, calc(100vh - 104px)); border: 1px solid var(--color-border); @@ -560,34 +574,76 @@ } .b01-knowledge-chat__panel header { - padding: 12px 16px; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; border-bottom: 1px solid var(--color-border); - font-weight: bold; +} + +.b01-knowledge-chat__avatar { + display: grid; + width: 34px; + height: 34px; + place-items: center; + border-radius: 50%; + background: var(--color-royal-amethyst, #6843a7); + color: #fff; +} + +.b01-knowledge-chat__avatar svg { + width: 21px; + height: 21px; +} + +.b01-knowledge-chat__close { + margin-left: auto; + border: 0; + background: transparent; + color: inherit; + font: inherit; + font-size: 26px; + cursor: pointer; } .b01-knowledge-chat__messages { - height: calc(100% - 106px); - padding: 12px; + display: flex; + flex: 1; + flex-direction: column; + gap: 12px; + min-height: 0; + padding: 16px 12px; overflow-y: auto; } .b01-knowledge-chat__user, .b01-knowledge-chat__reply { - margin: 0 0 12px; - padding: 10px; - border-radius: 8px; + max-width: 88%; + margin: 0; + padding: 10px 12px; + border-radius: 14px; + overflow-wrap: anywhere; white-space: pre-wrap; } .b01-knowledge-chat__user { + align-self: flex-end; + border-bottom-right-radius: 4px; background: var(--color-royal-amethyst, #6843a7); color: #fff; } .b01-knowledge-chat__reply { + align-self: flex-start; + border-bottom-left-radius: 4px; background: var(--color-background, #f4f4f4); } +.b01-knowledge-chat__reply details { + margin-top: 8px; + font-size: 0.9em; +} + .b01-knowledge-chat__reply summary { cursor: pointer; overflow-wrap: anywhere; @@ -602,6 +658,7 @@ .b01-knowledge-chat__form { display: flex; + align-items: center; gap: 8px; padding: 8px; border-top: 1px solid var(--color-border); @@ -615,3 +672,14 @@ border-radius: 6px; font: inherit; } + +.b01-knowledge-chat__form button { + padding: 9px 14px; + border-radius: 8px; +} + +.b01-knowledge-chat button:focus-visible, +.b01-knowledge-chat input:focus-visible { + outline: 2px solid var(--color-royal-amethyst, #6843a7); + outline-offset: 2px; +} From 22fc9b336fe34543c906f43a19af8f4e7e2943fa Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 15:35:41 +0900 Subject: [PATCH 3/7] =?UTF-8?q?chore(plan):=2010-6=20=ED=9A=A1=EB=8B=A8=20?= =?UTF-8?q?=ED=98=95=EC=83=81=20=EC=A0=9C=EC=95=BD=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=9B=90=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index c027a5d46..41aeb8a33 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -304,3 +304,6 @@ B 방향이 맞겠네. 새 프로젝트를 만드는건 문제없어보여. 이 (10-6 사용자) 다음으로 종단 패널의 종단도에서 알약선택시 나오는 선택정보 창에서 내용은 옵션 제목은 필요없고 구분기호 하나 넣고 옵션값만 구조물 이름 행과 2행은 옵션값만으로 표현해도 됨 + +(10-6 사용자) +횡단도에서 형상의 제약조건이 어떤게 있는지 확인 좀 해줘. 배관과 흙막이, 옹벽관련 내용들만 일단 찾아줘. From 8823499e696251d1101cb48a8b3f3d9acc63283a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 15:40:55 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix(section):=20=EC=9B=90=EC=A7=80=EB=B0=98?= =?UTF-8?q?=20=EC=9E=A0=EA=B8=88=20=ED=92=80=EA=B8=B0=20=E2=80=94=20?= =?UTF-8?q?=ED=9D=99=EB=A7=89=EC=9D=B4=20=EC=9E=90=EB=A6=AC=20=C2=B7=20?= =?UTF-8?q?=EB=8B=A4=EB=8B=A8=20=C2=B7=20=EB=86=92=EC=9D=B4=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EA=B7=B8=EB=8C=80=EB=A1=9C(54-45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관 유출 흙막이 · 독립 기슭막이 매몰 자리 되돌림 삭제(placePipeWall requireCrossing) - 유입 흙막이 유출 수용 가드 삭제(outletReceivable · placeInletWall) - 사면안정 흙막이 · 옹벽 매몰 자리 되돌림 삭제 - 추가 흙막이 매몰 자리 되돌림 · 다단 끊기(시작점 매몰 · 성토 0.7m 미만 · 앉을 지형 없음) 삭제 - 남김: 벽 윗면 노면 위 금지 · 관 m 단위 길이 - 시험 test_b06_ground_unlock 추가(화면 계산 · 서버 번들 같은 판) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- .../B06_Section_UI_Cross_Culvert_Extra.ts | 55 +------- .../B06_Section_UI_Cross_Culvert_Geom.ts | 33 +---- .../B06_Section_UI_Cross_Culvert_Solve.ts | 118 +----------------- B06_Section/B06_Section_UI_Cross_Revetment.ts | 33 +---- resources/tester/test_b06_ground_unlock.py | 101 +++++++++++++++ 5 files changed, 116 insertions(+), 224 deletions(-) create mode 100644 resources/tester/test_b06_ground_unlock.py diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts index f3a90ef8b..5f29d0e79 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Extra.ts @@ -238,10 +238,8 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult let prevBottom = input.startBottomElevation; for (let i = 0; i < input.adjusts.length; i += 1) { - // 시작점이 원지반 아래 = 윗단 벽이 0.5m 이상 묻힘 → 성토 불필요, 다단 종료. - if (groundAt(src.offset) >= src.elevation - 0.01) break; + // 원지반은 단을 끊지 않는다 — 요청한 단 수 그대로 세운다(54-45). const trailing = trailingFill(src, outward, groundAt); - if (trailing.lengthM < 0.7) break; const adjust = input.adjusts[i]; // 형태(B05 폼 6종)가 재질·높이 한계를 정한다 — 조작이 없으면 소유 벽 형태를 따른다 @@ -293,8 +291,8 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult bestOffset = x; } } - if (autoOffset === null) autoOffset = bestOffset; - if (autoOffset === null) break; // 세울 만한 지형이 아니다 — 되는 만큼만. + // 앉을 지형이 없으면(묻힘) 이음선이 시작점 바로 밖 — 원지반과 무관하게 선다. + autoOffset ??= bestOffset ?? src.offset + outward * jointRunOf(exposed, lean); const autoJointRun = (autoOffset - src.offset) * outward - jointRunOf(exposed, lean); /** 자동 자리(선반 0·수직 0)의 벽 상단 표고 — 조작은 여기서부터 잰다. */ const autoTop = src.elevation - autoJointRun / FILL_SLOPE_RATIO_MIN; @@ -314,52 +312,11 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult FILL_SLOPE_RATIO_MIN : (plan[i] ?? 0); const requestedX = plan === "user" ? adjust.x : 0; - /** 후보 자리의 바닥 — buildExtraWall과 같은 근입 0.5m 고정(2026-08-23). */ - const bottomAt = (_anchorX: number, baseElevation: number): number => - baseElevation - REVET_EMBED_DEPTH_M; - // 이동 한계 = 원지반 매몰 자리 금지(2026-08-22 사용자 확정): ① 이음선 상단이 - // 지반 아래(벽이 통째로 묻힘) ② 자기 성토선 시작점(실바닥+0.5 전면 교차점)이 - // 지반 아래 0.05m 이상(= 0.5m 이상 매몰 → 성토 불필요 자리). - const placeable = (xShift: number, vShift: number): boolean => { - const topE = autoTop - vShift; - const aX = autoOffset + outward * xShift; - const jointX = aX - outward * jointRunOf(exposed, lean); - if ( - Math.max(groundAt(jointX), groundAt(jointX + outward * REVET_THICKNESS_M)) >= - topE - 0.01 - ) - return false; - const baseE = topE - exposed; - const bottom = bottomAt(aX, baseE); - const startE = bottom + REVET_EMBED_DEPTH_M; - const topFrontX = jointX + outward * REVET_THICKNESS_M; - const startX = topFrontX + outward * lean * (topE - startE); - if (groundAt(startX) < startE + 0.05) return true; - // 매몰 — 3도 절토선(계류 쪽 내림)이 원지반과 다시 만나면 허용, 아니면 이동 금지. - return ( - slopedCrossing( - { offset: startX, elevation: startE }, - outward, - -PIPE_CONNECT_GRADE, - groundAt, - limitOffset, - ) !== null - ); - }; let appliedD = Math.max(requestedD, minV); /** 선반 길이가 음수가 되면 각도가 깨진다 — x가 최소 1.2·d까지 따라 나간다. */ const shelfFloor = (v: number): number => FILL_SLOPE_RATIO_MIN * v; + // 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45). let appliedX = Math.max(requestedX, 0, shelfFloor(appliedD)); - for (let pass = 0; pass < 400 && !placeable(appliedX, appliedD); pass += 1) { - // 매몰 자리 — 요청을 자동 자리 쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우). - if (appliedD > minV + 1e-9) { - appliedD = Math.max(minV, appliedD - 0.05); - appliedX = Math.max(appliedX, shelfFloor(appliedD)); - } else if (appliedX > shelfFloor(appliedD) + 1e-9) { - appliedX = Math.max(shelfFloor(appliedD), appliedX - 0.05); - } else break; - } - if (!placeable(appliedX, appliedD)) break; // 자동 자리조차 매몰 — 이 단은 불가. appliedD = Math.round(appliedD * 10) / 10; appliedX = Math.round(appliedX * 10) / 10; const topElevation = autoTop - appliedD; @@ -378,10 +335,6 @@ export function buildOutletExtras(input: OutletExtrasInput): OutletExtrasResult floatGapM: Math.max(0, base - groundBase), lean, }); - // 요청한 좌우 이동을 지형이 막았으면 그 양을 남긴다 — 「눌러도 안 움직인다」의 까닭을 - // 툴팁으로 알리기 위함이다(2026-09-06 실측: 다섯 측점 중 넷이 1.0m 요청에 0.0m 이동). - const blocked = Math.max(0, (requestedX ?? 0) - appliedX); - if (blocked > 0.005) wall.shiftBlockedM = Math.round(blocked * 100) / 100; // 하한에 눌려 요청이 통째로 무시된 경우 — 지형이 아니라 **선반 길이**가 원인이다 // (2026-09-07 실측: d 2.6m 이면 x 하한이 3.12m 라 1.0m 요청이 아무 변화도 못 냄). // 사용자가 실제로 넣었을 때만 알린다 — 요청이 없으면(0) 하한은 그냥 기본 자리라 diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts index dba881258..f97453203 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Geom.ts @@ -62,7 +62,6 @@ import { designInterpolator, slopeToeOffset, pipeAxisSolver, - placeInletWall, placePipeWall, slopeLengthAlong, } from "./B06_Section_UI_Cross_Culvert_Solve"; @@ -200,9 +199,8 @@ export function computeCulvertLayout( if (!basinReason && designAt) { const inletBaseAt = (offset: number): number => Math.min(groundAt(offset), invertCap(inletInfo.edge)); - // 4축 배치는 공용 풀이(placeInletWall — Solve). 유입을 내리면 유출도 따라 - // 내려가므로, 유출이 못 받으면 유입도 못 내려간다(outletGuard). - const placed = placeInletWall({ + // 4축 배치는 공용 풀이(placePipeWall — Solve). 원지반은 자리를 막지 않는다(54-45). + const placed = placePipeWall({ autoOffset: slopeZeroAnchor( inletInfo.edge, inletInfo.outward, @@ -213,30 +211,10 @@ export function computeCulvertLayout( height: inletWallSpec.height, baseElevation0: inletInfo.edge.elevation_m - inletWallSpec.height, edgeElevation: inletInfo.edge.elevation_m, - // 독립 기슭막이(관 숨김)는 관 토피·역경사 제약이 없다 — 유입도 노견까지 자유롭게 - // 오르내린다(관이 없어 유출 수용 검사 outletGuard도 뺀다 — 위 이동을 막던 원인). + // 독립 기슭막이(관 숨김)는 관 토피·역경사 제약이 없다 — 유입도 노견까지 자유롭게 오르내린다. invertCap: culvert.hidden_pipe ? Number.POSITIVE_INFINITY : invertCap(inletInfo.edge), adjust: adjInlet, defaultD: PIPE_WALL_DEFAULT_RUN_M, - groundAt, - limitOffset: inletInfo.limit, - // 독립 기슭막이는 유출 벽과 같은 자리 규칙 — 매몰-무교차 자리 금지(2026-08-29). - requireCrossing: culvert.hidden_pipe === true, - outletGuard: culvert.hidden_pipe - ? undefined - : { - edge: outletInfo.edge, - outward: outletInfo.outward, - height: outletWallSpec.height, - toeOffset: slopeToeOffset( - designAt, - groundAt, - outletInfo.edge.offset_m, - outletInfo.limit, - ), - limitOffset: outletInfo.limit, - lean: outletWallSpec.lean, - }, }); appliedAdjust.inlet.x = placed.x; appliedAdjust.inlet.d = placed.d; @@ -412,7 +390,7 @@ export function computeCulvertLayout( ? outletZeroInvert - ((offset - outletZeroAnchor) * outletInfo.outward) / FILL_SLOPE_RATIO_MIN : invertAt(offset); if (designAt) { - // 4축 배치(유입과 동일 풀이) + 유출 전용: 매몰-무교차 자리 금지(2026-08-22 ③). + // 4축 배치(유입과 동일 풀이). const placed = placePipeWall({ autoOffset: outletZeroAnchor, outward: outletInfo.outward, @@ -423,9 +401,6 @@ export function computeCulvertLayout( invertCap: culvert.hidden_pipe ? Number.POSITIVE_INFINITY : inlet.elevation, adjust: adjOutlet, defaultD: PIPE_WALL_DEFAULT_RUN_M, - groundAt, - limitOffset: outletInfo.limit, - requireCrossing: true, }); appliedAdjust.outlet.x = placed.x; appliedAdjust.outlet.d = placed.d; diff --git a/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts b/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts index 964cc7a34..6bc79e34b 100644 --- a/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts +++ b/B06_Section/B06_Section_UI_Cross_Culvert_Solve.ts @@ -8,7 +8,6 @@ import type { SectionSample } from "./B06_Section_Api_Fetch"; import { FILL_MIN_RISE_M, FILL_SLOPE_MAX_LENGTH_M, - PIPE_CONNECT_GRADE, FILL_SLOPE_RATIO_MIN, REVET_HEIGHT_STEP_M, REVET_LEAN_RATIO, @@ -21,7 +20,6 @@ import type { PipeEnd, WallAdjust, } from "./B06_Section_UI_Cross_Culvert_Types"; -import { slopedCrossing } from "./B06_Section_UI_Cross_Culvert_Extra"; /** 기슭막이 하단선 폭(m) — 사다리꼴 밑변. 자리 기준(하단선 중점) 환산에 쓴다. * `lean` = 그 벽의 전면 기울기(표준경사 · `revetWallSpec().lean`). */ @@ -370,8 +368,7 @@ export interface PipeWallPlacement { * 지점**(벽 상단 = 노견)의 자리·invert. d는 그 점 기준 절대값(null = 기본 * defaultD, 경사길이 4m 지점), 위 한계 = 성토선 0 지점(d ≥ 0)·invert 상한. * · x = 노견 연장 평행이동(안쪽 금지). - * · requireCrossing(유출): 매몰인데 0도 절토선이 원지반과 다시 못 만나는 자리는 - * 금지 — 위쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우). + * · 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45). */ export function placePipeWall(input: { /** 성토선 0 지점(벽 상단 = 노견)의 벽 자리(하단선 중점 offset). */ @@ -386,37 +383,14 @@ export function placePipeWall(input: { adjust: WallAdjust; /** d 미지정(null) 시의 기본 자리 — 성토사면 경사길이 4m 지점의 수평 성분. */ defaultD: number; - groundAt: (offset: number) => number; - limitOffset: number; - requireCrossing: boolean; }): PipeWallPlacement { const top0 = input.baseElevation0 + input.height; const dLow = Math.max( -FILL_SLOPE_RATIO_MIN * (input.edgeElevation - top0), -FILL_SLOPE_RATIO_MIN * (input.invertCap - (top0 - input.height)), ); - let d = Math.max(input.adjust.d ?? input.defaultD, Math.ceil(dLow * 10) / 10); - let x = Math.max(input.adjust.x, 0); - if (input.requireCrossing) { - const placeable = (anchorOffset: number, invertE: number): boolean => - invertE >= input.groundAt(anchorOffset) - 0.01 || - slopedCrossing( - { offset: anchorOffset, elevation: invertE }, - input.outward, - -PIPE_CONNECT_GRADE, - input.groundAt, - input.limitOffset, - ) !== null; - for (let pass = 0; pass < 400; pass += 1) { - const anchorOffset = input.autoOffset + input.outward * (x + d); - if (placeable(anchorOffset, top0 - d / FILL_SLOPE_RATIO_MIN - input.height)) break; - if (Math.abs(d) > 1e-9) d = d > 0 ? Math.max(0, d - 0.05) : Math.min(0, d + 0.05); - else if (x > 1e-9) x = Math.max(0, x - 0.05); - else break; - } - d = Math.round(d * 10) / 10; - x = Math.round(x * 10) / 10; - } + const d = Math.max(input.adjust.d ?? input.defaultD, Math.ceil(dLow * 10) / 10); + const x = Math.max(input.adjust.x, 0); const topElevation = top0 - d / FILL_SLOPE_RATIO_MIN; return { x, @@ -427,92 +401,6 @@ export function placePipeWall(input: { }; } -/** - * 유입 invert를 그 높이로 내렸을 때 **유출 벽이 성립하는가**(2026-08-22 사용자 확정). - * 유입이 유출보다 높아야 하므로 유입을 내리면 유출도 따라 내려간다 — 그런데 유출이 - * 원지반에 묻히고 3도 절토선이 지반과 다시 만나지 못하면 그 자리는 둘 수 없다. - * 이 경우 **유입도 더 내려가면 안 된다**(종전에는 유입만 내려가 유출이 깨졌다). - */ -export function outletReceivable(input: { - inletElevation: number; - edge: { offset_m: number; elevation_m: number }; - outward: number; - height: number; - toeOffset: number; - limitOffset: number; - groundAt: (offset: number) => number; - /** 유출 벽 전면 기울기(표준경사). */ - lean?: number; -}): boolean { - const invertAt = (offset: number): number => - Math.min(input.groundAt(offset), input.inletElevation); - const auto = - minShoulderWallOffset( - input.edge, - input.outward, - input.height, - invertAt, - input.toeOffset, - input.limitOffset, - input.lean, - ) ?? input.toeOffset; - const invert = invertAt(auto); - if (invert >= input.groundAt(auto) - 0.01) return true; - return ( - slopedCrossing( - { offset: auto, elevation: invert }, - input.outward, - -PIPE_CONNECT_GRADE, - input.groundAt, - input.limitOffset, - ) !== null - ); -} - -/** - * 유입 기슭막이 4축 배치 — placePipeWall에 **유출 수용 가능성 가드**를 얹은 것. - * 유입을 내리면 유출도 따라 내려가므로(유입 > 유출 조건), 유출이 못 받는 높이면 - * 유입도 그만큼 못 내려간다(2026-08-22 사용자 확정). - */ -export function placeInletWall(input: { - autoOffset: number; - outward: number; - height: number; - baseElevation0: number; - edgeElevation: number; - invertCap: number; - adjust: WallAdjust; - defaultD: number; - groundAt: (offset: number) => number; - limitOffset: number; - /** 유출 쪽 수용 검사 인자 — 없으면 가드 없이 배치한다. */ - outletGuard?: { - edge: { offset_m: number; elevation_m: number }; - outward: number; - height: number; - toeOffset: number; - limitOffset: number; - lean?: number; - }; - /** 매몰-무교차 자리 금지(유출 벽 규칙) — 독립 기슭막이는 유입도 같은 규칙을 탄다. */ - requireCrossing?: boolean; -}): PipeWallPlacement { - const place = (adjust: WallAdjust): PipeWallPlacement => - placePipeWall({ ...input, adjust, requireCrossing: input.requireCrossing === true }); - let placed = place(input.adjust); - const guard = input.outletGuard; - if (!guard) return placed; - const receivable = (invert: number): boolean => - outletReceivable({ inletElevation: invert, groundAt: input.groundAt, ...guard }); - for (let pass = 0; pass < 400 && placed.d > 1e-9 && !receivable(placed.invert); pass += 1) { - placed = place({ - ...input.adjust, - d: Math.max(0, Math.round((placed.d - 0.05) * 100) / 100), - }); - } - return placed; -} - /** 관 축·법선·양 끝단 꼭짓점을 한 번에 푸는 도구(본체 700줄 제한 — 2026-08-22). */ export interface PipeAxisSolver { axis: OffsetPoint; diff --git a/B06_Section/B06_Section_UI_Cross_Revetment.ts b/B06_Section/B06_Section_UI_Cross_Revetment.ts index 0ff31a13e..f64c64493 100644 --- a/B06_Section/B06_Section_UI_Cross_Revetment.ts +++ b/B06_Section/B06_Section_UI_Cross_Revetment.ts @@ -20,7 +20,6 @@ import { showToast } from "@ui/ui_template_elements"; import type { CrossSection } from "./B06_Section_Api_Fetch"; import { FILL_SLOPE_RATIO_MIN, - PIPE_CONNECT_GRADE, PIPE_WALL_DEFAULT_RUN_M, REVET_EMBED_DEPTH_M, REVET_THICKNESS_M, @@ -28,7 +27,7 @@ import { import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean"; import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const"; import { fillWallBaseWidth, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve"; -import { buildExtrasAt, slopedCrossing } from "./B06_Section_UI_Cross_Culvert_Extra"; +import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra"; import { ZERO_ADJUST, type CulvertDesignTrim, @@ -206,36 +205,12 @@ export function computeRevetmentLayout( * (확장량 t는 아래 폴리라인에서 역산 — t ≥ 0이 각도 유지의 한계다). */ const defaultDrop = autoRun / FILL_SLOPE_RATIO_MIN; - /** 그 자리에 벽을 세울 수 있나 — 매몰(원지반 아래 daylight 없음) 금지. */ - const placeable = (xShift: number, drop: number): boolean => { - const anchor = autoOffset + outward * (autoRun + xShift); - const invert = edge.elevation_m - drop - height; - return ( - invert >= groundAt(anchor) - 0.01 || - slopedCrossing( - { offset: anchor, elevation: invert }, - outward, - -PIPE_CONNECT_GRADE, - groundAt, - limit, - ) !== null - ); - }; const requestedX = Math.max(adjust?.x ?? 0, 0); /** 각도(1:1.2)를 지키려면 그만큼은 밖으로 나가 있어야 하는 최소 수평 자리. */ const xFloor = (drop: number): number => Math.max(0, FILL_SLOPE_RATIO_MIN * drop - autoRun); - let appliedDrop = Math.max(adjust?.d ?? defaultDrop, 0); - let appliedX = Math.max(requestedX, xFloor(appliedDrop)); - for (let pass = 0; pass < 400 && !placeable(appliedX, appliedDrop); pass += 1) { - // 매몰 자리 — 요청을 자동 자리 쪽으로 0.05m씩 되돌린다(상하 먼저, 다음 좌우). - if (appliedDrop > 1e-9) { - appliedDrop = Math.max(0, appliedDrop - 0.05); - appliedX = Math.max(Math.min(appliedX, requestedX), xFloor(appliedDrop)); - } else if (appliedX > 1e-9) appliedX = Math.max(0, appliedX - 0.05); - else break; - } - appliedX = Math.round(appliedX * 10) / 10; - appliedDrop = Math.round(appliedDrop * 10) / 10; + // 원지반은 자리를 막지 않는다 — 묻히든 뜨든 입력 그대로(54-45). 노면 위 금지 = d ≥ 0. + const appliedDrop = Math.round(Math.max(adjust?.d ?? defaultDrop, 0) * 10) / 10; + const appliedX = Math.round(Math.max(requestedX, xFloor(appliedDrop)) * 10) / 10; const anchorOffset = autoOffset + outward * (autoRun + appliedX); const baseElevation = edge.elevation_m - appliedDrop - height; diff --git a/resources/tester/test_b06_ground_unlock.py b/resources/tester/test_b06_ground_unlock.py new file mode 100644 index 000000000..7cd714e7f --- /dev/null +++ b/resources/tester/test_b06_ground_unlock.py @@ -0,0 +1,101 @@ +"""원지반 잠금 풀기(54-45) — 흙막이 자리 · 다단 · 높이를 원지반이 막지 않음. + +남는 규칙: 벽 윗면 노면 위 금지(d ≥ 0) · 관 보호공 흙막이 m 단위 자리(관 길이 정수). +서버 면적은 같은 TS 를 Node 로 돌림(`B06_Section_Server_Calc_Node.ts`) — 번들도 같은 판인지 봄. +""" + +import json +import re +import subprocess +from pathlib import Path + +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +B06 = PROJECT_ROOT / "B06_Section" +TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" +BUNDLE = PROJECT_ROOT / "config" / "server_calc_node" / "B06_Section_Server_Calc_Node.js" + +_RUNNER = """ +import { writeFileSync } from "node:fs"; +import { placePipeWall } from "./B06_Section_UI_Cross_Culvert_Solve.js"; +import { buildOutletExtras } from "./B06_Section_UI_Cross_Culvert_Extra.js"; +const pipe = (d, x) => placePipeWall({ + autoOffset: 10, outward: 1, height: 2, baseElevation0: 98, edgeElevation: 100, + invertCap: Number.POSITIVE_INFINITY, adjust: { x, d, h: null, m: null }, defaultD: 4.8, +}); +const extras = (ground, d) => buildOutletExtras({ + start: { offset: 10, elevation: 95 }, startBottomElevation: 94.5, outward: 1, + groundAt: () => ground, limitOffset: 40, + adjusts: [{ x: 0, d, h: 2, m: null }, { x: 0, d, h: 2, m: null }], +}); +const buried = extras(100, 1); +const high = extras(80, 3); +writeFileSync(process.argv[2], JSON.stringify({ + deep: pipe(6, 2), up: pipe(-5, 0), + buriedCount: buried.walls.length, buriedD: buried.appliedAdjusts.map((a) => a.d), + highCount: high.walls.length, highD: high.appliedAdjusts.map((a) => a.d), +})); +""" + + +@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치") +def test_원지반이_자리_다단을_막지_않고_노면_위만_막음(tmp_path: Path) -> None: + out = tmp_path / "js" + for name in ("Culvert_Solve", "Culvert_Extra"): + subprocess.run( # noqa: S603 — 고정 실행 파일 + [ + "node", + str(TSC), + str(B06 / f"B06_Section_UI_Cross_{name}.ts"), + "--outDir", + str(out), + "--module", + "esnext", + "--target", + "es2022", + "--moduleResolution", + "bundler", + "--skipLibCheck", + "--ignoreConfig", + ], + cwd=str(PROJECT_ROOT), + capture_output=True, + ) + for emitted in out.rglob("*.js"): + emitted.write_text( + re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")), + encoding="utf-8", + ) + base = next(path.parent for path in out.rglob("B06_Section_UI_Cross_Culvert_Solve.js")) + (base / "runner.mjs").write_text(_RUNNER, encoding="utf-8") + result = tmp_path / "out.json" + subprocess.run( # noqa: S603 + ["node", str(base / "runner.mjs"), str(result)], check=True, capture_output=True + ) + got = json.loads(result.read_text(encoding="utf-8")) + + assert (got["deep"]["d"], got["deep"]["x"]) == (6, 2), "관 흙막이 깊이 · 좌우가 되돌아감" + assert got["up"]["d"] == pytest.approx(0), "관 흙막이 윗면이 노면 위로 올라감" + assert got["up"]["topElevation"] == pytest.approx(100), "노면 멈춤 자리 틀림" + assert got["buriedCount"] == 2, "원지반에 묻힌 다단이 끊김" + assert got["buriedD"] == [1, 1], "묻힌 다단의 높이가 되돌아감" + assert got["highCount"] == 2, "원지반 위 다단이 끊김" + assert got["highD"] == [3, 3], "뜬 다단의 높이가 되돌아감" + + +def test_화면_서버_같은_판() -> None: + for name in ("Culvert_Solve", "Culvert_Geom", "Revetment"): + source = (B06 / f"B06_Section_UI_Cross_{name}.ts").read_text(encoding="utf-8") + assert "requireCrossing" not in source, f"{name} 에 매몰 자리 금지가 남음" + assert "outletReceivable" not in source, f"{name} 에 유출 수용 가드가 남음" + assert "const placeable" not in (B06 / "B06_Section_UI_Cross_Revetment.ts").read_text( + encoding="utf-8" + ), "사면안정 흙막이 · 옹벽 매몰 자리 금지가 남음" + assert "const placeable" not in (B06 / "B06_Section_UI_Cross_Culvert_Extra.ts").read_text( + encoding="utf-8" + ), "추가 흙막이 매몰 자리 금지가 남음" + bundle = BUNDLE.read_text(encoding="utf-8") + assert "outletReceivable" not in bundle and "requireCrossing" not in bundle, ( + "서버 번들이 옛 판 — npm run build:server-calc" + ) From 991f1478f9606268fa566571f90647d2d256c22f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 15:42:15 +0900 Subject: [PATCH 5/7] =?UTF-8?q?chore(plan):=2054-45=20=ED=9A=A1=EB=8B=A8?= =?UTF-8?q?=20=ED=98=95=EC=83=81=20=EC=A0=9C=EC=95=BD=ED=91=9C=20=EB=B3=B4?= =?UTF-8?q?=EA=B4=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/조사/2026-10-06_54-45_횡단_제약표.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/raw/조사/2026-10-06_54-45_횡단_제약표.md diff --git a/docs/raw/조사/2026-10-06_54-45_횡단_제약표.md b/docs/raw/조사/2026-10-06_54-45_횡단_제약표.md new file mode 100644 index 000000000..cad588697 --- /dev/null +++ b/docs/raw/조사/2026-10-06_54-45_횡단_제약표.md @@ -0,0 +1,55 @@ +# 54-45 횡단 형상 제약표 — 배관 · 흙막이 · 옹벽 + +범위: 관 · 집수정 · 관 보호공 흙막이(유입·유출) · 추가 흙막이(다단) · 사면안정 흙막이 · 옹벽. 화면 = 서버(같은 TS 를 Node 번들로 돎 — `config/server_calc_node`). 줄 번호는 B06_Section 기준. + +## 풂 + +| # | 무엇 | 어디 | 값 | 처리 | +|---|---|---|---|---| +| 1 | 관 유출 흙막이 — 원지반에 묻히면 위로 0.05m씩 되돌림(매몰-무교차 자리 금지) | 옛 `Culvert_Solve.ts:400` `requireCrossing` · 화면/서버 | 3도 절토선 교차 없으면 금지 | 이번에 풂 | +| 2 | 독립 기슭막이 유입 벽 — 1과 같은 되돌림 | 옛 `Culvert_Geom.ts:224` · 화면/서버 | 같음 | 이번에 풂 | +| 3 | 관 유입 흙막이 ▼ — 유출 벽이 원지반에 못 서면 유입도 못 내려감 | 옛 `Culvert_Solve.ts:436` `outletReceivable` · `placeInletWall` 가드 | 0.05m씩 되돌림 | 이번에 풂 | +| 4 | 사면안정 흙막이 · 옹벽 — 묻히는 자리면 위 · 안쪽으로 되돌림 | 옛 `Cross_Revetment.ts:209` `placeable` | 3도 절토선 교차 없으면 금지 | 이번에 풂 | +| 5 | 추가 흙막이 — 묻히는 자리면 d · x 되돌림 | 옛 `Culvert_Extra.ts:320` `placeable` | 이음선 위 지반 · 0.5m 이상 매몰 금지 | 이번에 풂 | +| 6 | 추가 흙막이 — 시작점이 원지반 아래면 다단 끊음 | 옛 `Culvert_Extra.ts:241` | 단 수 줄어듦 | 이번에 풂(`Culvert_Extra.ts:241`) | +| 7 | 추가 흙막이 — 남은 성토 0.7m 미만이면 다단 끊음 | 옛 `Culvert_Extra.ts:244` | 0.7m | 이번에 풂 | +| 8 | 추가 흙막이 — 앉을 지형 없으면 단 안 세움 | 옛 `Culvert_Extra.ts:296` | 지형 높이 0.05m 미만 | 이번에 풂 — 시작점 바로 밖에 섬(`Culvert_Extra.ts:295`) | +| 9 | 「좌우 이동 지형에 막힘」 값(`shiftBlockedM`) | 옛 `Culvert_Extra.ts:383` | 요청 − 적용 | 이번에 풂 — 막힘이 없어 안 나옴 | +| 10 | 형태별 높이 한도(메 2.0 · 찰 3.0 · 콘크리트 5.0) 자르기 | 54-42 지움 | — | 54-42 풂 | +| 11 | 추가 흙막이 원지반 맞춤 늘이기 | 54-42 지움 | — | 54-42 풂 | + +## 남김 + +| # | 무엇 | 어디 | 값 | 남긴 까닭 | +|---|---|---|---|---| +| 12 | 벽 윗면 노면 위 금지 | `Cross_Wall.ts:70` · `Culvert_Solve.ts:388`(d 하한) · `Cross_Revetment.ts:212`(d ≥ 0) | 노면 표고에서 멈춤 | 브레인 규칙 ① | +| 13 | 관 길이 m 단위 — 유출 흙막이 자리 = 관 끝 | `Culvert_Geom.ts:248` · `:435` | 올림 정수 m | 브레인 규칙 ② | +| 14 | 최소 토피 — 유입 관 끝 ≤ 노면 − 관경 − 토피 | `Culvert_Geom.ts:128` · `Culvert_Const.ts:14` | 0.5m | 관 규칙(노면 기준 · 원지반 아님) | +| 15 | 역경사 금지 — 유출 관 끝 ≤ 유입 관 끝 · 유출 흙막이 ▲ 한도 | `Culvert_Geom.ts:240` · `:401` · `Culvert_Solve.ts:390` | 실측 440.2: ▲ −5 요청 → d 0.5 | 물 흐름(관 규칙) · 유입이 원지반 자동값이면 간접으로 걸림 | +| 16 | 관 시작 관 끝 자동값 = 원지반(토피 상한 안) | `Culvert_Geom.ts:129` · `:186` | 자동 자리만 | 벽을 움직이면 벽 자리를 따라감 — 잠금 아님 | +| 17 | 관 흙막이 최소 높이 | `Culvert_Const.ts:142` | 관경 + 1.0m | 관이 벽 위로 · 바닥으로 안 나오게 | +| 18 | 흙막이 좌우 노견 안쪽 금지 | `Culvert_Solve.ts:393` · `Cross_Revetment.ts:208` | x ≥ 0 | 원지반 무관 · 도로 안으로 못 들어감 | +| 19 | 성토선 1:1.2 각도 유지 — 선반 하한 | `Culvert_Extra.ts:317` · `Cross_Revetment.ts:210` | x ≥ 1.2·d(실측 d 3 → x 3.6) | 원지반 무관 · 성토 물매 | +| 20 | 다단 관통 금지 — 아랫단 윗면 ≤ 윗단 바닥 | `Culvert_Extra.ts:300` | 실측 d 0 요청 → 0.5~0.9 | 벽끼리 규칙 | +| 21 | 다단 수 | `Cross_Structure_Panel.ts:340` | 0~9 | 화면 칸 한도 | +| 22 | 벽 최저 높이 | `Culvert_Extra.ts:253` · `Cross_Revetment.ts:184` | 근입 + 0.1 = 0.6m | 형상이 안 무너지게 | +| 23 | 근입 깊이 고정 | `Culvert_Const.ts:76` | 0.5m | 기하 기준선 | +| 24 | 벽 두께 | `Culvert_Const.ts:17` | 0.45m | 기하 기준 | +| 25 | 전면 기울기 = 표준경사 표(형태 무관) | `Cross_Lean.ts:22` | 단면유형 × 설치 측 × 높이 | 54-42 결정 | +| 26 | 집수정 I형 벽 바닥 원지반까지 묻음 | `Culvert_Basin.ts:244` | 지반 − 부재두께 | 집수정 — 54-45 벽 범위 밖 · 사용자 확인 필요 | +| 27 | 집수정 ㄴ·ㄷ 기본 판정(절토선 없으면 I) | `Culvert_Basin.ts:51` | 기본값만 | 고른 값은 그대로 · 잠금 아님 | +| 28 | 집수정 내공 | `Cross_Structure_Panel.ts:309` | 1~2m | 화면 칸 한도 | +| 29 | 집수정 좌우 노견 안쪽 금지 | `Cross_View_Structure.ts:134` | ≥ 0 | 원지반 무관 | +| 30 | 사면안정 흙막이 · 옹벽 설치 측 빈 칸 = 성토 쪽 · 양측 절토면 안 그림 | `Cross_Revetment.ts:175` | — | 단면유형 규칙(원지반 아님) | +| 31 | 성토사면 5m 초과 경고 | `Culvert_Const.ts:180` · `Cross_Culvert.ts:114` | 5m | 경고만 · 잠금 아님 | +| 32 | 바닥 원지반 이격 경고 | `Cross_Culvert.ts:186` · `Cross_Revetment.ts:372` | 이격 m | 경고만 · 잠금 아님 | +| 33 | 유입 흙막이 선택지 판정(`revetAllowed`) | `Culvert_Basin.ts:417` | 원지반 > 토피 상한 | 계산만 · 쓰는 곳 없음 — 잠금 아님 | + +## 확인 수치(검증_sub7 · 화면 eval = 서버 번들) + +- 440.2 · 85.1 · 720.4 관 — 유출 d 8 · x 1 그대로(깊게) · x 9 그대로(멀리 · 이격 4.3~7.2m) · 다단 2단 d 3 그대로 +- 옹벽(성토 측점에 붙임) — d 6 그대로 · ▲ −3 → 0(노면 멈춤) · 3단 중 3단 +- 440.2 유출 흙막이 ▼ 8번 누름 → 캐시 d 10 남음(되돌림 없음) +- 서버 면적 = 화면 9건 전부 같음(절토 · 성토 ㎡ 소수 넷째 자리) + +제약 33(남김 22) From 83cb0d88033fd3cd2f071f3ba5e05d83ddeb4d55 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 15:48:27 +0900 Subject: [PATCH 6/7] =?UTF-8?q?chore(plan):=2010-6=20=EC=83=81=EC=9E=90=20?= =?UTF-8?q?=EC=B9=B8=20=EA=B3=A0=EC=B9=A8=20=E2=86=92=20=ED=9A=A1=EB=8B=A8?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99=20=EC=9A=94=EC=B2=AD=20=EC=9B=90=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 41aeb8a33..40cb0ec38 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -307,3 +307,6 @@ B 방향이 맞겠네. 새 프로젝트를 만드는건 문제없어보여. 이 (10-6 사용자) 횡단도에서 형상의 제약조건이 어떤게 있는지 확인 좀 해줘. 배관과 흙막이, 옹벽관련 내용들만 일단 찾아줘. + +(10-6 사용자) +다음으로 좌측 패널에서 서브 컨테이너의 항목을 선택/변경/생성 할 경우에 횡단도에서 선택한것처럼 이동되어야할것같아. From 803599ef347d767d3262743fc0573579f5745d3e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 15:54:34 +0900 Subject: [PATCH 7/7] =?UTF-8?q?chore(plan):=2010-6=20=EC=95=8C=EC=95=BD=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=C2=B7=20=EC=9A=B0=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EB=A6=AC=EC=85=8B=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=9B=90=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FJqWQpoB1RE7hiaQ2wH8qV --- docs/raw/사용자요구/2026-10-05_수정요구.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index 40cb0ec38..fb1497120 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -310,3 +310,10 @@ B 방향이 맞겠네. 새 프로젝트를 만드는건 문제없어보여. 이 (10-6 사용자) 다음으로 좌측 패널에서 서브 컨테이너의 항목을 선택/변경/생성 할 경우에 횡단도에서 선택한것처럼 이동되어야할것같아. + +(10-6 사용자) +추가로 배관의 도수로/돌붙임의 경우에는 복수 값이 아니야. 생성이 되었다면 추가 버튼이 없어져야함. + +(10-6 사용자) +1. 종단도의 구조물 알약의 이름이 구조물군의 종류 값과 매칭이 안됨. 같은 값을 보게 개선. +2. 종단도에서 배관이 선택한 상태에서 다른 지점에 옹벽을 추가하기 위해 마우스 오른쪽 버튼을 클릭후 사면안정을 눌렀더니 하위 리스트는 안보이고 리셋된 상태가 됨. 확인