Files
Aislo/docs/raw/verification/2026-08-02_verify_mass_haul_multi_curve.md
T
eomsangdonandClaude Opus 5 eb30b774f8 chore(docs): docs 폴더 git 추적 전환 · PLAN·OWNERS 최상위 이관
- `docs/` 를 .gitignore 에서 빼 git 추적 대상으로 전환 (위키·완료 이력·검증 기록)
- `docs/raw/PLAN.md` · `docs/raw/OWNERS.md` 를 저장소 최상위로 이동 후 .gitignore 에 등록
  — 창끼리 공유하되 저장소에는 안 올리는 장부
- 살아 있는 경로 참조 7개 파일 정정 (아카이브 107건은 그때 사실이라 그대로 둠)
- graphify 날짜별 산출물(`docs/wiki/graphify-out/20*/`) 제외 — `graphify update` 가 다시 만듦

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 18:19:07 +09:00

14 KiB
Raw Blame History

검증 보고서 (Verification Report)

  • 검증 일자: 2026-08-02
  • 검증 대상: [C05] 유토곡선 다중 곡선화 — 종단/횡단 기준 × 지반유형 처리 3종 + 오버레이 범례
  • 대상 커밋: 6a87baa
  • 검증 담당: Claude Opus 5 (코드 검증자)
  • 검증 상태: CONDITIONAL PASS (조건부 합격) — 기능 요구 전부 충족, 렌더 결함 1건 발견

이해상충 고지: 이 코드는 같은 세션의 프로그래머 역할이 작성했습니다. 자기검증의 맹점을 줄이기 위해 "구현이 의도대로 도는가"가 아니라 "구현이 전제한 가정이 실제로 성립하는가" 를 반증하는 방향으로 검증했습니다. 그 결과 프로그래머가 놓친 결함 1건(F1)을 찾았습니다.


1. 검증 내용 및 목적

임도는 산복 사면을 따라가 편절·편성이 지배적이라, 종단 계획고 기준 절성토 균형과 실제 횡단 단면적 기준 균형이 어긋납니다. C05는 어느 쪽을 정식으로 삼을지 사용자가 그래프를 보고 결정하도록 두 기준을 한 축에 겹쳐 그리고, 미결 상태인 지반유형 처리 3종도 함께 냅니다.

검증 초점:

  1. 회귀 — 확정 저장 경로(computeMassHaul → massHaulPayload → DB)가 온전한가.
  2. 수치 정합 — 여섯 곡선의 값이 산술적으로 맞고 서로 모순이 없는가.
  3. 핵심 요구 3가지 — 두 기준 병기 / hide-show 오버레이 / 토글해도 축이 안 움직임.
  4. 미검증으로 남아 있던 3건 — 다크 모드, 패널 최소 높이, 좁은 화면.
  5. 법령 인용 — 계획서가 인용한 조항이 원문과 문자 그대로 일치하는가.

2. 검증 절차 및 결과

① 확정 저장 경로 회귀 (정적 대조 + 테스트)

B06_wf3_ProfileCross_UI_Page.ts:384-394를 직접 확인했습니다.

const massHaul =
  sectionDetail && context?.earthwork_conversion
    ? computeMassHaul(sectionDetail.cross_sections, context.earthwork_conversion)
    : null;
await confirmSections(..., massHaul ? massHaulPayload(massHaul) : undefined);
  • 시리즈 도입이 이 경로에 닿지 않습니다. computeMassHaul()은 시그니처·반환 구조가 유지되었고, 내부적으로 integrate(crossAreaSamples(...), conv, "ground_type")로 위임될 뿐입니다.
  • 테스트로 computeMassHaul() 결과 === cross_ground_type 시리즈 결과를 확인했습니다(deepEqual).
  • massHaulPayload(legacy) === massHaulPayload(series) 확인, basis: "compacted" 유지 확인.
  • 확정 버튼 실동작은 수행하지 않았습니다. 프로젝트 상태를 WF3로 전이시키는 실제 데이터 변경이므로 검증 목적으로 실행하지 않는 것이 옳다고 판단했습니다. 정적 대조와 테스트로 회귀 없음을 확인했습니다.
  • 결과: Pass

② 계산 엔진 테스트 (총 26건)

