- p.260 항타 그래프 등이 가로 조각 8개로 분할 추출된 문제(사용자 발견)
- 조각 삭제 후 원본 PDF 그림 영역을 2.5배 렌더 캡처로 교체 (품셈 p260·건협 p6·중기중앙회 p45)
- md 참조를 병합본 1건으로 치환, 챕터 하위폴더의 ../pic 상대경로 깊이 오류 10건 수정
- 해시 중복(5회 이상) 그룹 시각 확인 후 삭제: 페이지 배경 띠(90)·헤더 바(34)·
KBIZ 로고(18)·간지 배너 조각(54)·말풍선/빈 이미지(15)
- md 48건에서 해당 이미지 참조 라인 제거 (깨진 링크 방지)
- 유지 59건 = 실제 내용 그림 (조사 입력화면·품셈 삽도 등)
- ESTX_값사전 및 견적내역·물량도면·옵션메타·인쇄옵션·자재노무기계 등 값사전 세부 5건
- ESTX_검증근거, 데이터관계, 분석, 테이블사전, 파일구조 신규
- PLAN_2026-07-main_ESTX_정밀분석.md 갱신 (타 AI 작업분 커밋)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 통수단면.md §3: md 사본 2종 표 깨짐 → hwp(23.01판) BodyText 직접 판독으로 표 2.7-1 전체 복원
- 흙수로·토사·파형강관은 원본 전문 검색 무출현 — 소실 아닌 KDS 공백 영역으로 확정
- 03_미결 No.7: 잔여 = 최신 확률강우량도 확보만 남음
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 통수단면.md §3: 실무 관측 n값(콘크리트 0.013/0.017 KDS 일치, 거창 0.012·파형강관 0.025 근거 미확인) 대조표 추가
- 03_미결 No.7: 잔여를 원문 확보 2건(KDS n값표 깨끗한 판본, 최신 확률강우량도)으로 재정의 — 자료 대기 전환
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- No.17 신설: 일위대가·단가산출근거·환율및기초자료·중기사용료·재료비노무비경비·자재단가대비표·집계표의 현행 원천 데이터 입수 경로 조사 (STC는 결과물일 뿐 원천 미표기)
- 해소 항목은 행 삭제로 정책 변경 (이력 유지 안 함, 번호 재부여 없음)
- No.5(콘크리트 강도 210)·No.16(통수단면 2.0배 확정) 삭제
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
프로젝트·임시 보관함·사용자 관리·가입 요청·회사 관리·시스템 로그 컨테이너는 자료가
쌓일수록 세로로 계속 길어져 페이지가 늘어난다. 4행 높이에서 자르고 나머지는 컨테이너
안에서 스크롤하게 했다.
행 높이를 상수로 박지 않았다. 프로젝트 표는 셀 안에 단계 막대가 들어가고 보관함 묶음은
접힘 상태에 따라 높이가 제각각이라 4 x 고정값으로는 어긋난다. limitVisibleRows()가
4번째 항목의 실제 아래쪽 좌표를 재서 max-height를 넣는다 — 표든 묶음이든 같은 코드로
맞는다. 항목이 4개 이하면 max-height를 지워 스크롤바를 만들지 않는다.
스크롤 중 어느 열인지 알아야 하므로 표 머리행은 sticky로 붙였다. 배경이 투명하면 아래
행이 비쳐 보여 불투명 배경도 함께 준다.
임시 보관함은 표가 아니라 묶음이 한 행이다. 묶음 안 파일 표는 접혀 있어 제외했다.
묶음을 펼치면 높이가 달라지므로 토글 핸들러에서 다시 잰다. 목록을 다시 그릴 때마다
resize 청취자가 쌓이던 문제는 WeakMap으로 이전 것을 걷어내고 다시 등록해 막았다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
대시보드 삭제 버튼은 지금까지 projects.deleted_at만 찍는 소프트 삭제였다. 배포에서는
그게 맞다 — 사용자가 올린 라이다 원본은 다른 프로젝트에 재활용할 자산이다. 그러나 개발
중에는 프로젝트를 반복 생성·삭제하는데 정리 잡이 없어 수십 GB 원본이 계속 쌓인다.
config_system.py 맨 위에 PROJECT_DELETE_HARD_ENABLED를 두고 갈랐다. 기본값 False라
환경변수를 빠뜨린 배포 환경은 자동으로 안전한 쪽에 선다. True면 projects 행을 실제로
DELETE 하고(자식 테이블은 FK CASCADE로 함께 사라진다) storage/{회사}/{사용자}/{프로젝트ID}/
폴더를 통째로 지운다.
자식 테이블 목록은 코드에 나열하지 않았다. projects.id 참조가 전부 ON DELETE CASCADE라
행 하나면 충분하고, 목록을 복사해 두면 스키마가 바뀔 때 조용히 어긋난다.
순서는 DB 먼저 커밋, rmtree 나중이다. 파일을 먼저 지우면 DB 실패 시 실체 없는 프로젝트가
목록에 남아 화면이 깨진다. 반대면 rmtree가 실패해도 고아 폴더만 남고 정합성은 유지된다.
resolve_stored_project_path()는 끝에서 makedirs를 하므로 삭제에 쓸 수 없다 — 지우기 직전에
폴더를 되살린다. 검증만 하는 resolve_project_root_for_delete()를 따로 뒀고, 저장소 루트 안 ·
세그먼트 정확히 4개 · 마지막 세그먼트가 요청 project_id와 일치를 모두 요구한다. DB의
storage_path가 오염돼도 상위 폴더나 남의 폴더를 지우지 못한다.
하드 삭제 모드에서는 확인 모달 문구를 바꿔 원본까지 사라진다고 알린다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 결정(2026-08-08): 복수 파일을 허용하고 중복 기준은 파일명으로 둔다. 같은 이름으로
같은 내용이 다시 들어오면 덮어쓰지 말고 건너뛰고, 내용이 다르면 덮어쓴다.
파일 지문(부분 샘플링)
- B03_FileInput_Fingerprint.ts: 파일 크기 + 앞·중간·끝 8MB를 이어 SHA-256. 24MB만 읽어
1~2초면 끝난다. 전체 읽기(1.7GB, 10~30초)와 견줘 실용적이고, 자리를 앞·중간·끝으로
흩어 놓아 머리말만 같은 파일도 갈린다. 한계는 주석에 적었다.
- 화면이 업로드 세션 생성 요청에 지문을 실어 보내고, 서버가 같은 이름의 최신 입력 파일
메타데이터에 적힌 지문과 견준다. 같으면 already_uploaded=true로 답해 **전송 자체를**
건너뛴다(1.7GB면 3~5분 절약). 지문이 없거나 다르면 그냥 올린다 — 애매하면 올리는 쪽.
- 완료 요청에도 지문을 실어 input_files.metadata에 남긴다. upload_sessions에 컬럼을
더하지 않으려는 선택이라 DB 스키마 변경이 없다.
옛 행 정리
- supersede_previous_input_files(): 같은 이름의 이전 행을 SUPERSEDED로 내린다. 조회는
UPLOADED/PROCESSED만 보므로 목록·분석에서 자동으로 빠지고, 행은 이력으로 남는다.
- 직접 업로드 완료와 보관함 연결 양쪽에 적용.
검증(실서버 f45243b3)
- 같은 파일 재요청 → already_uploaded=true, 세션 미발급.
- 지문이 다르면 → 세션 발급(정상 업로드 경로).
- 옛 행이 SUPERSEDED로 내려가는 것 DB에서 확인.
- 화면 코드(B03_FileInput_Fingerprint.ts)를 그대로 실행해 만든 지문과 서버측 검증
스크립트의 지문이 20MB 표본에서 완전히 일치(f281fd08…93e4).
typecheck·ruff·prettier 통과, 정적 번들 재빌드.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
연결된 묶음은 파일이 프로젝트로 옮겨져 내용이 비어 있는데도 목록에 남아, 파일 0건·용량
0·만료 없음·버튼 없음인 빈 껍데기가 영구히 쌓였다(2026-08-08 사용자 지시로 제외).
- list_temp_batches: status <> 'linked' 만 조회. 행 자체는 이력용으로 DB에 남긴다 —
어느 자료가 어느 프로젝트로 갔는지 추적할 근거다.
- 대시보드 UI: 목록에 연결된 묶음이 오지 않으므로 "연결됨" 배지·삭제 버튼 숨김·파일 행
비활성 분기를 걷어냈다(죽은 코드).
검증: 서버 재시작 후 목록 응답 0건, DB 보관 행 3건 유지.
typecheck·prettier·ruff 통과, 정적 번들 재빌드.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
업로드 잠금(결함 3)
- 종전에는 전처리(B04)만 끝나면 버튼이 풀려, 노선·횡단이 도는 동안 자료를 또 올릴 수
있었다. isInitialPipelineRunning()/pollInitialPipeline()을 두어 **횡단 단계가 열릴
때까지**(3단계가 NOT_STARTED를 벗어날 때까지) 잠근다.
- relockWhileInitialPipelineRuns(): 새로고침·재진입해도 계산이 도는 중이면 다시 잠그고
스피너를 붙인다. 잠금이 화면 상태로만 있으면 새로고침 한 번에 풀렸다.
- 보관함 연결 경로도 같은 대기 흐름을 탄다.
자료 없으면 대시보드(결함 6)
- b_missing_data_guard 신설 — 근거 자료가 없으면 한 줄 안내 후 B01로 보낸다. 새 자료가
올라오면 서버가 옛 산출물을 지우므로, 열어 둔 뒷단계 화면은 빈 화면이 아니라
대시보드로 나가야 한다(2026-08-08 사용자 지시).
- B05: 확정 지표면이 없으면 이동. B06: 노선 또는 종단면이 없으면 이동.
검증: 자료가 없는 신규 프로젝트로 B05 진입 → 안내 후 #/b01-account 로 이동함을
헤드리스 크롬으로 확인. typecheck·prettier 통과, 정적 번들 재빌드.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
사용자 결정(2026-08-08): 새 자료를 올리면 재계산해 덮어쓰고, 화면은 불러올 자료가 없으면
대시보드로 보낸다. 그러려면 자료가 갈리는 순간 옛 계산 결과가 남아 있으면 안 된다.
- common_util_project_reset 신설: 단계별 산출물 폴더(B04~B09)와 산출물 DB 레코드
(surface_models·routes·route_points·route_statistics·longitudinal_sections·
cross_sections·structures·quantity_items·outputs·processed_point_cloud)를 함께 지운다.
**B03_FileInput(업로드 원본)은 지우지 않는다** — 같은 파일인지 가리는 중복 검사가 쓴다.
- 직접 업로드 완료·보관함 연결 양쪽에서 정리를 호출하고, 진행 단계도 1단계 이후를
NOT_STARTED로 되돌린다(reset_stages_after_input_change).
- is_analysis_running(): 전처리가 도는 중이면 업로드 세션 생성과 보관함 연결을 409로
막는다. 화면 버튼 잠금은 새로고침·다른 탭으로 우회되므로 서버에도 문을 단다.
- fail_stage 아래 있던 리비전(번호표) 안은 폐기 — 사용자가 "산출물이 없으면 대시보드"
방식으로 정리했다.
곁들여: os.replace 공유 위반 재시도(replace_with_retry)
진행률 파일은 서버가 쓰는 동안 화면이 계속 읽어 WinError 5가 났고, 전처리
structured.npz 교체에서는 같은 이유로 분석이 통째로 죽었다(결함 3의 사망 원인).
짧게 여러 번 다시 시도하도록 바꿨다.
검증(실서버 f45243b3, 계획노선 CSV 재업로드로 자료 교체):
전처리 결과 112개 -> 재분석분만, 노선 2->0, 횡단 20->0,
DB 지표면 15->0 / 노선 1->0 / 횡단 19->0, 업로드 원본 6개는 그대로.
진행 단계가 초기로 돌아가고 재분석이 자동 시작됨.
교체 재시도는 읽는 쪽이 파일을 잡고 있는 상황을 만들어 성공 확인.
ruff format·check 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
앞선 두 커밋(c3f643b, 7d50ded)으로도 증상이 남아 헤드리스 크롬으로 실제 앱에 로그인해
B05 3D를 끌어 보며 측정했다. 원인은 회전 계산이 아니라 **탑뷰 카메라 자리**였다.
카메라를 정확히 수직으로 세우면(기존 top = [0, distance, 0.001], 극각 1e-6도) lookAt이
화면의 가로·세로 방향을 정하지 못한다. 그 자리에서 벗어나는 순간 화면 축이 180° 돌아간다.
실측(2px 드래그 6걸음):
before 극각 0.00° -> 0.86°, 1걸음째 화면축 반전, 이후 같은 방향 드래그 먹통
after 극각 2.00° -> 1.15°, 반전 0회, 반대 방향 20걸음도 반전 0회(1.15° -> 130°)
- TOP_VIEW_TILT(2°) 신설 — 화면상 위에서 내려다보는 그림과 구분되지 않으면서 여유각
(POLAR_EPSILON, 약 1.15°)보다 바깥이라 첫 드래그부터 회전이 이어진다.
- B05 노선 뷰어, B04 지형·포인트클라우드 뷰어의 탑뷰 세 곳에 모두 적용.
typecheck·prettier 통과, 정적 번들 재빌드.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
기본 탑뷰는 시선이 up과 거의 나란하다(극각 1.9e-6 rad). 이때 회전축으로 쓰던
시선×up이 0에 수렴해 축 방향이 잡음으로 정해졌고, 살짝만 끌어도 엉뚱한 축으로 돌아
화면이 뒤집혔다. 앞 커밋의 극점 클램프도 각도를 여유각(0.02 rad) 경계까지 끌어올려,
작은 드래그가 여유각만큼 튀게 만들었다.
- 시선×up이 시선 길이 대비 무시할 만큼 짧으면 카메라 자신의 가로축으로 대체한다.
두 축은 모든 각도에서 방향이 같아(내적 1.000000) 전환 순간에도 조작감이 안 바뀐다.
- 클램프 경계를 현재 극각과 여유각 중 극점에서 먼 쪽으로 잡는다. 이미 여유각 안쪽인
탑뷰에서는 경계로 끌어올리지 않고, 극점에서 멀어지는 쪽만 열어 준다.
검증(탑뷰 재현): 0.005 요청 -> 0.005 적용, 0.05 -> 0.05, 극점 쪽 -0.005 -> 0(정지).
typecheck·prettier 통과, 정적 번들 재빌드.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
세로로 많이 끌면 화면이 멈췄다가 한 번에 움직이는 증상. 원인 두 가지였다.
1) 극점을 넘길 회전을 통째로 버렸다. 끄는 동안 화면이 굳어 있다가 각도가 다시 범위
안으로 들어오는 순간 밀린 만큼 한꺼번에 움직였다. 이제 극점 직전까지만 잘라서
적용한다 — right축이 up과 직교하므로 돌린 각이 곧 극각 변화량이고, 부호만 실제로
재서 클램프한다. 극점에 닿으면 더 안 가고 멈출 뿐, 반대로는 자유롭게 빠져나온다.
2) 드래그가 캔버스 밖(브라우저 위·아래 끝)으로 나가면 pointerleave로 회전이 끊겼다.
다시 누를 때 커서 아래로 회전축을 새로 집어 시점이 튀었다. 포인터를 캡처해 밖으로
나가도 회전을 이어 가고, 놓을 때만 끝낸다.
B04 지형·포인트클라우드 뷰어와 B05 노선 뷰어가 같은 모듈을 쓰므로 양쪽 모두 적용된다.
typecheck·prettier 통과, 정적 번들 재빌드.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
앞 커밋(53ab941)에서 "표고 없는 점은 안 그린다"를 넣었는데도 그대로 보였다.
restorePoints -> placed()가 마커에 넘기기 전에 z: point.z ?? 0으로 바꿔, 마커 쪽에서는
표고를 아는 점(0m)으로 보였기 때문이다. 판정이 걸릴 자리가 없었다.
- PlacedRoutePoint.z를 number | null로 바꿔 "모름"을 모름 그대로 들고 다닌다.
- placed()는 z ?? null, 서버로 보낼 때(routePoint)만 null -> undefined로 바꾼다.
- modelToScene은 표고가 정해진 점만 받는다(z: number) — 호출부에서 이미 해결한 값을 준다.
typecheck·prettier 통과. 백엔드가 정적으로 서빙하는 번들(config/node_modules/.build)도
다시 빌드했다 — :8000으로 보면 서버 기동 때 만든 옛 번들이 떠서 수정이 반영되지 않는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
계획노선 CSV로 만든 제어점에는 표고가 없다(z: null). modelToScene이 point.z - cz를
계산하는데 JS에서 null - 548.5 = -548.5이라, BP·EP·경유점 135개가 전부 지형 중심보다
548m 아래 수평면에 깔렸다. 화면에서는 노선 모양이 평면에 투영된 주황 구슬 커브로
보였고(경유점 색 0xf59e0b), 원근 때문에 지형 밖으로 밀려 나갔다.
- markerElevation() 신설: 표고가 있으면 그 값, 없으면 시·종점만 지형 표면을 찾아 얹는다.
표고 없는 경유점은 그리지 않는다 — 그 자리는 경로선으로 이미 보이고, CSV 정점
전부(여기선 133개)에 마커를 세우면 노선이 구슬에 덮인다.
- 뷰어에 terrainElevation() 추가: 지형 위에서 수직으로 광선을 쏴 표면 높이를 읽는다.
- renderRoute도 같은 함정을 막는다: z ?? 0 대신 지형 표고 → 직전 점 높이 순으로 채운다.
경고 구간이 인덱스로 점 배열을 다시 자르므로 점 개수는 그대로 둔다.
typecheck·prettier 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03(d21b0ec)에 곡선 기준을 R에서 L로 바꿨지만 배관 정착 계획선 산출부
한 곳에만 들어갔다. 공용 곡선 계산기와 정책값은 R 기준(측점간격 20m ×
curve_radius_ratio 0.40 = 8m)으로 남아, 사용자가 변화점을 새로 추가하면 그 자리만
R=8m가 적용됐다(대수차 3%면 L=0.24m — 도면에서 곡선이 사라진다). 화면 요약도
"기본 R 8.0 m"를 그대로 보여줘 오해를 키웠다.
- config: pipe_anchor_curve_length_m -> default_curve_length_m(15.0)로 승격.
배관 정착뿐 아니라 모든 변화점의 1차 기준값이다.
- AlignmentPolicy에 default_curve_length_m 추가, 정책 스냅샷에도 실어 보낸다.
옛 R 기준 값(default_curve_radius_m)은 옛 저장분 호환용으로 남긴다.
- build_curves(백엔드)와 buildCurves(프론트) 규칙 일원화:
사용자 지정 R > 기본 L > (옛 저장분) 기본 R × 대수차.
- 인접 직선이 짧아 목표 L을 못 넣으면 **넣을 수 있는 최대 L**까지만 줄이고 경고를
남긴다(2026-08-08 사용자 지시). 경고 문구도 길이 우선으로 고쳤다.
- 옛 저장분(정책에 L 없음)은 양쪽 모두 R 기준을 유지한다 — 서버만 L로 올리면
화면과 다른 곡선이 나온다. 새 기준은 계획선을 다시 산출할 때 적용된다.
- 패널 요약 라벨: "기본 R" -> "기본 곡선길이 L"(옛 저장분은 "기본 R(옛 저장분)").
검증(측점간격 20m, 대수차 3%): 기본 L=15.00m/R=500m, 사용자 R=300 지정 시
L=9.00m/R=300m, 인접 직선 10m면 L=9.00m로 잘리고 경고 1건, 옛 스냅샷은 종전대로
L=0.24m/R=8m. typecheck·ruff format·check 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
get_db_pool()은 동기 함수인데 B07만 await로 불렀다. 그 결과 풀 대신
_PoolConnectionContextManager가 잡혀 acquire()에서 터졌다.
AttributeError: '_PoolConnectionContextManager' object has no attribute 'acquire'
좌측 패널 [확정]을 누를 때마다 stage 4가 전이되지 않고 500이 났다(서버 로그 확인).
프로젝트 안에서 await를 붙인 호출부는 여기 한 곳뿐이다.
ruff format·check 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
화면에서 막 찍은 경유점은 order가 아직 없어 None으로 온다. 정렬 키가
x.get("order", 0)이라 키가 있고 값만 None인 경우를 걸러내지 못해
TypeError: '<' not supported between instances of 'NoneType' and 'NoneType'로
경로 탐색 전체가 실패했다(서버 로그 확인, project_id=fc86f247).
None을 0으로 눕힌다 — 파이썬 정렬이 안정적이라 번호가 없는 점들은 받은 순서를
유지한다. 같은 패턴을 쓰던 능선/계곡 탐색기(RidgeValley)도 함께 고쳤다.
ruff format·check 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>