기본 18건 — 시리즈 6종 생성, 기존 함수와의 동일성, 평균단면법 검산, 환산 방식별 절토량, 성토 불변, 자연상태 절토 불변, 종단 적분, 지반유형 상속, 두 기준의 값 차이, 방어 케이스 5건, 잉여/부족 배타성, 기본 표시 키 정합.

엣지 8건 (검증 단계에서 신규 작성)

# 케이스 결과
E1 종단 샘플이 측점 범위 밖(BP 이전 / EP 이후)까지 뻗을 때 Pass — 최근접 측점 상속, 좌표 유한
E2 최근접 측점의 roadbed_width_m이 없을 때 Pass — 곡선이 0으로 평탄, NaN 미발생
E3 difference_m == 0 (계획고 = 지반고) Pass — 절토·성토 둘 다 0
E4 difference_m이 NaN인 샘플 Pass — 해당 샘플만 제외
E5 알 수 없는 ground_type Pass — soil로 정규화
E6 음수 단면적 입력 Pass — 0으로 차단
E7 저장 payload가 항상 횡단·지반유형별인지 Pass
E8 종단 곡선의 합성 station_id(A@0 형태) 누출 Pass — 저장 경로에 닿지 않음
  • 결과: Pass (26/26)

③ 수치 정합 검산 (실데이터: 테스트1 / route 48 / 24측점 중 23측점 설계)

곡선 절토(자연) 환산 후 성토 최종
횡단 · 지반유형별 216.1 244.8 321.4 토취 76.6
횡단 · 토사 단일 216.1 194.5 321.4 토취 126.9
횡단 · 환산 없음 216.1 216.1 321.4 토취 105.3
종단 · 지반유형별 154.8 174.0 158.2 사토 15.8
종단 · 토사 단일 154.8 139.3 158.2 토취 18.9
종단 · 환산 없음 154.8 154.8 158.2 토취 3.4
  • 환산 검산: 14.9×0.90 + 201.2×1.15 = 244.79 ✓ / 216.1×0.90 = 194.49 ✓ / 16.2×0.90 + 138.7×1.15 = 174.09 ✓
  • 최종값 검산: 모든 행에서 환산 후 − 성토 = 최종 성립 ✓
  • 불변식: 자연상태 절토가 같은 기준 안에서 환산 방식과 무관하게 동일 — 횡단 3행 모두 216.1, 종단 3행 모두 154.8 ✓
  • 결과: Pass

④ 핵심 요구 3가지 (브라우저 실측)

요구 측정 결과 판정
두 기준 병기 범례 6개 생성, 기본 2개(양 기준의 지반유형별) 켜짐 Pass
hide/show 토글 2 → 6 → 0 → 2 전부 정상. 전부 끄면 안내문 표시 Pass
토글해도 축 고정 Y눈금이 세 상태 모두 60.5 / -46.3 / -153.0 완전 동일 Pass
X축 정렬 (종단도 대비) 종단도 측점선 [62 … 1454] 24개 = 유토곡선 [62 … 1454] 24개 Pass
상단 오버레이 범례 top 318px = SVG top 318px (겹침) Pass
세션 보존 새로고침 후 켜 둔 2개 복원 Pass

축 고정은 volumeRange()가 표시 여부와 무관하게 전체 시리즈를 훑기 때문입니다. 구현 근거와 실측이 일치합니다.

⑤ 미검증 3건 해소

항목 측정 판정
다크 모드 :root[data-theme="dark"]에서 --color-success가 #3fbb6f로, --color-royal-amethyst가 #b99aff로 재정의됨. 배경 rgb(37,31,56) 위에서 두 곡선 모두 판독 가능(스크린샷 확인) Pass
범례가 곡선을 덮는가 세 시나리오(기본/최소높이/좁은화면) 모두 maxOverlapPx = 0. 범례가 플롯 상단(LONG_PAD.top 30px) 안에 들어감 Pass
좁은 화면 (900px) 범례 1줄 유지. 가로 스크롤 컨테이너 안에서 sticky; left: 0이라 스크롤해도 왼쪽에 남음 Pass (아래 F4 참조)
패널 최소 높이 (296px) 결함 발견 — F1 참조 Fail

⑥ 규칙 준수

항목 결과
700줄 제한 _UI_MassHaul.ts 346 / _UI_MassHaul_View.ts 318 / _UI_Section_View.ts 432 / _UI_Style_Cross.css 606 / _UI_Style_MassHaul.css 142 / _UI_Page.ts 499 / locale_b2.ts 450 — 전원 Pass
색상 하드코딩 _UI_Style_MassHaul.css에 #/rgb()/색상 키워드 0건. theme.css 변수만 사용
문자열 하드코딩 신규 TS 코드에 한글 문자열 리터럴 0건. 전부 L()/ui_locales 경유
타입 안전성 tsc --noEmit 오류 0건. 로케일 키를 LocaleKey 타입으로 묶어 오타 시 컴파일 실패
포맷 prettier --check 전 파일 통과
로직 이중화 금지 평균단면법·환산이 integrate() 한 곳뿐. 두 기준이 AreaSample[]로 정규화된 뒤 같은 코드를 탐

⑦ 법령 인용 대조

계획서 인용문을 [임도의 설계 및 시설기준] 별표 PDF 원문에서 공백 제거 후 문자열 대조했습니다.

조항 판정
2.가.(1)(가) 측점 20m / 보조말뚝 일치
2.다.(3)(나) "시공계획고는 절토량과 성토량이 균형을 이루게 하되" 일치
2.다.(4)(다) "절토부분은 토사·암반으로 구분하되, 암반부분은 추정선으로 기입한다" 일치
2.다.(4)(마) 측점별 지반고·계획고·절토고·성토고·단면적 일치
2.라.(1) 토적표 일치
2.차.(2) 암석지 1:0.4 / 토사지역 1:0.9 일치
2.차.(3) 성토 1:0.9 이하 일치
2.차.(7) "적정한 장소에 사토장 또는 토취장을 지정한다" 일치
3.나.(1) 간선 4m 내외 / 지선 3m 내외 일치

11건 전원 일치, 불일치 0건. 다만 F3 참조.


3. 발견 사항

[F1] 저장된 패널 높이가 첫 렌더에 반영되지 않아 종단면도가 뭉개짐 (중요도: 중)

  • 증상: 사용자가 패널을 줄여 둔 세션에서 B06에 다시 진입하면, 패널 높이는 296px로 복원되지만 차트 높이 배분이 기본값(종단 220 / 유토 190)으로 계산됩니다. 합계 410px가 200px 공간에 들어가면서 종단면도가 20px로 잘려 사실상 사라집니다.

  • 재현 실측:

    시점 패널 높이 종단도 유토곡선
    첫 렌더 직후 296px (정상 복원) 20px 190px
    창 크기 변경 후 재렌더 296px 129px 111px
  • 근본 원인: _UI_Section_View.ts의 draw()가 root.replaceChildren(panel, …) 이전에 drawPanel()을 호출합니다. 첫 렌더 시점에 panel은 아직 root에 붙기 전(detached)이라 getComputedStyle(panel).getPropertyValue("--b06-profile-height")가 빈 문자열을 반환하고, panelHeight()가 BASE_PANEL_HEIGHT(506)로 폴백합니다. 리사이저가 남긴 값은 인라인 스타일에는 정상적으로 들어가 있습니다(panel.style 조회 시 296px 확인).

  • 자가 치유: 창 크기를 조금이라도 바꾸면 정상으로 잡힙니다. 그래서 프로그래머 단계의 검증에서 잡히지 않았습니다(당시 저장된 높이가 없어 폴백 값이 곧 정답이었음).

  • 유입 시점: 구조 자체는 C02(f35ca7e)에서 들어왔습니다. 다만 C02 시점에는 차트가 하나뿐이라 영향이 작았고, C05에서 차트가 둘로 늘면서 배분 실패가 종단면도 소멸로 확대되었습니다.

  • 권고 수정: panelHeight()가 인라인 스타일을 먼저 읽고, 없을 때만 computed로 폴백하도록 바꿉니다. detached 상태에서도 동작합니다.

    const panelHeight = (): number => {
      // 첫 렌더 때 panel은 아직 root에 붙기 전이라 getComputedStyle이 빈 값을 준다.
      // 리사이저가 남긴 인라인 변수부터 읽어야 저장된 높이가 첫 화면부터 반영된다.
      const inline = Number.parseFloat(panel.style.getPropertyValue("--b06-profile-height"));
      if (Number.isFinite(inline) && inline > 0) return inline;
      const raw = Number.parseFloat(getComputedStyle(panel).getPropertyValue("--b06-profile-height"));
      return Number.isFinite(raw) && raw > 0 ? raw : BASE_PANEL_HEIGHT;
    };
    
  • 조치: 검증자는 코드를 수정하지 않습니다. PLAN.md 다음 작업으로 올렸습니다.

[F2] 화면 요약값과 확정 저장값이 다를 수 있음 (중요도: 하 — 설계 의도, 문서화 필요)

  • 요약줄은 켜 둔 첫 곡선의 수치를 보여 주지만, 확정 시 DB에 저장되는 값은 항상 횡단 · 지반유형별입니다.
  • 예: 사용자가 "종단 · 환산 없음"만 켜 두면 화면에는 토취 3.4㎥가 보이지만 저장되는 값은 토취 76.6㎥입니다.
  • 요약줄 끝에 곡선명이 항상 표기되므로 오독 위험은 낮고, "정식 곡선은 하나"라는 설계 의도에도 부합합니다. 결함이 아니라 명시 필요 사항으로 판정합니다.
  • 조치: 아카이빙 계획서에 명시했습니다. 정식 기준이 확정되면(사용자 판단 대기) 자연히 해소됩니다.

[F3] "유토곡선의 법정 지위" 서술이 과장 (중요도: 하 — 문서)

  • C05는 2.라.(1)을 근거로 "유토곡선/토적표 산출의 법정 지위"라고 적었습니다.
  • 원문 대조 결과 법령이 설계서 구성으로 요구하는 것은 토적표이며, '유토곡선'이라는 단어는 별표 전문에 등장하지 않습니다.
  • 유토곡선은 토적표에서 파생되는 실무 관행 도면이지 법정 요구 도면이 아닙니다.
  • 조치: 아카이빙 계획서에서 "토적표는 법정 요구, 유토곡선은 그로부터 파생되는 실무 도면"으로 정정했습니다.

[F4] 좁은 화면에서 범례 6개가 가로로 뻗음 (중요도: 정보)

  • 900px 뷰포트에서 범례가 1줄을 유지하며, 좁으면 가로 스크롤이 필요합니다.
  • 다만 범례가 position: sticky; left: 0이라 차트를 가로로 스크롤해도 왼쪽에 남습니다. 실사용에 지장 없음.
  • 향후 1번(암반 경계선 분리)이 완료되면 곡선이 6개 → 2개로 줄어 자연 해소됩니다. 조치 불요.

4. 종합 판정

검증 항목 결과
확정 저장 경로 회귀 Pass
계산 엔진 테스트 26건 Pass
실데이터 수치 정합·검산 Pass
두 기준 병기 / 토글 / 축 고정 Pass
X축 종단도 정렬 Pass
다크 모드 Pass
범례 오버레이 (곡선 가림 없음) Pass
좁은 화면 Pass (F4 정보)
패널 최소 높이 첫 렌더 Fail (F1)
700줄 / 색·문자열 하드코딩 / 타입 / 포맷 Pass
법령 인용 11건 대조 Pass (F3 서술 정정)

CONDITIONAL PASS. 사용자가 요구한 기능(종횡단 2개 기준 + 지반유형 처리 전부 표시, 상단 오버레이 hide/show, 축 불변)은 전부 충족되었고 계산도 정확합니다. 다만 F1은 사용자가 패널을 줄여 쓰는 순간 종단면도가 사라지는 눈에 보이는 결함이므로, 수정 전까지 완전 합격으로 볼 수 없습니다.


5. 후속 조치

  1. [F1] 수정 — PLAN.md 다음 작업으로 이관. 프로그래머 역할이 5줄 수정으로 처리 가능.
  2. 정식 기준 채택 판단 — 사용자 판단 대기 사항이므로 PLAN.md 미결사항으로 이관해 아카이빙 후에도 보이게 유지합니다.
  3. 향후 1번(암반 경계선 분리) — 이번 검증으로 법정 근거(2.다.(4)(다))가 확정되었습니다. 완료되면 곡선 6종 중 4종이 비교 목적을 잃어 제거 대상이 됩니다.
  4. 위키 반영(ingest/lint/graphify)은 위키 관리자 몫으로 수행하지 않았습니다.