chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합

- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ahnyongjooandClaude Sonnet 5.5 committed 2026-10-28 11:20:18 +09:00
commit 964a841de1
3978 files changed
+58013356

No files matched your search

View File
Whitespace-only changes.
@@ -0,0 +1,331 @@
# 교각 도면 모델링 앱 요구사항 검토서 — 운곡교 P1 실작업 기반
| 버전 | 수정일 | 수정자 | 주요 변경 내용 | 비고 |
| :--- | :--- | :--- | :--- | :--- |
| v0.1.00 | 2026-10-08 | 심영표 | 신규 작성 | 초안 — 기술 기반 선택(8장)은 의사결정 대기 |
| v0.1.01 | 2026-10-08 | 심영표 | 담당자 검토 확정 | 확정 (8.2 권고안·9.3 결정 사항은 별도 의사결정 대기) |
---
## 1. 개요
### 1.1 목적
교각 도면(DWG)을 입력하면 3D 모델(콘크리트·받침부·철근)과 도면 검토보고서를 만드는 앱을 개발하기 전에, 필요한 기능·입력·검토 기준·예외·기술 기반을 정리합니다.
### 1.2 근거 — 운곡교 P1 실작업
본 검토서는 2026-10-07~08에 운곡교 교각(완도방향 P1)을 수작업 + 스크립트로 모델링한 경험을 요구사항으로 옮긴 것입니다.
| 항목 | 실작업 규모 | 근거 |
| :--- | :--- | :--- |
| 입력 | DWG 3매 (C0050205-025/026/027) | `RAG/` |
| 모델 | 3,190객체 — 콘크리트 34, 받침 대좌·패드 20, 교량받침·솔플레이트 20, 철근 1,513본(관 + 중심선) | `Code/교각모델링/출력/실행_운곡교P1교각모델_2026-10-08_v0.1.09.3dm` |
| 자동 검사 | 약 590항목 (콘크리트 172, 기둥 철근 43, 기초 74, 코핑 117, 받침부 184) | 검증 보고서 v0.1.05 |
| 사람 결정 | 32건 (5장) | 작업 로그 `prompt/심영표/2026-10-07~08/` |
| 도면 지적 | 22건 (오류·불일치 12, 누락 10) | 도면 검토보고서 v0.1.01 |
| 스크립트 | 25종 (부록 B) | `Code/교각모델링/` |
| 참고 앱 | 통로박스 모델링 웹앱(Drawing Model Studio) | 분석서 `DOC/02_분석/분석_통로박스모델링과정_2026-10-07_v0.1.00.md` |
### 1.3 범위
- 입력: 교각 도면(DWG/DXF) + 사용자 결정 입력(5장)
- 출력: 3D 모델(.3dm 또는 웹 뷰어 모델), 단계별 검토 결과, 도면 검토보고서(.md·.pdf)
- 공정: 형상모델링 → 검토확인 → 기둥 철근 → 검토확인 → 확대기초 철근 → 검토확인 → 코핑 철근 → 검토확인 → 솔플레이트·받침 → 검토보고서
- 근거가 교각 1세트(원형 단주, T형 코핑, 말뚝 기초)이므로 다른 형식(벽식, 다주식, 라멘 등)으로 넓히는 판단은 `[추론]`으로 표기합니다.
---
## 2. 전체 구성
### 2.1 공정 흐름
공통 전처리 2단계와 지정 공정 10단계로 구성합니다. 각 "검토확인"은 **게이트**입니다. 통과하거나 사용자가 예외를 승인해야 다음 단계로 갑니다.
| # | 단계 | 주요 출력 | 게이트 |
| :-- | :--- | :--- | :--- |
| 0-1 | 도면 입력·변환 | DXF, 원본 해시 | 변환 엔티티 수, 원본 무결성 |
| 0-2 | 도면 판독 | 뷰 분류, 치수·문자·점 집계, 재료표·상세표 추출 | 판독 결과 사용자 확인(추가 입력 5장) |
| 1 | 형상모델링 | 기초·기둥·코핑·버림·말뚝, 받침 대좌·패드 | **G1** (4.2) |
| 2 | 기둥 철근 | 주철근·띠철근·크로스타이 | **G2** |
| 3 | 확대기초 철근 | 하·상면 망, 둘레근, 수직 결속근 | **G3** |
| 4 | 코핑 철근 | 상·하면 주철근, 스터럽, 폐합근, 보조근, 교좌면 보강 | **G4** |
| 5 | 솔플레이트·받침 | 교량받침, 솔플레이트 | **G5** |
| 6 | 검토보고서 | 도면 오류·개선·추가·누락·재료표·상세 | 보고서 검사(장 구성·출처·버전) |
### 2.2 데이터 모델
실작업에서 쓴 구조를 앱의 기본 데이터로 정합니다.
| 데이터 | 내용 | 실작업 대응 |
| :--- | :--- | :--- |
| 프로젝트 | 도면 묶음(파일명·SHA-256), 좌표계, 단위(mm, Z = EL×1000) | `conversion_audit.json` |
| 판독 장부 | 뷰·엔티티 핸들·치수 사슬·표 행, 출처 | `판독/*.json` |
| 파라미터(정본) | 부재·철근 그룹별 값과 **값마다 출처(src)**, 결정 기록 | `params.json` (lean_concrete … sole_plates) |
| 객체 | ID, 역할, 호스트·정착 호스트, 마크·직경·형상, occurrence, 원길이, 출처 | 객체 속성(UserText) |
| 결정 기록 | 결정 지점, 채택안, 기각안, 근거, 일시, 결정자 | 작업 로그 DECISIONS |
| 검사 결과 | 항목·기대값·실측·허용오차·판정·파일 | `검증/*.json` |
| 산출물 버전 | 모델·문서·파라미터의 판(vMAJOR.MINOR.PATCH)과 구판 보관 | `출력/backup/`, `DOC/*/backup/` |
### 2.3 사람 개입 지점
- **판독 확인**: 표에서 읽은 값과 해석(상동 기호, 절반 작도 대칭 등)을 사용자가 확인
- **결정 입력**: 도면만으로 정할 수 없는 항목(5장)을 입력. 입력하지 않으면 해당 단계를 진행하지 않음
- **게이트 판정**: FAIL 항목을 예외(6장)로 승인하거나 수정을 요청
- **보고서 확정**: '오류'·'불일치' 판정은 사람이 확정
---
## 3. 단계별 요구사항
### 3.1 도면 입력·판독
| 구분 | 요구사항 | 실작업 근거·어려운 점 |
| :--- | :--- | :--- |
| 변환 | DWG→DXF 변환기 내장(ODA File Converter 또는 LibreDWG). 원본은 읽기 전용, 해시 기록 | 박스 앱은 LibreDWG만 허용했으나 실작업 PC에는 ODA만 있었음 |
| 시트 영역 | 도면틀 안만 판독, 틀 밖 잔여물 제외 | D1 틀 밖에 교량 선형·사본 뷰가 흩어져 있었음 |
| 뷰 분류 | 제목 문자(정면도·단면 A-A 등)와 축척으로 뷰 영역 자동 분류, 사용자 확인 | — |
| 치수 | DIMENSION 엔티티와 **분해된 치수(선 + 문자)** 모두 지원. 치수 사슬(`100`, `200`, `70@125=8.750` …)을 위치 목록으로 변환. **양끝 눈금도 철근 위치로 포함**하는 규칙 | 실작업 도면은 DIMENSION 0건, 모든 치수가 분해됨. 양끝 포함 시 본수 일치(73 = 38+35) |
| 축약 작도 | 파단선으로 줄인 부재(기둥)는 작도 길이 대신 치수 문자 사용. 파단선 위·아래 좌표계 분리 | 기둥 12,003이 6,100으로 작도됨 |
| 절반 작도 | 정면도·평면도 절반 작도를 감지하고 대칭 환산. 비대칭 부위(선단 높이·소단)는 경고 | 코핑 좌측·거더선 1·2 미작도 |
| 철근 점·선 | 단면의 철근 점(원 r 27)을 높이별로 전수 집계해 층·본수 판독 | 기초 6층 구성이 이 방법으로 확정됨 |
| 재료표 | 마크·형상·직경·본수·a~e·각도·R·L·총연장·그룹 소계 추출. **상동(") 기호를 그룹 순서로 해석**, 빈칸 경고(B9 수량) | — |
| 가공도 | 형상 번호(I~XVII)별 길이식 라이브러리 | 7.1 |
| 문자 인코딩 | CP949·UTF-8 자동 판별 | — |
### 3.2 형상모델링
| 요구사항 | 실작업 근거 |
| :--- | :--- |
| 객체 장부를 먼저 만든 뒤(역할·발생 수·사용 뷰·접합 객체) 형상 생성 | 박스 앱 원칙 |
| 3뷰(평면·정면·단면)가 동시에 만족될 때만 폐합 솔리드 생성 | — |
| 형상 유형: 직육면체, 원기둥, 정면 프로파일 압출(곡선 하면·경사 선단·소단 상면), 회전 사변형(대좌), 사각뿔대(패드), 쐐기(솔플레이트) | 코핑 하면 R 12,000 원호, 선단 200 경사, 5단 소단 |
| 원호 중심은 끝점과 R로 계산(도면 정점 미사용) 후 도면 정점과 대조 | 계산값과 DXF가 0.1 mm 이내로 일치 |
| 접합면 공유·겹침 0 검사, 의도된 겹침(말뚝 근입 100) 예외 등록 | — |
| 받침 대좌 높이 = 코핑 상면 + h1, 패드 상단 = EL.'X' (= 코핑 상면 + h1 + h2) | E 열(h1~h4 합)로 해석하면 211 mm 틀림 — 표 정의 해석이 핵심 |
### 3.3 기둥 철근
| 요구사항 | 실작업 근거 |
| :--- | :--- |
| 주철근: 원주 n등분(Div.46), 반경방향 다발(2본), 시작각 0°, 마크별 반지름 | P1 870.5 / P2 825.5 |
| 주철근 상·하단: 재료표 길이에서 역산(갈고리 수평부 높이 + R + a), 작도선과 비교 | 작도 P2가 기둥 상단에서 끝나 2.65 m 차이 |
| 띠철근: 치수 사슬 구간별 간격·본수, 구간별 마크(P4/P5/P3), 반지름 R | @100 표기 vs @120 사슬 충돌 |
| 형상: 반원 2 + 갈고리(VI), 반원 + 커플러(V), 링 + 커플러(IX), 이음 위치 엇갈림 | 커플러 길이가 재료표 L에 포함 |
| 크로스타이: 단마다 방향 수, 띠철근 위 얹힘 | 4본 교차 — 간섭 예외 |
| 관 솔리드 생성은 형상별 단건 시험 후 일괄 | `fitRail=True`에서 Rhino 크래시 |
### 3.4 확대기초 철근
| 요구사항 | 실작업 근거 |
| :--- | :--- |
| 층 구성 자동 판독(하면 4층 Y·X·Y·X, 상면 2층, 중앙, 둘레, 수직 결속) | 단면 점 높이 집계 |
| 교대 배치(U자 + 직선, 양끝 U자) 규칙 | B2/B4 38+35, B6/B8 32+29 |
| 체크무늬 등 2차원 배치 패턴 | B9 34×29 격자의 절반 = 493 |
| 층 높이 기준 선택(작도 점 / 치수선 + 직경 적층) — 입력 사항 | 작도 점 채택 시 상면 피복 58.5 |
| 수량 빈칸 시 총연장 ÷ L 역산과 경고 | B9 |
### 3.5 코핑 철근
| 요구사항 | 실작업 근거 |
| :--- | :--- |
| 변단면 대응: 하면 곡선(오프셋 동심원)·선단 경사·소단 상면에 따른 철근 길이·높이 | 스터럽 높이, 폐합근 길이가 위치마다 다름 |
| 재료표 변길이 `~` 해석: 범위 양끝 대조 + L = (최대 + 최소)/2 관례 | 평균으로 해석하면 350~400 mm 불일치 |
| 재료표 치수와 작도 중심선의 오프셋 자동 추정(실작업 54.3 mm) | C9~C13 범위 양끝 모두 일치 |
| 꺾인 다리(VIII, 98°·96°) = 선단면 평행, 하면근(VII) = 원호 + 다리 | C1 다리 끝과 C7 원호 끝이 만남 |
| 다중 폐합 스터럽(1개 위치에 3개, 겹침 배치), 스터럽 안쪽 수평 폐합근 | 단면 A-A 다리 위치·27 mm 안쪽 점 |
| 받침 대좌 회전 좌표계의 교좌면 보강(S1·S2) | 대좌 4.405° |
| 갈고리 방향 추정 시 콘크리트 내부 검사로 검증 | C14 아래 갈고리가 하면 관통 |
### 3.6 솔플레이트·받침
| 요구사항 | 실작업 근거 |
| :--- | :--- |
| 받침 표(ELEVATION TABLE)·솔플레이트 표(S, L, W, T, T1, T2) 연동 | L = A+100, W = B+100 |
| 교축방향 = h2 패드 방향(긴 변이 아님) | NO.3는 W > L |
| 경사 방향 입력, 표의 T1·T2와 S의 반올림 차 기록 | 37.6/22.4 vs 38/22 |
### 3.7 검토보고서
| 요구사항 | 실작업 근거 |
| :--- | :--- |
| 단계 진행 중 발견 사항을 자동 수집(충돌·누락·추정·예외) → 보고서 초안 | 미해결 목록 16건 → 지적 22건 |
| 장 구성: 개요, 오류·불일치, 개선방안, 추가 사항, 누락, 철근재료표(원본 vs 모델), 철근상세 | 보고서 v0.1.01 |
| 재료표 집계: 그룹 소계 검산, 단위중량 × 총연장, 모델 본수 대조 | `compile_rebar_schedule.py` |
| 판정 어휘 고정(오류·불일치·누락·확인 필요), 지적마다 도면·뷰·값 출처 | `check_report.py` |
| .md 정본 + .pdf 변환(한글 글꼴, 표 열 폭, 특수기호 글리프 점검) | 맑은 고딕에 U+2212 글리프 없음 |
---
## 4. 검토확인(게이트) 요구사항
### 4.1 공통 원칙
- 기대값은 **생성 입력(파라미터)이 아니라 도면 치수·표 값에서 따로** 가져온다(독립 기대값)
- 판정마다 기대값·실측·허용오차·모집단을 기록하고, 결과 파일을 보관한다
- **음성 대조**: 검사기 자체를 확인하기 위해 객체를 일부러 옮겨(수평 위치·절대 높이 각각) FAIL이 나오는지 매 단계 확인한다
- **재로드 검증**: 저장 파일을 새로 열어 같은 검사를 반복한다
- 숨김·잠금 객체를 포함해 센다
- 앞 단계 객체의 지문(바운딩·체적 또는 면 수·길이)이 바뀌지 않았는지 확인한다
### 4.2 단계별 검사표
| 게이트 | 주요 검사 | 허용오차 | 실작업 항목 수 |
| :--- | :--- | :--- | :--- |
| G1 형상 | 치수·표고, 폐합, 접합면·겹침, 단면 재투영(원호 R), 체적 산식, 대좌·패드 높이·크기·회전 | ±1 mm, 체적 0.1 % | 172 |
| G2 기둥 철근 | 마크별 본수, 원길이 = L(커플러 포함), 반지름·각도(±0.01°), 띠철근 단 높이, 콘크리트 내부·피복 | ±1 mm | 43 |
| G3 기초 철근 | 본수, 원길이, 층 높이, 치수 사슬 위치, 교대 패턴, 체크무늬, 내부·피복 | ±1 mm | 74 |
| G4 코핑 철근 | 본수, 원길이, 변길이 범위 양끝, L 관례 재현, 단·교대·스터럽 위치, 내부·피복 | ±1 mm | 117 |
| G5 받침부 | 높이(EL.'X'·h3), 크기, 경사(±0.01 %), 회전(±0.01°), 평면 중심, 겹침, 색 | ±1 mm | 184 |
| 공통 | 철근 색(직경별), 기존 객체 지문, 원본 DWG 해시 | 0 | — |
### 4.3 실작업에서 발견된 검사기 결함 (앱 검사 설계 시 반영)
| 결함 | 원인 | 대책 |
| :--- | :--- | :--- |
| 수평 이동을 못 잡음 | 높이·크기만 검사 | 수평 위치·절대 높이 검사를 모두 포함하고, 음성 대조로 확인 |
| 피복이 음수로 나옴 | 부재 간 내부 접합면까지 거리를 잼 | 콘크리트 합집합 외곽면 기준 |
| 객체를 '없음'으로 판정 | 기본 객체 목록이 숨긴 객체를 제외 | 숨김·잠금 포함 열거 |
| 비대칭 철근 위치 오판 | 곡선 매개변수 중간점 사용 | 바운딩·형상 특징점 사용 |
| 갈고리 끝을 높이로 잡음 | 바운딩 최고점 | 수평부 중간점 |
| 교축방향 오판 | 긴 변 = 교축 가정 | 방향(축과의 각도)으로 판별 |
| 길이 FAIL 오판 | 재료표 L에 커플러·겹침 포함 여부 불일치 | 형상별 L 정의를 라이브러리화 |
### 4.4 자기검증 한계
생성과 검증을 같은 주체가 하면 독립 검증이 아닙니다. 앱에서는 ① 기대값 출처 분리 ② 음성 대조 ③ 재로드에 더해, 최종 단계에서 **사람의 육안 확인(뷰어 체크리스트)**을 게이트에 넣어야 합니다. 실작업에서는 헤드리스 3D 캡처가 실패해 육안 확인이 끝까지 미완이었습니다(미해결 #3).
---
## 5. 추가 입력 사항
### 5.1 사용자 결정 입력 (실작업 32건을 유형화)
| 유형 | 입력 항목 | 실작업 사례 | 기본값 제안 |
| :--- | :--- | :--- | :--- |
| 범위 | 모델링 범위(콘크리트만/철근 포함), 말뚝·대좌·패드·조립용 철근(UA1)·교좌면 보강 포함 여부 | 말뚝 포함, 대좌 처음엔 제외 후 추가, UA1 제외, S1·S2 포함 | 도면에 형상 근거가 있는 것은 포함 |
| 표현 | 철근 표현(중심선/관), 커플러·겹침 이음 표현, 색상 규칙 | 관 + 중심선, 커플러는 틈, 직경별 5색 | 관 + 중심선, 이음은 길이만 반영 |
| 충돌 해소 | 도면 값 A/B 중 채택(표기 vs 치수 사슬, 작도 vs 재료표, 치수 vs 작도 점) | 띠철근 @120, 주철근 상단 재료표, 반지름 작도, B12 작도 28, 스터럽 폭 작도, 대좌 높이 표 | 치수 사슬 > 작도. 재료표 수량은 작도로 위치 확인 |
| 추정 확인 | 미작도 부위 대칭·평행 이동, 갈고리 방향, 층 순서, 교대 시작, 경사 방향 | 거더선 1·2, C14 갈고리, S1 위층, ①CYCLE부터, 솔플레이트 −Y | 추정값 제시 + `[추론]` 표시 |
| 기준 | 피복 판정 방식, 층 높이 기준, 높이 기준면(EL.'X') | 작도 위치 ±1 mm, 작도 점 높이 | 작도 위치 기준 |
| 보고 | 보고서 독자, 산출 형식(PDF 위치) | 설계사·발주처, PDF는 출력 폴더 | — |
| 표시 상태 | 콘크리트 숨김 등 저장 시 표시 상태 | P1-FT·P1-CP 숨김 유지 | 모두 표시 |
### 5.2 외부 자료 입력
| 항목 | 필요 이유 |
| :--- | :--- |
| 교량 선형·측점, 교각 중심 측량좌표 | 도면에 없음 → 모델이 로컬 원점(0, 0)에 머묾 (미해결 #4) |
| 설계기준(피복·정착·굽힘 반지름) | 도면 미표기 부위 기본값. 실작업은 박스 앱 기준(D16 이하 2d, D19 이상 3.5d) 사용 |
| 표준 상세(받침·솔플레이트·커플러) | 커플러 형상 미표현 |
| 디자인 지침(색·서체) | 조직 지침 없음 → 수용 기준으로 대체 |
### 5.3 입력 UI 요구사항
- 결정 항목마다 도면 근거(값 A·B와 위치 이미지)와 기본값·영향을 함께 보여 주고 선택하게 한다
- 결정은 파라미터 파일에 기록하고(결정자·일시·근거), 이후 단계와 보고서가 자동으로 참조한다
- 결정을 바꾸면 영향받는 단계만 다시 생성·검사한다
---
## 6. 검토 예외 사항
### 6.1 예정된 FAIL (사용자 결정에 따른 불일치)
| 예외 | 실작업 사례 | 처리 |
| :--- | :--- | :--- |
| 재료표 본수와 작도 본수 불일치를 작도로 채택 | B12 28/34 | FAIL로 기록, 보고서 '불일치' 자동 등재, 게이트는 예외 승인으로 통과 |
| 재료표 치수 대신 작도 폭 채택 | C9~C13 원길이 | 위와 같음, 변하는 치수(범위)는 별도 PASS 검사 |
### 6.2 기록 전용 항목 (판정하지 않고 값만 남김)
| 항목 | 실작업 사례 |
| :--- | :--- |
| 표 반올림 차 | 솔플레이트 T1/T2 ±0.4, 받침 패드 상단 −1 mm |
| 굽힘 반영 곡선길이와 원길이 차 | P3 −493 mm 등 |
| 피복 실측값(기준 미달 포함) | 기초 B5 58.5, S1 40.5(중심 기준 표기) |
| 굽힘 반지름 최소값 미달 | B9 상단 갈고리 R 20.4 < 24 |
### 6.3 검사 대상 외 (현 단계 범위 밖)
- 철근 간 간섭 정량 검사(교차 위치 목록만 기록) — 크로스타이 교차, 다리 겹침, 스터럽과 하면근 동일 높이 등
- 시공성(조립 순서, 이음 위치 적정성)
- 구조 계산 적합성
### 6.4 예외 승인 절차
1. 게이트가 FAIL 목록과 함께 예외 후보(6.1~6.3 유형)를 표시
2. 사용자가 항목별로 예외 승인 또는 수정 요청
3. 승인 내역(사유·결정자·일시)을 결정 기록과 보고서에 자동 반영
4. 예외 없이 남은 FAIL이 있으면 다음 단계 진행 불가
---
## 7. 비기능 요구사항
| 구분 | 요구사항 | 실작업 근거 |
| :--- | :--- | :--- |
| 변환 | ODA/LibreDWG 선택, 변환 실패 시 DXF 직접 입력 | — |
| 3D 커널 | B-rep 불리언(차집합·합집합·교집합), 관(pipe) 생성, 체적 계산 | 버림-말뚝 차집합, 피복 판정용 합집합 |
| 안정성 | 대량 생성 전 형상별 단건 시험, 작업 단위 분할, 장애 시 이어서 실행 | `fitRail=True` 크래시, 세션 종료 2회, 호출 300초 제한 |
| 성능 | 철근 1,500본 규모 생성·검사가 단계당 수 분 이내 | 철근 관 1,000개 체적 계산 300초 초과 |
| 파일 | 모델 50~70 MB(철근 포함) 처리, 경량 뷰어용 메시 별도 | v0.1.07 72 MB |
| 재현성 | 파라미터 → 모델 재생성이 결정적(같은 입력 = 같은 결과), 스크립트·버전 보관 | 지문 비교 0.000 |
| 버전·감사 | 판 올리기(구판 보관), 변경 이력, 결정·검사 로그 | 모델 v0.1.00~v0.1.09 |
| 문서 오염 방지 | 파일 열기 직후 객체 수 확인, 실패 시 새 세션 | 숨김 객체가 남아 3,172객체로 오염 |
| 보안 | 원본 도면 외부 전송 시 승인, 인증 정보 비저장 | 박스 앱 정책 |
---
## 8. 기술 기반 비교·권고
### 8.1 후보 비교
| 기준 | ① 박스 웹앱 확장 | ② Rhino 플러그인·스크립트 | ③ 신규 웹앱 |
| :--- | :--- | :--- | :--- |
| 재사용 | 판독(DXF 파서·치수 사슬·시트 분할), 객체 장부, 캐시·JSON, 보고서·PDF, 뷰어 | 이번 실작업 스크립트 25종 그대로 | 낮음 |
| 3D 품질 | JS 메시 커널 — 관·불리언·피복 판정 정밀도 제한 `[추론]` | B-rep 정밀, 불리언·관 생성 검증됨 | 커널 선택에 따름 |
| 산출물 | 웹 뷰어 + OBJ·JSON | .3dm(BIM 연계 용이) | 선택 |
| 배포 | 서버 1곳, 브라우저 접속 | 사용자별 Rhino 라이선스 필요, 헤드리스 불안정 | 서버 |
| 검증 자동화 | 기존 회귀검사 체계 있음 | 실작업 검사 스크립트 있음 | 신규 |
| 개발 비용 | 중 | 낮음(초기) / 운영 부담 | 높음 |
### 8.2 권고안 — 혼합 구조 `[추론, 의사결정 필요]`
- **판독·규칙·검사 엔진**은 3D 커널과 무관한 Python 라이브러리로 분리합니다. 실작업 스크립트의 extract·verify 계열을 정리하고, 박스 앱 판독 모듈과 통합합니다.
- **형상 생성**은 B-rep 커널에서 수행합니다. 서버형은 Rhino.Compute, 데스크톱은 Rhino 플러그인입니다. 웹 뷰어는 박스 앱 뷰어를 재사용합니다.
- **사용자 입력(5장)·게이트(4장)·예외 승인(6장)**은 웹 UI에서 처리합니다.
- 이유는 두 가지입니다. 실작업에서 정밀 B-rep 연산(불리언·관·체적)이 검사의 핵심이었고, 박스 앱의 판독·UI 자산은 교각에도 재사용 가치가 큽니다.
### 8.3 단계적 로드맵 `[추론]`
| 단계 | 내용 | 완료 기준 |
| :--- | :--- | :--- |
| P0 | 실작업 스크립트 25종을 라이브러리로 재구성, 운곡교 P1 회귀 테스트 고정 | 같은 입력으로 v0.1.09와 지문 일치 |
| P1 | CLI 파이프라인(단계·게이트·결정 파일), 보고서 자동 생성 | 5개 게이트 + 보고서 무인 실행 |
| P2 | 웹 UI(판독 확인, 결정 입력, 예외 승인, 뷰어 육안 확인) | 사용자 1명이 처음부터 끝까지 수행 |
| P3 | 다른 교각 도면 2~3세트로 범용화(형식별 규칙 추가) | 신규 도면에서 규칙 수정 없이 게이트 통과율 측정 |
---
## 9. 리스크·미해결
### 9.1 기술 리스크
| 리스크 | 영향 | 대응 |
| :--- | :--- | :--- |
| 근거 도면이 1세트 | 규칙의 과적합(예: 오프셋 54.3, 층 구성) | P3에서 복수 도면 검증. 고정 상수 금지, 도면에서 매번 추정 |
| 3D 커널 불안정 | 대량 생성 중 크래시 | 단건 시험·분할 실행·재시작 |
| 시각 확인 자동화 실패 | 형상 오류 누락 | 뷰어 육안 체크리스트를 게이트에 포함 |
### 9.2 도면 품질 리스크
실작업 도면에서 지적 22건이 나왔습니다(도면 검토보고서 v0.1.01). 범용 앱은 다음과 같은 도면 편차를 **정상 입력**으로 다뤄야 합니다.
- 분해된 치수
- 절반 작도
- 표기와 치수 사슬의 충돌
- 재료표 빈칸·상동 기호
- 작도와 재료표의 수량·폭 불일치
- 표 정의(EL.'X')의 생략
### 9.3 결정 필요 사항
1. 기술 기반(8.2 권고안 채택 여부)
2. 1차 대상 교각 형식 범위(원형 단주 T형 코핑만 / 확장)
3. 산출 모델 형식(.3dm, IFC 연계 여부)
4. 측량좌표·선형 자료의 입력 방식
---
## 부록
### A. 근거 파일
| 구분 | 경로 |
| :--- | :--- |
| 박스 앱 분석 | `DOC/02_분석/분석_통로박스모델링과정_2026-10-07_v0.1.00.md` |
| 판독서 | `DOC/02_분석/분석_운곡교P1교각도면판독_2026-10-07_v0.1.01.md`, `DOC/02_분석/분석_운곡교P1코핑철근판독_2026-10-07_v0.1.02.md` |
| 설계서 | `DOC/03_설계/설계_운곡교P1교각모델링_2026-10-08_v0.1.10.md` |
| 검증 보고서 | `DOC/05_검증/검증_운곡교P1교각모델_2026-10-08_v0.1.05.md` |
| 도면 검토보고서 | `DOC/06_보고서/보고서_운곡교P1교각도면검토_2026-10-08_v0.1.01.md` |
| 미해결 목록·기술노트 | `context-migration/OPEN_미해결목록.md`, `context-migration/기술노트_DWG판독_Rhino모델링환경.md` |
| 작업 로그 | `prompt/심영표/2026-10-07/`, `prompt/심영표/2026-10-08/` |
### B. 실작업 스크립트 → 앱 모듈 대응
| 앱 모듈 | 실작업 스크립트 (`Code/교각모델링/`) |
| :--- | :--- |
| 입력·변환 | `convert_dwg.py` |
| 판독 | `survey_dxf.py`, `render_region.py`, `extract_outline.py`, `calc_bearing_seat.py`, `extract_bearing_seats.py`, `extract_column_rebar.py`, `extract_footing_rebar.py`, `extract_coping_rebar.py`, `extract_sole_plates.py` |
| 생성 | `build_rhino.py`, `build_rebar_rhino.py`, `build_footing_rebar_rhino.py`, `build_coping_rebar_rhino.py`, `build_bearing_plates_rhino.py`, `apply_rebar_colors.py` |
| 검사(게이트) | `verify_rhino.py`, `verify_rebar.py`, `verify_footing_rebar.py`, `verify_coping_rebar.py`, `verify_bearing_plates.py`, `check_outputs.py` |
| 보고서 | `compile_rebar_schedule.py`, `check_report.py`, `md_to_pdf.py` |
| 데이터 | `params.json`, `판독/*.json`, `검증/*.json` |
@@ -0,0 +1,280 @@
# 교각 모델링 앱 개선 기획서 — 「2. 입력 묶음 확인·확정」 개편
| 버전 | 수정일 | 수정자 | 주요 변경 내용 | 비고 |
| :--- | :--- | :--- | :--- | :--- |
| v0.1.00 | 2026-10-14 | 심영표 | 신규 작성 | 초안 `[미확정]` — D1·D2는 사용자 결정 반영, D3~D6은 결정 대기(7장) |
> 상태: **초안 `[미확정]`**. 코드는 고치지 않았고, 이 문서는 계획만 담습니다. 수치는 2026-10-14 현재 앱(`Code/교각모델링앱/`)과 보호 템플릿 4종을 직접 센 값입니다. `[추론]`은 확인이 필요한 판단입니다.
---
## 1. 개요
### 1.1 목적
앱 교각 화면의 「2. 입력 묶음 확인·확정」을 다음 네 가지가 되도록 고칩니다.
1. 도면 **표 밖 치수**(표 3종에 없는 치수·EL·위치)를 앱이 찾아 후보로 제시합니다.
2. 묶음별 **확인**(도면 대조)을 앱이 수행하고 근거를 보여 줍니다. **확정**은 사람이 합니다(결정 D1).
3. 같은 템플릿 안에서 **바꿔도 되는 값과 바꾸면 안 되는 값**을 구분해 화면에 표시합니다.
4. 지금의 문제점과 해결방법을 정리해 개발 순서를 정합니다.
### 1.2 범위
| 포함 | 제외 |
| :--- | :--- |
| 교각 화면 2절(묶음 표·일괄 확정)과 [표 후보값]·[도면 문자 검색] 화면 | 3절 게이트 실행·승인 흐름(그대로) |
| 값 확인 엔진(`engine/`)에 검사·탐색 추가, 확정 기록(`confirm.json`) 확장 | 새 교각 형식의 생성기·검사기(형식이 다르면 별도 Task) |
| 보호 템플릿의 묶음 분류 메타 | 구조계산·간섭 검사·측량좌표(기존 한계 그대로) |
### 1.3 용어
| 용어 | 뜻 |
| :--- | :--- |
| 묶음 | `params.json`·`expected.json`의 최상위 항목 하나(예: `footing`, `G4`) |
| 확인 | 묶음 값을 도면 원자료와 대조하는 일. 이 기획에서는 **앱이 수행** |
| 확정 | "내가 이 값을 도면과 대조했다"는 **사람의 검토 기록**(해시로 값 변경 시 자동 무효) |
| 표 밖 치수 | 받침 ELEVATION TABLE·솔플레이트 TABLE·철근재료표 3종에 없는 값. 정면·측면·평면·단면의 치수 문자와 작도선·철근 점에서 읽음 |
| 앵커 | 원본 교각 판독에서 "이 값을 도면의 어디서(뷰·레이어·객체·상대 위치·문자 패턴) 읽었는가"를 적은 기록 |
---
## 2. 현황
### 2.1 현재 화면과 동작
- 화면: 교각 화면 **「2. 입력 묶음 확인·확정」** = 묶음 표(파일·묶음·상태·확정자·확정 시각) + **[필요한 묶음 일괄 확정]** 버튼 (`web/templates/pier.html` 18~35행).
- 동작: 사용자가 묶음을 열어 값을 도면과 **눈으로** 대조·수정하고 확정합니다. 일괄 확정은 미확정 필수 묶음에 확정자 이름·시각·해시를 **그대로 찍습니다**(`web/app.py` `confirm_all`: 값 검사 없음).
- 값을 고치면 해시가 달라져 '변경됨 · 재확정 필요'가 되고 승인된 게이트도 '재실행 필요'가 됩니다(이 부분은 잘 동작).
- 표 밖 치수: [도면 문자 검색](글자·정규식)과 [핸들 조회](객체 핸들의 좌표·길이·반지름)로 사람이 찾습니다. EL 문자는 목록만 보여 줍니다(`reader.el_list`). 값 자동 반영은 없습니다.
- 표 3종은 자동 인식해 [표 후보값]에서 현재 값과 비교합니다(`reader.proposals`).
### 2.2 규모 (보호 템플릿 4종, 직접 집계)
| 템플릿 | 전체 묶음 | 참고(확정 불필요) | **확정 필요** | 확정 필요 묶음 합계 용량 | 철근 중심선 목록(손으로 못 고침)의 비중 |
| :--- | --: | --: | --: | --: | --: |
| 운곡교P1형 | 30 | 7 | 23 | 32 KB | 0 % (철근이 params 규칙 값) |
| 산외교울산P1형 | 31 | 5 | 26 | 117 KB | 84 % (`coping_rebar` 98 KB) |
| Ramp-A교P1형 | 30 | 5 | 25 | 135 KB | 89 % (`coping_rebar` 121 KB) |
| 옥성교P1형 | 32 | 5 | 27 | 463 KB | 95 % (`coping_rebar` 301 KB + `footing_rebar` 142 KB) |
---
## 3. 문제점
| # | 문제 | 근거 | 영향 |
| :-- | :--- | :--- | :--- |
| P1 | **일괄 확정이 값을 검사하지 않음.** 확정 = 도장 | `confirm_all`은 `p.confirm()`만 호출. 화면 문구는 "값을 도면·판독서와 대조하셨습니까?" 한 줄 | 확정 기록의 신뢰가 사람의 성실에만 의존. 일괄 버튼이 쉬워 검토가 생략되기 쉬움 |
| P2 | **템플릿 원본 값이 그대로 확정될 수 있음.** 복사 직후 값은 원본 교각의 값 | 사용설명서 5.1 주의문. 묶음 표에 '원본 값 그대로'라는 표시 없음 | 새 도면 대조 없이 확정·승인하면 **원본 교각 모델이 PASS**로 나옴(가장 위험한 실수) |
| P3 | **근거가 안 보임.** 묶음마다 "이 값이 도면 어디서 왔는지"가 없음 | 묶음 표에 출처 열 없음. `_src` 메모는 '확정 불필요(참고)'로 숨음 | 사람이 매번 판독서·도면을 따로 열어 대조 |
| P4 | **표 밖 치수는 전부 수작업.** 판독 스크립트를 교각마다 새로 씀 | 새 형식 1개당 판독·등록 스크립트 약 10개(`Code/옥성P1판독/` 등). 앱은 문자 검색·핸들 조회까지 | 확인 단계 병목. 오타·누락·잘못된 뷰 기준 위험 |
| P5 | **손으로 대조할 수 없는 묶음.** 철근 중심선 목록이 용량의 84~95 % | 2.2절 | 사람이 '열어서 눈으로 확인'할 수 없으므로 확정이 형식적 |
| P6 | **같은 템플릿에서 바꿔도 되는 값과 안 되는 값이 구분되지 않음.** 모두 같은 JSON 편집 화면 | 묶음 편집은 '형식(객체·목록·숫자) 유지'만 강제(`set_section`) | 형식을 정의하는 값(`form`, `builders`, 마크 구성)을 실수로 고쳐 생성기가 깨지거나, 값이 아닌 형식이 달라진 도면을 같은 템플릿으로 밀어 넣음 |
| P7 | **params와 expected가 같은 후보에서 함께 채워짐.** | 사용설명서 5.3 주의문(두 줄 함께 반영 시 검사 무의미) | 검사의 독립성(독립 기대값 원칙)이 사용 습관에 의존 |
| P8 | **확인 상태의 층이 하나.** '미확정 / 확정 / 변경됨' 3값뿐 | `store.section_state` | "앱이 확인했다", "도면에 근거 없음", "불일치 의심"을 표현할 방법이 없음 |
---
## 4. 해결 방향
### 4.1 화면 개편 — 「2. 입력 묶음 확인·확정」
**원칙**: 앱이 확인하고 근거를 보이며, 사람은 근거를 보고 확정합니다(결정 D1). 앱은 **확정하지 않습니다**.
현행 표(파일·묶음·상태·확정자·확정 시각)를 아래 열로 바꿉니다.
| 열 | 내용 |
| :--- | :--- |
| 묶음 | 파일·이름. 클릭하면 편집 화면 |
| 변수 등급 | V1 / V2 / V3 / F (4.4절). 색과 글자 병기 |
| 값 출처 | 표 / 표 밖 치수(도면 위치) / 판독 스크립트 / **템플릿 원본 그대로** / 사람 입력 |
| **앱 확인** | 일치 / 주의 / 불일치 / 근거 없음 / 해당 없음(참고) + 근거 보기 |
| 사람 확정 | 확정·미확정·변경됨, 확정자·시각 |
- 표 위에 한 줄 요약: "필요 27개 중 앱 확인 일치 19 · 주의 4 · 불일치 1 · 근거 없음 3 · 사람 확정 0".
- **[일치 묶음 일괄 확정]**: 앱 확인이 '일치'인 묶음만 대상. 누르기 전 근거표를 한 번 열어야 활성화(D4 결정 대기). '주의·불일치·근거 없음'은 묶음마다 사유를 적어 개별 확정합니다.
- '템플릿 원본 그대로'인 묶음(P2)은 앱 확인이 **'근거 없음(원본값)'**이며 일괄 확정에서 제외됩니다. 새 도면과 대조(표 후보값 반영·표 밖 치수 반영·사람 입력)해 값이 바뀌거나 근거가 붙어야 풀립니다. 새 도면 값이 우연히 원본과 같은 묶음은 '일치(원본과 동일, 근거 있음)'로 구분합니다.
### 4.2 도면 표 밖 치수 찾기 (결정 D2: 규칙 + 템플릿 앵커)
**탐색 규칙 6종** (4개 교각 도면에서 판독 스크립트가 쓴 방법을 앱 규칙으로 일반화합니다)
| 규칙 | 찾는 것 | 방법 | 이미 있는 것 |
| :--- | :--- | :--- | :--- |
| R1 레벨 | 기초·기둥·코핑 EL | `EL.` 문자 + 인접 레벨선·치수선 연결 | `reader.el_list`(목록만) |
| R2 치수 사슬 | `22@120=2.640`, `100 \| 2@100 \| 72@125=9.000` | 문자 패턴 → 치수 사슬로 파싱, 합계가 전체 치수와 맞는지 검산 | `store.chain`(입력용 파서만) |
| R3 철근 점·선 | 철근 위치 목록 | 철근 레이어의 점 심볼·선의 좌표(뷰 기준) → 위치 목록, R2와 대조 | 없음(스크립트) |
| R4 콘크리트 외곽 | 코핑 정면 외곽 점 목록 | 콘크리트 레이어 선 → 중심선(y = 0) 단면 점열 | 없음(스크립트) |
| R5 뷰 기준 | 정면·측면·평면 각각의 기준점·좌표 변환(예: z = 도면 y − 309,179.8) | 뷰 제목 문자('정면도' 등)·축척·EL 문자로 기준 후보 | 없음(스크립트에 상수) |
| R6 표 수량 대조 | 재료표 N과 작도 점·선 개수 | R3 결과 개수 = 재료표 수량이면 근거 강화(옥성교: 스터럽 106 = 39 + 27 + 40) | `reader.proposals` 일부 |
**앵커(템플릿 원본의 판독 위치 기억)**
- 앵커 한 줄 = `{값 경로, 뷰, 도면 레이어, 객체 종류, 기준 대비 상대 위치 또는 문자 패턴, 변환 규칙}`. 원본 교각의 판독 스크립트가 값을 읽을 때 같이 남기는 형식으로 정합니다(소급 추출 범위는 D6).
- 새 도면에서 같은 레이어·뷰·상대 위치·문자 패턴을 따라가 **후보**를 만들고, 규칙 R1~R6의 검산 결과로 **신뢰 등급**을 붙입니다.
| 신뢰 등급 | 조건 | 화면 |
| :--- | :--- | :--- |
| A | 앵커 일치 + 검산(사슬 합계·재료표 수량) 일치 | 후보 + 근거, 반영 가능 |
| B | 앵커 일치, 검산 없음 또는 일부 불일치 | 후보 + 주의 |
| C | 규칙만 일치, 앵커 없음(형식이 다른 도면 등) | 후보 목록만, 반영 시 사유 필요 |
| 없음 | 어떤 규칙도 일치하지 않음 | **'근거 없음'으로 표시, 억지 후보를 만들지 않음** |
- 출력 = [표 후보값]과 같은 비교 표에 '표 밖 치수' 구역을 추가: 현재 값 / 후보 / 도면 위치(D1·좌표·핸들) / 신뢰 등급 / [도면 보기](`dxftools.render`와 같은 확대 이미지). 선택해 반영하는 것은 사람입니다(자동 반영 없음).
- **한계**: 규칙과 앵커는 4개 교각의 도면 틀(운곡교 P1·P2, 산외교 울산 P1, Ramp-A교 P1, 옥성교 P1)에서만 검증됩니다. 틀이 다르면 등급 C 또는 '없음'이 나오는 것이 **정상 동작**입니다.
### 4.3 입력 묶음 확인·확정을 앱이 수행하기 (결정 D1: 앱 확인 + 사람 확정)
**앱 확인 8종** (묶음마다 해당하는 것만 수행, 결과는 일치·주의·불일치·근거 없음·해당 없음)
| # | 확인 | 방법 | 막는 문제 |
| :-- | :--- | :--- | :--- |
| C1 | 출처 있음 | 값마다 표·표 밖 치수·스크립트·사람 입력 중 어디서 왔는지 기록 여부 | P3 |
| C2 | 도면 재대조 | 표 후보값·표 밖 치수 후보와 현재 값 비교 | P4 |
| C3 | 템플릿 원본 잔존 | 현재 값이 템플릿 원본과 같은데 새 도면 근거가 없는 항목 수 | **P2** |
| C4 | 내부 정합 | 치수 사슬 합 = 전체 치수, 본수 = 재료표 N, 직경 = 재료표, 위치 목록 길이 = 사슬 개수 | P5 |
| C5 | params ↔ expected 독립 | expected 값이 params에서 복사됐는지(두 값이 같고 근거도 같은 한 후보) | **P7** |
| C6 | 범위·단위 | EL(m×1000)·mm 단위 상식 범위, 음수·0·순서 역전 | 오타 |
| C7 | 변수 등급 위반 | F(고정) 항목이 템플릿 원본과 달라졌는지, V3(생성 변수)가 생성 스크립트 없이 손으로 고쳐졌는지 | **P6** |
| C8 | 재생성 일치(V3) | 판독 스크립트로 재생성한 목록의 해시와 현재 해시 | P5 |
**상태와 기록 (기존 `confirm.json` 호환)**
```
미확인 → 앱 확인(일치|주의|불일치|근거 없음) → 사람 확정(확정자·시각·근거 해시)
↑ 값이 바뀌면 둘 다 자동 무효(기존 해시 방식 유지)
```
- `confirm.json`의 확정 기록은 그대로 두고, 앱 확인은 `autocheck.json`(묶음 해시·결과·근거 목록·검사 버전·시각)에 별도로 둡니다. **앱 확인은 확정 기록을 만들지 않습니다.**
- 게이트 실행 조건은 지금처럼 '필요 묶음이 사람 확정됨'입니다(변경 없음). 앱 확인은 조건이 아니라 **판단 자료**입니다.
- 보고서 초안 8장(검증 방법)에 "앱 확인 결과 요약"을 추가해 사람 확정 근거가 남게 합니다.
**앱이 스스로 확정하지 않는 이유 (결정 D1 근거)**
1. 이 앱의 검증 구조는 **생성과 검사의 독립**에 기대고 있습니다. 앱이 값을 만들고(후보 반영) 같은 앱이 확정하면 검토자가 없어집니다.
2. 앱 확인 '일치'는 "후보를 만든 규칙과 값이 같다"이지 "도면이 맞다"가 아닙니다(규칙이 틀린 도면에서 거짓 일치 가능).
3. 확정은 책임 기록입니다. 앱이 근거를 모아 주면 사람의 확정이 **빨라지되 의미는 유지**됩니다.
### 4.4 같은 템플릿에서 변수 가능과 불가능 구분
**4단계 분류** (묶음마다, 필요하면 필드마다 표시)
| 등급 | 뜻 | 예 | 앱에서의 처리 |
| :--- | :--- | :--- | :--- |
| **V1 값 변수** | 같은 형식에서 도면마다 달라지는 숫자·문자 값 | 기초·버림 크기, 기둥 지름·높이, 코핑 치수·EL, 말뚝 개수·간격·길이, 받침 표 값, 직경, fck | 화면 편집 가능, 앱 확인 대상 |
| **V2 목록 변수** | 같은 형식에서 도면마다 달라지는 위치·점 목록 | 철근 위치 목록, 치수 사슬, 코핑 정면 외곽 점 | 치수 사슬 입력 가능, 앱 확인 대상(C4) |
| **V3 생성 변수** | 값이 도면마다 다르지만 **판독 스크립트가 만든** 큰 목록 | `coping_rebar.bars`(98~301 KB), `footing_rebar.bars`(옥성) | 화면 직접 편집 **차단**, 판독 스크립트 재실행 안내, 재생성 일치(C8) |
| **F 고정** | 형식을 정의하는 값. 바꾸면 같은 템플릿이 아님 | `scope`, `builders`, `checkers`, 각 묶음의 `form`·`shape`·`kind`, 철근 마크 체계, 게이트 검사기 이름 | 편집 시 **경고(D3: 차단 또는 경고)**, 바꾸면 '형식 변경 — 새 템플릿 필요' 표시 |
**같은 템플릿에서 바꿔도 되는가** (예시, `[추론]` 항목은 시험 후 확정)
| 변경 | 판정 | 이유 |
| :--- | :--- | :--- |
| 기초 크기·두께, 기둥 지름·높이, 말뚝 개수·간격·길이 | 가능(V1) | 생성기가 값으로 받음 |
| 받침 표 값(EL.'X', h1~h3, A·B, S, L·T) | 가능(V1) | 표 후보값으로 이미 지원 |
| 철근 간격·개수·직경(재료표) | 가능(V1·V2) | 치수 사슬 입력 |
| 코핑 상면 단 수·받침 개수 변경 | 조건부 `[추론]` | 검사기 기대값·음성 시험 대상이 개수에 묶여 있음(OPEN #19 A7: 받침 10개 전제) |
| 코핑 철근 중심선 목록 | 스크립트 필요(V3) | 사람이 좌표 수만 개를 고칠 수 없음 |
| 하면 곡선 ↔ 직선 경사, 평면 사각 유무, 기초판 유무, 기둥 단면 형식 | **불가(F)** | 생성기·검사기가 다름 → 다른 템플릿 |
| 철근 형상 종류(U형 → 폐합 등), 마크 체계 | **불가(F)** | 생성기·expected 스키마가 정함 |
| 게이트 범위(`scope`)·생성기·검사기 이름 | **불가(F)** | 형식 정의 |
**새 도면이 이 템플릿으로 가능한지 판별하는 체크리스트** (교각 만들기 화면에 질문으로 제공, 예/아니오)
1. 기초 형식(확대기초+말뚝 / 직접기초 / 기초판 없는 단일 말뚝)이 같은가
2. 기둥 단면 형식(원형 / 기타)이 같은가
3. 코핑 하면 형상(곡선 / 직선 경사 / 나팔형)과 평면 형식(직각 / 사각 / 볼록 끝면)이 같은가
4. 받침 배치 구조(받침마다 대좌 / 앞뒤 공유 대좌)가 같은가
5. 철근 형상 종류가 재료표 형식 열과 같은가
6. 철근 생성 방식(규칙 값 / 철근별 중심선)이 같은가
하나라도 '아니오'면 "같은 템플릿으로 만들 수 없음 — 가장 가까운 템플릿 + 생성기·검사기 추가 필요"로 안내하고, 억지로 진행하면 묶음 표에 '형식 불일치 경고'를 상시 표시합니다.
**구현**: 보호 템플릿에 `variability.json`(묶음·필드 경로 → 등급)을 추가합니다. 편집 화면과 앱 확인 C7이 이 파일을 읽습니다. 4개 템플릿 분류는 부록 A.
---
## 5. 문제점과 해결방법
| 문제 | 해결방법 | 기대 효과 | 난이도 | 우선순위 |
| :--- | :--- | :--- | :--: | :--: |
| P1 일괄 확정이 검사 안 함 | 앱 확인(C1~C8) + '일치 묶음만 일괄 확정' + 근거표 열람 조건(D4) | 일괄 확정이 도장이 아닌 근거 기반 확정이 됨 | 중 | 1 |
| P2 템플릿 원본값 확정 위험 | C3 '템플릿 원본 잔존' + 일괄 확정 제외 + 묶음 표에 '원본값' 표시 | 원본 모델이 PASS로 나오는 사고 방지 | **하** | **1** |
| P3 근거가 안 보임 | 값 출처 열 + 근거 보기(도면 위치·표 행·스크립트·사슬 검산) | 판독서를 따로 열 필요 감소 | 중 | 2 |
| P4 표 밖 치수 수작업 | 규칙 R1~R6 + 앵커 + 신뢰 등급 후보 표(결정 D2) | 새 도면 확인 시간 단축, 오타·누락 감소 | **상** | 3 |
| P5 손으로 대조 불가 | V3 분류 + C4·C8(내부 정합·재생성 일치) + 편집 차단 | 확정이 '요약된 검산 결과'에 근거 | 중 | 2 |
| P6 변수·고정 미구분 | 4단계 분류 + `variability.json` + 판별 체크리스트 + 형식 불일치 경고 | 형식이 다른 도면을 같은 템플릿에 밀어 넣는 실수 방지 | 중 | 2 |
| P7 params·expected 동시 채움 | C5(독립성) + 후보 반영 화면에서 params 줄 별도 선택 안내 강화 | 검사 독립성 유지 | 하 | 2 |
| P8 확인 상태 1층 | `autocheck.json` 별도 층(앱 확인), 확정 기록은 불변 | "앱이 확인", "근거 없음", "불일치 의심" 표현 가능 | 하 | 1 |
---
## 6. 구현 계획
| 단계 | 내용 | 주요 변경 | 시험·판정 |
| :--- | :--- | :--- | :--- |
| **A 분류·표시** | `variability.json` 작성(4템플릿), 묶음 표에 변수 등급·값 출처 열, '템플릿 원본 그대로' 표시(C3) | `web/templates/pier.html`, `web/store.py`, `data/_템플릿/*/variability.json` | 4템플릿에서 분류 100 %, 새 교각(템플릿 복사 직후) 전 묶음이 '원본값' 표시 |
| **B 앱 확인 엔진** | C1~C8 구현, `autocheck.json`, 근거표 화면, '일치 묶음 일괄 확정'(D4), 보고서 8장 요약 | `engine/verify.py`(신설), `web/app.py`, 템플릿, `engine/report.py` | 5개 교각 자기 데이터 전 묶음 '일치'(거짓 불일치 0), 값을 일부러 바꾸면 '불일치'(거짓 일치 0, 음성 시험), 기존 회귀 9종 FAIL 0 |
| **C 표 밖 치수 탐색** | 규칙 R1~R6, 앵커 형식·추출 도구, 후보 표 구역, 도면 보기 | `engine/reader.py`, `engine/anchors.py`(신설), 판독 스크립트 앵커 기록 | 4개 도면에서 원본 params 값 재현율(등급 A·B 비율)·거짓 후보 0건 집계, 틀이 다른 도면에서 '없음' 정상 표시 |
| **D 통합·문서** | 일괄 확정 문구·흐름, 사용설명서 개정(5.3·5.4·9장), 회귀 전체 | 사용설명서 v0.2.08, 시험 `tools/test_confirm_check.py` | 웹 E2E 포함 전체 회귀 PASS |
- 각 단계는 **정지·승인**(사용자) 후 진행합니다(HITL).
- 확정·게이트 승인은 어느 단계에서도 앱이 대신하지 않습니다. 시험은 별도 시험 폴더에서만 확정 흐름을 실행합니다(실제 교각 데이터 보호: 웹 일괄 실행이 Ramp-A·울산·옥성 게이트 보고서를 '재로드 안 함' 상태로 덮어쓴 일이 있음, 원인(시험 또는 앱 사용) 미확인, OPEN #25).
- 수용 기준(착수 전 확정): ① 모든 필수 묶음에 앱 확인 상태가 있다 ② '근거 없음'을 숨기지 않는다 ③ 거짓 일치 0·거짓 불일치 0(4개 도면 기준) ④ 앱이 확정 기록을 만들지 않는다 ⑤ 기존 시험 FAIL 0.
---
## 7. 리스크·결정 사항
### 7.1 리스크
| # | 리스크 | 대응 |
| :-- | :--- | :--- |
| R1 | 앱 확인 '일치'를 "도면이 맞다"로 오해 | 근거 보기에서 '무엇과 무엇이 같은가'를 문장으로 표시, 신뢰 등급 병기 |
| R2 | 규칙·앵커가 4개 도면에 과적합 | 등급 C·'없음'을 정상으로 표시, 새 형식 도면마다 앵커 추가 |
| R3 | 앵커 소급 추출 비용(원본 4교각) | D6 결정(신규부터 vs 소급) |
| R4 | 앱 확인 규칙이 생성 스크립트와 같은 논리여서 순환 | 도면 원자료(DXF·문자 색인)를 직접 읽는 경로만 사용, 스크립트 재생성 일치(C8)는 '독립성 낮음'으로 표시 |
| R5 | 일괄 확정이 다시 도장화 | 일치만 대상, 근거표 열람 조건, 주의·불일치는 사유 필수 |
| R6 | 변수 등급 오분류 | `variability.json`은 템플릿 원본 교각 담당자가 확정, 분류 변경은 별도 Task |
### 7.2 결정 사항
| # | 내용 | 상태 | 권고 |
| :-- | :--- | :--- | :--- |
| D1 | 확정 주체 | **결정 2026-10-14 / 심영표: 앱 확인 + 사람 확정** | — |
| D2 | 표 밖 치수 탐색 방식 | **결정 2026-10-14 / 심영표: 규칙 + 템플릿 앵커** | — |
| D3 | F(고정) 항목 편집 시 처리 | **결정 대기** | 경고 + 사유 입력(차단은 생성기 개발자 모드에서만) |
| D4 | 일괄 확정 조건 | **결정 대기** | '일치' 묶음만, 근거표를 한 번 열어야 활성화 |
| D5 | 착수 범위·순서 | **결정 대기** | A → B 먼저(위험 큰 P1·P2 해소), C는 B 검증 뒤 |
| D6 | 앵커 소급 추출 | **결정 대기** | 옥성교·Ramp-A(최근, 판독 스크립트 최신)부터, 나머지는 필요 시 |
---
## 부록 A. 보호 템플릿 묶음 분류 (등급 초안, 담당자 확정 전)
| 묶음 | 운곡교P1형 | 울산형 | Ramp-A형 | 옥성교형 |
| :--- | :-: | :-: | :-: | :-: |
| `pier_id` | F | F | F | F |
| `scope`·`builders`·`checkers`·`negative` (실행 설정) | F (없음=기본) | F | F | F |
| `lean_concrete`·`footing`·`column`·`materials` | V1 | V1 | V1(기초 없음) | V1 |
| `piles` / `pile` | V1 | — | V1(`pile` 구간) | V1 |
| `coping` | V1 + 점 목록 V2 (`form` F) | V1 + V2 (`form` F) | V1 + V2 (`form` F) | V1 + V2 (`form`·`skew_deg` 구조는 F) |
| `bearing_seats`·`sole_plates` | V1 + 위치 V2 (대좌 구조 F) | V1 + V2 | V1 + V2 | V1 + V2 (대좌 공유 구조 F) |
| `rebar_marks` | 직경 V1, 마크 체계 F | 동일 | 동일 | 동일 |
| `column_rebar` | V1·V2 (`form` F) | V1·V2 | V1·V2 | V1·V2 |
| `footing_rebar` | V1·V2 | V1·V2 | — (`shaft_rebar` V1·V2) | **V3** (중심선 목록) |
| `coping_rebar` | V1·V2 (규칙 값) | **V3** | **V3** | **V3** |
| `expected.G1~G5` | V1·V2 (**params와 독립 입력**) | 동일 | 동일 | 동일 |
| `expected.planned_fail`·`rebar_palette` | 사람 결정 기록(교각별) | 동일 | 동일 | 동일 |
| `display`·`_schema`·`_meta`·`*_src`·`hitl_decisions*` | F / 참고 | 동일 | 동일 | 동일 |
> 이 표는 묶음 수준의 초안입니다. 필드 수준 분류(`variability.json`)는 A단계에서 템플릿마다 작성하고 담당자가 확정합니다.
## 부록 B. 근거 파일
| 구분 | 경로 |
| :--- | :--- |
| 화면·동작 | `Code/교각모델링앱/web/templates/pier.html`, `web/app.py`(`confirm_all`, `unconfirmed`), `web/store.py`(`GATE_SECTIONS`, `section_state`, `confirm`, `set_section`) |
| 표 판독·후보 | `engine/reader.py`(`table_candidates`, `field_candidates`, `proposals`, `el_list`, `entity_info`) |
| 사용 방법 | `DOC/04_실행/실행_T형교각모델링앱사용설명서_2026-10-14_v0.2.07.md` §4~5, §9 |
| 일반화 이력 | `context-migration/OPEN_미해결목록.md` #19·#23·#24·#25, `DOC/02_분석/분석_운곡교P2교각도면판독_2026-10-12_v0.1.00.md` §7 |
| 표 밖 치수 판독 사례 | `Code/옥성P1판독/extract_g*.py`, `DOC/02_분석/분석_옥성교P1교각도면판독_2026-10-14_v0.1.07.md` |
@@ -0,0 +1,680 @@
# 교각 모델링 앱 개선 기획서 — 「2. 입력 묶음 확인·확정」 개편
| 버전 | 수정일 | 수정자 | 주요 변경 내용 | 비고 |
| :--- | :--- | :--- | :--- | :--- |
| v0.1.00 | 2026-10-14 | 심영표 | 신규 작성 | 초안 `[미확정]` — D1~D6 결정 반영(7.2절) |
| v0.1.01 | 2026-10-15 | 심영표 | 7.2절 D3~D6을 권고안대로 결정 반영 | 사용자 지시 "권고안대로" |
| v0.1.02 | 2026-10-15 | 심영표 | 4.4절 템플릿 구분 기준 재정의(각도=값, 기초=말뚝 유무, 도면 먼저 추천) | 사용자 결정 3건(OPEN #30 ①②④) |
| v0.1.03 | 2026-10-16 | 심영표 | A단계(분류·표시) 구현 결과 6.1절 추가 | 사용자 "개편 구현 진행" 지시; B단계는 사용자 승인 후 |
| v0.1.04 | 2026-10-16 | 심영표 | B단계(앱 확인 엔진·근거표·일치 묶음 일괄 확정) 구현 결과 6.2절 추가, Prism 적용 시점 결정 반영 | 사용자 "B단계 착수" + "Prism은 B단계 새 화면부터, 기존 화면 교체는 D단계" |
| v0.1.05 | 2026-10-19 | 심영표 | 이전 방식 일괄 확정 제거 반영, C단계 1차(앵커 학습·따라가기, R1·R2·R5) 구현 결과 6.3절 추가 | 사용자 "이전방식 일괄 확정은 지금 제거하고, C단계 착수" |
| v0.1.06 | 2026-10-19 | 심영표 | C단계 2차 결과 6.4절 추가(표 값 교차 확인, 운곡교 P3·P4 실사용 확인, R3·R4 기하 앵커 시험 결과 = 미채택) | 사용자 "C단계 2차 착수, 운곡교 P3·P4에 앵커를 불러와 실제 후보 확인" |
| v0.1.07 | 2026-10-19 | 심영표 | D단계(전 화면 Prism 교체, 사용설명서 v0.2.12, 전체 회귀, 운곡교 P3·P4 A등급 반영) 결과 6.5절 추가 | 사용자 "D단계 착수, 운곡교 P3·P4 후보 반영" |
| v0.1.08 | 2026-10-20 | 심영표 | 운곡교 P3·P4 실사용 결과와 앱 개선점 6.6절 추가(OPEN #34·#35) | 사용자 "앱의 개선점을 알려줘" → "진행해줘" |
| v0.1.09 | 2026-10-20 | 심영표 | 개선 ②③④⑥ 구현 결과 6.7절 추가 | 사용자 "#34의 2·3·4·6번 순으로 진행" |
| v0.1.10 | 2026-10-20 | 심영표 | 개선 ①⑤ 구현 결과 6.8절 추가 | 사용자 "①(읽기 도구 통합), ⑤(일괄 실행 중단 표시)를 진행해줘" |
| v0.1.11 | 2026-10-20 | 심영표 | 검토 요약 확정(한 화면 검토 + 한 번 확정) 6.9절 추가 | 사용자 "확정 판정이 오래 걸리고 확인하기 어렵다 → 앱이 일괄 처리, 사용자는 일괄 검토" 제안 → 권고안 진행 |
| v0.1.12 | 2026-10-20 | 심영표 | 6.9절에 후속 변경 추가(도면에서 읽기에 표 값 통합, 검토 요약 모두 체크·주의 표시) | 사용자 "템플릿 원본 그대로도 모두 도면에서 읽기로 바꾸고, 미확정도 일괄 확정" |
| v0.1.13 | 2026-10-20 | 심영표 | 운곡교 P4 복구 결과와 앱 수정 계획 6.10절 추가 | 사용자 "앱을 수정계획을 정리해줘" (P4 결정 소실 사고 기반) |
| v0.1.14 | 2026-10-20 | 심영표 | W4(원본 잔존 검사 확대) 상세 계획 6.11절 추가, 실측 근거 스크립트 보관 | 사용자 "「원본 잔존 검사 확대」에 대한 계획을 세워줘" |
> 상태: **초안 `[미확정]`**. 코드는 고치지 않았고, 이 문서는 계획만 담습니다. 수치는 2026-10-14 현재 앱(`Code/교각모델링앱/`)과 보호 템플릿 4종을 직접 센 값입니다. `[추론]`은 확인이 필요한 판단입니다.
---
## 1. 개요
### 1.1 목적
앱 교각 화면의 「2. 입력 묶음 확인·확정」을 다음 네 가지가 되도록 고칩니다.
1. 도면 **표 밖 치수**(표 3종에 없는 치수·EL·위치)를 앱이 찾아 후보로 제시합니다.
2. 묶음별 **확인**(도면 대조)을 앱이 수행하고 근거를 보여 줍니다. **확정**은 사람이 합니다(결정 D1).
3. 같은 템플릿 안에서 **바꿔도 되는 값과 바꾸면 안 되는 값**을 구분해 화면에 표시합니다.
4. 지금의 문제점과 해결방법을 정리해 개발 순서를 정합니다.
### 1.2 범위
| 포함 | 제외 |
| :--- | :--- |
| 교각 화면 2절(묶음 표·일괄 확정)과 [표 후보값]·[도면 문자 검색] 화면 | 3절 게이트 실행·승인 흐름(그대로) |
| 값 확인 엔진(`engine/`)에 검사·탐색 추가, 확정 기록(`confirm.json`) 확장 | 새 교각 형식의 생성기·검사기(형식이 다르면 별도 Task) |
| 보호 템플릿의 묶음 분류 메타 | 구조계산·간섭 검사·측량좌표(기존 한계 그대로) |
### 1.3 용어
| 용어 | 뜻 |
| :--- | :--- |
| 묶음 | `params.json`·`expected.json`의 최상위 항목 하나(예: `footing`, `G4`) |
| 확인 | 묶음 값을 도면 원자료와 대조하는 일. 이 기획에서는 **앱이 수행** |
| 확정 | "내가 이 값을 도면과 대조했다"는 **사람의 검토 기록**(해시로 값 변경 시 자동 무효) |
| 표 밖 치수 | 받침 ELEVATION TABLE·솔플레이트 TABLE·철근재료표 3종에 없는 값. 정면·측면·평면·단면의 치수 문자와 작도선·철근 점에서 읽음 |
| 앵커 | 원본 교각 판독에서 "이 값을 도면의 어디서(뷰·레이어·객체·상대 위치·문자 패턴) 읽었는가"를 적은 기록 |
---
## 2. 현황
### 2.1 현재 화면과 동작
- 화면: 교각 화면 **「2. 입력 묶음 확인·확정」** = 묶음 표(파일·묶음·상태·확정자·확정 시각) + **[필요한 묶음 일괄 확정]** 버튼 (`web/templates/pier.html` 18~35행).
- 동작: 사용자가 묶음을 열어 값을 도면과 **눈으로** 대조·수정하고 확정합니다. 일괄 확정은 미확정 필수 묶음에 확정자 이름·시각·해시를 **그대로 찍습니다**(`web/app.py` `confirm_all`: 값 검사 없음).
- 값을 고치면 해시가 달라져 '변경됨 · 재확정 필요'가 되고 승인된 게이트도 '재실행 필요'가 됩니다(이 부분은 잘 동작).
- 표 밖 치수: [도면 문자 검색](글자·정규식)과 [핸들 조회](객체 핸들의 좌표·길이·반지름)로 사람이 찾습니다. EL 문자는 목록만 보여 줍니다(`reader.el_list`). 값 자동 반영은 없습니다.
- 표 3종은 자동 인식해 [표 후보값]에서 현재 값과 비교합니다(`reader.proposals`).
### 2.2 규모 (보호 템플릿 4종, 직접 집계)
| 템플릿 | 전체 묶음 | 참고(확정 불필요) | **확정 필요** | 확정 필요 묶음 합계 용량 | 철근 중심선 목록(손으로 못 고침)의 비중 |
| :--- | --: | --: | --: | --: | --: |
| 운곡교P1형 | 30 | 7 | 23 | 32 KB | 0 % (철근이 params 규칙 값) |
| 산외교울산P1형 | 31 | 5 | 26 | 117 KB | 84 % (`coping_rebar` 98 KB) |
| Ramp-A교P1형 | 30 | 5 | 25 | 135 KB | 89 % (`coping_rebar` 121 KB) |
| 옥성교P1형 | 32 | 5 | 27 | 463 KB | 95 % (`coping_rebar` 301 KB + `footing_rebar` 142 KB) |
---
## 3. 문제점
| # | 문제 | 근거 | 영향 |
| :-- | :--- | :--- | :--- |
| P1 | **일괄 확정이 값을 검사하지 않음.** 확정 = 도장 | `confirm_all`은 `p.confirm()`만 호출. 화면 문구는 "값을 도면·판독서와 대조하셨습니까?" 한 줄 | 확정 기록의 신뢰가 사람의 성실에만 의존. 일괄 버튼이 쉬워 검토가 생략되기 쉬움 |
| P2 | **템플릿 원본 값이 그대로 확정될 수 있음.** 복사 직후 값은 원본 교각의 값 | 사용설명서 5.1 주의문. 묶음 표에 '원본 값 그대로'라는 표시 없음 | 새 도면 대조 없이 확정·승인하면 **원본 교각 모델이 PASS**로 나옴(가장 위험한 실수) |
| P3 | **근거가 안 보임.** 묶음마다 "이 값이 도면 어디서 왔는지"가 없음 | 묶음 표에 출처 열 없음. `_src` 메모는 '확정 불필요(참고)'로 숨음 | 사람이 매번 판독서·도면을 따로 열어 대조 |
| P4 | **표 밖 치수는 전부 수작업.** 판독 스크립트를 교각마다 새로 씀 | 새 형식 1개당 판독·등록 스크립트 약 10개(`Code/옥성P1판독/` 등). 앱은 문자 검색·핸들 조회까지 | 확인 단계 병목. 오타·누락·잘못된 뷰 기준 위험 |
| P5 | **손으로 대조할 수 없는 묶음.** 철근 중심선 목록이 용량의 84~95 % | 2.2절 | 사람이 '열어서 눈으로 확인'할 수 없으므로 확정이 형식적 |
| P6 | **같은 템플릿에서 바꿔도 되는 값과 안 되는 값이 구분되지 않음.** 모두 같은 JSON 편집 화면 | 묶음 편집은 '형식(객체·목록·숫자) 유지'만 강제(`set_section`) | 형식을 정의하는 값(`form`, `builders`, 마크 구성)을 실수로 고쳐 생성기가 깨지거나, 값이 아닌 형식이 달라진 도면을 같은 템플릿으로 밀어 넣음 |
| P7 | **params와 expected가 같은 후보에서 함께 채워짐.** | 사용설명서 5.3 주의문(두 줄 함께 반영 시 검사 무의미) | 검사의 독립성(독립 기대값 원칙)이 사용 습관에 의존 |
| P8 | **확인 상태의 층이 하나.** '미확정 / 확정 / 변경됨' 3값뿐 | `store.section_state` | "앱이 확인했다", "도면에 근거 없음", "불일치 의심"을 표현할 방법이 없음 |
---
## 4. 해결 방향
### 4.1 화면 개편 — 「2. 입력 묶음 확인·확정」
**원칙**: 앱이 확인하고 근거를 보이며, 사람은 근거를 보고 확정합니다(결정 D1). 앱은 **확정하지 않습니다**.
현행 표(파일·묶음·상태·확정자·확정 시각)를 아래 열로 바꿉니다.
| 열 | 내용 |
| :--- | :--- |
| 묶음 | 파일·이름. 클릭하면 편집 화면 |
| 변수 등급 | V1 / V2 / V3 / F (4.4절). 색과 글자 병기 |
| 값 출처 | 표 / 표 밖 치수(도면 위치) / 판독 스크립트 / **템플릿 원본 그대로** / 사람 입력 |
| **앱 확인** | 일치 / 주의 / 불일치 / 근거 없음 / 해당 없음(참고) + 근거 보기 |
| 사람 확정 | 확정·미확정·변경됨, 확정자·시각 |
- 표 위에 한 줄 요약: "필요 27개 중 앱 확인 일치 19 · 주의 4 · 불일치 1 · 근거 없음 3 · 사람 확정 0".
- **[일치 묶음 일괄 확정]**: 앱 확인이 '일치'인 묶음만 대상. 누르기 전 근거표를 한 번 열어야 활성화(D4 결정 대기). '주의·불일치·근거 없음'은 묶음마다 사유를 적어 개별 확정합니다.
- '템플릿 원본 그대로'인 묶음(P2)은 앱 확인이 **'근거 없음(원본값)'**이며 일괄 확정에서 제외됩니다. 새 도면과 대조(표 후보값 반영·표 밖 치수 반영·사람 입력)해 값이 바뀌거나 근거가 붙어야 풀립니다. 새 도면 값이 우연히 원본과 같은 묶음은 '일치(원본과 동일, 근거 있음)'로 구분합니다.
### 4.2 도면 표 밖 치수 찾기 (결정 D2: 규칙 + 템플릿 앵커)
**탐색 규칙 6종** (4개 교각 도면에서 판독 스크립트가 쓴 방법을 앱 규칙으로 일반화합니다)
| 규칙 | 찾는 것 | 방법 | 이미 있는 것 |
| :--- | :--- | :--- | :--- |
| R1 레벨 | 기초·기둥·코핑 EL | `EL.` 문자 + 인접 레벨선·치수선 연결 | `reader.el_list`(목록만) |
| R2 치수 사슬 | `22@120=2.640`, `100 \| 2@100 \| 72@125=9.000` | 문자 패턴 → 치수 사슬로 파싱, 합계가 전체 치수와 맞는지 검산 | `store.chain`(입력용 파서만) |
| R3 철근 점·선 | 철근 위치 목록 | 철근 레이어의 점 심볼·선의 좌표(뷰 기준) → 위치 목록, R2와 대조 | 없음(스크립트) |
| R4 콘크리트 외곽 | 코핑 정면 외곽 점 목록 | 콘크리트 레이어 선 → 중심선(y = 0) 단면 점열 | 없음(스크립트) |
| R5 뷰 기준 | 정면·측면·평면 각각의 기준점·좌표 변환(예: z = 도면 y − 309,179.8) | 뷰 제목 문자('정면도' 등)·축척·EL 문자로 기준 후보 | 없음(스크립트에 상수) |
| R6 표 수량 대조 | 재료표 N과 작도 점·선 개수 | R3 결과 개수 = 재료표 수량이면 근거 강화(옥성교: 스터럽 106 = 39 + 27 + 40) | `reader.proposals` 일부 |
**앵커(템플릿 원본의 판독 위치 기억)**
- 앵커 한 줄 = `{값 경로, 뷰, 도면 레이어, 객체 종류, 기준 대비 상대 위치 또는 문자 패턴, 변환 규칙}`. 원본 교각의 판독 스크립트가 값을 읽을 때 같이 남기는 형식으로 정합니다(소급 추출 범위는 D6).
- 새 도면에서 같은 레이어·뷰·상대 위치·문자 패턴을 따라가 **후보**를 만들고, 규칙 R1~R6의 검산 결과로 **신뢰 등급**을 붙입니다.
| 신뢰 등급 | 조건 | 화면 |
| :--- | :--- | :--- |
| A | 앵커 일치 + 검산(사슬 합계·재료표 수량) 일치 | 후보 + 근거, 반영 가능 |
| B | 앵커 일치, 검산 없음 또는 일부 불일치 | 후보 + 주의 |
| C | 규칙만 일치, 앵커 없음(형식이 다른 도면 등) | 후보 목록만, 반영 시 사유 필요 |
| 없음 | 어떤 규칙도 일치하지 않음 | **'근거 없음'으로 표시, 억지 후보를 만들지 않음** |
- 출력 = [표 후보값]과 같은 비교 표에 '표 밖 치수' 구역을 추가: 현재 값 / 후보 / 도면 위치(D1·좌표·핸들) / 신뢰 등급 / [도면 보기](`dxftools.render`와 같은 확대 이미지). 선택해 반영하는 것은 사람입니다(자동 반영 없음).
- **한계**: 규칙과 앵커는 4개 교각의 도면 틀(운곡교 P1·P2, 산외교 울산 P1, Ramp-A교 P1, 옥성교 P1)에서만 검증됩니다. 틀이 다르면 등급 C 또는 '없음'이 나오는 것이 **정상 동작**입니다.
### 4.3 입력 묶음 확인·확정을 앱이 수행하기 (결정 D1: 앱 확인 + 사람 확정)
**앱 확인 8종** (묶음마다 해당하는 것만 수행, 결과는 일치·주의·불일치·근거 없음·해당 없음)
| # | 확인 | 방법 | 막는 문제 |
| :-- | :--- | :--- | :--- |
| C1 | 출처 있음 | 값마다 표·표 밖 치수·스크립트·사람 입력 중 어디서 왔는지 기록 여부 | P3 |
| C2 | 도면 재대조 | 표 후보값·표 밖 치수 후보와 현재 값 비교 | P4 |
| C3 | 템플릿 원본 잔존 | 현재 값이 템플릿 원본과 같은데 새 도면 근거가 없는 항목 수 | **P2** |
| C4 | 내부 정합 | 치수 사슬 합 = 전체 치수, 본수 = 재료표 N, 직경 = 재료표, 위치 목록 길이 = 사슬 개수 | P5 |
| C5 | params ↔ expected 독립 | expected 값이 params에서 복사됐는지(두 값이 같고 근거도 같은 한 후보) | **P7** |
| C6 | 범위·단위 | EL(m×1000)·mm 단위 상식 범위, 음수·0·순서 역전 | 오타 |
| C7 | 변수 등급 위반 | F(고정) 항목이 템플릿 원본과 달라졌는지, V3(생성 변수)가 생성 스크립트 없이 손으로 고쳐졌는지 | **P6** |
| C8 | 재생성 일치(V3) | 판독 스크립트로 재생성한 목록의 해시와 현재 해시 | P5 |
**상태와 기록 (기존 `confirm.json` 호환)**
```
미확인 → 앱 확인(일치|주의|불일치|근거 없음) → 사람 확정(확정자·시각·근거 해시)
↑ 값이 바뀌면 둘 다 자동 무효(기존 해시 방식 유지)
```
- `confirm.json`의 확정 기록은 그대로 두고, 앱 확인은 `autocheck.json`(묶음 해시·결과·근거 목록·검사 버전·시각)에 별도로 둡니다. **앱 확인은 확정 기록을 만들지 않습니다.**
- 게이트 실행 조건은 지금처럼 '필요 묶음이 사람 확정됨'입니다(변경 없음). 앱 확인은 조건이 아니라 **판단 자료**입니다.
- 보고서 초안 8장(검증 방법)에 "앱 확인 결과 요약"을 추가해 사람 확정 근거가 남게 합니다.
**앱이 스스로 확정하지 않는 이유 (결정 D1 근거)**
1. 이 앱의 검증 구조는 **생성과 검사의 독립**에 기대고 있습니다. 앱이 값을 만들고(후보 반영) 같은 앱이 확정하면 검토자가 없어집니다.
2. 앱 확인 '일치'는 "후보를 만든 규칙과 값이 같다"이지 "도면이 맞다"가 아닙니다(규칙이 틀린 도면에서 거짓 일치 가능).
3. 확정은 책임 기록입니다. 앱이 근거를 모아 주면 사람의 확정이 **빨라지되 의미는 유지**됩니다.
### 4.4 같은 템플릿에서 변수 가능과 불가능 구분
**4단계 분류** (묶음마다, 필요하면 필드마다 표시)
| 등급 | 뜻 | 예 | 앱에서의 처리 |
| :--- | :--- | :--- | :--- |
| **V1 값 변수** | 같은 형식에서 도면마다 달라지는 숫자·문자 값 | 기초·버림 크기, 기둥 지름·높이, 코핑 치수·EL, 말뚝 개수·간격·길이, 받침 표 값, 직경, fck | 화면 편집 가능, 앱 확인 대상 |
| **V2 목록 변수** | 같은 형식에서 도면마다 달라지는 위치·점 목록 | 철근 위치 목록, 치수 사슬, 코핑 정면 외곽 점 | 치수 사슬 입력 가능, 앱 확인 대상(C4) |
| **V3 생성 변수** | 값이 도면마다 다르지만 **판독 스크립트가 만든** 큰 목록 | `coping_rebar.bars`(98~301 KB), `footing_rebar.bars`(옥성) | 화면 직접 편집 **차단**, 판독 스크립트 재실행 안내, 재생성 일치(C8) |
| **F 고정** | 형식을 정의하는 값. 바꾸면 같은 템플릿이 아님 | `scope`, `builders`, `checkers`, 각 묶음의 `form`·`shape`·`kind`, 철근 마크 체계, 게이트 검사기 이름 | 편집 시 **경고(D3: 차단 또는 경고)**, 바꾸면 '형식 변경 — 새 템플릿 필요' 표시 |
**같은 템플릿에서 바꿔도 되는가** (예시, `[추론]` 항목은 시험 후 확정)
| 변경 | 판정 | 이유 |
| :--- | :--- | :--- |
| 기초 크기·두께, 기둥 지름·높이, 말뚝 개수·간격·길이 | 가능(V1) | 생성기가 값으로 받음 |
| 받침 표 값(EL.'X', h1~h3, A·B, S, L·T) | 가능(V1) | 표 후보값으로 이미 지원 |
| 철근 간격·개수·직경(재료표) | 가능(V1·V2) | 치수 사슬 입력 |
| 코핑 상면 단 수·받침 개수 변경 | 조건부 `[추론]` | 검사기 기대값·음성 시험 대상이 개수에 묶여 있음(OPEN #19 A7: 받침 10개 전제) |
| 코핑 철근 중심선 목록 | 스크립트 필요(V3) | 사람이 좌표 수만 개를 고칠 수 없음 |
| 평면 skew(사각)·받침 회전·기초 회전 등 **각도** | 가능(V1) `[2026-10-15 결정]` | 각도는 템플릿이 아닌 값. 옥성교(skew 18°)·새재1교(0°) 검사기는 하나로 통합(선행: 두 교각 시험 재실행) |
| 말뚝 개수·간격·배치 | 가능(V1·V2) | 말뚝 **유무**만 형식 |
| 하면 곡선 ↔ 직선 경사, 기초의 말뚝 유무, 기둥 단면 형식, 대좌 공유 여부 | **불가(F)** | 생성기·검사기가 다름 → 다른 템플릿 |
| 철근 형상 종류(U형 → 폐합 등), 마크 체계 | **불가(F)** | 생성기·expected 스키마가 정함 |
| 게이트 범위(`scope`)·생성기·검사기 이름 | **불가(F)** | 형식 정의 |
**새 도면이 이 템플릿으로 가능한지 판별하는 체크리스트** (교각 만들기 화면에 질문으로 제공, 예/아니오)
1. 기초 형식(말뚝 유무: 기초판+말뚝 / 기초판만 / 기초판 없는 단일 말뚝)이 같은가
2. 기둥 단면 형식(원형 / 기타)이 같은가
3. 코핑 하면 형상(곡선 / 직선 경사 / 나팔형)이 같은가. 평면의 사각·직각과 각도는 **문항이 아니라 값 확인**(skew·회전 각도 입력)으로 옮긴다 `[2026-10-15 결정]`
4. 받침 배치 구조(받침마다 대좌 / 앞뒤 공유 대좌)가 같은가
5. 철근 형상 종류가 재료표 형식 열과 같은가
6. 철근 생성 방식(규칙 값 / 철근별 중심선)이 같은가
하나라도 '아니오'면 "같은 템플릿으로 만들 수 없음 — 가장 가까운 템플릿 + 생성기·검사기 추가 필요"로 안내하고, 억지로 진행하면 묶음 표에 '형식 불일치 경고'를 상시 표시합니다.
**도면 먼저 추천 (2026-10-15 결정)**: 교각 만들기 화면의 순서를 "도면 올리기 → 추천 순위(근거) → 템플릿 선택 → 6문항 확인"으로 바꾼다. 도면의 단서(말뚝·PILE 문자, Ø, 재료표 형식 열, 받침 표 행 수, 정면도 R 문자)로 문항 값을 추정해 미리 채우고 템플릿을 일치 문항 수로 순위 매긴다. 사람이 바꿀 수 있고, 맞는 템플릿이 없으면 "판단 불가"로 표시한다. R·각도 문자는 정면도·평면도 영역으로 한정해 철근 굽힘 반경 r 등과 구분한다. 논의 근거: `context-migration/템플릿구분기준과도면기반추천_논의.md`.
**구현**: 보호 템플릿에 `variability.json`(묶음·필드 경로 → 등급)을 추가합니다. 편집 화면과 앱 확인 C7이 이 파일을 읽습니다. 4개 템플릿 분류는 부록 A.
---
## 5. 문제점과 해결방법
| 문제 | 해결방법 | 기대 효과 | 난이도 | 우선순위 |
| :--- | :--- | :--- | :--: | :--: |
| P1 일괄 확정이 검사 안 함 | 앱 확인(C1~C8) + '일치 묶음만 일괄 확정' + 근거표 열람 조건(D4) | 일괄 확정이 도장이 아닌 근거 기반 확정이 됨 | 중 | 1 |
| P2 템플릿 원본값 확정 위험 | C3 '템플릿 원본 잔존' + 일괄 확정 제외 + 묶음 표에 '원본값' 표시 | 원본 모델이 PASS로 나오는 사고 방지 | **하** | **1** |
| P3 근거가 안 보임 | 값 출처 열 + 근거 보기(도면 위치·표 행·스크립트·사슬 검산) | 판독서를 따로 열 필요 감소 | 중 | 2 |
| P4 표 밖 치수 수작업 | 규칙 R1~R6 + 앵커 + 신뢰 등급 후보 표(결정 D2) | 새 도면 확인 시간 단축, 오타·누락 감소 | **상** | 3 |
| P5 손으로 대조 불가 | V3 분류 + C4·C8(내부 정합·재생성 일치) + 편집 차단 | 확정이 '요약된 검산 결과'에 근거 | 중 | 2 |
| P6 변수·고정 미구분 | 4단계 분류 + `variability.json` + 판별 체크리스트 + 형식 불일치 경고 | 형식이 다른 도면을 같은 템플릿에 밀어 넣는 실수 방지 | 중 | 2 |
| P7 params·expected 동시 채움 | C5(독립성) + 후보 반영 화면에서 params 줄 별도 선택 안내 강화 | 검사 독립성 유지 | 하 | 2 |
| P8 확인 상태 1층 | `autocheck.json` 별도 층(앱 확인), 확정 기록은 불변 | "앱이 확인", "근거 없음", "불일치 의심" 표현 가능 | 하 | 1 |
---
## 6. 구현 계획
| 단계 | 내용 | 주요 변경 | 시험·판정 |
| :--- | :--- | :--- | :--- |
| **A 분류·표시** | `variability.json` 작성(4템플릿), 묶음 표에 변수 등급·값 출처 열, '템플릿 원본 그대로' 표시(C3) | `web/templates/pier.html`, `web/store.py`, `data/_템플릿/*/variability.json` | 4템플릿에서 분류 100 %, 새 교각(템플릿 복사 직후) 전 묶음이 '원본값' 표시 |
| **B 앱 확인 엔진** | C1~C8 구현, `autocheck.json`, 근거표 화면, '일치 묶음 일괄 확정'(D4), 보고서 8장 요약 | `engine/verify.py`(신설), `web/app.py`, 템플릿, `engine/report.py` | 5개 교각 자기 데이터 전 묶음 '일치'(거짓 불일치 0), 값을 일부러 바꾸면 '불일치'(거짓 일치 0, 음성 시험), 기존 회귀 9종 FAIL 0 |
| **C 표 밖 치수 탐색** | 규칙 R1~R6, 앵커 형식·추출 도구, 후보 표 구역, 도면 보기 | `engine/reader.py`, `engine/anchors.py`(신설), 판독 스크립트 앵커 기록 | 4개 도면에서 원본 params 값 재현율(등급 A·B 비율)·거짓 후보 0건 집계, 틀이 다른 도면에서 '없음' 정상 표시 |
| **D 통합·문서** | 일괄 확정 문구·흐름, 사용설명서 개정(5.3·5.4·9장), 회귀 전체 | 사용설명서 v0.2.08, 시험 `tools/test_confirm_check.py` | 웹 E2E 포함 전체 회귀 PASS |
- 각 단계는 **정지·승인**(사용자) 후 진행합니다(HITL).
### 6.1 구현 현황 (2026-10-16)
**A단계 분류·표시 — 구현 완료, 사용자 승인 대기**
| 항목 | 구현 |
| :--- | :--- |
| 변수 등급 분류 | `engine/variability.py`(규칙 표 + 묶음 내용으로 V3 판정: 철근 `bars` 목록 100개 이상), 보호 템플릿 6종에 `variability.json`(초안) 작성 — `tools/make_variability.py`. 템플릿 저장·교각 생성 때 자동 작성·복사 |
| 묶음 표 | 「변수 등급」「값 출처」 열 추가. 값 출처는 등급 파일의 일반 출처(표 / 표 밖 치수 / 판독 스크립트 / 사람 입력 / 템플릿 고정) |
| 템플릿 원본 그대로(C3) | 교각 생성 시 묶음별 해시를 `pier.json`의 `origin`에 기록 → 값이 그대로면 표에 '▲ 템플릿 원본 그대로'. 필수 묶음 중 원본인 것은 표 위에 목록 경고 |
| F 항목 편집(D3) | F 묶음 또는 묶음 안의 고정 필드(`column.shape`, `coping.form` 등)를 바꾸면 **사유 입력 필수**, `edit_log.json`에 기록. 차단하지 않음 |
| V3 편집 | 화면에서 저장 거부, 판독 스크립트 재실행 안내 |
| 시험 | `tools/test_variability.py` 전부 PASS(분류 100 %, 부록 A 일치, 신규 교각 26/26 원본 표시·수정 시 해제, F 사유·V3 차단, 기록 없는 기존 교각은 주장하지 않음) |
**기획과 다르게 한 점·한계**
1. 「값 출처」는 **일반 출처**입니다. 묶음 값이 실제로 도면의 어디서 왔는지(C1·C2 근거)는 B·C단계 몫입니다.
2. **기존 교각**(2026-10-16 이전 생성)은 템플릿 복사 기록이 없어 '원본 그대로' 여부를 판단하지 않습니다(표시도 안 함). 새로 만드는 교각부터 적용됩니다.
3. `variability.json`은 묶음 수준 초안입니다. 필드 수준 분류와 담당자 확정(R6)은 남아 있습니다.
4. 시안의 Prism 시각 디자인(UX1~UX3)은 아직 앱에 적용하지 않았습니다(기존 화면 스타일 유지). 적용 시점은 사용자 결정 대기.
5. 「앱 확인」 열·요약 띠·일괄 확정 근거표는 B단계입니다.
### 6.2 B단계 구현 현황 (2026-10-16)
**앱 확인 엔진 `engine/verify.py` — 구현 완료, 사용자 승인 대기**
| 항목 | 구현 |
| :--- | :--- |
| 앱 확인 C1~C8 | C1 출처 기록, C2 도면 재대조(ELEVATION·솔플레이트·재료표 후보 vs 현재 값), C3 템플릿 원본 잔존, C4 내부 정합(철근 수량 vs 재료표 수량; 계획 FAIL은 '주의'), C5 params/expected 독립, C6 범위·단위, C7 변수 등급 위반(F 항목), C8 재생성(연결된 스크립트 없음 → 해당 없음) |
| 상태 판정 | 불일치(검사 하나라도 실패) > 근거 없음(템플릿 원본 그대로, 독립 근거 없음) > 일치(표 대조 일치) / 주의(수량만 확인, 계획된 예외 등) / 해당 없음(참고·사람 결정 기록). F 묶음은 템플릿 원본(기록 없는 기존 교각은 보호 템플릿들)과 같으면 일치 |
| 저장 | `autocheck.json`(묶음 해시·결과·근거 목록·검사 버전) — `confirm.json`과 별도, **앱은 확정 기록을 만들지 않음**. 값·도면·검사 버전이 바뀌면 자동 재실행(약 1초) |
| 화면(Prism) | 묶음 표 위 **요약 띠**(필요 N개 중 일치·주의·불일치·근거 없음·해당 없음·사람 확정), 「앱 확인」 열 + 근거 한 줄, **근거 보기**(8종 검사별 결과·비교 내용·도면 위치), **일치 묶음 일괄 확정**(근거표를 끝까지 내려야 활성, 대상 목록 해시 표식, 이름 필수), 앱 확인이 일치가 아닌 묶음은 개별 확정 시 **사유 필수**(확정 기록에 사유·당시 앱 확인 상태 저장) |
| 보고서 | 8장에 「8.1 입력 묶음 앱 확인 결과 요약」(묶음별 앱 확인·근거·사람 확정·사유) |
| Prism | `web/static/prism.css`(시안 pier.css에서 `.prism` 범위로 생성, `tools/make_prism_css.py`)를 새 화면(요약 띠·표·근거 보기·일괄 확정)에만 적용. 기존 화면 교체는 D단계 |
**실제 교각 6종 결과** (`tools/report_autocheck.py`, 필요 묶음 기준)
| 교각 | 일치 | 주의 | 불일치 | 근거 없음 | 해당 없음 |
| :--- | --: | --: | --: | --: | --: |
| Ramp-A교P1 | 11 | 2 | 0 | 9 | 3 |
| 산외교울산P1 | 12 | 4 | 0 | 7 | 3 |
| 산외교울산P2 | 12 | 4 | 0 | 8 | 3 |
| 새재1교P1 | 9 | 4 | 0 | 10 | 3 |
| 옥성교P1 | 9 | 4 | 0 | 11 | 3 |
| 운곡교P1 | 9 | 0 | 0 | 11 | 3 |
| (참고) 운곡교P3·P4 | 0~1 | 0~1 | **8** | 11 | 3 |
- **거짓 불일치 0**: 모델링된 교각의 자기 데이터에서 불일치 0건. 운곡교 P3·P4(P1 값을 복사만 한 교각)는 표 대조에서 **불일치 8건**으로 정확히 잡힙니다.
- **거짓 일치 0**(시험): 받침 h1 +5 → 불일치, expected 값 +1 → 불일치, 코핑 철근 1본 삭제 → 불일치, 두께 −5 → 불일치. 수량만 확인한 철근 목록은 '일치'가 아니라 '주의'.
- 근거 없음이 많은 이유: 기초·기둥·코핑 치수, 철근 위치·좌표, EL은 **표 밖 치수**라 앱이 아직 대조하지 못합니다(C단계). 숨기지 않고 '근거 없음'으로 표시합니다.
**기획과 다르게 한 점·한계 (B단계)**
1. 기획의 수용 기준 "5개 교각 자기 데이터 전 묶음 '일치'"는 **달성하지 못함** — 표 밖 치수 묶음은 C단계 전에는 일치가 될 수 없고 '근거 없음'입니다(거짓 일치를 만들지 않기 위해). 거짓 불일치 0·거짓 일치 0은 달성.
2. C8(재생성 일치)은 앱에 생성 스크립트가 연결되어 있지 않아 '해당 없음'입니다.
3. ~~이전 방식 일괄 확정은 접힌 항목으로 유지~~ → **2026-10-19 제거**(사용자 결정). `confirm_all` 경로와 화면 항목을 없앴고, 묶음은 [일치 묶음 일괄 확정] 또는 근거 보기에서 하나씩(일치가 아니면 사유 필수) 확정합니다. 이 경로를 쓰던 시험 9종은 `tools/testlib.confirm_required`(묶음마다 사유를 붙여 개별 확정)로 바꿨습니다. P1 '확정 = 도장' 문제 해소.
4. 개별 확정(`section/.../confirm`)은 앱 확인이 일치가 아니면 사유가 서버에서 강제됩니다. 이에 맞춰 시험 4곳이 사유를 넣도록 바뀌었습니다.
5. 기존 교각의 C3(템플릿 원본)은 판단하지 않고, F 묶음만 보호 템플릿과 비교합니다.
6. 육안 확인: 임시 서버+헤드리스 Edge로 요약 띠·표·근거 보기·일괄 확정 화면을 캡처해 확인했습니다(`검증/ui_check_shots/`).
### 6.3 C단계 1차 구현 현황 (2026-10-19)
**표 밖 치수 탐색: 앵커 학습·따라가기 — 구현 완료, 사용자 승인 대기** (R1 레벨·R2 치수 사슬·R5 뷰 기준 구현, R3·R4 미구현)
| 항목 | 구현 |
| :--- | :--- |
| 앵커 학습 `engine/anchors.py` | 모델링된 교각의 숫자 필드(V1·V2 묶음, 1 m 이상)마다, 값과 같은 도면 치수·EL 문자(레이어·도면·좌표)를 기록. 같은 값이 1~6곳에서 나올 때만 앵커. 같은 파일 안에서 다른 종류 필드가 같은 값을 가지면(예: 두께와 지름 2500) '공유'로 표시해 A등급에서 뺌 |
| 따라가기(R5) | 새 도면과 앵커 도면에 공통인 문자(제목·라벨)로 뷰별 이동량을 구하고(가까운 5개 중앙값), 예측 위치 반경 1,200 mm 안에서 같은 레이어·같은 종류의 문자를 후보로 취함. **도면 틀 일치율**(공통 문자 비율)이 70 % 미만이면 모든 후보를 C로 낮춤 |
| 신뢰 등급 | A = 2곳 이상 따라가 모두 같고 이동 편차 작음 / B = 2곳 이상 일치 또는 1곳 / C = 서로 다름·틀이 다른 도면. 후보가 없으면 '없음'(억지 후보를 만들지 않음) |
| R1 레벨 | 'EL. 38.916' 형식 문자를 앵커 종류 EL로 처리(z 계열 필드) |
| R2 치수 사슬 | `n@간격=합계` 문자를 파싱해 개수 × 간격 = 합계 검산. 맞지 않으면 묶음 표 위 경고와 후보 화면 목록(도면 보기 링크) |
| 저장 | 보호 템플릿 6종에 `anchors.json`(`tools/make_anchors.py`, 앵커 도면은 각 템플릿의 원본 교각). 교각 생성 시 복사, 앵커 없는 교각은 후보 화면에서 템플릿을 골라 불러옴. 앵커가 자기 도면에서 나왔으면(`sources` 해시 일치) 근거로 쓰지 않음 |
| 화면(Prism) | [표 후보값]에 「표 밖 치수 후보」 구역: 요약 띠(후보·다름·A·B·C 수, 틀 일치율), 표(반영·대상·현재·후보·등급·근거 n곳 중 m곳·도면 위치·**도면 보기**), 반영 사유(C 필수). 도면 보기 = 해당 위치 확대 PNG(`engine/render.py`) |
| 반영 규칙 | 같은 도면 문자에서 나온 후보를 params와 expected에 함께(한 번에 또는 따로) 반영 불가(P7), C등급은 사유 필수, 이미 같은 값은 반영 안 함, 반영·이전 값·등급·위치를 `anchor_log.json`에 기록, 바뀐 묶음은 재확정. 앱이 스스로 반영하지 않음 |
| 앱 확인 연동 | 새 검사 **C2b 표 밖 치수 재대조**: A 후보가 현재 값과 다르면 불일치, B가 다르면 주의, A가 같고 앵커 필드의 60 % 이상 대조되면 일치(강한 근거), 일부만이면 주의. 자기 도면 앵커는 제외 |
**검증 결과** (`tools/test_anchors.py`, 실제 교각 9종 도면)
| 항목 | 결과 |
| :--- | :--- |
| 같은 도면 틀(울산 P1↔P2, 운곡교 P1↔P2) A등급 | **45건 중 틀린 후보 0건**, 값이 바뀐 필드를 A로 정확히 복원한 것 13건 |
| 같은 도면 틀 B등급 | 451건 중 388건 일치(86 %), 틀린 후보 약 14 % — 경고로 표시 |
| 같은 도면 틀 C등급 | 48건 중 12건 일치 — 목록만, 반영 시 사유 필요 |
| 다른 도면 틀(36쌍) | **A·B 후보 0건**(틀 일치율 < 70 % → 전부 C) |
| R2 사슬 검산 | 9개 도면 중 **운곡교 P4 도면의 사슬 2건이 불일치**(`62@125=7.250`은 계산 7,750, `29@250=7.750`은 계산 7,250 — 합계가 서로 바뀐 것으로 보임). 나머지 8개 도면은 0건 |
| 새 교각 시험 | 템플릿 운곡교P1형 + 운곡교 P2 도면: 후보 146건, A 11건(다른 값 7건 — 모두 운곡교 P2 모델링 값과 일치), 후보가 다른 묶음 6개가 앱 확인에서 불일치 |
| 시험 중 사고 | 처음 시험이 `store`를 환경변수 설정 전에 불러와 실제 `data/`에 시험 교각 2개를 만들었음 → 즉시 삭제, 시험은 `PIERAPP_DATA` 설정 뒤에 불러오도록 수정하고 `data/`를 건드리지 않는지 단언 추가 |
**기획과 다르게 한 점·한계 (C단계 1차)**
1. **R3(철근 점·선)·R4(콘크리트 외곽 점열)는 미구현.** 철근 위치 목록·코핑 외곽 점열은 앵커로 읽을 수 없어 여전히 '근거 없음'입니다. 스칼라 치수·EL 문자로 읽히는 필드만 후보가 됩니다.
2. 수용 기준 "거짓 후보 0건"은 **A등급에서만 달성**(같은 틀 45건 중 0건). B등급은 약 14 %가 틀립니다(단, B는 경고 표시이고 앱 확인에서도 주의까지만 반영).
3. 앵커는 단일 숫자 문자와 값이 같은 필드만 학습합니다. 계산해서 얻는 값(예: 사슬 합, EL 차)은 앵커가 없습니다. 1 m 미만 값은 대상이 아닙니다.
4. D6(소급 추출: 옥성교·Ramp-A교부터)에 따라 만들었으나, 학습이 자동이라 **보호 템플릿 6종 모두**에 앵커를 만들었습니다.
5. 같은 도면 틀이어야 작동합니다. 틀이 다른 도면(다른 설계 회사 도면 등)은 후보가 C등급 이하로 떨어지는 것이 정상 동작입니다.
6. 기존 교각 9종은 앵커가 없어 후보 화면에서 템플릿을 골라 불러와야 합니다(자동 선택 안 함).
7. 육안 확인: 임시 서버+헤드리스 Edge로 후보 화면을 캡처해 확인(`검증/ui_check_shots/candidates.png`).
### 6.4 C단계 2차 결과 (2026-10-19)
**① 운곡교 P3·P4에 운곡교P1형 앵커를 불러와 실제 후보 확인** (`tools/report_anchor_pier.py`, 실제 `data/운곡교P3·P4`에 `anchors.json` 복사 — 값은 하나도 반영하지 않음)
- 두 교각의 현재 값은 `운곡교p2`를 복사한 것이고(`pier.json`의 template 항목), 도면은 P3·P4 자신의 것입니다. 도면 틀 일치율 0.77(≥ 0.70, 같은 틀로 인정).
- 후보 146건 중 현재 값과 다른 것 91건(P3) · 102건(P4).
| 단계 | P3 | P4 |
| :--- | :--- | :--- |
| 표 교차 확인 전 | A 11 · B 116 · C 19 | A 9 · B 118 · C 19 |
| **표 교차 확인 후** | **A 56**(표 값과도 일치 47) · B 65 · C 25(표 값과 달라 낮춘 것 18) | A 54(47) · B 67 · C 25(18) |
- 표 값이 있는 필드만 비교하면 B등급은 34건 중 23건(68 %)만 맞았습니다. 틀린 것은 대부분 **같은 값을 공유하는 마크의 재료표 길이**(P3·P4·P5의 L이 서로 같아 후보가 서로 바뀜)였고, 표 교차 확인으로 모두 C로 내려갑니다.
- 화면 확인: 후보 구역 최상단에 표 값과도 일치하는 A등급이 나열되고(받침 EL, 철근 길이·수량, 기초 EL, 기둥 높이), 앱 확인에서 불일치가 P3 10개·P4 11개로 표시됩니다(표 후보 8개 묶음 + 앵커 A 후보 묶음)(`검증/ui_check_shots/candidates_P3.png`, `pier_P3.png`).
- 이 후보는 **아직 반영하지 않았습니다.** 반영 여부는 사용자가 [표 후보값] 화면에서 결정합니다(P3·P4는 모델링 전이고, 반영 후에도 앵커가 못 읽는 철근 위치 목록·코핑 외곽 등은 직접 판독해야 합니다).
**② 표 값 교차 확인 구현** (`engine/anchors.cross_check`, 화면에 "표 값과도 일치" / "표 값 X와 다름 — 표를 따르세요")
- 표(ELEVATION·솔플레이트·재료표) 값이 있는 필드: 앵커 후보가 표와 같으면 **A**, 다르면 표 값을 병기하고 **C**. 표 값이 없는 필드는 그대로.
- 같은 도면 틀 4쌍 시험: A등급 45건 → **165건, 전부 맞음**(128건이 표로 확인, 47건은 표와 달라 C로 하향).
**③ R3·R4(기하 앵커) 시험 결과 — 채택하지 않음**
- 시도: 앵커가 없는 숫자 필드(≥ 1 m)에 대해 콘크리트·철근·중심선 레이어의 선분 길이가 같은 값인지 찾아, 새 도면에서 같은 위치·방향의 선분 길이를 후보로 삼음(울산 P1↔P2, 운곡교 P1↔P2).
- 결과: A등급 15~18건/쌍이 **값이 바뀐 필드를 하나도 복원하지 못했고**(바뀐 값 복원 0건), 같은 값이 우연히 맞은 것과 철근 위치 목록(좌표인데 길이로 잘못 대응)의 오답 A가 2~7건 섞임. 위치 목록·외곽 점열은 길이가 아니라 **뷰 기준점 대비 좌표**이므로 뷰 기준점(R5)을 도면 틀마다 확정해야 하는데, 이는 현재 구조(문자 기반 이동량)로는 신뢰할 수 있게 구할 수 없었습니다.
- 결론: 철근 점·선(R3)과 콘크리트 외곽 점열(R4)은 **여전히 '근거 없음'**으로 두고 판독 스크립트(사람)로 읽습니다. 억지 후보를 만들지 않는 방침 유지. 시도 코드는 보관하지 않고 결과만 기록(프로토타입 삭제).
**④ 화면 편의**: A등급 중 현재 값과 다른 후보를 expected만 / params만 한 번에 선택(두 파일 동시 선택은 P7 때문에 제공하지 않음).
**한계 (C단계 2차)**
1. 표 교차 확인은 표가 있는 필드만 개선합니다. 표 밖 필드의 B등급 정확도(약 86 %)는 그대로입니다.
2. R3·R4 미구현 상태 유지 — 철근 위치·코핑 외곽 묶음은 앱 확인에서 계속 '근거 없음'이 됩니다.
3. P3·P4의 후보를 반영하지 않은 상태라 두 교각의 앱 확인은 불일치 10~11개 그대로입니다(반영 후 다시 확인).
- 확정·게이트 승인은 어느 단계에서도 앱이 대신하지 않습니다. 시험은 별도 시험 폴더에서만 확정 흐름을 실행합니다(실제 교각 데이터 보호: 웹 일괄 실행이 Ramp-A·울산·옥성 게이트 보고서를 '재로드 안 함' 상태로 덮어쓴 일이 있음, 원인(시험 또는 앱 사용) 미확인, OPEN #25).
- 수용 기준(착수 전 확정): ① 모든 필수 묶음에 앱 확인 상태가 있다 ② '근거 없음'을 숨기지 않는다 ③ 거짓 일치 0·거짓 불일치 0(4개 도면 기준) ④ 앱이 확정 기록을 만들지 않는다 ⑤ 기존 시험 FAIL 0.
---
### 6.5 D단계 결과 (2026-10-19)
**① 전 화면 Prism 교체**: `web/templates/base.html`을 Prism 틀(위쪽 띠·본문)로 바꾸고 `web/static/prism-app.css`를 추가해, 옛 화면의 표·버튼·입력칸·안내 문구가 Prism 토큰(색·간격·Pretendard)을 따르게 했습니다(템플릿별 개별 수정 없이 공통 틀로 교체). 상태 표시는 색과 함께 글자·기호를 씁니다. 형식 불일치 경고는 계속 닫을 수 없습니다. 어두운 테마는 시험하지 않았습니다.
**② 사용설명서 v0.2.12**: 앱 확인·근거 보기·일치 묶음 일괄 확정·변수 등급·표 밖 치수 후보·도면 먼저 추천·6번째 템플릿을 반영하고, 이전 방식 일괄 확정 설명을 삭제했습니다.
**③ 운곡교 P3·P4 후보 반영(A등급, 파일 하나씩)**: `tools/apply_anchor_a.py`로 앱의 후보 반영 경로를 거쳐 expected 쪽 A등급 후보를 반영했습니다(P3 29필드, P4 34필드, 반영 전 params·expected·confirm.json은 `backup/before_anchor_A_*`에 보관, 기록은 `anchor_log.json`). params 쪽 A등급 후보는 같은 도면 문자를 expected에 이미 썼기 때문에 P7 규칙으로 막혀 반영하지 못했습니다. 그 결과 묶음 단위 앱 확인 불일치는 P3 10·P4 11로 그대로이고, 필드 단위 expected 차이 줄은 69→52(P3), 75→63(P4)로 줄었습니다. 남은 불일치는 A등급 후보가 아닌 값(표 후보·params 독립 판독이 필요한 값)입니다.
**④ 회귀**: fast 15/15 PASS, Rhino 그룹 결과는 OPEN #26에 기록합니다.
**남은 결정**: 표 후보(expected 쪽) 검토 후 반영 여부, params 독립 판독, 운곡교 P4 도면 판(OPEN #33).
---
### 6.6 운곡교 P3·P4 실사용 결과와 앱 개선점 (2026-10-20)
**결과 요약**
| 교각 | 입력 | 게이트 결과 | 검사 | 계획된 FAIL | 계획 외 FAIL |
| :--- | :--- | :--- | :--- | :--- | :--- |
| 운곡교 P3 | 도면에서 읽은 값으로 params·expected 갱신(77곳 + 표 후보 105건 + 2차 48곳) | G1~G5 모두 PASS·승인 | 578/585 | 7 (B12, C9~C13 원래 길이 5건, C13 규칙) | 0 |
| 운곡교 P4 | `apply_pier_read.py` 로 144곳 + 표 후보 118건 | G1~G5 모두 PASS·승인 | 577/585 | 8 (B11, B12, C9~C13 원래 길이 5건, C13 규칙) | 0 |
- 첫 실행 → 최종 실행의 계획 외 FAIL: P3 G1 34·G2 7·G3 5·G4 28 → 0, P4 G1 18·G2 10·G3 22·G5 10(허위 G4 PASS) → 0.
- 새 도구: `tools/verify_pier_geometry.py`(도면 값 55항목을 params 와 비교; P1·P3 55/55 로 재현 확인), `tools/apply_pier_read.py`(읽은 값을 params·expected 에 반영; P1·P3 `--dry` 로 재현 확인), `tools/infer_footing_rebar.py`(기초 철근 층 규칙; P1 재현), `tools/extract_coping_outline.py`.
- 읽을 수 있는 값: 표 3종(받침·솔플레이트·재료표), 치수 글자, 코핑 외곽·철근 단·C15·C16, 받침 위치·회전, 기초·말뚝·기둥, 기초 철근 층(규칙 + 재료표 개수 일치), 기둥 철근(후프·결속). 읽을 수 없거나 가정: 스터럽 좌측 윗면, c14 좌측 규칙, 결속철근 끝 높이, 설계 의도.
**앱 개선점 (OPEN #34, 효과 큰 순)**
| # | 개선 | 근거(실제 사례) |
| :--- | :--- | :--- |
| 1 | 도면 읽기 도구를 후보 화면에 통합, 템플릿 재현 검증 포함 | P3 수작업 77곳, P4 144곳 |
| 2 | params↔expected 일관성 검사(Rhino 실행 전) | 게이트 1회 약 150초, P3 실패의 대부분이 옛 값 혼재 |
| 3 | 필드 단위 '템플릿 원본 그대로 남음' 검사 | P3: 앱 확인 불일치 0인데 게이트 40여 건 실패 |
| 4 | 표 칸에서 학습한 앵커의 거짓 후보 제거 | P3·P4: 재료표 S1 길이 칸 '3.800' 을 G3 사슬 끝으로 오인 |
| 5 | 일괄 실행 중단 시 중간 결과·이전 승인 요약 구분 표시 | P4: `display.py` 오류 후 승인 요약이 이전 실행 것으로 남음 |
| 6 | 점 좌표를 키로 쓰는 목록 정합 검사 | P3: `hooks_up` 옛 좌표로 G4 코핑 밖 점 56개 |
| 7 | 확정 사유 최소 품질 | 사유 'asf', 'afs', '12' |
| 8 | 임시 등록 '설계자 확인 대기' 상태 관리, 해당 없음 항목 자동 탐지 | P2 의 G3 B9 551/493 항목이 P3·P4 에 남음 |
| 9 | 보고서에 독립 근거 구분 표시 | PASS = 내부 일관성(params·expected 가 같은 읽기에서 나온 값 다수) |
| 10 | P1 규칙 가정 단계(스터럽 좌측 윗면, c14 좌측 규칙)의 도면 읽기 | P4: 규칙이 틀려 재료표 역산값으로 교체 |
| 11 | 도면 파일 해시·개정 번호 기록·비교 | P4 D2 사슬 오류: 사용자 CAD 판과 올린 파일 불일치 의심 |
| 12 | 다른 도면 틀·형식(울산·새재1교)에서의 일반성 확인 | 교각 2개·템플릿 1개로만 확인 |
**설계자 확인 대상**(OPEN #35): P3 C13, P4 B11·B12·C9·C13, D2 사슬(#33). 모두 임시 등록이며 보고서에 '설계자 확인 전'으로 표시해야 한다.
**이번 작업에서 생긴 오류(재발 방지)**: `apply_pier_read.py` 가 pier_id 를 바꾸면서 `display.hidden_ids` 를 바꾸지 않아 P4 일괄 실행이 마지막 단계에서 중단됨 → 스크립트 수정(개선 3·6 과 같은 종류: 파생 값의 동시 갱신 누락).
---
### 6.7 개선 ②③④⑥ 구현 결과 (2026-10-20)
| 개선 | 구현 | 시험 결과 |
| :--- | :--- | :--- |
| ② params↔expected 일관성(C9) | `engine/consistency.py`: 같은 양을 나타내는 두 파일 값의 규칙 103~106건(기초·말뚝·기둥, 코핑 윗면 단·하면, 받침 피치·열 위치, 코핑 철근 단·C15·C16·루프·스터럽 하단, 후프·결속 사슬, 기초 철근 층·B9·B10, 재료표 사본). 다르면 해당 묶음이 불일치(b). 규칙의 경로가 없는 교각(새재1교 등)은 건너뜀 | 완성된 P1·P3·P4: 불일치 0. P3 최초 백업 상태(`before_stage2`): 54건 모두 검출, expected G1~G4 전부 불일치. P4 `before_table_apply`: 16건 검출. 다른 형식 4종: 거짓 불일치 0 |
| ③ 필드 단위 원본 잔존(C3b) | 교각 생성 시 `origin_values.json` 기록(이전 교각은 `tools/make_origin_values.py`로 초기 백업에서 재구성). 바뀐 묶음 안에서 높이로 보이는 값(10~60 m, z·단 높이·점 좌표의 z)이 기록값 그대로이면 주의(w) | P3 `before_hooks_up` 상태에서 `hooks_up` 좌표 4개를 정확히 지목. 완성된 상태: 경고 0(처음에는 길이 12400·12000 등을 높이로 오인해 경고가 많았으나 높이 키로 제한) |
| ④ 거짓 앵커 | `engine/anchors.py` `TABLE_RULE="single"`: 후보를 따라간 근거가 재료표 칸 **하나**뿐이면 C등급(`table_only`). 두 방식(`none`: 표 칸을 아예 제외)과 비교 | P3·P4 확인값 기준 B등급 오답: 끔 10/65 · single 2/39 · none 3/29. A등급 오답 0 유지. 부작용: P3에서 expected:G2(일치→주의)·params:column_rebar(일치→근거 없음) 2개 묶음의 근거가 줄어듦(표 칸 하나로 맞던 B등급 후보) |
| ⑥ 점 좌표 목록 | C9 규칙: `c14.hooks_up`의 각 점이 `c14.points`에 있어야 함 | 최초 오류(P3 `before_hooks_up`)의 4점 모두 검출, 점 하나를 옮기면 해당 항목만 깨짐 |
**검사 환경 정리**: 실제 사용 중 `운곡교p2`(휴지통으로 이동)에 의존하던 시험 5종(`test_open19`·`test_rampa`·`test_saejae`·`test_okseong`·`test_ulsan_p2`)과 `test_anchors`를 `tools/testlib.pier_dir`(휴지통 최신 복사본, 대소문자 구분)로 바꿈. `test_autocheck`는 P3가 완성 값을 갖게 되어 최초 백업(다른 교각 값) 상태로 시험하도록 바꿈. 회귀 fast 16/16 PASS(`test_consistency` 추가).
**남은 개선**: ① 읽기 도구 통합, ⑤ 일괄 실행 중단 표시, ⑦ 확정 사유 품질, ⑧ 임시 등록 관리, ⑨ 독립 근거 구분 표시, ⑩~⑫.
---
### 6.8 개선 ①⑤ 구현 결과 (2026-10-20)
| 개선 | 구현 | 시험 결과 |
| :--- | :--- | :--- |
| ① 읽기 도구 통합 | `engine/geomread.py`: P3·P4 모델링 때 따로 만든 읽기 도구(코핑 외곽·철근 선·원, 기초·말뚝·기둥 글자, 받침 폴리라인, 재료표, 기초 철근 규칙)를 하나로 합친 순수 함수 `read()`(부작용 없음, 사본과 변경 목록 반환)와 `apply_selected()`. `engine/footing_rule.py`(기초 철근 규칙). 화면 「도면에서 읽기」(`/pier/<이름>/geomread`): 묶음별 현재/읽은 값/근거 표, 체크한 줄만 반영, 반영 전 `backup/before_geomread_*`, `read_log.json`, 추론 줄 기본 미선택, 도면 틀 일치율 70 % 미만 또는 필요한 선·글자가 없으면 이유를 알리고 읽지 않음. `tools/apply_pier_read.py` 는 같은 읽기의 명령줄 판 | `tools/test_geomread.py` PASS: 운곡교 P1·P3 에서 읽은 값 = 현재 값(변경 0), P4 는 추론 줄 1건만, 초기 P4 백업(P1 복사)에서 144건 제안이 검증된 P4 값과 모두 같음(표시 선택 `hidden_ids` 제외), 다른 도면 틀 2종 거절, 화면에서 고른 줄만 기록·백업·로그·확정 무효화 |
| ⑤ 일괄 실행 중단 표시 | `batch.json` 에 `started`·`reason`(error/fail/blocked, 서버 중단은 killed)·`error`·`gate`, 게이트 결과에 `at`. 게이트 화면: 중단 이유 안내, 결과 시각과 '이전 실행 결과' 표시, 중단되기 전에 계산된 검사기 원본(`<게이트>.json` 이 기록(`_gate.json`)보다 새로울 때)을 '중단된 실행의 임시 결과'로 표시(승인·보고서에 쓰이지 않음), 입력이 바뀐 뒤의 승인은 '승인 기록은 이전 입력 것(무효)' | `tools/test_batch_notice.py` PASS: 오류·FAIL·조건 미충족·서버 중단 안내, 이전 실행 결과 표시, 임시 결과, 무효 승인 표시, 완료된 실행에는 안내 없음, 실제 작업자(`_batch`)에 가짜 Rhino 를 연결해 세 중단 경로의 기록 확인 |
**선택과 한계**
- 읽기 결과를 params 와 expected 에 함께 채우는 것은 두 파일의 독립성을 떨어뜨림 → 화면 문구와 앱 확인 C9 설명으로 알림, 표(재료표) 값은 표 후보값 화면에서 따로 대조.
- 표시 선택 `display.hidden_ids` 는 교각 이름이 바뀌면 템플릿의 숨김을 새 이름으로 옮겨 제안(P4 는 사용자가 비움으로 정함).
- 읽기 규칙의 범위: 운곡교 도면 틀 1종, 교각 4개. 좌측 스터럽 윗면(추론)·c14 좌측 규칙·결속철근 끝 높이는 P1 규칙 가정.
- 환경: 일괄 실행 오류 시 중간 결과가 `<게이트>.json` 으로 남는 것을 이용(별도 저장 없음).
**남은 개선**: ⑦ 확정 사유 품질, ⑧ 임시 등록 관리, ⑨ 독립 근거 구분 표시, ⑩ P1 규칙 가정 단계 읽기, ⑪ 도면 해시·개정 번호, ⑫ 다른 도면 틀 일반성.
---
### 6.9 검토 요약 확정 (2026-10-20)
**문제**: 묶음 20~23개를 하나씩 확정하는 데 시간이 오래 걸리고, 사유가 비거나('12', 'asf') 무엇이 바뀌었는지 보기 어려웠다. 사용자 제안: 앱이 일괄 확정하고 사용자는 일괄 검토만 한다.
**결정(권고안)**: "앱이 확정"은 확정의 의미(사람이 도면과 대조했다는 기록)를 없애므로 채택하지 않고, **앱이 검토를 준비하고 사람이 한 화면에서 검토·체크해 한 번에 확정**한다(D1 유지). 근거 없음 묶음은 **항목별 체크**(a)를 요구, 기존 개별 확정 화면은 예외 처리용으로 유지.
| 요소 | 구현 |
| :--- | :--- |
| 화면 | `/pier/<이름>/review` (교각 화면 [검토 요약으로 확정]). 확정이 필요한 묶음을 네 구역으로: 불일치(확정 불가), 주의·근거 없음(항목별 체크), 사람 결정 기록(체크), 일치(근거 훑어보기 + 구역 체크 1회) |
| 앱이 준비하는 것 | `engine/review.py`: 바뀐 것(교각 생성 시점 값 기록 `origin_values.json` 대비 변경 값 수와 예), 앱이 확인하지 못한 것(근거 없음·주의 검사 항목), 값의 출처(`read_log.json`·`anchor_log.json` 이력), 사유 자동 작성(`auto_reason`) |
| 사람이 하는 것 | 도면과 대조한 묶음에 체크, 목록 끝까지 읽기(표식 `review_view.json` = 그때 목록의 해시), 확정자 이름, 공통 의견(선택) |
| 기록 | 묶음별 확정 기록에 사유(앱 확인·변경·출처·의견 포함), 확정 당시 앱 확인 상태, 이름. 앱은 체크가 없으면 아무것도 확정하지 않음 |
| 안전장치 | 이름·끝까지 읽기·체크 없으면 거절, 값이 바뀌면 표식 무효, 불일치는 요청에 id 가 들어와도 확정하지 않음 |
**시험** `tools/test_review.py`(PASS): 화면 조회만으로는 확정 0, 표식·이름 없으면 거절, 체크 없으면 0, 일치 구역은 구역 체크로만, B·D 는 체크한 것만, 불일치는 거절, 기록에 사유·앱 확인 상태·이름, 값이 바뀌면 표식 무효, 불일치를 고치면 모든 필수 묶음이 이 화면으로 확정되어 게이트가 열림. 회귀 fast 19/19.
**후속 변경 (같은 날)**: ① 「도면에서 읽기」가 표 값(받침·솔플레이트·재료표 후보)을 같은 화면에 "표 값" 구역으로 포함(`geomread.table_changes`), 반영은 `reader.apply_proposals` 와 같은 규칙, 받침마다 다른 솔플레이트 길이는 `{번호: 값}` 으로 정규화 — 새 교각에서 템플릿 원본 값이 한 화면으로 모두 도면 값이 됨(시험: 초기 P4 백업 + 읽기 + 표 행 = 검증된 P4 의 표 관련 필드, 알려진 예외는 앵커 값·`tie_offset` 상수·낡은 직경 기록). ② 「검토 요약 확정」에 [모두 체크]·[모두 해제] 버튼, 주의 표시(템플릿 원본 그대로·설계자 확인 전 임시 등록 포함)를 칩과 확정 사유에 기록, 주의 표시가 있으면 모두 체크 시 확인창. 불일치 묶음은 여전히 확정 불가, 이름·끝까지 읽기·체크 없이는 확정되지 않음. 앞서 제안한 '낮은 위험/높은 위험 두 단계' 대신 사용자 요청에 따라 일괄 체크 + 주의 표시로 구현(위험도별 분류는 미구현).
**한계**: 체크는 사용자의 선언이라 실제 대조 여부는 앱이 확인할 수 없다. 사유의 "무엇을 어떻게 대조했는지"는 공통 의견에 적어야 남는다. 앞으로 앱 확인 'C10 도구 읽기 이력'을 근거(출처, 독립 검증 아님)로 연결하면 근거 없음 항목이 줄 수 있다(미구현).
### 6.10 운곡교 P4 복구 결과와 앱 수정 계획 (2026-10-20)
> 상태: **계획 `[미확정]`** — 코드는 고치지 않았다. 아래 W1~W8은 사용자 승인 후 PDCA 순서로 구현한다.
#### 6.10.1 사고 경위와 복구
| 시각 | 사실 |
| :--- | :--- |
| 09:08 | 첫 P4 G1~G5 통과·승인 → 11:20 사용자가 삭제(휴지통 `운곡교P4_20261020_112015`) |
| 11:22~12:01 | 템플릿에서 다시 만든 판에 읽기·결정 반영 → 12:01 삭제(휴지통 `운곡교P4_20261020_121116`) |
| 12:11 | 템플릿(운곡교P1형)에서 또 새로 만들고 읽기 279건 반영 → 12:12 검토 요약으로 5개 게이트 묶음 일괄 확정 |
| 12:15 | 일괄 실행: G5만 통과, G1~G4 **계획 외 FAIL 11건**으로 G1에서 중단 |
| 복구 | 휴지통 판의 `expected.json`·`params.json` 복원(현재 판 백업 `data/운곡교P4/backup/before_restore_decisions_20261020/`). 임시 폴더 시험: G1 171/171·G2 42/42·G3 71/73(계획 2)·G4 110/116(계획 6)·G5 183/183, 계획 외 0 |
12:11 판에서 템플릿 값으로 되돌아가 있던 곳(결정 소실 6곳): ① 앵커 `rear_first_from_tip` 1198 → 1232 ② `tie_offset` 22.5 → 17.5 ③ 좌측 스터럽 윗면 40447([추론]) ④ 코핑 상단 `a` 11856/11881/11905 → 11855/11880/11904 ⑤ c14 높이 .5 → .1 ⑥ 계획된 FAIL B11·C13 누락, B12 사유가 P1 값(28/34)으로 복귀. 1232는 기준 템플릿 운곡교P1형의 값이며 교각마다 다르다(P1 1232·P2 1223·P3 1212·P4 1198).
#### 6.10.2 원인 (반복될 수 있는 실패)
| # | 원인 | 근거 |
| :-- | :--- | :--- |
| C1 | 사람의 결정(앵커 채택, 계획된 FAIL, 역산값)이 교각 데이터 안에만 있어 교각을 지우면 같이 사라지고, 새 교각에 다시 적용할 수단이 없다 | 6곳 소실 |
| C2 | 앱이 알고 있는 후보(B등급 1198)가 읽기에 자동 반영되지 않고, 경고만 남은 채 확정됨 | G1 확정 사유 `현재 1232 ≠ 도면 1198` |
| C3 | 원본 잔존 검사(C3b)가 높이로 보이는 값만 비교해 `tie_offset`·`a` 등을 놓침 | 12:15 FAIL |
| C4 | params와 expected가 서로 다른 값을 가리켜도 실행 전에 막지 못함(모델은 1198, 기준값은 1232) | G1 후열 패드 중심 X 5건 |
| C5 | 계획된 FAIL이 템플릿(P1) 승인 상태로 상속되고, 새 교각에 필요한 항목은 사람이 추가해야 함 | B11·C13 누락 |
| C6 | 게이트 실패가 어느 입력 필드 때문인지 앱이 알려 주지 않아 휴지통 판과 수동 비교가 필요했음 | 이번 복구 |
#### 6.10.3 가능과 불가능의 경계
| 값의 종류 | 앱이 채울 수 있나 | 방식 |
| :--- | :--- | :--- |
| 표·글자·치수(재료표, ELEVATION TABLE, 사슬) | 가능 | 도면에서 읽기(같은 틀 A등급 165건 전부 정답) |
| 식이 정해진 계산값 | 가능(식 확인 후) | 규칙으로 다시 계산, 출처 「계산」 표시 |
| 도면에 없는 역산·추론값 | 제안만 가능 | 사람이 채택, 결정 기록에 남겨 재적용 |
| 선·점으로 그린 기하(R3·R4) | 불가능 | 시험에서 미채택(오답 A 2~7건/쌍) |
| 도면끼리 충돌하는 값 | 불가능 | 사람이 선택, 계획된 FAIL·설계자 확인 대기 |
| 다른 도면 틀 | 불가능 | 앵커 새로 학습 |
모든 묶음이 읽기로 채워지지는 않는다(P4: `tol_mm`·`rebar_palette`·`planned_fail`·`materials`는 읽기 0건). 게이트 시작에는 자기 묶음 확정과 이전 게이트 승인이 필요하나, 확정은 사람의 선언일 뿐 값의 정확성을 보장하지 않는다(P4 12:15).
#### 6.10.4 수정 항목
설계 원칙: 앱이 준비하고 사람이 한 화면에서 체크해 한 번에 확정한다는 기존 결정(D1, 6.9절)을 유지한다. 확정 부담을 늘리지 않고, **앱이 더 많이 채우고 사실로 확인되는 모순만 차단**한다.
| # | 수정 | 해결하는 원인 | 내용 |
| :-- | :--- | :--- | :--- |
| W1 | 실행 전 관문 | C4·C2 | 일괄 실행 전에 params↔expected 불일치(사실)와 후보≠현재 값을 모아 보여 주고, 불일치는 실행을 막고 후보 차이는 확인을 받는다. 기존 C9(`engine/consistency.py`)에 앵커 대상 필드를 추가하고 검사 항목 → 깨질 게이트 항목을 예측한다 |
| W2 | 읽기에 후보 빠짐없이 | C2 | 앱이 아는 B등급 이상 후보 중 현재 값과 다른 것을 「도면에서 읽기」 화면에 기본 선택으로 올린다(추론 줄은 지금처럼 기본 미선택) |
| W3 | 결정 기록 분리 | C1·C5 | `decisions.json`에 사람 결정(앵커 채택, 계획된 FAIL, 역산·추론값과 사유)을 읽기 이력과 따로 보관, 교각 삭제 시 휴지통에 함께 이동. 「이전 결정 다시 적용」 화면에서 같은 도면 틀일 때만 기본 선택, 고른 것만 반영 |
| W4 | 원본 잔존 검사 확대 | C3 | C3b를 변수 등급 V1·V2 숫자 필드 전체로 넓혀 템플릿 값 그대로인 필드를 「확인 필요」로 모은다. 파생값(`a`, c14 높이)이 어떤 입력에서 나오는지 식을 먼저 조사 |
| W5 | 실패 → 입력 필드 연결 | C6 | 계획 외 FAIL마다 원인 후보 필드를 표시(예: `rear pad centre X` → `G1.seats.rear_first_from_tip`), 「계획된 FAIL로 등록」 버튼(사유 필수) |
| W6 | 계획된 FAIL 상속 상태 | C5 | 템플릿에서 상속된 항목은 「미승인(상속)」으로 표시하고 게이트 결과로 해당 여부를 대조 (OPEN #34 ⑧과 통합) |
| W7 | 재현 시험 | 전체 | 검증된 첫 P4를 정답으로 「템플릿 + 읽기 + 재적용한 결정」 결과가 같은지 비교하는 `tools/test_pier_rebuild.py`. 이번 6곳 소실을 잡아야 통과 |
| W8 | 템플릿 승격 | C1 | 같은 도면 틀에서 결정이 반복되면(C13 L 28.966, B12 32/38) 「운곡교P3·P4형」 템플릿 후보로 올린다. 사용자 승인 필수, 설계자 확인 대기 항목은 상속 상태 유지 |
시험 방식: 각 항목마다 P4 12:11 판(결정 소실 상태)을 입력으로 쓰는 음성 시험과, 완성된 P1·P3·P4에서 거짓 차단 0건을 확인하는 양성 시험을 둔다.
#### 6.10.5 순서와 결정 필요 사항
| 단계 | 항목 | 이유 |
| :--- | :--- | :--- |
| 1 | W1·W2 | 변경이 작고 이번 사고의 직접 원인을 막는다(같은 화면) |
| 2 | W3·W7 | 근본 대책과 그 검증 |
| 3 | W5·W6 | 실패 해석과 계획된 FAIL 정리 |
| 4 | W4·W8 | 파생식 조사·템플릿 승인이 필요해 뒤로 미룸 |
| # | 결정 필요 | 권고 |
| :-- | :--- | :--- |
| D7 | 이전 결정의 자동 재적용 범위 | 같은 도면 틀일 때만 기본 선택, 나머지는 목록에서 고름 |
| D8 | 기존 교각에 결정 기록을 소급할지 | 새 교각부터만 적용 |
| D9 | 미확정 상태의 시험 실행(승인 없이 임시 폴더에만 결과)을 앱에 둘지 | 둔다. 승인 기록은 남기지 않음 |
| D10 | W4의 파생식 조사를 1~3단계 뒤로 미룰지 | 미룬다 |
**미해소(P4 승인)**: 복구한 값은 아직 확정되지 않았다. 미확정 5개 묶음(`expected:planned_fail`·G1·G2·G4, `params:coping_rebar`)을 검토 요약에서 재확정하고, 일괄 실행·G4·G5 승인 후 보고서 초안을 쓴다(OPEN #37).
### 6.11 W4 원본 잔존 검사 확대 계획 (2026-10-20)
> 상태: **계획 `[미확정]`** — 코드는 고치지 않았다. 실측 스크립트: `Code/교각모델링앱/tools/analysis_w4_residual_sizing.py`.
#### 6.11.1 목적
새 교각을 템플릿에서 만든 뒤 **도면 값으로 바뀌어야 하는데 템플릿 값 그대로 남은 필드**를 150초짜리 게이트 실행 전에 찾아 사람에게 보여 준다.
#### 6.11.2 현행 C3b와 한계
`engine/verify.py` `stale_fields_check`: ① 키 이름이 높이처럼 생긴 필드(`height_like`)만 ② 값이 10~60 m ③ 같은 묶음에서 그런 값이 3개 이상 바뀐 경우에만 ④ 생성 시점 값(`origin_values.json`)과 같으면 경고(w, 실패 아님). 그래서 높이가 아닌 값(`tie_offset`, 앵커 `rear_first_from_tip`, 코핑 상단 `a`)은 비교 대상이 아니다.
#### 6.11.3 실측 (P4 12:11 판 = 결정 소실 상태, 정답 = 복원한 P4)
| 항목 | 값 |
| :--- | :--- |
| 템플릿 숫자 필드 / P1~P3에서 교각마다 달라진 필드 | 1,676 / 958 |
| 12:11 판에서 템플릿 값 그대로인 숫자 필드 | 767 |
| 현행 C3b가 잡은 후보 | **0건** |
| 「템플릿 값 그대로 + 교각마다 달라지는 필드」(단순 확대) | 88건 중 실제 틀림 5건 → **정밀도 6 %** (거짓 경보 83건) |
| 위 기준 + 3교각 모두 서로 다른 값 | 2건 중 1건(`rear_first_from_tip`) → 정밀도 50 %, 놓침 많음 |
해석: **통계만으로 확대하면 경보가 94 % 거짓이라 쓸 수 없다.** 거짓 경보 대부분은 P4가 P1과 같은 구조라 맞게 같은 값인 필드(받침 이동 검사, 기초 철근 층, 솔플레이트 표 등)다. 실제로 게이트를 깬 필드는 소수(`rear_first_from_tip`, `tie_offset`, 코핑 상단 `a` 3개)다. 한편 c14 높이(.1 대 .5)와 계획된 FAIL 차이는 템플릿 잔존이 아니다(c14는 0.4 mm 차이로 게이트는 통과, 계획된 FAIL은 목록 항목 누락) → W3·W5·W6 몫이다.
필드별 이력(검증된 교각): `tie_offset` P1·템플릿 17.5, P2·P3·P4 22.5 / `a` P1·P2 11855·11880·11904, P3 11856·11880·11905, P4 11856·11881·11905 / `rear_first_from_tip` 1232·1223·1212·1198(전부 다름). 즉 교각마다 달라지는 폭이 작은(1~5 mm) 값은 사람이 눈으로 놓치기 쉽다.
#### 6.11.4 설계 — 통계 경보 대신 단계별 확인
| # | 내용 | 효과·비고 |
| :-- | :--- | :--- |
| W4a | **교각별 확인 목록(watch-list)**: `variability.json`에 필드 단위로 「교각마다 달라지며 템플릿 값이면 확인 필요」를 지정. 초기 목록은 이력으로 실증된 필드(위 3개와 `G2.schedule_L` 등)로 시작. 템플릿 값 그대로면 앱 확인 「주의」(C3b 확장), W1 실행 전 관문에 연결 | 정밀도가 높은 소수 목록. 새 교각이 검증될 때 도구가 후보 추가를 제안하고 담당자가 승인 |
| W4b | **통계 후보는 경보가 아니라 묶음 요약**: 「교각마다 달라지는 필드 N개가 템플릿 값 그대로」를 묶음 단위로 접어 보여 주고 필드 목록은 펼쳐 볼 때만 표시 | 거짓 94 %를 경보로 쓰지 않으면서 훑어볼 수 있게 함 |
| W4c | **읽기 후보 연동**(W2): 목록·통계 후보 중 도면에서 후보(앵커·표)가 있는 필드는 「도면에서 읽기」 화면에 기본 선택으로 올림 | `rear_first_from_tip`은 B등급 후보가 있었다 |
| W4d | **파생값 조사**: `a`, `tie_offset` 등이 어떤 입력·도면 글자에서 나오는지 P1~P4 값과 도면 문자로 조사. 식이 확인되면 계산값으로 제안(출처 「계산」), 못 찾으면 W4a 목록에 남김 | 현재 식을 모름. 조사 결과에 따라 W4a 목록이 줄어듦 |
| W4e | **교각 그룹별 기본값 승격**(W8 연계): `tie_offset` 22.5처럼 P2 이후 전부 같은 값은 템플릿 P1 값(17.5)이 오히려 예외이므로, 같은 도면 틀 그룹 템플릿에서 기본값을 바꾸는 후보로 제안 | 승인 필수 |
#### 6.11.5 판정 규칙 (C3b 확장안)
| 조건 | 판정 |
| :--- | :--- |
| W4a 목록 필드가 생성 시점 값 그대로 | **주의(w)**, 검토 요약의 해당 묶음에 필드명·템플릿 값·교각별 이력 표시 |
| 위 값이 읽기 후보(A·B등급)와 다름 | 주의 + 읽기 후보로 제시 |
| 통계 후보(W4b) | 정보(건수만), 판정에 영향 없음 |
| 묶음 전체가 그대로 | 기존 C3 |
실패(불일치)로 판정하지 않는다: 같은 값이 맞을 수도 있고(P2~P4 `a` 11880 등), 근거 없이 막으면 6.9절의 일괄 검토 흐름을 해치기 때문이다. 막는 것은 W1(params↔expected 사실 불일치)이 맡는다.
#### 6.11.6 시험
| 시험 | 기준 |
| :--- | :--- |
| 음성(잡아야 함) | P4 12:11 판에서 `rear_first_from_tip`·`tie_offset`·`a` 3개 모두 주의로 표시 |
| 양성(거짓 경보 억제) | 완성된 P1·P2·P3·P4에서 W4a 목록 경보 0건 (값이 도면 값이면) |
| 통계 후보 | 12:11 판 기준 건수만 표시, 판정 불변 |
| 회귀 | `tools/test_autocheck.py`·`test_consistency.py`·`test_review.py` 통과, 기존 C3b 동작 유지 |
| 신규 | `tools/test_stale_watchlist.py` |
#### 6.11.7 순서와 결정 필요
1. W4d 파생값 조사(코드 변경 없음, 결과를 보고) → 2. W4a·W4b 구현 → 3. W4c(W2 구현 시 함께) → 4. W4e(W8 승인 시).
| # | 결정 필요 | 권고 |
| :-- | :--- | :--- |
| D11 | 초기 watch-list 필드 범위 | 게이트를 실제로 깬 3개 + `G2.schedule_L`로 시작해 사고 때마다 추가 |
| D12 | 통계 후보(W4b)를 화면에 둘지 | 묶음 요약만 두고 경보 아님 |
| D13 | 템플릿 값 그대로를 실패가 아닌 주의로 둘지 | 주의 유지(차단은 W1 사실 불일치만) |
---
## 7. 리스크·결정 사항
### 7.1 리스크
| # | 리스크 | 대응 |
| :-- | :--- | :--- |
| R1 | 앱 확인 '일치'를 "도면이 맞다"로 오해 | 근거 보기에서 '무엇과 무엇이 같은가'를 문장으로 표시, 신뢰 등급 병기 |
| R2 | 규칙·앵커가 4개 도면에 과적합 | 등급 C·'없음'을 정상으로 표시, 새 형식 도면마다 앵커 추가 |
| R3 | 앵커 소급 추출 비용(원본 4교각) | D6 결정(신규부터 vs 소급) |
| R4 | 앱 확인 규칙이 생성 스크립트와 같은 논리여서 순환 | 도면 원자료(DXF·문자 색인)를 직접 읽는 경로만 사용, 스크립트 재생성 일치(C8)는 '독립성 낮음'으로 표시 |
| R5 | 일괄 확정이 다시 도장화 | 일치만 대상, 근거표 열람 조건, 주의·불일치는 사유 필수 |
| R6 | 변수 등급 오분류 | `variability.json`은 템플릿 원본 교각 담당자가 확정, 분류 변경은 별도 Task |
### 7.2 결정 사항
| # | 내용 | 상태 | 권고 |
| :-- | :--- | :--- | :--- |
| D1 | 확정 주체 | **결정 2026-10-14 / 심영표: 앱 확인 + 사람 확정** | — |
| D2 | 표 밖 치수 탐색 방식 | **결정 2026-10-14 / 심영표: 규칙 + 템플릿 앵커** | — |
| D3 | F(고정) 항목 편집 시 처리 | **결정 2026-10-15 / 심영표: 권고안** | 경고 + 사유 입력(차단은 생성기 개발자 모드에서만) |
| D4 | 일괄 확정 조건 | **결정 2026-10-15 / 심영표: 권고안** | '일치' 묶음만, 근거표를 한 번 열어야 활성화 |
| D5 | 착수 범위·순서 | **결정 2026-10-15 / 심영표: 권고안** | A → B 먼저(위험 큰 P1·P2 해소), C는 B 검증 뒤 |
| D6 | 앵커 소급 추출 | **결정 2026-10-15 / 심영표: 권고안** | 옥성교·Ramp-A(최근, 판독 스크립트 최신)부터, 나머지는 필요 시 |
---
## 부록 A. 보호 템플릿 묶음 분류 (등급 초안, 담당자 확정 전)
| 묶음 | 운곡교P1형 | 울산형 | Ramp-A형 | 옥성교형 |
| :--- | :-: | :-: | :-: | :-: |
| `pier_id` | F | F | F | F |
| `scope`·`builders`·`checkers`·`negative` (실행 설정) | F (없음=기본) | F | F | F |
| `lean_concrete`·`footing`·`column`·`materials` | V1 | V1 | V1(기초 없음) | V1 |
| `piles` / `pile` | V1 | — | V1(`pile` 구간) | V1 |
| `coping` | V1 + 점 목록 V2 (`form` F) | V1 + V2 (`form` F) | V1 + V2 (`form` F) | V1 + V2 (`form`·`skew_deg` 구조는 F) |
| `bearing_seats`·`sole_plates` | V1 + 위치 V2 (대좌 구조 F) | V1 + V2 | V1 + V2 | V1 + V2 (대좌 공유 구조 F) |
| `rebar_marks` | 직경 V1, 마크 체계 F | 동일 | 동일 | 동일 |
| `column_rebar` | V1·V2 (`form` F) | V1·V2 | V1·V2 | V1·V2 |
| `footing_rebar` | V1·V2 | V1·V2 | — (`shaft_rebar` V1·V2) | **V3** (중심선 목록) |
| `coping_rebar` | V1·V2 (규칙 값) | **V3** | **V3** | **V3** |
| `expected.G1~G5` | V1·V2 (**params와 독립 입력**) | 동일 | 동일 | 동일 |
| `expected.planned_fail`·`rebar_palette` | 사람 결정 기록(교각별) | 동일 | 동일 | 동일 |
| `display`·`_schema`·`_meta`·`*_src`·`hitl_decisions*` | F / 참고 | 동일 | 동일 | 동일 |
> 이 표는 묶음 수준의 초안입니다. 필드 수준 분류(`variability.json`)는 A단계에서 템플릿마다 작성하고 담당자가 확정합니다.
## 부록 B. 근거 파일
| 구분 | 경로 |
| :--- | :--- |
| 화면·동작 | `Code/교각모델링앱/web/templates/pier.html`, `web/app.py`(`confirm_all`, `unconfirmed`), `web/store.py`(`GATE_SECTIONS`, `section_state`, `confirm`, `set_section`) |
| 표 판독·후보 | `engine/reader.py`(`table_candidates`, `field_candidates`, `proposals`, `el_list`, `entity_info`) |
| 사용 방법 | `DOC/04_실행/실행_T형교각모델링앱사용설명서_2026-10-14_v0.2.07.md` §4~5, §9 |
| 일반화 이력 | `context-migration/OPEN_미해결목록.md` #19·#23·#24·#25, `DOC/02_분석/분석_운곡교P2교각도면판독_2026-10-12_v0.1.00.md` §7 |
| 표 밖 치수 판독 사례 | `Code/옥성P1판독/extract_g*.py`, `DOC/02_분석/분석_옥성교P1교각도면판독_2026-10-14_v0.1.07.md` |
+569
View File
@@ -0,0 +1,569 @@
# T형 교각 모델링 앱 화면 PRD — 「입력 묶음 확인·확정」 개편 중심
| 버전 | 수정일 | 수정자 | 주요 변경 내용 | 비고 |
| :--- | :--- | :--- | :--- | :--- |
| v0.1.00 | 2026-10-15 | 심영표 | 신규 작성 — 기획서 v0.1.00의 기능을 구현하는 화면 13종의 요구사항 | 초안 `[미확정]`. 코드는 고치지 않음. 결정 대기 항목은 11장 |
| v0.1.01 | 2026-10-15 | 심영표 | 11.1절 D3~D6을 권고안대로 결정 반영 | 사용자 지시 "권고안대로" |
| v0.1.02 | 2026-10-15 | 심영표 | 11.2절 UX1~UX6 결정 반영 | 사용자 지시(UX1 대체, UX2 변경, UX3 내장, UX4 승인, UX5 제외, UX6 제작) |
> 상태: **초안 `[미확정]`**. 근거는 `DOC/01_기획/기획_입력묶음확인확정개편_2026-10-14_v0.1.00.md`(이하 기획서)와 현행 앱(`Code/교각모델링앱/web/`)이다. 화면 디자인 규칙은 같은 폴더의 `design.md`가 정하고, 그 근거는 `RAG/design system_v0.1.1`(Prism)이다. `[추론]`은 확인이 필요한 판단이다.
---
## 1. 문서 개요
### 1.1 목적
기획서가 제안한 네 가지 기능을 사용자가 실제로 쓸 수 있는 **화면 단위의 요구사항**으로 바꾼다.
| 기획서 기능 | 이 문서의 화면 |
| :--- | :--- |
| 「2. 입력 묶음 확인·확정」 개편 (4.1) | S03, S04, S10 |
| 앱이 수행하는 확인과 근거 제시 (4.3) | S04, S05, S12 |
| 도면 표 밖 치수 찾기 (4.2) | S07, S08 |
| 같은 템플릿에서 변수 가능과 불가능 구분 (4.4) | S02, S06, S13 |
### 1.2 범위
| 포함 | 제외 |
| :--- | :--- |
| 교각 작업 화면의 1~4단계(도면·묶음·게이트·보고서) 중 변경이 필요한 화면 | 게이트 실행·승인의 동작 자체(그대로) |
| 신규 화면 5종(S02, S05, S08, S10, S13)과 S04의 요약 띠 | 새 교각 형식의 생성기·검사기 |
| 화면 공통 어휘(상태·등급·출처)와 문안 | 모바일·태블릿 화면(로컬 데스크톱 전용) |
| 수용 기준과 시험 방법 | 서버·엔진의 내부 구현(필요한 자료 계약만 6장에 기술) |
### 1.3 사용자와 전제
| 구분 | 내용 |
| :--- | :--- |
| 주 사용자 | 담당자(교각 도면을 모델링하는 구조엔지니어). 한 번에 한 사람이 사용한다 |
| 보조 사용자 | 검토자(보고서와 확정 기록을 읽는 사람). 읽기 전용 |
| 환경 | 사용자 PC의 로컬 Flask 앱. 화면 폭 1280px 이상 데스크톱. 인터넷 없이 동작한다 |
| 전제 | 확정과 게이트 승인은 **사람의 검토 기록**이다. 앱은 어느 화면에서도 대신하지 않는다 |
| 언어 | 화면 문구는 한국어, 코드·API·키 이름은 영어 |
### 1.4 용어
기획서 1.3절의 용어(묶음·확인·확정·표 밖 치수·앵커)를 그대로 쓴다. 이 문서에서 추가하는 용어는 다음과 같다.
| 용어 | 뜻 |
| :--- | :--- |
| 근거 | 앱 확인 한 건이 어떤 두 값을 비교해 어떤 결과가 나왔는지 적은 한 문장과 그 출처 |
| 원본값 | 템플릿 원본 교각에서 복사한 뒤 새 도면 근거 없이 그대로인 값 |
| 변수 등급 | V1 값 변수 · V2 목록 변수 · V3 생성 변수 · F 고정 (기획서 4.4) |
| 작업대 | 교각 한 개의 작업 화면 전체(S03). 1~4단계를 한곳에서 진행한다 |
---
## 2. 제품 원칙
| ID | 원칙 | 화면에 미치는 영향 |
| :--- | :--- | :--- |
| **PP-1** | **앱은 확인하고, 사람은 확정한다.** 앱 확인은 확정 기록을 만들지 않는다 | 확정 버튼은 사람의 이름 입력과 클릭으로만 동작한다. 어느 화면에도 자동 확정이 없다 |
| **PP-2** | **근거 없음을 숨기지 않는다.** 억지 후보를 만들지 않는다 | '근거 없음'은 일치와 같은 비중으로 표시하고 요약 띠에 개수를 낸다 |
| **PP-3** | **원본 값 그대로 확정되는 사고를 막는다.** (기획서 P2) | '원본값' 표시를 묶음 표에 상시 노출하고 일괄 확정에서 제외한다 |
| **PP-4** | **색만으로 구분하지 않는다.** | 모든 상태는 글자와 형태를 함께 쓴다(Prism BD-1) |
| **PP-5** | **판정 위에 근거를 둔다.** 결론을 먼저, 경위를 뒤에 | 각 화면의 첫 영역이 결론(요약 띠·판정)이고 근거는 이어서 둔다 |
| **PP-6** | **값을 바꾸면 이전 판단이 무효가 된다.** (현행 해시 방식 유지) | 앱 확인·사람 확정·게이트 승인 모두 '재확인 필요'를 같은 어휘로 표시한다 |
---
## 3. 화면 목록
### 3.1 화면 지도
```
S01 교각 목록 ──┬─ S02 새 교각 적합성 판별 ── (만들기)
│
└─ S03 교각 작업대 (1 도면 · 2 입력 묶음 · 3 게이트 · 4 보고서)
├─ 1 도면 ── S09 도면 문자 검색·핸들 조회
├─ 2 묶음 ── S04 입력 묶음 확인·확정
│ ├─ S05 근거 보기 ── S06 값 편집
│ ├─ S07 표 후보값·표 밖 치수 ── S08 도면 보기
│ └─ S10 일괄 확정 확인
├─ 3 게이트 ── S11 게이트
└─ 4 보고서 ── S12 검토보고서 초안
S13 변수 등급 안내 (템플릿별, S01·S02·S06에서 진입)
```
### 3.2 화면 일람
| ID | 화면 | 구분 | 패턴 | 해결하는 문제(기획서) | 단계 |
| :--- | :--- | :--- | :--- | :--- | :-: |
| S01 | 교각 목록 | 변경 | W5 | P6 (템플릿 형식 설명 노출) | A |
| S02 | 새 교각 적합성 판별 | **신규** | W3 | P6 | A |
| S03 | 교각 작업대 | 변경 | W1 + W2 | P1~P3, P8 (진행 요약) | A·B |
| S04 | 입력 묶음 확인·확정 | **변경(핵심)** | W5 | P1, P2, P3, P5, P6, P8 | A·B |
| S05 | 근거 보기 | **신규** | W5 | P3, P5, P7 | B |
| S06 | 값 편집 | 변경 | W3 | P6, P7 | A |
| S07 | 표 후보값·표 밖 치수 | 변경 | W5 | P4, P7 | C |
| S08 | 도면 보기 | **신규** | 모달 | P3, P4 | C |
| S09 | 도면 문자 검색·핸들 조회 | 유지(토큰 적용) | W3 + W5 | — | — |
| S10 | 일괄 확정 확인 | **신규** | 모달 | P1 | B |
| S11 | 게이트 | 유지 + 소변경 | W2 | 앱 확인 요약 링크만 추가 | D |
| S12 | 검토보고서 초안 | 변경 | W4 | 8장에 앱 확인 요약 | B·D |
| S13 | 변수 등급 안내 | **신규** | W4 | P6 | A |
단계는 기획서 6장의 A 분류·표시, B 앱 확인 엔진, C 표 밖 치수 탐색, D 통합·문서이다. 화면은 해당 단계가 끝나야 켜진다. 아직 구현되지 않은 단계의 열은 화면에 나타나지 않는다(빈 열을 두지 않는다).
---
## 4. 공통 요구사항
### 4.1 화면 골격
| 항목 | 요구 |
| :--- | :--- |
| 헤더 | 서비스명, 화면 ID, 교각 이름, 마지막 저장 시각, 실행 환경(로컬)을 한 줄에 둔다 |
| 좌측 레일 | 1280px 이상에서 교각 안의 단계 내비게이션(1 도면 · 2 입력 묶음 · 3 게이트 · 4 보고서)과 단계별 완료 표시를 둔다 |
| 본문 폭 | 연속 문장은 38em 이내. 표는 가로 스크롤을 허용한다 |
| 언어 | 안내·근거·오류 문장은 규정문체(평서 종결 `~한다`)를 쓴다. 버튼 레이블은 동사로 시작하고 8자 이내로 쓴다 |
| 폭 | 1280px에서 표 외 가로 스크롤이 없다 |
### 4.2 상태 어휘 (전 화면 공통)
모든 상태는 **글자 + 형태 + 색**을 함께 쓴다. 형태와 색의 대응은 `design.md` 6장이 정한다.
| 어휘 | 값 | 표시 위치 |
| :--- | :--- | :--- |
| **앱 확인** | 일치 · 주의 · 불일치 · 근거 없음 · —(해당 없음) · 확인 안 함 | S03, S04, S05, S10, S12 |
| **사람 확정** | 확정 · 미확정 · 변경됨 | S03, S04, S05, S10, S12 |
| **변수 등급** | V1 · V2 · V3 · F | S04, S05, S06, S13 |
| **값 출처** | 표 · 표 밖 치수 · 판독 스크립트 · 원본값 · 사람 입력 | S04, S05 |
| **신뢰 등급** | A · B · C · 없음 | S07, S08 |
| **게이트 판정** | PASS · FAIL · 계획 FAIL · 재실행 필요 | S03, S11, S12 |
규칙은 다음과 같다.
- 상태 글자는 6자 이내다. 설명이 필요하면 상태 옆이 아니라 근거 문장(S05)에 쓴다.
- 값이 바뀌면 앱 확인과 사람 확정이 함께 '변경됨 · 재확인 필요'가 된다. 표시 어휘를 두 개로 나누지 않는다.
- '해당 없음'은 배지가 아니라 `—`로 쓴다. '확인 안 함'은 배지가 아니라 [확인 실행] 버튼으로 대신한다.
### 4.3 오류·빈 상태·진행 표시
| 상황 | 요구 |
| :--- | :--- |
| 오류 | 무엇이·왜·어떻게 3요소와 기호를 함께 쓴다. 예: "도면 색인이 없다. DWG 변환이 끝나지 않았다. 1단계에서 도면을 다시 올린다" |
| 빈 상태 | 원인 + 다음 행동 한 문장(60자 이내). 예: "올린 도면이 없다. DWG 파일을 올린다" |
| 진행 | 2초를 넘는 작업(앱 확인, 게이트)은 진행 표시를 보이고 `aria-live`로 낭독한다. 중요 결과를 토스트로만 알리지 않는다 |
| 파괴적 동작 | 삭제·승인 반려·형식 변경 확정은 확인 절차를 거친다 |
### 4.4 접근성·시험
| 항목 | 기준 |
| :--- | :--- |
| 표준 | WCAG 2.2 AA를 목표로 한다 |
| 키보드 | 전 기능을 키보드만으로 조작한다. 탭 순서가 시각 순서와 같다 |
| 히트 영역 | 44px 이상. 인접 간격 8px 이상 |
| 표 | `caption`, `th scope`, 고정 헤더. 상태 셀은 글자를 포함한다 |
| 색 | 그레이스케일로 인쇄해도 모든 상태를 구분할 수 있다 |
---
## 5. 화면별 요구사항
각 화면은 **목적 → 진입 → 구성 → 행동 → 상태 → 수용 기준** 순서로 쓴다. 문안은 예시이며 최종 문구는 디자인 시안에서 확정한다.
### S01 교각 목록 (변경)
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 교각을 고르고 새 교각을 만든다. 현행 화면의 기능을 유지한다 |
| 구성 | ① 교각 표(교각·형식·만든 날·템플릿·진행 요약) ② 새 교각 영역 ③ 보호 템플릿 표 |
| 변경 | 교각 표에 **진행 요약** 열(예: 묶음 27 중 사람 확정 12, 게이트 3/5 승인)을 추가한다. 템플릿 표의 '형식 설명' 열에 [변수 등급 보기](S13) 링크를 둔다 |
| 행동 | [새 교각 만들기] → S02, 교각 이름 → S03, 삭제(확인 절차) |
| 수용 기준 | 진행 요약이 실제 `confirm.json`·게이트 승인 기록과 100% 일치한다 |
### S02 새 교각 적합성 판별 (신규)
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 새 도면이 선택한 템플릿으로 모델링 가능한 형식인지 교각을 만들기 전에 판별한다 (기획서 4.4 체크리스트 6문항) |
| 진입 | S01의 [새 교각 만들기] |
| 구성 | ① **도면 올리기** ② **추천 순위(근거 표시, 맞는 것이 없으면 "판단 불가")** ③ 이름·템플릿 선택(추천 1위 기본) ④ 6문항(추정 답 미리 채움, 사람이 변경) ⑤ 각도 값 확인(skew·받침 회전·기초 회전) ⑥ 결과 판정 영역 ⑦ [교각 만들기] |
| 6문항 | 기초 형식(말뚝 유무) · 기둥 단면 형식 · 코핑 하면 형상 · 받침 배치 구조 · 철근 형상 종류 · 철근 생성 방식. 평면 사각·직각과 각도는 문항이 아니라 ⑤의 값으로 확인한다 |
| 추천 | 도면 단서로 문항 값을 추정하고 5개 템플릿과 일치 문항 수로 순위를 낸다. 단서와 도면 위치를 근거로 보인다. 추정은 확정이 아니며 사람이 바꾼다. 교각 폴더는 [교각 만들기] 시점에 생성한다 |
| 추정 범위 `[구현 2026-10-15]` | 6문항 중 **3문항만 추정**한다: 기초 형식(도면의 '말뚝' 문자 유무) · 코핑 하면 형상(정면도 R 문자: 10 m 이상 곡선, 1~10 m 나팔형(약함), 없으면 직선 경사, 작은 R은 평면 모서리로 제외) · 받침 개수(ELEVATION TABLE 행 수). 기둥 단면·철근 형상·철근 생성 방식은 도면 문자로 판별할 근거가 없어 '미확인'으로 두고 사람이 답한다. 근거마다 신뢰(보통·약함)를 표시한다. 규칙은 9개 도면 세트로만 확인했다 |
| 행동 | 모든 문항에 답하면 판정이 나온다. 전부 '예'이면 "이 템플릿으로 만들 수 있다". 하나라도 '아니오'이면 "같은 템플릿으로 만들 수 없다. 가장 가까운 템플릿에 생성기·검사기를 추가해야 한다"를 보이고, 문항별로 어떤 부분이 다른지 적는다 |
| 상태 | 미응답 문항은 오류로 표시하고 어느 문항인지 이름으로 알린다 |
| 진행 허용 | '아니오'가 있어도 [그래도 만들기]로 진행할 수 있다. 이 경우 교각 이름 옆과 S03·S04 상단에 **형식 불일치 경고**가 상시 표시된다(경고는 닫을 수 없다) |
| 수용 기준 | 6문항의 답이 교각 메타에 저장된다. 경고 교각의 S03·S04에서 경고가 100% 보인다 |
| 구현 현황 | **전부 구현됨(2026-10-15)**: 추천 순위·근거, 6문항 답과 각도 값 3종(skew·받침 회전·기초 회전)을 `pier.json`의 `fit_answers`에 저장, 결과 판정 영역(답을 바꾸면 즉시 갱신, 불일치 시 버튼이 [그래도 만들기]로 바뀜), 불일치 항목을 `fit_mismatch`에 저장하고 교각의 모든 화면 상단에 닫을 수 없는 경고 표시(S03·S04 포함). 화면 시안: `시안/index.html`의 `#/new` |
### S03 교각 작업대 (변경)
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 교각 한 개의 진행 상태를 한눈에 보고 다음 단계로 간다 |
| 구성 | ① 결론 띠(다음에 할 일 한 가지) ② 1~4단계 카드 ③ 단계별 상세 진입 |
| 결론 띠 | 현재 상태에서 가장 앞선 미완 단계를 말한다. 예: "필요 묶음 27개 중 앱 확인 일치 19 · 주의 4 · 불일치 1 · 근거 없음 3, 사람 확정 0. 2단계에서 근거를 확인한다" |
| 1 도면 | DWG 목록(DXF 변환 상태, 해시 앞 16자), 색인 문자 수, 도면 올리기, S09·S07 링크 |
| 2 입력 묶음 | S04의 요약(4.2절 어휘별 개수)과 [입력 묶음 열기] |
| 3 게이트 | G1~G5 판정·승인 요약과 [게이트 열기] |
| 4 보고서 | 초안 생성 여부와 [보고서 열기] |
| 행동 | 단계 카드의 주 버튼은 화면에 **하나**(결론 띠가 가리키는 단계의 버튼)만 강조한다 |
| 상태 | 도면 없음 → 1단계만 활성 안내. 형식 불일치 경고가 있으면 결론 띠 위에 경고 |
| 수용 기준 | 결론 띠의 숫자가 S04 표의 집계와 100% 일치한다. 주 버튼이 화면에 1개다 |
### S04 입력 묶음 확인·확정 (변경, 핵심)
기획서 4.1절의 화면이다.
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 모든 입력 묶음의 앱 확인 결과와 사람 확정 상태를 보고, 근거를 확인한 뒤 확정한다 |
| 진입 | S03의 2단계, 레일 |
| 구성 | ① **요약 띠** ② 도구 줄(필터·확인 실행·일괄 확정) ③ **묶음 표** ④ 형식 불일치 경고(해당 시) |
**① 요약 띠**
- 한 줄: "필요 27개 중 앱 확인 일치 19 · 주의 4 · 불일치 1 · 근거 없음 3 · 사람 확정 0".
- 각 숫자는 필터 버튼이다(누르면 표가 해당 상태만 보인다).
- 마지막 앱 확인 시각과 검사 버전을 함께 보인다. 값이 바뀌어 오래된 결과가 있으면 "재확인 필요 N개"를 별도 표시한다.
**③ 묶음 표 (열 정의)**
| 열 | 내용 | 비고 |
| :--- | :--- | :--- |
| 묶음 | 파일·이름. 누르면 S05 | 이름은 영문 키와 한글 설명 병기 |
| 변수 등급 | V1 · V2 · V3 · F | S13 링크 |
| 값 출처 | 표 · 표 밖 치수 · 판독 스크립트 · 원본값 · 사람 입력 | '원본값'은 강조(PP-3) |
| 앱 확인 | 일치 · 주의 · 불일치 · 근거 없음 · — | 누르면 S05의 해당 근거로 |
| 사람 확정 | 확정 · 미확정 · 변경됨, 확정자·시각 | |
| 확인 | 개별 [확인 실행] | 앱 확인 결과가 오래되었거나 없을 때 |
- 정렬 기본값: 앱 확인이 '불일치 → 근거 없음 → 주의 → 일치' 순, 같은 상태는 파일 순. 사용자가 열 머리를 눌러 바꿀 수 있다.
- '확정 불필요(참고)' 묶음은 표의 맨 끝 구분 영역으로 내리고 기본으로 접는다. 접힌 개수를 표시한다.
- 숫자 열은 우측 정렬이며 등폭 숫자를 쓴다.
**② 도구 줄**
| 요소 | 동작 |
| :--- | :--- |
| 필터 | 앱 확인 상태·사람 확정 상태·변수 등급·출처로 좁힌다. 현재 적용 필터를 글자로 보인다 |
| [전체 확인] | 필요 묶음 전체의 앱 확인 C1~C8을 실행한다. 진행 표시 후 요약 띠를 갱신한다 |
| [일치분 확정] | 앱 확인이 '일치'이고 원본값이 아닌 묶음만 대상으로 S10을 연다 (기획서 D4) |
**상태**
| 상태 | 동작 |
| :--- | :--- |
| 앱 확인 전 | 앱 확인 열이 모두 [확인 실행]. 요약 띠는 "앱 확인 전 — [전체 확인]을 실행한다" |
| 확인 실행 중 | 행마다 진행 표시, 결과가 나오는 대로 갱신. 실행 중에도 표 읽기는 가능 |
| 원본값 묶음 | 앱 확인은 '근거 없음(원본값)'. 일괄 확정 대상에서 제외하고 행에 제외 사유를 쓴다 |
| 형식 불일치 경고 | 표 상단에 닫을 수 없는 경고 띠 |
**수용 기준**
| # | 기준 |
| :-: | :--- |
| 1 | 필수 묶음 100%에 앱 확인 상태(또는 [확인 실행])가 있다 |
| 2 | '근거 없음'과 '원본값'이 숨겨지지 않는다 |
| 3 | 새로 복사한 교각(템플릿 직후)에서 값이 바뀌지 않은 묶음은 전부 '원본값'이다 |
| 4 | 표와 요약 띠의 개수가 일치한다 |
| 5 | 1280px 폭에서 표 외 가로 스크롤이 없다 |
| 6 | 키보드만으로 필터·행 열기·확인 실행·일괄 확정 진입이 가능하다 |
### S05 근거 보기 (신규)
기획서 4.3절의 C1~C8 결과를 사람이 읽는 화면이다.
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 한 묶음의 앱 확인 근거를 문장으로 읽고 확정 여부를 판단한다 |
| 진입 | S04의 묶음 이름 또는 앱 확인 셀 |
| 패턴 | W5 목록-상세. 좌측에 묶음 이동 목록(상태 포함), 우측에 상세 |
**상세 구성**
1. **결론**: 묶음 이름, 변수 등급, 앱 확인 결과 한 줄, 사람 확정 상태.
2. **근거 표**: C1~C8 각각의 `검사 · 결과 · 근거 문장 · 도면 위치`. 해당 없는 검사는 `—`.
3. **값 비교**: 현재 값과 후보 값 또는 템플릿 원본 값을 나란히 보인다(차이가 있는 항목만).
4. **확정 영역**: 확정자 이름, 사유(주의·불일치·근거 없음이면 필수), [확정].
**근거 문장 규칙** (R1: "일치"를 "도면이 맞다"로 오해하지 않게)
- 모든 근거는 '무엇과 무엇이 같은가'를 쓴다. 예: "기둥 지름 2,200은 도면 단면도(D1, 좌표 …)의 치수 문자 '2.200'과 같다."
- 규칙으로 만든 후보와의 비교임을 밝힌다. 예: "이 결과는 후보를 만든 규칙과 값이 같다는 뜻이다. 도면이 맞다는 판정이 아니다."
- 검사에 쓴 방법의 독립성을 표기한다(예: C8 재생성 일치는 '독립성 낮음').
**행동**
| 행동 | 조건·동작 |
| :--- | :--- |
| [도면 보기] | 근거의 도면 위치마다 S08을 연다 |
| [값 편집] | S06으로 이동 |
| [확정] | 확정자 이름이 있고, 주의·불일치·근거 없음이면 사유가 있어야 한다. 조건이 모자라면 버튼은 활성인 채 오류 메시지를 보인다 |
| 이전·다음 묶음 | 목록의 순서로 이동. 확정하면 다음 미확정 묶음으로 이동하는 선택지를 준다 |
**상태**
| 상태 | 동작 |
| :--- | :--- |
| 앱 확인 전 | 근거 표 대신 [확인 실행]과 "이 묶음은 아직 확인하지 않았다" |
| 값 변경됨 | 결론에 "값이 바뀌었다. 이전 확인과 확정이 무효가 되었다" 표시, [다시 확인] |
| V3 묶음 | 값 편집 대신 "판독 스크립트로 다시 만든다" 안내와 C8 결과 |
| 원본값 | 값 비교에 "템플릿 원본과 같다. 새 도면 근거가 없다" |
**수용 기준**
| # | 기준 |
| :-: | :--- |
| 1 | 검사 8종 각각의 결과·문장·도면 위치가 보인다 |
| 2 | '일치' 근거마다 비교한 두 값과 출처가 문장에 있다 |
| 3 | 사유 없는 '주의·불일치·근거 없음' 확정이 불가능하다(오류 메시지 표시) |
| 4 | 확정 기록에는 사람이 입력한 이름만 들어가고, 앱 확인 서버 경로는 확정 기록을 쓰지 않는다 |
### S06 값 편집 (변경)
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 묶음의 값을 수정하고 저장한다. 현행 `section` 화면의 기능 유지 |
| 변경 | ① 상단에 **변수 등급 안내**(이 묶음에서 바꿔도 되는 값과 안 되는 값) ② F 항목 편집 시 경고 ③ V3 묶음은 편집 차단 ④ 저장 뒤 앱 확인 결과가 무효가 됨을 알림 ⑤ [저장된 값으로 확정] 버튼 제거(확정은 S05·S10에서만) |
| F 항목 | 필드 수준 등급이 F이면 편집 영역에 "형식을 정의하는 값이다. 바꾸면 같은 템플릿이 아니다" 경고. 저장하려면 사유를 입력해야 한다(기획서 D3 권고). 저장하면 묶음에 '형식 변경' 표시가 붙고 S03에 새 템플릿 필요 안내가 뜬다 |
| V3 묶음 | 편집창 대신 읽기 전용 요약(개수·크기·해시)과 "판독 스크립트로 다시 만든다" 안내, 스크립트 경로 |
| 입력 보조 | 치수 사슬 입력(`22@120=2.640`)을 지원하는 필드는 합계 검산 결과를 즉시 보인다 |
| 수용 기준 | V3 묶음에서 편집 영역이 없다. F 편집은 사유 없이 저장되지 않는다. 저장 직후 S04 상태가 '변경됨'으로 바뀐다 |
### S07 표 후보값·표 밖 치수 (변경)
기획서 4.2절의 화면이다. 현행 `candidates` 화면을 확장한다.
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 도면 표와 표 밖 치수에서 찾은 후보를 현재 값과 비교하고, 사람이 골라 반영한다 |
| 구성 | ① 요약 띠(표 후보 N · 표 밖 치수 후보 N(A·B·C 개수) · 다름 N) ② 후보 비교 표 ③ 인식한 표 ④ EL 표기 목록 |
| 후보 비교 표 열 | 반영(선택) · 대상 · 현재 값 · 후보 값 · 구분(표/표 밖 치수) · 상태(같음/다름) · 신뢰 등급 · 도면 위치 · 근거 |
| 표 밖 치수 구역 | 규칙 R1~R6 중 어느 규칙이 찾았는지 '규칙' 열에 쓴다. 신뢰 등급 A·B·C·없음을 병기한다 |
| 행동 | 행 선택 → [선택한 후보값 반영]. 등급 C는 반영 시 사유 필수. 등급 '없음'은 후보 행이 아니라 "근거 없음" 목록에 따로 나열하고 반영 버튼이 없다 |
| params·expected 분리 | 한 후보가 params와 expected 양쪽에 반영될 수 있는 경우, 반영 대상 줄을 **둘 중 하나씩** 고르게 하고 둘을 함께 고르면 경고한다(기획서 P7) |
| 상태 | 후보 0건 → "후보가 없다. 도면의 틀이 앱의 규칙과 다르면 정상이다. 문자 검색(S09)으로 직접 찾는다" |
| 수용 기준 | 자동 반영이 없다. 모든 반영은 사람이 선택한 행에만 적용된다. 등급 '없음'이 후보로 둔갑하지 않는다 |
### S08 도면 보기 (신규)
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 근거·후보의 도면 위치를 확대 이미지로 확인한다 (`dxftools.render`와 같은 이미지) |
| 형태 | 모달. 대상 좌표 주변 확대 이미지에 대상 위치를 표시한다 |
| 구성 | 이미지, 캡션(도면 이름·뷰·좌표·핸들), 확대·축소·이동, 인접 후보로 이동 |
| 행동 | Esc로 닫는다. 닫으면 호출한 행으로 포커스가 돌아간다 |
| 접근성 | 이미지에 대체 텍스트로 위치·객체 설명을 쓴다. 이동·확대는 버튼과 방향키 둘 다 지원한다 |
| 수용 기준 | 모든 이미지에 캡션이 있다. 모달 안에 모달이 열리지 않는다 |
### S09 도면 문자 검색·핸들 조회 (유지)
현행 기능을 유지한다. 디자인 토큰만 새 규칙으로 바꾼다. 검색 결과 행에 [후보로 보내기]는 두지 않는다(자동 반영 금지, PP-1).
### S10 일괄 확정 확인 (신규)
기획서 4.1절 D4의 화면이다.
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 앱 확인이 '일치'인 묶음을, 근거를 확인한 뒤 한꺼번에 확정한다 |
| 형태 | 모달 |
| 구성 | ① 결론("일치 묶음 19개를 확정한다. 주의·불일치·근거 없음·원본값 8개는 대상이 아니다") ② **근거표**(묶음·근거 한 줄·출처) ③ 확정자 이름 ④ [확정] ⑤ [취소] |
| 근거표 열람 조건 | 근거표를 끝까지 보지 않으면 [확정]이 오류를 낸다. 오류는 "근거표를 먼저 확인한다. 일치 묶음의 근거를 읽지 않으면 확정할 수 없다. 근거표를 끝까지 내린다"로 알린다 (비활성화하지 않고 오류 메시지를 낸다) |
| 제외 목록 | 대상에서 빠진 묶음과 사유(주의·불일치·근거 없음·원본값)를 같은 모달에서 보여 준다. 누르면 S05로 이동 |
| 완료 | 확정 기록 개수와 확정자·시각을 결과로 보인다. 토스트만으로 알리지 않고 S04 요약 띠에도 반영한다 |
| 수용 기준 | 원본값·주의·불일치·근거 없음 묶음이 대상에 0건이다. 근거표를 읽지 않은 상태에서는 확정되지 않는다. 확정자 이름이 비어 있으면 확정되지 않는다 |
### S11 게이트 (유지 + 소변경)
현행 기능을 유지한다. 변경은 두 가지이다.
- 게이트 실행 조건 문구에 "필요 묶음이 사람 확정됨" 외에 앱 확인 요약 링크(S04)를 추가한다. 앱 확인은 조건이 아니라 판단 자료임을 문구로 밝힌다.
- 판정 표시를 4.2절 게이트 어휘로 통일한다.
### S12 검토보고서 초안 (변경)
| 항목 | 내용 |
| :--- | :--- |
| 변경 | 보고서 8장(검증 방법)에 **앱 확인 결과 요약**을 넣는다 |
| 요약 내용 | 묶음별 앱 확인 상태·근거 한 줄·신뢰 등급, 사람 확정자·시각, 확정 시점의 앱 확인 검사 버전, 근거 없음·원본값 목록 |
| 화면 | 생성한 초안을 화면에서 열람하고 MD·PDF를 받는다. 앱 확인 요약이 어느 시점 결과인지 표기한다 |
| 수용 기준 | 보고서의 요약 숫자가 S04 요약 띠와 같다. 값이 바뀐 뒤 만든 초안과 그 이전 초안을 구분한다 |
### S13 변수 등급 안내 (신규)
| 항목 | 내용 |
| :--- | :--- |
| 목적 | 한 템플릿에서 바꿔도 되는 값과 바꾸면 안 되는 값을 문서처럼 열람한다 (기획서 4.4, 부록 A) |
| 패턴 | W4 문서. 좌측 목차(묶음), 우측 본문 |
| 구성 | ① 등급 4종 정의 ② 묶음별 등급 표 ③ "바꿔도 되는가" 예시 표 ④ 새 도면 판별 체크리스트 요약(S02 링크) |
| 진입 | S01 템플릿 표, S02 템플릿 선택, S04·S06의 등급 배지 |
| 수용 기준 | 4개 보호 템플릿 모두에 등급 표가 있다. 등급 배지에서 해당 묶음 위치로 이동한다 |
---
## 6. 자료 계약 (화면이 읽고 쓰는 파일)
화면 구현에 필요한 자료만 적는다. 내부 구현은 기획서 4.3~4.4절과 엔진 설계를 따른다.
| 파일 | 위치 | 내용 | 쓰는 주체 | 읽는 화면 |
| :--- | :--- | :--- | :--- | :--- |
| `confirm.json` | 교각 폴더 | 사람 확정 기록(확정자·시각·해시) | **사람 조작만** | S03~S05, S10, S12 |
| `autocheck.json` | 교각 폴더 | 묶음 해시·앱 확인 결과·근거 목록·검사 버전·시각 | 앱 확인 엔진 | S03~S05, S10, S12 |
| `variability.json` | 템플릿 폴더 | 묶음·필드 경로 → 변수 등급 | 템플릿 담당자 | S04~S06, S13 |
| 앵커 | 템플릿 폴더 | 값이 도면의 어디서 읽혔는지 기록 | 판독 스크립트 | S07, S08 |
| 교각 메타 | 교각 폴더 | 적합성 6문항 답, 형식 불일치 여부 | S02 | S03, S04 |
`autocheck.json`의 근거 항목은 다음 필드를 가진다: `check`(C1~C8), `result`(match / caution / mismatch / no_basis / n/a), `sentence`(근거 문장), `refs`(도면 이름·뷰·좌표·핸들 또는 표 행), `confidence`(A / B / C / none), `independence`(high / low).
---
## 7. 비기능 요구
| 항목 | 요구 |
| :--- | :--- |
| 성능 | 필요 묶음 전체의 앱 확인(C1~C8, 도면 재대조 제외)을 10초 이내에 마친다 `[추론]`. 도면 재대조(C2)가 포함되면 진행 표시로 대신한다 |
| 신뢰 | 앱 확인 서버 경로는 `confirm.json`을 쓰지 않는다. 자동화 시험이 이를 확인한다 |
| 보호 | 시험은 실제 교각 데이터가 아닌 시험 폴더에서만 확정 흐름을 실행한다 (OPEN #25 사례) |
| 이력 | 앱 확인 결과는 검사 버전과 시각을 남긴다. 규칙이 바뀌면 이전 결과에 '이전 버전' 표시를 붙인다 |
| 호환 | 기존 `confirm.json`·`params.json`·`expected.json`을 바꾸지 않는다. 새 파일을 추가하는 방식이다 |
| 테마 | 라이트를 기본으로 하고 다크 토큰을 같이 정의한다. 다크 전수 시험은 사용자 요청 시 수행한다 `[추론]` |
---
## 8. 수용 기준 요약 (기획서 6장 승계)
| # | 기준 | 시험 방법 |
| :-: | :--- | :--- |
| 1 | 모든 필수 묶음에 앱 확인 상태가 있다 | 5개 교각 데이터에서 S04 행 전수 확인 |
| 2 | '근거 없음'을 숨기지 않는다 | 근거 없음 묶음이 표·요약 띠·S10 제외 목록에 모두 나온다 |
| 3 | 거짓 일치 0, 거짓 불일치 0 | 4개 도면 기준. 값을 일부러 바꾼 음성 시험에서 '불일치'가 나온다 |
| 4 | 앱이 확정 기록을 만들지 않는다 | 앱 확인 실행 전후 `confirm.json` 해시 불변 |
| 5 | 기존 시험 FAIL 0 | 기존 회귀 시험 일괄 |
| 6 | 화면 접근성 | 모든 상태 셀에 글자 100%, 1280px 가로 스크롤 0, 키보드 전 기능, axe 위반 0 |
| 7 | 디자인 시스템 적합 | `design.md` 11장 점검표 전항 |
---
## 9. 단계와 화면의 대응
| 단계 | 켜지는 화면·기능 | 선행 |
| :--- | :--- | :--- |
| A 분류·표시 | S01 진행 요약, S02, S03 결론 띠(확정 기준), S04의 변수 등급·값 출처·원본값, S06 등급 안내·V3 차단, S13 | 변수 등급 파일 작성 |
| B 앱 확인 엔진 | S04 앱 확인 열·요약 띠·전체 확인, S05, S10, S12 | A |
| C 표 밖 치수 탐색 | S07 표 밖 치수 구역, S08, 앵커 | B 검증 후 |
| D 통합·문서 | S11 문구, 사용설명서 개정, 회귀 | A~C |
---
## 10. 리스크
| # | 리스크 | 대응 |
| :-: | :--- | :--- |
| R1 | 사용자가 '일치'를 '도면이 맞다'로 읽는다 | S05 근거 문장에 비교 대상을 쓰고 한계 문장을 고정으로 넣는다 |
| R2 | 요약 띠·표 숫자가 서로 어긋난다 | 같은 집계 함수를 쓰고 수용 기준 4로 시험한다 |
| R3 | 일괄 확정이 다시 도장이 된다 | S10 근거표 열람 조건과 제외 목록, 사유 필수 |
| R4 | 상태 어휘가 늘어나 판독이 어려워진다 | 4.2절에서 어휘를 고정하고 배지는 열마다 4종 이내 |
| R5 | 디자인 시스템에 없는 요소가 필요하다 | `design.md` 10장의 공백 목록으로 제안하고 승인 전에는 조합 컴포넌트로 대신한다 |
---
## 11. 결정 사항
### 11.1 기획서에서 승계한 결정 (기획서 7.2절)
| # | 내용 | 상태 |
| :-- | :--- | :--- |
| D1 | 확정 주체: 앱 확인 + 사람 확정 | **결정 2026-10-14** |
| D2 | 표 밖 치수 탐색: 규칙 + 템플릿 앵커 | **결정 2026-10-14** |
| D3 | F(고정) 항목 편집 처리 | **결정 2026-10-15 / 심영표: 권고안** — 경고 + 사유 입력, 차단은 생성기 개발자 모드에서만 |
| D4 | 일괄 확정 조건 | **결정 2026-10-15 / 심영표: 권고안** — '일치' 묶음만, 근거표 열람 뒤 확정 |
| D5 | 착수 범위·순서 | **결정 2026-10-15 / 심영표: 권고안** — A → B 먼저, C는 B 검증 뒤 |
| D6 | 앵커 소급 추출 | **결정 2026-10-15 / 심영표: 권고안** — 옥성교·Ramp-A교부터, 나머지는 필요 시. S07·S08에만 영향 |
### 11.2 화면 설계에서 새로 생긴 결정
| # | 내용 | 상태 |
| :-- | :--- | :--- |
| UX1 | 디자인 시스템 적용: 기존 R8 기준(맑은 고딕 14px·흰 배경·철근 팔레트 판정색)을 Prism 앱 프로파일(`design.md`)로 대체한다 | **결정 2026-10-15 / 심영표: 대체.** `디자인지침_경로.md` 갱신 완료 |
| UX2 | 판정 색: Prism 상태색을 쓴다. 철근 팔레트는 Rhino 모델과 범례에만 둔다 | **결정 2026-10-15 / 심영표: 변경** |
| UX3 | 서체: Pretendard를 앱에 내장한다 | **결정 2026-10-15 / 심영표: 내장.** 파일을 `web/static/fonts/PretendardVariable.woff2`에 배치(OFL). 적용은 A단계 구현 시 |
| UX4 | 상태 마커(●▲■◆◇) 도형 사용 | **결정 2026-10-15 / 심영표: 승인** |
| UX5 | 다크 테마 시험 | **결정 2026-10-15 / 심영표: 제외.** 토큰은 정의하되 시험하지 않는다 |
| UX6 | 핵심 화면 HTML 시안 | **결정 2026-10-15 / 심영표: 제작.** `시안/index.html` (S04·S05·S07·S10 동작 시안) |
## 부록 A. 핵심 화면 와이어프레임 (정보 구조, 외형 아님)
### S04 입력 묶음 확인·확정 (1280px)
```
┌ 헤더 ──────────────────────────────────────────────────────────────────────────────┐
│ 서비스명 · S04 · 옥성교P1 · 저장 14:32 · 로컬 │
├ 레일 ┬─────────────────────────────────────────────────────────────────────────────┤
│ 1 도면 ✓│ 2. 입력 묶음 확인·확정 │
│ 2 묶음 ●│ ┌ 요약 띠 ─────────────────────────────────────────────────────────────┐ │
│ 3 게이트│ │ 필요 27개 중 일치 19 · 주의 4 · 불일치 1 · 근거 없음 3 · 사람 확정 0 │ │
│ 4 보고서│ │ 마지막 앱 확인 14:30 · 검사 v0.1 · 재확인 필요 0 │ │
│ │ └──────────────────────────────────────────────────────────────────────┘ │
│ │ 필터: [전체 ▾] [전체 확인] [일치분 확정] (주 버튼 1개) │
│ │ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ │묶음 │등급│출처 │앱 확인 │사람 확정 │확인 │ │
│ │ │G4.stirrups │V2 │표 밖치수│● 일치 │미확정 │ │ │
│ │ │footing │V1 │원본값 │◆ 근거 없음 │미확정 │[확인 실행] │ │
│ │ │coping_rebar │V3 │스크립트│▲ 주의 │미확정 │ │ │
│ │ │… │ │
│ │ └────────────────────────────────────────────────────────────────────┘ │
│ │ ▸ 확정 불필요(참고) 5개 (접힘) │
└─────────┴─────────────────────────────────────────────────────────────────────────┘
```
### S05 근거 보기 (1280px)
```
├ 레일 ┬───────────────┬────────────────────────────────────────────────────────────┤
│ │ 묶음 이동 │ G4.stirrups V2 목록 변수 앱 확인: ● 일치 사람: 미확정 │
│ │ ● G4.stirrups │ ┌ 근거 표 ──────────────────────────────────────────────┐ │
│ │ ▲ coping_rebar │ │ C1 출처 ● 일치 표 밖 치수(D1 정면도) … [도면 보기] │ │
│ │ ◆ footing │ │ C4 내부 ● 일치 사슬 합 9,000 = 전체 치수 9,000 │ │
│ │ … │ │ C5 독립 ● 일치 params와 expected의 근거가 다르다 │ │
│ │ │ └────────────────────────────────────────────────────────┘ │
│ │ │ 이 결과는 후보를 만든 규칙과 값이 같다는 뜻이다. 도면이 맞다는 판정이 아니다. │
│ │ │ ┌ 값 비교 ┐ ┌ 확정 ─────────────────────────────────────┐ │
│ │ │ │ … │ │ 확정자 [ ] 사유 [ ] [확정] │ │
│ │ │ └─────────┘ └────────────────────────────────────────────┘ │
└──────┴───────────────┴────────────────────────────────────────────────────────────┘
```
### S10 일괄 확정 확인 (모달)
```
┌ 일치 묶음 확정 ─────────────────────────────────────────────┐
│ 일치 19개를 확정한다. 주의 4 · 불일치 1 · 근거 없음 3 · 원본값 1은 대상이 아니다. │
│ ┌ 근거표 (끝까지 내려 확인) ─────────────────────────────┐ │
│ │ G4.stirrups 사슬 합 9,000 = 전체 치수 9,000 │ │
│ │ … │ │
│ └────────────────────────────────────────────────────────┘ │
│ ▸ 제외된 묶음 9개 (누르면 근거 보기) │
│ 확정자 [ ] │
│ [취소] [확정] │
└──────────────────────────────────────────────────────────────┘
```
### S07 표 후보값·표 밖 치수
```
요약: 표 후보 14 · 표 밖 치수 후보 31 (A 18 · B 9 · C 4) · 다름 12 · 근거 없음 3
┌────────────────────────────────────────────────────────────────────────────────┐
│반영│대상 │현재 │후보 │구분 │상태│등급│도면 위치 │근거 │
│ ☐ │footing.height │2,500 │2,500 │표 │같음│ — │D1 표 행 3 │보기 │
│ ☑ │coping.z_tip │31,232 │31,231.5│표 밖 치수│다름│ B │D1 정면 (225817,…) │보기 │
└────────────────────────────────────────────────────────────────────────────────┘
[선택한 후보값 반영] 근거 없음 3개 ▸ (후보 없음, 문자 검색으로 직접 찾는다)
```
---
## 부록 B. 근거 파일
| 구분 | 경로 |
| :--- | :--- |
| 기획서 | `DOC/01_기획/기획_입력묶음확인확정개편_2026-10-14_v0.1.00.md` |
| 디자인 시스템 | `RAG/design system_v0.1.1/` (`Design.md`, `design.tokens.json`, `design.components.md`, `webapp/module.webapp.md`) |
| 현행 화면 | `Code/교각모델링앱/web/templates/` (`index`, `pier`, `section`, `candidates`, `search`, `gates`) |
| 화면 설계 규칙 | `DOC/01_기획/웹앱디자인시안/design.md` |
| 디자인 지침 기록 | `context-migration/디자인지침_경로.md` (R8) |
@@ -0,0 +1,740 @@
# T형 교각 모델링 앱 디자인 시스템 — Prism 앱 프로파일
| 버전 | 수정일 | 수정자 | 주요 변경 내용 | 비고 |
| :--- | :--- | :--- | :--- | :--- |
| v0.1.00 | 2026-10-15 | 심영표 | 신규 작성 — Prism Design System v0.1.1을 교각 모델링 앱에 적용하는 프로파일(토큰·상태 체계·조합 컴포넌트·문안·점검표) | 초안 `[미확정]`. PRD v0.1.00의 화면 13종이 소비한다 |
| v0.1.01 | 2026-10-15 | 심영표 | PRD UX1~UX6 결정 반영: R8 대체 확정, Pretendard 내장 파일 지정, 마커 도형 승인, 다크 시험 제외 | 사용자 결정 |
| 메타 | 값 |
| :--- | :--- |
| 문서 | 앱 프로파일 (Prism 4계층 중 **Layer 3 모듈의 앱 한정 보강**) |
| 상위 | `RAG/design system_v0.1.1/` — `Design.md` 0.1.1 · `design.tokens.json` 0.1.1 · `design.foundations.md` · `design.medium.screen.md` · `design.components.md` · `webapp/module.webapp.md` 0.1.0 |
| 브랜드 | `saman` (활성). 앱 전용 브랜드를 새로 만들지 않는다 |
| 매체 | `screen.web` (로컬 데스크톱, 1280px 이상) |
| 독자 | 화면을 구현하는 에이전트와 개발자(1차), 디자이너(2차) |
| 짝 문서 | `PRD.md` (같은 폴더) — 이 문서의 규칙이 어느 화면에서 쓰이는지는 PRD 5장이 정한다 |
---
## 1. 위치와 우선순위
### 1.1 목적
PRD의 13개 화면을 **같은 부품과 같은 규칙으로** 만들도록 디자인을 시스템으로 묶는다. 이 문서는 Prism을 대체하지 않고, Prism이 정하지 않은 앱 고유의 부분(상태 어휘의 형태 부호, 조합 컴포넌트, 문안)만 더한다.
### 1.2 값 충돌 우선순위
Prism `Design.md` 1.4절을 그대로 따른다.
| 순위 | 출처 |
| :-: | :--- |
| 1 | 사용자의 명시적 지시(해당 화면 한정) |
| 2 | `design.tokens.json` 정식값 |
| 3 | Prism 각 `.md`의 표·수치 |
| 4 | **이 문서** — Prism과 충돌하면 Prism이 이긴다 |
> 이 문서의 어느 규칙이 Prism과 충돌하면 이 문서를 고친다. 충돌을 해소하려고 Prism 값을 바꾸지 않는다.
### 1.3 기존 기준(R8)과의 관계
`context-migration/디자인지침_경로.md`의 R8(2026-10-12)은 조직 디자인 지침이 없던 때의 대체 수용 기준이다(맑은 고딕 14px, 흰 배경, 철근 팔레트 판정색). R8은 2026-10-15에 **대체되었다**(PRD 11.2절 UX1, `디자인지침_경로.md` 갱신). 두 기준이 같이 적용되는 기간을 두지 않는다.
R8에서 **유지하는** 조건은 세 가지이다.
| R8 조건 | 이 문서에서 |
| :--- | :--- |
| 색과 글자를 함께 쓴다(색만으로 구분 금지) | 6장으로 강화(형태 부호 추가) |
| 1280px 폭에서 표 외 가로 스크롤 없음 | 4.2절 |
| 화면 문구 한국어 | 9장 |
철근 팔레트(D32 적·D29 주황 등)는 **Rhino 모델 철근과 범례 전용**이다. 화면의 판정·상태 표시에는 쓰지 않는다.
---
## 2. 모듈 선언
```yaml
module:
id: pier-app
name: T형 교각 모델링 앱
base_module: webapp # RAG/design system_v0.1.1/webapp/module.webapp.md
brand: saman
medium: screen
medium_variant: screen.web
dual_medium: false
format: desktop-first # lg(1240~1599) 기준 설계, md 이하는 지원하되 시험 제외
min_width_px: 1280
columns: 12
type_set: compact
patterns: [w1-shell, w2-dashboard, w3-form, w4-document, w5-list-detail]
color_theme: [light, dark] # light 기본, dark는 토큰만 정의
accessibility: WCAG 2.2 AA
depends_on: { design_system: 0.1.1, webapp_module: 0.1.0 }
```
화면별 패턴 배정(Prism webapp 10절 1단계)은 PRD 3.2절 일람의 '패턴' 열을 따른다.
---
## 3. 토큰
### 3.1 원칙
| ID | 규칙 |
| :--- | :--- |
| **TK-1** | 산출물 CSS는 **변수 정의부 밖에 HEX·px 리터럴을 두지 않는다** (Prism W1·W2). 보더·헤어라인도 stroke 토큰을 쓴다 |
| **TK-2** | 3.2절 `prism.sys` 블록은 Prism `webapp` 3절의 정본을 **그대로** 옮긴 것이다. 값을 고치지 않는다 |
| **TK-3** | 3.3절 `comp` 토큰은 `sys`·`ref`만 참조한다. 리터럴을 두지 않는다 |
| **TK-4** | `ref` 토큰(원색)은 컴포넌트에서 직접 참조하지 않는다 |
| **TK-5** | 철근 팔레트는 UI 토큰이 아니다. Rhino 범례 이미지로만 쓴다 |
| **TK-6** | 새 토큰이 필요하면 `comp` 계층에 추가하고 이 문서의 버전을 올린다. `sys` 이하는 건드리지 않는다 |
### 3.2 sys 토큰 (Prism 정본, 수정 금지)
```css
:root {
/* color (light) */
--prism-bg-canvas: #F4F3F0;
--prism-bg-card: #FFFFFF;
--prism-text-body: #474235;
--prism-text-heading: #1F524B;
--prism-text-subtle: #474235;
--prism-text-accent: #D05C26; /* 대형 전용(24px 이상 또는 19px 이상 Bold) */
--prism-border-default: #474235;
--prism-border-strong: #1F524B;
--prism-status-do: #005A4E; --prism-status-do-bg: #DCEAE6;
--prism-status-dont: #8F2A11; --prism-status-dont-bg: #F6DFDA;
--prism-status-caution: #6B4A00; --prism-status-caution-bg: #F5E7CC;
--prism-status-tip: #0A4E86; --prism-status-tip-bg: #DDE8F3;
/* space (기본 8px = 0.5rem) */
--prism-space-025: 0.125rem; --prism-space-050: 0.25rem; --prism-space-100: 0.5rem;
--prism-space-150: 0.75rem; --prism-space-200: 1rem; --prism-space-300: 1.5rem;
--prism-space-400: 2rem; --prism-space-600: 3rem; --prism-space-800: 4rem;
/* type */
--prism-type-caption: 0.8125rem; --prism-lh-caption: 1.55;
--prism-type-body-sm: 0.875rem; --prism-lh-body: 1.70;
--prism-type-body: 1rem; --prism-type-body-lg: 1.125rem;
--prism-type-title-sm: 1.25rem; --prism-lh-title: 1.50;
--prism-type-title: 1.625rem; --prism-type-title-lg: 2rem;
/* radius · stroke · z · motion · focus · measure */
--prism-radius-sm: 4px; --prism-radius-md: 8px; --prism-radius-lg: 12px; --prism-radius-pill: 9999px;
--prism-stroke-hairline: 1px; --prism-stroke-regular: 2px; --prism-stroke-bold: 3px;
--prism-z-raised: 100; --prism-z-sticky: 200; --prism-z-dropdown: 300; --prism-z-scrim: 400;
--prism-z-modal: 500; --prism-z-popover: 600; --prism-z-toast: 700; --prism-z-tooltip: 800;
--prism-scrim: rgba(0, 0, 0, 0.32);
--prism-motion-fast: 100ms; --prism-motion-moderate: 200ms; --prism-motion-slow: 300ms;
--prism-ease-standard: cubic-bezier(0.2, 0, 0, 1);
--prism-focus-ring: 3px; --prism-focus-offset: 2px;
--prism-measure-max: 38em;
}
:root[data-theme="dark"] {
--prism-bg-canvas: #111111; --prism-bg-card: #111111;
--prism-text-body: #F4F3F0; --prism-text-heading: #F4F3F0; /* 다크에서 그린 텍스트 금지 */
--prism-text-subtle: #D3D3D3; --prism-text-accent: #D05C26;
--prism-border-default: #D3D3D3; --prism-border-strong: #F4F3F0;
--prism-status-do: #DCEAE6; --prism-status-do-bg: #005A4E;
--prism-status-dont: #F6DFDA; --prism-status-dont-bg: #8F2A11;
--prism-status-caution: #F5E7CC; --prism-status-caution-bg: #6B4A00;
--prism-status-tip: #DDE8F3; --prism-status-tip-bg: #0A4E86;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { /* data-theme dark 블록과 같은 값 세트를 적용 */ }
}
```
Prism 토큰 파일에 있는 `--prism-dv-*`(데이터 시각화 6색), `--prism-type-display*`, `--prism-ease-enter/exit`는 이 앱에서 쓰지 않아 옮기지 않는다. 쓰게 되면 정본에서 그대로 옮긴다.
### 3.3 comp 토큰 (앱 전용, 이 문서가 정의)
모두 `sys`를 참조한다. 라이트·다크 전환은 `sys` 값이 바뀌므로 `comp`는 두 번 정의하지 않는다.
```css
:root {
/* 표면 */
--prism-comp-surface-page: var(--prism-bg-canvas);
--prism-comp-surface-card: var(--prism-bg-card);
--prism-comp-surface-sunken: var(--prism-bg-canvas);
--prism-comp-line: var(--prism-border-default);
--prism-comp-line-strong: var(--prism-border-strong);
/* 상태색 4쌍의 앱 의미 */
--prism-comp-state-good-fg: var(--prism-status-do); --prism-comp-state-good-bg: var(--prism-status-do-bg);
--prism-comp-state-bad-fg: var(--prism-status-dont); --prism-comp-state-bad-bg: var(--prism-status-dont-bg);
--prism-comp-state-warn-fg: var(--prism-status-caution); --prism-comp-state-warn-bg: var(--prism-status-caution-bg);
--prism-comp-state-info-fg: var(--prism-status-tip); --prism-comp-state-info-bg: var(--prism-status-tip-bg);
/* 배지 */
--prism-comp-badge-pad-y: var(--prism-space-025);
--prism-comp-badge-pad-x: var(--prism-space-100);
--prism-comp-badge-radius: var(--prism-radius-sm);
--prism-comp-badge-font: var(--prism-type-caption);
--prism-comp-badge-line-solid: var(--prism-stroke-regular);
--prism-comp-badge-line-bold: var(--prism-stroke-bold);
/* 표 */
--prism-comp-table-cell-pad-y: var(--prism-space-100);
--prism-comp-table-cell-pad-x: var(--prism-space-150);
--prism-comp-table-font: var(--prism-type-body-sm);
--prism-comp-table-head-bg: var(--prism-bg-canvas);
--prism-comp-table-rule: var(--prism-stroke-hairline);
/* 레이아웃 */
--prism-comp-rail-width: 15rem; /* 240px */
--prism-comp-header-height: 3.5rem; /* 56px */
--prism-comp-hit-min: 2.75rem; /* 44px */
/* 도면 이미지 */
--prism-comp-drawing-frame-line: var(--prism-border-default);
}
```
### 3.4 상태 시프트 토큰 (공백 — 산출 필요)
Prism은 hover ±4·active ±8·selected ±12의 **GRADE 시프트**를 정하지만(screen 6.2절), 시프트된 HEX 표는 토큰 파일에 없다. 이 문서는 값을 지어내지 않는다.
| 처리 | 내용 |
| :--- | :--- |
| 지금 | hover는 보강일 뿐이므로(HV-3) 색 변화 없이도 모든 기능이 성립한다. 시안 단계에서는 hover에 밑줄 또는 보더 굵기 변화만 쓴다 |
| 구현 전 | `design.foundations.md` 2장의 GRADE 산출식으로 green·brown·white 면의 ±4·±8·±12 HEX를 산출해 `comp` 토큰(`--prism-comp-*-hover/-active/-selected`)으로 추가한다 |
| 책임 | 산출은 Prism 담당자 승인 사항(공백 목록 10장 G7) |
---
## 4. 레이아웃
### 4.1 브레이크포인트
Prism webapp 2절을 따른다. 임의 브레이크포인트를 두지 않는다(WA-1).
| BP | 범위(px) | 컬럼 | 마진 | 거터 | 이 앱에서 |
| :--- | :--- | :-: | ---: | ---: | :--- |
| `lg` | 1240–1599 | 12 | 32 | 24 | **설계 기준.** 1280px에서 모든 화면이 성립해야 한다 |
| `xl` | 1600– | 12 | 24 | 24 | 컨테이너 1440 고정 |
| `md` | 904–1239 | 8 | 32 | 24 | 지원하되 시험 제외. 레일은 상단으로 접힌다 |
| `sm` `xs` | 0–903 | 8 · 4 | 24 · 16 | 16 | 비대상. 가로 스크롤 없이 단일 열로만 표시 |
### 4.2 1280px 규칙
| ID | 규칙 |
| :--- | :--- |
| **LY-1** | 1280px 폭에서 **표 외** 가로 스크롤이 없다 |
| **LY-2** | 표는 가로 스크롤을 허용한다(MS-3 예외). 스크롤 시 첫 열(묶음 이름)과 헤더를 고정한다 |
| **LY-3** | 연속 문장 블록은 `max-width: var(--prism-measure-max)`. 표·도면 이미지·근거 표는 제외 |
| **LY-4** | 최상위 영역만 그리드에 맞춘다. 영역 내부는 간격 토큰으로 배치한다 (WA-4) |
### 4.3 앱 셸 (W1)
```
┌ header (sticky, comp-header-height) ──────────────────────────────────────────┐
│ 서비스명 │ 화면 ID · 교각 │ │ 저장 시각 · 환경(로컬) │ 테마 │
├ rail (comp-rail-width) ┬ main ────────────────────────────────────────────────┤
│ 교각 목록 │ 화면 제목 (h1) │
│ ─ 교각 이름 │ 결론 영역 (요약 띠 · 판정) │
│ 1 도면 │ 본문 영역 (표 · 비교 · 폼) │
│ 2 입력 묶음 │ │
│ 3 게이트 │ │
│ 4 보고서 │ │
└─────────────────────────┴──────────────────────────────────────────────────────┘
```
| 요소 | 규칙 |
| :--- | :--- |
| header | 메타 4슬롯(화면 ID·권한·갱신 시각·환경)을 쓴다. 권한 슬롯은 로컬 앱에서 "담당자"로 고정 표시한다 |
| rail | `stepper` 컴포넌트(단계 3~7개 규칙, SP-2)로 구현한다. 완료·현재·미완을 형태로 구분한다(SP-3) |
| main | 첫 영역이 결론이다(PD-1). 결론 영역은 viewport당 1개 |
| skip link | 모든 화면 첫 요소가 "본문 바로가기"다 |
### 4.4 패턴 적용
| 패턴 | 화면 | 이 앱에서의 구체화 |
| :--- | :--- | :--- |
| W1 앱 셸 | 전 화면 | 4.3절 |
| W2 대시보드 | S03, S11 | 카드 그리드. 존당 stat 3개 이하. 1280px에서 3열 |
| W3 폼 | S02, S06, S09 검색 | 1열 수직, 다열 폼 금지, 본문 폭 38em, 주 버튼 1개. 단 `S06`의 값 편집창(JSON)은 38em 예외를 둔다(코드 영역) |
| W4 문서 | S12, S13 | 좌측 목차 4col + 본문 8col, 본문 38em |
| W5 목록-상세 | S01, S04, S05, S07 | 목록이 7열 이상의 표이면 분할 뷰를 쓰지 않고 **전폭 표 + 별도 상세 화면**으로 나눈다. S05만 `4:8` 분할 |
---
## 5. 타이포그래피
### 5.1 서체
| 슬롯 | 값 |
| :--- | :--- |
| 패밀리 | Pretendard. 폴백 `-apple-system, BlinkMacSystemFont, "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic", sans-serif` |
| 웨이트 | 400 · 500 · 700 |
| 숫자 | `font-variant-numeric: tabular-nums` 전 화면 적용 (표의 수치 열은 필수) |
| 내장 | `web/static/fonts/PretendardVariable.woff2`(OFL, 가변 폰트)를 `@font-face`로 불러온다 (PRD UX3 결정). 파일이 없으면 폴백 맑은 고딕으로 렌더되며, 이 경우에도 5.2절 크기는 유지한다 |
`html { font-size: ... }` 선언을 금지한다(CSS-1).
### 5.2 크기 사용표
| 용도 | 토큰 | 웨이트 | 비고 |
| :--- | :--- | :-: | :--- |
| 화면 제목(h1) | `type-title` | 700 | 한 화면에 1개. 20자 이내 |
| 영역 제목(h2) | `type-title-sm` | 500 | 규정문체 명사형 |
| 소영역 제목(h3) | `type-body-lg` | 700 | |
| 본문 | `type-body` | 400 | 38em 이내 |
| **표 셀** | `type-body-sm` | 400 | 표 최소 13px (TB 규정) |
| 배지·캡션·출처 | `type-caption` | 500 | |
| 큰 숫자(요약 띠) | `type-title` | 700 | 숫자만 키운다 |
### 5.3 기술 표기 (해시·핸들·좌표·키)
해시 앞 16자, DXF 핸들, 좌표, JSON 키는 **본문과 구분**해야 한다. Prism에는 등폭 서체가 없다(공백 G3). 임시 규칙은 다음과 같다.
| 규칙 | 내용 |
| :--- | :--- |
| 형태 | 보더 1px(hairline)이 있는 인라인 칩. 폰트는 본문과 같고 `tabular-nums` |
| 복사 | 칩을 누르면 복사한다. 키보드로 접근 가능하다 |
| 줄바꿈 | 칩 내부에서 줄바꿈하지 않는다. 길면 앞 16자만 보이고 전체는 `title` 대신 도움말로 연다 |
| 금지 | 등폭 서체 임의 지정 금지(토큰 외 서체). 승인 후 `sys` 토큰으로 편입 |
---
## 6. 상태 표시 체계
PRD 4.2절의 어휘를 **글자 + 형태 + 색** 세 신호로 부호화한다. 한 열에서 같은 신호를 두 상태가 공유하지 않는다.
### 6.1 형태 신호
| 신호 | 의미 |
| :--- | :--- |
| 선행 마커 `●` | 정상·완료·충족 |
| 선행 마커 `▲` | 주의·확인 필요 |
| 선행 마커 `■` | 실패·불일치 |
| 선행 마커 `◆` | 변경됨·재실행 필요 |
| 선행 마커 `◇` | 근거 없음(빈 마름모) |
| 보더 실선 2px | 확정된 상태 |
| 보더 점선 2px | 근거·확정이 없는 상태 |
| 보더 굵게 3px | 실패 |
마커는 CSS로 그리는 도형이며 아이콘이 아니다(PRD UX4: 2026-10-15 승인). 이모지와 아이콘 폰트를 쓰지 않는다 (IC-3).
### 6.2 앱 확인
| 값 | 글자 | 마커 | 보더 | 전경·배경 |
| :--- | :--- | :-: | :--- | :--- |
| 일치 | 일치 | ● | 실선 2px | good-fg / good-bg |
| 주의 | 주의 | ▲ | 실선 2px | warn-fg / warn-bg |
| 불일치 | 불일치 | ■ | 굵게 3px | bad-fg / bad-bg |
| 근거 없음 | 근거 없음 | ◇ | 점선 2px | text-body / 투명 |
| 해당 없음 | `—` | — | — | text-body (배지 아님) |
| 확인 안 함 | [확인 실행] 버튼 | — | — | (배지 아님) |
### 6.3 사람 확정
| 값 | 글자 | 마커 | 보더 | 전경·배경 |
| :--- | :--- | :-: | :--- | :--- |
| 확정 | 확정 | ● | 실선 2px | `surface-emphasis`: green 면 + white 글자 |
| 미확정 | 미확정 | — | 점선 2px | text-body / 투명 |
| 변경됨 | 변경됨 | ◆ | 실선 2px | warn-fg / warn-bg |
앱 확인(6.2)이 상태색 면을 쓰고 사람 확정(6.3)이 green 면을 쓰므로, 한 행 안에서 두 배지가 **색·형태 모두 다르다**.
### 6.4 변수 등급
| 값 | 글자 | 보더 | 면 |
| :--- | :--- | :--- | :--- |
| V1 | V1 값 | 실선 2px | 없음 |
| V2 | V2 목록 | 실선 2px | 없음 |
| V3 | V3 생성 | 점선 2px | 없음 |
| F | F 고정 | 실선 2px | green 면 + white 글자 |
편집 가능(V1·V2) → 스크립트 필요(V3, 점선) → 편집 불가(F, 면 채움)의 순서로 시각 무게가 커진다.
### 6.5 신뢰 등급
| 값 | 글자 | 보더 |
| :--- | :--- | :--- |
| A | 등급 A | 실선 2px |
| B | 등급 B | 실선 2px + 마커 ▲ |
| C | 등급 C | 점선 2px |
| 없음 | 후보 구역에 행으로 나오지 않는다. '근거 없음' 목록에만 나온다 | — |
### 6.6 게이트 판정
| 값 | 글자 | 마커 | 보더 | 전경·배경 |
| :--- | :--- | :-: | :--- | :--- |
| PASS | PASS | ● | 실선 2px | good-fg / good-bg |
| FAIL | FAIL | ■ | 굵게 3px | bad-fg / bad-bg |
| 계획 FAIL | 계획 FAIL | ▲ | 실선 2px | warn-fg / warn-bg |
| 재실행 필요 | 재실행 | ◆ | 실선 2px | info-fg / info-bg |
### 6.7 규칙
| ID | 규칙 |
| :--- | :--- |
| **ST-1** | 배지 텍스트는 6자 이내(BD-3). 문장을 넣지 않는다 |
| **ST-2** | 한 열의 배지 종류는 4종 이내(BD-2). 5종 이상이 필요하면 열을 나눈다 |
| **ST-3** | 색을 지우고(그레이스케일) 읽어도 모든 상태가 구분되어야 한다 |
| **ST-4** | 배지 색은 악센트 경제에 세지 않는다(BD-4). 그러나 **orange는 상태에 쓰지 않는다** |
| **ST-5** | 다크 테마에서 상태색 쌍은 `sys` 정의대로 전경·배경이 반전된다. `comp`가 따로 정의하지 않는다 |
| **ST-6** | 배지 안에 `aria-label`로 "앱 확인: 일치"처럼 열 이름을 포함해 낭독하게 한다 |
---
## 7. 컴포넌트
### 7.1 Prism 컴포넌트 사용표
Prism이 정한 20종 중 이 앱이 쓰는 것과 쓰는 곳이다. 카탈로그에 있는 변형만 쓴다.
| 컴포넌트 | 변형 | 쓰는 곳 | 비고 |
| :--- | :--- | :--- | :--- |
| `header` | 1 | 전 화면 셸 | 메타 4슬롯 |
| `eyebrow` | 2 | 화면 제목 위 단계 표기("2단계") | |
| `rule` | 1 | 영역 구분 | |
| `card` | 4 | S03 단계 카드, S05 값 비교 | |
| `table` | `base` `compact` | S01, S04, S05, S07, S11 | S04는 `compact` + sticky |
| `stat` | 1 | S03·S04 요약 띠의 숫자 | 존당 3개 이하 |
| `specsheet` | 1 | S05 결론(묶음 메타), S03 도면 목록 | |
| `directive` | `caution` `tip` 등 3 | 형식 불일치 경고, 원본값 경고, 한계 문장 | |
| `badge` | 4 | 6장의 모든 상태 | |
| `scope` | 1 | 각 화면 최상단 "적용 조건/제외 조건" | 해당 없음 즉시 이탈 |
| `toc` | 1 | S13 좌측 목차 | |
| `stepper` | 1 | 레일의 1~4단계 | |
| `xref` | 1 | 화면 간 링크(S05 → S06 등) | 방향어 금지 |
| `mediaframe` | 1 | S08 도면 이미지 | 캡션 필수 |
| `button` | `primary` `secondary` `quiet` `danger` | 전 화면 | 화면당 primary 1개 |
| `input` | 1 | 이름·확정자·사유·검색 | 레이블 필수 |
| `modal` | 1 | S08, S10 | 포커스 트랩·Esc |
| `toast` | 1 | 보조 알림 | 단독 전달 금지 |
| `disclosure` | 1 | 참고 묶음 접기, 제외 목록 | 인쇄 시 전개 |
| `chartframe` | — | **쓰지 않는다** | |
`hero`, `footer`는 미선별이며 이 앱에서 쓰지 않는다.
### 7.2 조합 컴포넌트 (이 문서가 정의)
Prism 카탈로그의 컴포넌트를 **조합·제약**해 만든 앱 전용 구성이다. 새 원시 컴포넌트가 아니므로 카탈로그 외 0건 원칙(webapp A-검증)에 어긋나지 않는다. 단, 조합이 반복되므로 이름을 붙여 일관성을 보장한다.
#### C-1 요약 띠 (`summary-strip`)
| 항목 | 내용 |
| :--- | :--- |
| 구성 | `stat`(숫자 3개 이하의 강조) + `badge`(상태별 개수) + 갱신 시각 캡션 |
| 해부 | 결론 문장 → 상태별 개수 → 시각·버전 |
| 규칙 | 각 개수는 필터 버튼이다(`button quiet`). 합계는 표와 같은 집계를 쓴다(PRD 수용 기준). 숫자는 등폭 |
| 접근성 | 전체를 `role="group"` + 요약 문장 `aria-label`. 개수 변경은 `aria-live="polite"` |
| 쓰는 곳 | S03, S04, S07 |
#### C-2 묶음 표 (`section-table`)
| 항목 | 내용 |
| :--- | :--- |
| 구성 | `table compact` + sticky 헤더 + 첫 열 고정 + 행 선택 |
| 열 | 묶음 · 변수 등급 · 값 출처 · 앱 확인 · 사람 확정 · 확인 |
| 규칙 | 값 출처 열은 배지가 아닌 글자다(5종, ST-2). 단 '원본값'만 `badge marked`(▲ warn)를 단다. 정렬 기본값 불일치 → 근거 없음 → 주의 → 일치. 빈 셀은 `—` (TB-4) |
| 행 상태 | hover(보강), selected(GRADE 시프트), focus(링). 행 전체가 링크가 아니라 **묶음 이름만** 링크다 |
| 접근성 | `caption` "입력 묶음", `th scope="col"`, 정렬 가능 열은 `aria-sort` |
| 쓰는 곳 | S04 |
#### C-3 근거 행 (`evidence-row`)
| 항목 | 내용 |
| :--- | :--- |
| 구성 | 검사 ID(C1~C8) · 검사 이름 · 결과 배지 · 근거 문장 · 도면 위치 칩 · [도면 보기] |
| 규칙 | 근거 문장은 40어절 이내, **비교한 두 값과 출처를 모두 쓴다**. 문장 끝에 독립성(낮음)이 있으면 괄호로 쓴다. 해당 없는 검사는 한 줄로 접는다 |
| 한계 문장 | 표 하단에 고정 `directive tip`: "일치는 후보를 만든 규칙과 값이 같다는 뜻이다. 도면이 맞다는 판정이 아니다." |
| 쓰는 곳 | S05, S10 근거표 |
#### C-4 값 비교 (`diff-row`)
| 항목 | 내용 |
| :--- | :--- |
| 구성 | 항목 이름 · 현재 값 · 비교 값(후보/원본) · 차이 표시 |
| 규칙 | 차이는 색이 아닌 **글자(`다름`) + 마커 `▲`**로 표시한다. 같은 항목은 기본으로 접는다. 수치는 우측 정렬·등폭, 단위는 열 헤더에 1회(TB-6) |
| 쓰는 곳 | S05, S07 |
#### C-5 확정 폼 (`confirm-form`)
| 항목 | 내용 |
| :--- | :--- |
| 구성 | `input`(확정자) + `input`(사유, 조건부 필수) + `button primary` |
| 규칙 | 필수 표시는 `*`가 아니라 글자 "필수"(IN-4). 조건이 모자라도 버튼은 활성이고 클릭 시 오류 메시지를 낸다(BT-6). 오류는 무엇이·왜·어떻게 + 색·기호·텍스트 3중(IN-2·IN-3) |
| 사유 필수 조건 | 앱 확인이 주의·불일치·근거 없음이거나 F 항목 변경일 때 |
| 쓰는 곳 | S05, S06(F 변경 사유), S10(확정자만) |
#### C-6 일괄 확정 대화상자 (`bulk-confirm-dialog`)
| 항목 | 내용 |
| :--- | :--- |
| 구성 | `modal` + 결론 문장 + 근거표(스크롤 영역) + `disclosure`(제외 묶음) + `confirm-form` |
| 규칙 | 근거표 스크롤 끝 도달 여부를 기록하고, 도달 전 [확정]은 오류 메시지를 낸다(비활성 아님). 포커스 트랩·Esc 닫기·배경 스크롤 잠금 (MD-1~5). 모달 안에 모달을 열지 않는다. 제외 묶음 링크는 모달을 닫고 S05로 이동한다 |
| 쓰는 곳 | S10 |
#### C-7 도면 뷰 (`drawing-view`)
| 항목 | 내용 |
| :--- | :--- |
| 구성 | `mediaframe` + 캡션(도면·뷰·좌표·핸들 칩) + 확대·축소·이동 버튼 |
| 규칙 | 이미지 대체 텍스트는 대상 설명, 캡션은 필수. 대상 위치 표시는 **링 + 십자선**(색 단독 금지). 이동은 방향키 지원. 새 윈도우 이미지 생성은 `dxftools.render`와 같은 결과를 쓴다 |
| 쓰는 곳 | S08, S05(축소 보기) |
#### C-8 경고 띠 (`state-banner`)
| 항목 | 내용 |
| :--- | :--- |
| 구성 | `directive` (`caution`) + 한 문장 + 조치 링크 |
| 종류 | 형식 불일치(닫을 수 없음) · 원본값 포함 · 값 변경됨(재확인 필요) · 앱 확인 규칙 버전 변경 |
| 규칙 | 한 viewport에 2개를 넘기지 않는다. 겹치면 가장 심각한 한 개 + "외 N건" |
| 쓰는 곳 | S03, S04, S05 |
#### C-9 진행 표시 (`run-progress`)
| 항목 | 내용 |
| :--- | :--- |
| 구성 | 단계 문장("11/27 묶음 확인 중") + 막대 + [중단] |
| 규칙 | 색이 아닌 **길이와 숫자**로 읽는다. `role="progressbar"` + `aria-valuenow`. 완료는 토스트가 아니라 결과 영역에 남긴다. 모션은 `transform`만 쓴다 |
| 쓰는 곳 | S04 전체 확인, S11 게이트 실행 |
### 7.3 버튼 배치
| 규칙 | 내용 |
| :--- | :--- |
| 화면당 `primary` 1개 | 다음에 할 일을 가리키는 한 개. S04는 [일치분 확정], S05는 [확정], S10은 [확정] |
| 위험 동작 | `danger` + 확인 절차. 교각 삭제, 반려, 형식 변경 확정 |
| 순서 | 취소·보조 → 주 버튼(우측 끝). 키보드 순서도 같다 |
| 레이블 | 동사로 시작, 8자 이내, 결과를 말한다 (9.2절 표) |
| 면 | `primary`는 `surface-emphasis`(green 면 + white 글자), `secondary`는 보더만, `quiet`는 보더 없는 텍스트, `danger`는 `bad` 상태색 보더 + 확인 절차. hover·active의 면 변화는 3.4절 산출 뒤에 적용한다 |
---
## 8. 인터랙션과 접근성
### 8.1 상태와 포커스
| 항목 | 규칙 |
| :--- | :--- |
| 상태 | default · hover(보강) · focus(링만) · active · selected · disabled(저강조 토큰, 불투명도 금지) · loading(진행 표시, 색 유지) |
| 포커스 링 | 3px 이중 링 + 오프셋 2px, `:focus-visible`. 대체 없는 `outline: none` 금지 (FC-1·FC-2) |
| disabled | 가능하면 활성 + 오류 메시지. 비활성은 권한·물리적 불가능일 때만 |
| hover | `@media (hover: hover)`에 격리. hover 없이 전 기능이 성립해야 한다 |
| 히트 영역 | 44px 이상, 인접 간격 8px 이상 |
### 8.2 키보드
| 화면 | 필수 동작 |
| :--- | :--- |
| S04 | Tab으로 필터 → 도구 줄 → 표(행의 링크·[확인 실행]) 순. 열 머리 Enter로 정렬. 요약 띠 필터는 Enter·Space |
| S05 | 좌측 목록은 위/아래 방향키 이동, Enter로 묶음 전환. [이전][다음] 단축 버튼 제공 |
| S08 | 방향키 이동, `+`/`-` 확대·축소, Esc 닫기 |
| S10 | 포커스 트랩. 근거표는 방향키·PageDown으로 끝까지 내릴 수 있다 |
### 8.3 오류 메시지
3요소(무엇이·왜·어떻게)를 한 문장군으로 쓴다. 색·기호·텍스트를 함께 쓴다.
| 상황 | 문구 예시(60자 이내) |
| :--- | :--- |
| 확정자 없음 | "확정자 이름이 비어 있다. 확정은 사람의 기록이다. 이름을 입력한다" |
| 근거표 미열람 | "근거표를 읽지 않았다. 일치 묶음의 근거를 읽어야 확정한다. 근거표를 끝까지 내린다" |
| 사유 없음 | "사유가 비어 있다. 주의 묶음은 사유가 있어야 확정된다. 사유를 입력한다" |
| 도면 없음 | "도면이 없다. 앱 확인에 쓸 색인이 없다. 1단계에서 도면을 올린다" |
### 8.4 모션
`transform`·`opacity`만 애니메이션한다. `prefers-reduced-motion`에서는 모션을 제거하고 상태 변화는 즉시 반영한다 (MT-3·RM-1). 진행 표시는 정지 상태에서도 숫자로 읽혀야 한다.
---
## 9. 문안 규정
### 9.1 보이스
Prism `saman` 인스턴스를 따른다.
| 속성 | 규칙 | 금지 예 |
| :--- | :--- | :--- |
| 자세 | 단정적·3인칭 | "~인 것 같습니다" → "~이다" 또는 `[추론]` |
| 거리 | 전문가 대 전문가 | "우리 모두", "여러분" |
| 격 | 규정문체(평서 종결). 조작 안내도 같다 | "꼭 확인해 주세요!" → "확인한다" |
| 절제 | 과장·감탄·마케팅 어휘 금지 | "혁신적", "간편하게" |
어미는 4종만 쓴다: `~한다`(의무) · `~할 수 있다`(재량) · `~를 권장한다`(권고) · `~하지 않는다`(금지).
### 9.2 버튼 레이블 (동사로 시작, 8자 이내)
| 동작 | 레이블 | 근거 |
| :--- | :--- | :--- |
| 앱 확인 한 묶음 | 확인 실행 | 검사한다 ≠ 확정한다 |
| 앱 확인 전체 | 전체 확인 | |
| 일치 묶음 확정 | 일치분 확정 | "확정"은 사람의 기록임을 명시. 대상을 앞에 둔다 |
| 확정 | 확정 | |
| 후보 반영 | 후보 반영 | 반영 = 기존 문서에 편입(동사 판별표) |
| 값 저장 | 저장 | |
| 도면 확대 | 도면 보기 | |
| 교각 만들기 | 교각 만들기 | |
| 삭제 | 삭제 | `danger` |
'변경'과 '수정'을 구분한다: 승인 효력에 영향이 있으면 **변경**, 오기 정정이면 **수정**(동사 판별표). 형식 불일치 확정은 "변경"이다.
### 9.3 상태 문구
| 대상 | 문구 |
| :--- | :--- |
| 일치 근거 | "<항목>은 <출처>의 <대상>과 같다" |
| 주의 근거 | "<항목>이 <기준>과 <차이>만큼 다르다. 허용 범위 안이다" |
| 불일치 근거 | "<항목>이 <출처>의 <대상>과 다르다 (현재 <값>, 도면 <값>)" |
| 근거 없음 | "<항목>의 출처를 찾지 못했다. 사람이 도면에서 직접 확인한다" |
| 원본값 | "템플릿 원본과 같다. 새 도면 근거가 없다" |
| 값 변경됨 | "값이 바뀌었다. 이전 확인과 확정이 무효가 되었다" |
### 9.4 금지 목록
| # | 금지 | 근거 |
| :-: | :--- | :--- |
| 1 | 이모지 | IC-3 |
| 2 | 순백 전면 배경 (캔버스는 sand, 카드만 white) | 브랜드 금지 |
| 3 | 오렌지 본문 텍스트, 오렌지 상태 표시 | 브랜드 금지, ST-4 |
| 4 | 다크 배경 위 그린 텍스트 | CSS-4 |
| 5 | 악센트 3개 이상(viewport당) | 악센트 경제 |
| 6 | 그라디언트 배경, 장식 이미지 | 브랜드 금지 |
| 7 | 방향어("위의 표", "아래 버튼", "앞서") | 5.7. 절 번호·항목 이름으로 참조한다 |
| 8 | "100% 자동", "완벽", "간편" 등 과장 표현 | 절제 |
| 9 | 앱이 "확정했다"고 쓰는 문장 | PP-1 |
| 10 | 상태를 색만으로 알리는 표현 | PP-4 |
---
## 10. 디자인 시스템의 공백과 제안
PRD 구현에 필요하지만 Prism에 없는 요소이다. 승인 전에는 **조합 컴포넌트(7.2절)와 임시 규칙**으로 대신한다. 새 아이콘과 새 서체는 Prism 거버넌스(제안 → 승인 대기 → 등재)를 따른다.
| ID | 공백 | 쓰는 곳 | 임시 처리 | 제안 |
| :-: | :--- | :--- | :--- | :--- |
| G1 | 라디오·체크박스·셀렉트 (`input` 변형) | S02 6문항, 필터, S07 반영 선택 | 네이티브 요소 + 토큰 보더·포커스 링 | `input` 변형 3종 신설 |
| G2 | 파일 선택 | 도면 올리기 | 네이티브 `input[type=file]` + 레이블·도움말 | `input` 변형 |
| G3 | 등폭 서체 | 해시·핸들·좌표·JSON | 5.3절 칩 | 등폭 패밀리를 `ref.type`에 편입 |
| G4 | 진행 표시 | 앱 확인, 게이트 실행 | C-9 | `progress` 컴포넌트 신설 |
| G5 | 행 선택·정렬이 있는 표 | S04, S07 | C-2 | `table` 변형 `selectable` |
| G6 | 확대 뷰어 | S08 | C-7 | `mediaframe` 변형 `zoomable` |
| G7 | 상태 시프트 HEX | 전 상호작용 | 3.4절 | foundations 산출표 등재 |
| G8 | 상태 마커 도형(●▲■◆◇) | 6장 | CSS 도형 | 아이콘 거버넌스 승인 (PRD UX4) |
| G9 | 코드·로그 블록 | S11 로그, S06 편집창 | 본문 서체 + 보더 | `codeblock` 신설 |
| G10 | 탭 | (사용하지 않음) | 별도 화면으로 분리 | 필요 시 제안 |
---
## 11. 적용 점검표
PRD 수용 기준 7번의 근거이다. 시안·구현 모두 전항을 확인한다.
### 11.1 자동
| # | 항목 | 기준 |
| :-: | :--- | :--- |
| A1 | 색·치수 리터럴 | 변수 정의부 밖 0건 |
| A2 | root font-size 고정 | 0건 |
| A3 | `outline: none` (대체 없음) | 0건 |
| A4 | z-index, tabindex | 토큰 외 0건 / 양수 0건 |
| A5 | 그리드 정합 | 전 BP 잔차 0 |
| A6 | axe 대비·이름·alt | 위반 0건 |
| A7 | 헤딩 레벨 건너뜀 | 0건 |
| A8 | `prefers-reduced-motion` | 구현 존재 |
| A9 | 히트 영역 | 44px 이상 |
| A10 | 본문 max-width | 38em 이하 |
| A11 | 다크 페어 | green 텍스트 0건 |
| A12 | 텍스트 알파·hover 의존 | 0건 |
| **A13** | **상태 셀 글자** | 모든 상태 셀에 글자 100% |
| **A14** | **배지 길이** | 6자 초과 0건 |
| **A15** | **primary 버튼** | 화면당 1개 |
| **A16** | **orange 사용** | 상태·본문 0건 |
### 11.2 수동
| # | 항목 |
| :-: | :--- |
| V1 | 키보드만으로 PRD 5장 모든 화면의 모든 행동 수행 |
| V2 | 스크린리더로 S04 → S05 → S10 흐름 낭독 |
| V3 | 200% 확대 시 손실·가로 스크롤 없음(표 제외) |
| V4 | 그레이스케일로 6장 모든 상태 구분 |
| V5 | 1280px에서 가로 스크롤(표 외) 0 |
| V6 | 9.4절 금지 목록 0건 |
| V7 | 도면 이미지 캡션 100% |
| V8 | '앱이 확정했다'로 읽히는 문장 0건 |
---
## 12. 거버넌스
| 항목 | 내용 |
| :--- | :--- |
| 변경 | 이 문서의 개정은 버전표에 기록한다. Prism 개정은 이 문서에 자동 소급되지 않는다 (Prism DC-1) |
| 소유 | 앱 화면 규칙은 담당자(심영표). 공백 G1~G10의 Prism 반영은 Prism 소유자가 결정한다 |
| 드리프트 | 3.2절 sys 블록이 `design.tokens.json`·`webapp` 3절과 달라지면 이 문서를 고친다. 정합은 구현 시 빌드 게이트(A1)로 검증한다 |
| R8 | 확정 시 `디자인지침_경로.md`의 R8 행을 대체 표기하고 '디자인 지침 폴더 경로'를 `RAG/design system_v0.1.1`로 갱신한다 (사용자 결정 UX1 후) |
| 면책 | 이 프로파일 준수가 서비스의 접근성·적합성을 보증하지 않는다 (Prism DC-3). 서비스 단위 검증은 별도로 한다 |
---
## 부록 A. 핵심 컴포넌트 스타일 골격
토큰만 참조하는 CSS 골격이다. 값은 3장 변수에서 온다.
```css
.pier-badge {
display: inline-flex; align-items: center; gap: var(--prism-space-050);
padding: var(--prism-comp-badge-pad-y) var(--prism-comp-badge-pad-x);
font-size: var(--prism-comp-badge-font); font-weight: 500;
border: var(--prism-comp-badge-line-solid) solid var(--prism-comp-line);
border-radius: var(--prism-comp-badge-radius);
color: var(--prism-text-body); background: transparent; white-space: nowrap;
}
.pier-badge[data-kind="good"] { color: var(--prism-comp-state-good-fg); background: var(--prism-comp-state-good-bg); border-color: var(--prism-comp-state-good-fg); }
.pier-badge[data-kind="warn"] { color: var(--prism-comp-state-warn-fg); background: var(--prism-comp-state-warn-bg); border-color: var(--prism-comp-state-warn-fg); }
.pier-badge[data-kind="info"] { color: var(--prism-comp-state-info-fg); background: var(--prism-comp-state-info-bg); border-color: var(--prism-comp-state-info-fg); }
.pier-badge[data-kind="bad"] { color: var(--prism-comp-state-bad-fg); background: var(--prism-comp-state-bad-bg); border-color: var(--prism-comp-state-bad-fg);
border-width: var(--prism-comp-badge-line-bold); }
.pier-badge[data-kind="none"] { border-style: dashed; }
.pier-badge[data-kind="fixed"] { color: var(--prism-bg-card); background: var(--prism-text-heading); border-color: var(--prism-text-heading); }
/* 선행 마커: CSS 도형, 아이콘이 아님 */
.pier-badge[data-mark]::before { content: attr(data-mark); aria-hidden: true; }
.pier-table { width: 100%; border-collapse: collapse; font-size: var(--prism-comp-table-font); font-variant-numeric: tabular-nums; }
.pier-table caption { text-align: left; font-weight: 700; padding-block: var(--prism-space-100); }
.pier-table th, .pier-table td { padding: var(--prism-comp-table-cell-pad-y) var(--prism-comp-table-cell-pad-x);
border-bottom: var(--prism-comp-table-rule) solid var(--prism-comp-line); text-align: left; vertical-align: top; }
.pier-table thead th { position: sticky; top: 0; z-index: var(--prism-z-sticky); background: var(--prism-comp-table-head-bg); }
.pier-table td.num { text-align: right; }
:focus-visible { outline: var(--prism-focus-ring) solid var(--prism-comp-line-strong); outline-offset: var(--prism-focus-offset); }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
```
`.pier-badge[data-mark]::before`의 `aria-hidden`은 CSS 속성이 아니므로 구현 시 마커를 `<span aria-hidden="true">`로 둔다(골격 단순화를 위해 표기).
## 부록 B. 화면별 소비 매트릭스
| 화면 | 패턴 | 주요 컴포넌트 (7.1·7.2절) |
| :--- | :--- | :--- |
| S01 | W5 | table, button, badge, xref |
| S02 | W3 | input(G1), directive, button, scope |
| S03 | W2 | card, stat, summary-strip, stepper, state-banner, button |
| S04 | W5 | summary-strip, section-table, badge, state-banner, disclosure, button |
| S05 | W5 | evidence-row, diff-row, confirm-form, specsheet, directive, xref |
| S06 | W3 | input, confirm-form, state-banner, button, codeblock(G9) |
| S07 | W5 | summary-strip, table(selectable), diff-row, badge, button |
| S08 | 모달 | modal, drawing-view, mediaframe |
| S09 | W3 + W5 | input, table, button |
| S10 | 모달 | bulk-confirm-dialog, evidence-row, disclosure, confirm-form |
| S11 | W2 | card, table, badge, run-progress, button |
| S12 | W4 | toc, table, badge, directive |
| S13 | W4 | toc, table, badge, directive, scope |
## 부록 C. 근거 파일
| 구분 | 경로 |
| :--- | :--- |
| Prism 진입점 | `RAG/design system_v0.1.1/Design.md` |
| 토큰 정본 | `RAG/design system_v0.1.1/design.tokens.json` |
| 웹앱 모듈 | `RAG/design system_v0.1.1/webapp/module.webapp.md` |
| 컴포넌트·브랜드 | `RAG/design system_v0.1.1/design.components.md`, `design.brand.md` |
| 화면 요구사항 | `DOC/01_기획/웹앱디자인시안/PRD.md` |
| 기존 디자인 기준 | `context-migration/디자인지침_경로.md` (R8) |
@@ -0,0 +1,431 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PierProof 시안 — 입력 묶음·새 교각 판별</title>
<link rel="stylesheet" href="pier.css">
<style>
@font-face { font-family: "Pretendard"; src: url("fonts/PretendardVariable.woff2") format("woff2-variations"); font-weight: 45 920; font-display: swap; }
body { margin: 0; }
.toast { position: fixed; right: var(--prism-space-300); bottom: var(--prism-space-300); z-index: 700; max-width: 28rem; background: var(--prism-bg-card);
border: var(--prism-stroke-regular) solid var(--prism-comp-line-strong); border-radius: var(--prism-radius-md); padding: var(--prism-space-150) var(--prism-space-200); }
.toast[hidden] { display: none; }
.scrim { position: fixed; }
.demo { background: var(--prism-text-heading); color: var(--prism-comp-on-emphasis); font-size: var(--prism-type-caption); padding: var(--prism-space-050) var(--prism-space-300); }
.rail a.step { cursor: pointer; }
a[data-r] { cursor: pointer; }
td .btn.sm { min-height: var(--prism-comp-hit-min); }
.pbar.run > i { transition: width var(--prism-motion-fast) linear; }
</style>
</head>
<body>
<a class="skip" href="#main">본문 바로가기</a>
<div class="demo">시안 — 동작 흐름 확인용. 데이터는 예시이며 실제 앱 결과가 아니다. 새로고침하면 처음 상태로 돌아간다.</div>
<div class="app" id="app">
<header class="top">
<div class="brand"><i></i>PierProof</div>
<span class="where" id="where"></span>
<div class="meta"><span>권한 담당자</span><span>저장 14:32</span><span>환경 로컬</span></div>
</header>
<div class="body">
<nav class="rail" aria-label="교각 작업 단계">
<span class="cap">교각</span>
<span class="pier">옥성교P1</span>
<ol>
<li><a class="step done" tabindex="0"><span class="n" aria-hidden="true">✓</span>1 도면</a></li>
<li><a class="step" id="nav-bundles" href="#/bundles"><span class="n">2</span>2 입력 묶음</a></li>
<li><a class="step" tabindex="0"><span class="n">3</span>3 게이트</a></li>
<li><a class="step" tabindex="0"><span class="n">4</span>4 보고서</a></li>
</ol>
<hr>
<div class="sub"><a href="#/candidates" id="nav-cand">표 후보값</a></div>
<div class="sub"><a href="#/new" id="nav-new">새 교각 (S02)</a></div>
</nav>
<main class="main" id="main" tabindex="-1"></main>
</div>
</div>
<div id="modalHost"></div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script>
(function () {
'use strict';
/* ---------- data (sample) ---------- */
var GRADE = { V1: ['', 'V1 값'], V2: ['', 'V2 목록'], V3: ['dash', 'V3 생성'], F: ['fixed', 'F 고정'] };
var ST = { g: ['good', '●', '일치', 3], w: ['warn', '▲', '주의', 2], b: ['bad', '■', '불일치', 0], n: ['dash', '◇', '근거 없음', 1] };
var RAW = [
['params · lean_concrete', 'V1', '표 밖 치수', 'g'], ['params · footing', 'V1', '표 밖 치수', 'g'], ['params · column', 'V1', '표 밖 치수', 'g'],
['params · materials', 'V1', '표', 'g'], ['params · coping', 'V2', '표 밖 치수', 'w'], ['params · bearing_seats', 'V1', '표', 'g'],
['params · sole_plates', 'V1', '표', 'g'], ['params · rebar_marks', 'V1', '표', 'g'], ['params · column_rebar', 'V2', '표 밖 치수', 'g'],
['params · footing_rebar', 'V3', '판독 스크립트', 'g'], ['params · coping_rebar', 'V3', '판독 스크립트', 'w'], ['params · display', 'V1', '', 'n', true],
['expected · G1', 'V1', '표 밖 치수', 'g'], ['expected · G1.pedestal', 'V1', '표', 'g'], ['expected · G2', 'V2', '표 밖 치수', 'g'],
['expected · G3', 'V2', '표 밖 치수', 'g'], ['expected · G4', 'V2', '표 밖 치수', 'b'], ['expected · G5', 'V1', '표', 'g'],
['expected · planned_fail', 'V1', '사람 입력', 'n'], ['expected · rebar_palette', 'V1', '', 'n', true], ['expected · checkers', 'F', '템플릿 고정', 'g'],
['expected · schedule', 'V1', '표', 'g'], ['expected · dia', 'V1', '표', 'g'], ['expected · fixed_length', 'V2', '표 밖 치수', 'w'],
['expected · loops', 'V2', '표 밖 치수', 'w'], ['expected · seats', 'V2', '표 밖 치수', 'g'], ['expected · c18', 'V2', '표 밖 치수', 'g']
];
var rows = RAW.map(function (r, i) {
return { i: i, key: r[0], g: r[1], src: r[2], st: r[3], orig: !!r[4], human: null, stale: false };
});
var SRC_TEXT = { '표': '도면 표(D1)', '표 밖 치수': '구조도 정면·단면의 치수 문자(D1)', '판독 스크립트': '판독 스크립트 실행 기록', '사람 입력': '담당자 입력 기록', '템플릿 고정': '템플릿 고정값' };
var CAND = [
{ id: 1, bundle: 'params · coping', file: 'params', target: 'coping.z_tip', cur: 31232, cand: 31231.5, kind: '표 밖 치수', rule: 'R1 레벨', trust: 'B', pos: 'D1 (225817, 1802)', pair: 'ztip' },
{ id: 2, bundle: 'expected · G1', file: 'expected', target: 'G1.coping.z_tip', cur: 31232, cand: 31231.5, kind: '표 밖 치수', rule: 'R1 레벨', trust: 'B', pos: 'D1 (225817, 1802)', pair: 'ztip' },
{ id: 3, bundle: 'params · coping', file: 'params', target: 'coping.z_top_right', cur: 32936, cand: 32936, kind: '표 밖 치수', rule: 'R1 레벨', trust: 'A', pos: 'D1 (225817, 4366)' },
{ id: 4, bundle: 'params · footing', file: 'params', target: 'footing.height', cur: 2500, cand: 2450, kind: '표 밖 치수', rule: 'R2 사슬', trust: 'A', pos: 'D1 (231004, 902)' },
{ id: 5, bundle: 'params · bearing_seats', file: 'params', target: 'bearing_seats[0].h1', cur: 209, cand: 209, kind: '표', rule: 'ELEVATION TABLE', trust: 'T', pos: 'D1 표 행 1' },
{ id: 6, bundle: 'params · sole_plates', file: 'params', target: 'sole_plates[3].S', cur: -0.46, cand: -0.52, kind: '표', rule: 'SOLE PLATE TABLE', trust: 'T', pos: 'D1 표 행 4' },
{ id: 7, bundle: 'params · column_rebar', file: 'params', target: 'column_rebar.hoops[2]', cur: '4@300', cand: '4@300', kind: '표 밖 치수', rule: 'R2 사슬', trust: 'A', pos: 'D2 (224410, 7120)' },
{ id: 8, bundle: 'params · coping_rebar', file: 'params', target: 'coping_rebar.stirrups.count', cur: 104, cand: 106, kind: '표 밖 치수', rule: 'R6 수량 대조', trust: 'C', pos: 'D1 점 94곳' }
];
var TRUST = { A: ['', '등급 A'], B: ['warn', '등급 B'], C: ['dash', '등급 C'], T: ['', '표'] };
/* ---------- state ---------- */
var S = { filter: 'all', fGrade: '', fSrc: '', by: '심영표', checking: false, checkedAt: '14:30', sel: {}, lastBulk: '', flash: null };
/* ---------- helpers ---------- */
function $(sel, root) { return (root || document).querySelector(sel); }
function esc(t) { return String(t).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); }
function badge(kind, mark, label) { return '<span class="b ' + kind + '">' + (mark ? '<span class="m" aria-hidden="true">' + mark + '</span>' : '') + esc(label) + '</span>'; }
function stBadge(r) {
if (r.stale) return badge('warn', '◆', '변경됨');
var s = ST[r.st]; return badge(s[0], s[1], s[2]);
}
function humanBadge(r) { return r.human ? '<span class="b fixed"><span class="m" aria-hidden="true">●</span>확정</span>' : (r.stale && r.staleHuman ? badge('warn', '◆', '변경됨') : '<span class="b dash">미확정</span>'); }
function fmt(v) { return typeof v === 'number' ? v.toLocaleString('ko-KR') : v; }
function toast(msg) {
var t = $('#toast'); t.textContent = msg; t.hidden = false;
clearTimeout(toast._t); toast._t = setTimeout(function () { t.hidden = true; }, 6000);
}
function counts() {
var c = { g: 0, w: 0, b: 0, n: 0, human: 0 };
rows.forEach(function (r) { c[r.st]++; if (r.human) c.human++; });
return c;
}
function sorted() { return rows.slice().sort(function (a, b) { return (ST[a.st][3] - ST[b.st][3]) || (a.i - b.i); }); }
/* ---------- evidence ---------- */
function evidence(r) {
var src = SRC_TEXT[r.src] || '';
var hex = (0x2c41 + r.i * 37).toString(16).toUpperCase();
var pos = 'D1 · 핸들 ' + hex;
var isExp = r.key.indexOf('expected') === 0;
var ev = [
{ id: 'C1', name: '출처 있음', st: 'g', text: '이 묶음의 값은 ' + src + '에서 읽었다.', pos: pos },
{ id: 'C2', name: '도면 재대조', st: 'g', text: '현재 값이 ' + src + '의 후보 값과 같다.', pos: pos },
{ id: 'C3', name: '템플릿 원본 잔존', st: 'g', text: '원본과 다른 값은 ' + (30 + r.i) + '개이고 같은 값 ' + (r.i % 4) + '개는 모두 도면 근거가 있다.', pos: '—' },
{ id: 'C4', name: '내부 정합', st: 'g', text: '치수 사슬 합이 전체 치수와 같고 본수가 재료표 수량과 같다.', pos: '재료표' },
{ id: 'C5', name: 'params와 독립', st: isExp ? 'g' : 'x', text: isExp ? 'expected의 출처가 params의 출처와 다르다.' : '해당 없음. params 묶음이다.', pos: '—' },
{ id: 'C6', name: '범위·단위', st: 'g', text: 'EL과 길이가 상식 범위 안에 있고 단위가 mm이다.', pos: '—' },
{ id: 'C7', name: '변수 등급 위반', st: 'g', text: r.g === 'F' ? 'F 고정 항목이 템플릿 원본과 같다.' : 'F 고정 항목이 템플릿 원본과 다르지 않다.', pos: '—' },
{ id: 'C8', name: '재생성 일치', st: r.g === 'V3' ? 'g' : 'x', text: r.g === 'V3' ? '재생성 목록의 해시가 현재 값과 같다 (독립성 낮음).' : '해당 없음. V3 묶음이 아니다.', pos: '판독 스크립트' }
];
function set(id, st, text) { ev.forEach(function (e) { if (e.id === id) { e.st = st; e.text = text; } }); }
if (r.src === '표') set('C1', 'g', '이 묶음의 값은 도면 표(D1)의 행에서 읽었다.');
if (r.key === 'expected · G4') set('C4', 'b', '스터럽 곳 수 합 104가 재료표 C8~C13 수량 합 106(39 + 27 + 40)과 다르다.');
if (r.key === 'params · coping') set('C2', 'w', '단 높이가 표 값과 1.1 mm 어긋난다. 허용 범위 안이다.');
if (r.key === 'params · coping_rebar') set('C8', 'w', '재생성 목록의 해시가 같다. 독립성이 낮아 도면 직접 대조가 필요하다.');
if (r.key === 'expected · fixed_length') set('C4', 'w', '길이가 재료표 L과 겹이음만큼 다르다. 허용 범위 안이다.');
if (r.key === 'expected · loops') set('C2', 'w', '측면 고리 한 단이 점 목록과 3 mm 다르다. 허용 범위 안이다.');
if (r.st === 'n') {
set('C1', 'n', r.key === 'expected · planned_fail' ? '사람 결정 기록이다. 도면 출처가 없다.' : '출처를 찾지 못했다. 사람이 도면에서 직접 확인한다.');
set('C2', 'x', '비교할 후보가 없다.'); set('C4', 'x', '비교할 항목이 없다.'); set('C6', 'x', '검사 대상 수치가 없다.');
if (r.orig) set('C3', 'n', '현재 값이 템플릿 원본과 같고 새 도면 근거가 없다.');
}
return ev;
}
var EVST = { g: ['good', '●', '일치'], w: ['warn', '▲', '주의'], b: ['bad', '■', '불일치'], n: ['dash', '◇', '근거 없음'], x: ['', '', '—'] };
/* ---------- views ---------- */
var main = $('#main');
function setWhere(t) { $('#where').textContent = t; }
function viewBundles() {
setWhere('S04 · 옥성교P1 · 입력 묶음');
var c = counts();
var list = sorted().filter(function (r) {
if (S.filter !== 'all' && r.st !== S.filter) return false;
if (S.fGrade && r.g !== S.fGrade) return false;
if (S.fSrc && (r.orig ? '원본값' : r.src) !== S.fSrc) return false;
return true;
});
var origN = rows.filter(function (r) { return r.orig; }).length;
var html = '<div class="head"><span class="eyebrow">2단계</span><h1>입력 묶음 확인·확정</h1><p class="lead">앱이 묶음을 도면과 대조해 근거를 보인다. 확정은 근거를 읽은 담당자가 한다.</p></div>';
if (S.flash) { html += '<div class="banner good" role="status"><span class="m" aria-hidden="true">●</span><div>' + esc(S.flash) + '</div></div>'; }
html += '<section class="strip" aria-label="요약"><p class="concl">필요 27개 중 앱 확인 일치 ' + c.g + ' · 주의 ' + c.w + ' · 불일치 ' + c.b + ' · 근거 없음 ' + c.n + ' · 사람 확정 ' + c.human + '</p><div class="counts">';
[['all', '전체', '', 27], ['g', '일치', ST.g, c.g], ['w', '주의', ST.w, c.w], ['b', '불일치', ST.b, c.b], ['n', '근거 없음', ST.n, c.n]].forEach(function (f) {
var lab = f[2] ? badge(f[2][0], f[2][1], f[1]) : '<span>' + f[1] + '</span>';
html += '<button class="cnt' + (S.filter === f[0] ? ' sel' : '') + '" type="button" data-filter="' + f[0] + '" aria-pressed="' + (S.filter === f[0]) + '">' + lab + '<b class="num">' + f[3] + '</b></button>';
});
html += '</div>';
if (S.checking) html += '<div class="stack" style="gap:0.25rem"><span class="cap" id="runtxt">앱 확인 중 0/27 묶음</span><div class="pbar run" role="progressbar" aria-valuemin="0" aria-valuemax="27" aria-valuenow="0" id="runbar"><i style="width:0%"></i></div></div>';
else html += '<span class="cap muted">마지막 앱 확인 ' + S.checkedAt + ' · 검사 v0.1 · 재확인 필요 ' + rows.filter(function (r) { return r.stale; }).length + '개</span>';
html += '</section>';
html += '<div class="banner"><span class="m" aria-hidden="true">▲</span><div><b>원본값 ' + origN + '개.</b> 템플릿 원본과 같고 새 도면 근거가 없다. 일괄 확정에서 제외되며, 새 도면과 대조해 값이 바뀌거나 근거가 붙어야 풀린다.</div></div>';
html += '<div class="row"><div class="field"><label for="fg">변수 등급</label><select id="fg"><option value="">전체</option>' +
['V1', 'V2', 'V3', 'F'].map(function (g) { return '<option value="' + g + '"' + (S.fGrade === g ? ' selected' : '') + '>' + GRADE[g][1] + '</option>'; }).join('') + '</select></div>' +
'<div class="field"><label for="fs">값 출처</label><select id="fs"><option value="">전체</option>' +
['표', '표 밖 치수', '판독 스크립트', '원본값', '사람 입력', '템플릿 고정'].map(function (g) { return '<option value="' + g + '"' + (S.fSrc === g ? ' selected' : '') + '>' + g + '</option>'; }).join('') + '</select></div>' +
'<span class="grow"></span><button class="btn" type="button" id="runAll">전체 확인</button><button class="btn primary" type="button" id="bulk">일치분 확정</button></div>';
html += '<div class="tw"><table class="t"><caption>입력 묶음 ' + list.length + '개 · 불일치 → 근거 없음 → 주의 → 일치 순</caption><thead><tr><th scope="col">묶음</th><th scope="col">변수 등급</th><th scope="col">값 출처</th><th scope="col">앱 확인</th><th scope="col">사람 확정</th><th scope="col">근거</th></tr></thead><tbody>';
if (!list.length) html += '<tr><td colspan="6">조건에 맞는 묶음이 없다. 필터를 바꾼다.</td></tr>';
list.forEach(function (r) {
html += '<tr><td><a href="#/evidence/' + r.i + '">' + esc(r.key) + '</a></td><td>' + badge(GRADE[r.g][0], '', GRADE[r.g][1]) + '</td><td>' + esc(r.src) +
(r.orig ? badge('warn', '▲', '원본값') : '') + '</td><td>' + stBadge(r) + (r.stale ? ' <button class="btn quiet sm" type="button" data-recheck="' + r.i + '">확인 실행</button>' : '') +
'</td><td>' + (r.human ? humanBadge(r) + ' <span class="cap muted">' + esc(r.human.by) + ' ' + esc(r.human.at) + '</span>' : humanBadge(r)) +
'</td><td><a class="btn quiet sm" href="#/evidence/' + r.i + '">근거 보기</a></td></tr>';
});
html += '</tbody></table></div>';
html += '<details class="card" style="padding:0"><summary style="min-height:2.75rem;display:flex;align-items:center;cursor:pointer;font-weight:700;padding:0 0.75rem">확정 불필요(참고) 5개</summary><div style="padding:0 1rem 1rem" class="muted">_schema · _meta · pier_id_src · hitl_decisions_note · display_notes. 확정 대상이 아니며 앱 확인도 하지 않는다.</div></details>';
main.innerHTML = html;
main.querySelectorAll('[data-filter]').forEach(function (b) { b.onclick = function () { S.filter = b.getAttribute('data-filter'); viewBundles(); $('[data-filter="' + S.filter + '"]').focus(); }; });
$('#fg').onchange = function (e) { S.fGrade = e.target.value; viewBundles(); $('#fg').focus(); };
$('#fs').onchange = function (e) { S.fSrc = e.target.value; viewBundles(); $('#fs').focus(); };
$('#runAll').onclick = runAll;
$('#bulk').onclick = openBulk;
main.querySelectorAll('[data-recheck]').forEach(function (b) { b.onclick = function () { var r = rows[+b.getAttribute('data-recheck')]; r.stale = false; r.staleHuman = false; toast(r.key + ' 앱 확인을 다시 실행했다. 결과는 이전과 같다.'); viewBundles(); }; });
S.flash = null;
}
function runAll() {
if (S.checking) return;
S.checking = true; viewBundles();
var n = 0, bar = $('#runbar'), txt = $('#runtxt');
var timer = setInterval(function () {
n += 3; if (n > 27) n = 27;
bar.firstElementChild.style.width = (n / 27 * 100) + '%'; bar.setAttribute('aria-valuenow', n); txt.textContent = '앱 확인 중 ' + n + '/27 묶음';
if (n >= 27) {
clearInterval(timer); S.checking = false; S.checkedAt = '14:36';
rows.forEach(function (r) { r.stale = false; r.staleHuman = false; });
toast('앱 확인을 마쳤다. 결과는 요약 띠에 반영되었다.'); viewBundles();
}
}, 120);
}
function viewEvidence(idx) {
var r = rows[idx]; if (!r) { location.hash = '#/bundles'; return; }
setWhere('S05 · 옥성교P1 · 근거 보기');
var ev = evidence(r), s = ST[r.st];
var order = sorted();
var pos = order.indexOf(r);
var next = null;
for (var k = 1; k <= order.length; k++) { var cand = order[(pos + k) % order.length]; if (!cand.human && cand !== r) { next = cand; break; } }
var html = '<div class="head"><span class="eyebrow">2단계 · 입력 묶음</span><h1>근거 보기</h1></div><div class="split"><aside class="card" style="padding:0" aria-label="묶음 이동"><h3 style="padding:1rem 1rem 0.5rem">묶음 이동</h3><ul class="list">';
order.forEach(function (o) {
html += '<li class="' + (o === r ? 'cur' : '') + '"><a href="#/evidence/' + o.i + '"' + (o === r ? ' aria-current="true"' : '') + '>' + esc(o.key) + '</a>' + stBadge(o) + '</li>';
});
html += '</ul><div class="row" style="padding:0.75rem 1rem"><a class="btn sm" href="#/bundles">목록으로</a></div></aside><div class="stack">';
var concl = r.stale ? esc(r.key) + ' — 값이 바뀌었다. 이전 확인과 확정이 무효가 되었다.' :
esc(r.key) + ' — 앱 확인 ' + s[2] + '. ' + (r.st === 'g' ? '근거를 읽고 확정한다.' : '사람 확정 전에 사유가 필요하다.');
html += '<section class="strip"><p class="concl">' + concl + '</p><dl class="kv"><dt>변수 등급</dt><dd>' + badge(GRADE[r.g][0], '', GRADE[r.g][1]) + '</dd><dt>앱 확인</dt><dd>' + stBadge(r) + '</dd><dt>사람 확정</dt><dd>' +
(r.human ? humanBadge(r) + ' ' + esc(r.human.by) + ' · ' + esc(r.human.at) + (r.human.why ? ' · 사유: ' + esc(r.human.why) : '') : '<span class="b dash">미확정</span>') + '</dd><dt>검사</dt><dd>v0.1 · ' + S.checkedAt + '</dd></dl></section>';
html += '<div class="tw"><table class="t"><caption>앱 확인 근거 C1 ~ C8</caption><thead><tr><th scope="col">검사</th><th scope="col">결과</th><th scope="col">근거</th><th scope="col">도면 위치</th></tr></thead><tbody>';
ev.forEach(function (e) { var q = EVST[e.st]; html += '<tr><td style="white-space:nowrap"><b>' + e.id + '</b> ' + e.name + '</td><td>' + (e.st === 'x' ? '—' : badge(q[0], q[1], q[2])) + '</td><td style="max-width:36rem">' + esc(e.text) + '</td><td>' + esc(e.pos) + '</td></tr>'; });
html += '</tbody></table></div><div class="banner tip"><span class="m" aria-hidden="true">◆</span><div>일치는 후보를 만든 규칙과 값이 같다는 뜻이다. 도면이 맞다는 판정이 아니다.</div></div>';
if (r.st === 'b' || r.st === 'w') {
html += '<section class="card"><h3>값 비교</h3><table class="t"><caption class="cap">다른 항목만 표시</caption><thead><tr><th scope="col">항목</th><th class="num" scope="col">현재</th><th class="num" scope="col">기준</th><th scope="col">차이</th></tr></thead><tbody>' +
(r.st === 'b' ? '<tr><td>스터럽 곳 수 합(개)</td><td class="num">104</td><td class="num">106</td><td>' + badge('warn', '▲', '다름') + '</td></tr>' : '<tr><td>표시된 항목</td><td class="num">1.1</td><td class="num">0</td><td>' + badge('warn', '▲', '다름') + '</td></tr>') + '</tbody></table></section>';
}
if (!r.human) {
html += '<section class="card"><h3>확정</h3><div class="field"><label for="by">확정자<span class="req">필수</span></label><input id="by" type="text" value="' + esc(S.by) + '" style="max-width:20rem"></div>' +
'<div class="field" id="whyf"><label for="why">사유<span class="req">' + (r.st === 'g' ? '선택' : '주의·불일치·근거 없음 확정에는 필수') + '</span></label><textarea id="why" rows="3"></textarea><span class="err" id="err" hidden></span></div>' +
'<div class="row"><button class="btn primary" type="button" id="conf">확정</button>' + (next ? '<a class="btn" href="#/evidence/' + next.i + '">다음 미확정 묶음</a>' : '') + '</div></section>';
}
html += '</div></div>';
main.innerHTML = html;
var cb = $('#conf');
if (cb) cb.onclick = function () {
var by = $('#by').value.trim(), why = $('#why').value.trim(), err = $('#err'), f = $('#whyf');
function fail(m) { err.hidden = false; err.innerHTML = '<span aria-hidden="true">■</span>' + esc(m); f.classList.add('bad'); }
f.classList.remove('bad'); err.hidden = true;
if (!by) { fail('확정자 이름이 비어 있다. 확정은 사람의 기록이다. 이름을 입력한다'); $('#by').focus(); return; }
if (r.st !== 'g' && !why) { fail('사유가 비어 있다. ' + ST[r.st][2] + ' 묶음은 사유가 있어야 확정된다. 사유를 입력한다'); $('#why').focus(); return; }
S.by = by; r.human = { by: by, at: '14:41', why: why };
toast(r.key + ' 확정을 기록했다. 확정자 ' + by + '.'); viewEvidence(idx);
};
}
function viewCandidates() {
setWhere('S07 · 옥성교P1 · 표 후보값');
var diffN = CAND.filter(function (c) { return c.cur !== c.cand; }).length;
var html = '<div class="head"><span class="eyebrow">2단계 · 입력 묶음</span><h1>표 후보값·표 밖 치수</h1><p class="lead">도면 표와 표 밖 치수에서 찾은 후보를 현재 값과 비교한다. 반영은 선택한 행에만 적용되고 자동 반영은 없다.</p></div>';
html += '<section class="strip"><p class="concl">후보 ' + CAND.length + '건 — 다름 ' + diffN + '건, 근거 없음 3건</p><div class="counts"><span class="b">표 2</span><span class="b">표 밖 치수 6</span><span class="b dash">등급 C 1</span></div></section>';
html += '<div class="banner tip"><span class="m" aria-hidden="true">◆</span><div>params와 expected에 같은 후보를 함께 반영하면 검사의 독립성이 사라진다. 반영 대상 줄은 하나씩 고른다.</div></div>';
html += '<div class="tw"><table class="t"><caption>후보 비교 — 선택 ' + Object.keys(S.sel).filter(function (k) { return S.sel[k]; }).length + '건</caption><thead><tr><th scope="col">반영</th><th scope="col">대상</th><th scope="col">줄</th><th class="num" scope="col">현재</th><th class="num" scope="col">후보</th><th scope="col">구분</th><th scope="col">규칙</th><th scope="col">상태</th><th scope="col">신뢰</th><th scope="col">도면 위치</th></tr></thead><tbody>';
CAND.forEach(function (c) {
var d = c.cur !== c.cand;
html += '<tr' + (S.sel[c.id] ? ' class="sel"' : '') + '><td><input type="checkbox" data-c="' + c.id + '"' + (S.sel[c.id] ? ' checked' : '') + ' aria-label="' + esc(c.target) + ' 반영" style="width:1.25rem;height:1.25rem;accent-color:var(--prism-text-heading)"></td><td><b>' + esc(c.target) + '</b></td><td>' + c.file + '</td><td class="num">' + fmt(c.cur) + '</td><td class="num">' + fmt(c.cand) + '</td><td>' + c.kind + '</td><td>' + c.rule + '</td><td>' +
(d ? badge('warn', '▲', '다름') : '같음') + '</td><td>' + badge(TRUST[c.trust][0], c.trust === 'B' ? '▲' : '', TRUST[c.trust][1]) + '</td><td><span class="chip">' + esc(c.pos) + '</span></td></tr>';
});
html += '</tbody></table></div><div class="field" id="cwhy" hidden><label for="cw">반영 사유<span class="req">등급 C 반영에는 필수</span></label><input id="cw" type="text" style="max-width:36rem"></div><span class="err" id="cerr" hidden></span>';
html += '<div class="row"><button class="btn primary" type="button" id="apply">후보 반영</button></div>';
main.innerHTML = html;
function needWhy() { return CAND.some(function (c) { return S.sel[c.id] && c.trust === 'C'; }); }
$('#cwhy').hidden = !needWhy();
main.querySelectorAll('[data-c]').forEach(function (cb) { cb.onchange = function () { S.sel[cb.getAttribute('data-c')] = cb.checked; var y = window.scrollY; viewCandidates(); window.scrollTo(0, y); $('[data-c="' + cb.getAttribute('data-c') + '"]').focus(); }; });
$('#apply').onclick = function () {
var picks = CAND.filter(function (c) { return S.sel[c.id]; }), err = $('#cerr');
function fail(m) { err.hidden = false; err.innerHTML = '<span aria-hidden="true">■</span>' + esc(m); }
err.hidden = true;
if (!picks.length) { fail('선택한 행이 없다. 반영할 후보가 없다. 행을 선택한다'); return; }
var pairs = {}, bad = null;
picks.forEach(function (c) { if (c.pair) { if (pairs[c.pair] && pairs[c.pair] !== c.file) bad = c; pairs[c.pair] = c.file; } });
if (bad) { fail('같은 후보를 params와 expected에 함께 반영할 수 없다. 두 줄의 값이 같은 근거에서 나오면 검사가 무의미해진다. 한 줄만 선택한다'); return; }
if (needWhy() && !$('#cw').value.trim()) { fail('반영 사유가 비어 있다. 등급 C 후보는 사유가 있어야 반영된다. 사유를 입력한다'); $('#cw').focus(); return; }
var touched = {};
picks.forEach(function (c) { c.cur = c.cand; touched[c.bundle] = true; });
rows.forEach(function (r) { if (touched[r.key]) { r.stale = true; if (r.human) r.staleHuman = true; r.human = null; } });
S.sel = {};
toast(picks.length + '건을 반영했다. 영향받은 묶음 ' + Object.keys(touched).length + '개의 앱 확인과 확정이 무효가 되었다.');
viewCandidates();
};
}
/* ---------- bulk confirm (S10) ---------- */
var lastFocus = null;
function closeModal() { $('#modalHost').innerHTML = ''; document.removeEventListener('keydown', trap, true); if (lastFocus) lastFocus.focus(); }
function trap(e) {
var m = $('#modalHost .modal'); if (!m) return;
if (e.key === 'Escape') { e.preventDefault(); closeModal(); return; }
if (e.key !== 'Tab') return;
var f = m.querySelectorAll('button, [href], input, select, textarea, summary, [tabindex="0"]');
if (!f.length) return;
var first = f[0], last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
function openBulk() {
lastFocus = document.activeElement;
var targets = rows.filter(function (r) { return r.st === 'g' && !r.orig && !r.human && !r.stale; });
var ex = rows.filter(function (r) { return !(r.st === 'g' && !r.orig) && !r.human || r.stale; });
var read = false;
var html = '<div class="scrim"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="mt"><header><h2 id="mt">일치 묶음 확정</h2><button class="btn quiet sm" type="button" id="mx" aria-label="닫기">닫기</button></header>';
if (!targets.length) {
html += '<p>확정할 일치 묶음이 없다. 모든 일치 묶음이 확정되었거나 재확인이 필요하다.</p><div class="row end"><button class="btn" type="button" id="mc">닫기</button></div></div></div>';
$('#modalHost').innerHTML = html; $('#mx').onclick = $('#mc').onclick = closeModal; document.addEventListener('keydown', trap, true); $('#mx').focus(); return;
}
html += '<p class="concl" style="font-size:var(--prism-type-body-lg);font-weight:700;color:var(--prism-text-heading)">일치 ' + targets.length + '개를 확정한다. 주의·불일치·근거 없음·변경됨 ' + ex.length + '개는 대상이 아니다.</p>' +
'<div><h3>근거표 <span class="cap muted">끝까지 내려 확인한다</span></h3><div class="scroll" id="sc" tabindex="0" role="region" aria-label="일치 묶음 근거표"><table class="t"><caption class="cap" style="position:absolute;left:-999rem">일치 묶음 근거</caption><thead><tr><th scope="col">묶음</th><th scope="col">근거</th><th scope="col">출처</th></tr></thead><tbody>';
targets.forEach(function (r) { html += '<tr><td><b>' + esc(r.key) + '</b></td><td>' + esc(evidence(r)[1].text) + '</td><td>' + esc(r.src) + '</td></tr>'; });
html += '</tbody></table></div></div><details class="card sunken" style="padding:0 1rem"><summary style="min-height:2.75rem;display:flex;align-items:center;cursor:pointer;font-weight:700">제외된 묶음 ' + ex.length + '개 — 누르면 근거 보기로 이동</summary><table class="t" style="margin-bottom:1rem"><caption class="cap" style="position:absolute;left:-999rem">제외된 묶음</caption><thead><tr><th scope="col">묶음</th><th scope="col">사유</th></tr></thead><tbody>';
ex.forEach(function (r) { html += '<tr><td><a href="#/evidence/' + r.i + '" data-close="1">' + esc(r.key) + '</a></td><td>' + stBadge(r) + (r.orig ? ' 원본값. 새 도면 근거가 없다' : '') + '</td></tr>'; });
html += '</tbody></table></details><div class="field"><label for="mby">확정자<span class="req">필수</span></label><input id="mby" type="text" value="' + esc(S.by) + '" style="max-width:20rem"><span class="help">확정 기록에는 입력한 이름만 남는다. 앱은 확정하지 않는다</span></div>' +
'<div class="err" id="merr" hidden></div><div class="row end"><button class="btn" type="button" id="mc">취소</button><button class="btn primary" type="button" id="mok">확정</button></div></div></div>';
$('#modalHost').innerHTML = html;
var sc = $('#sc');
function chk() { if (sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 2) read = true; }
sc.addEventListener('scroll', chk); chk();
$('#mx').onclick = $('#mc').onclick = closeModal;
$('#modalHost').querySelectorAll('[data-close]').forEach(function (a) { a.onclick = function () { closeModal(); }; });
$('#mok').onclick = function () {
var err = $('#merr'), by = $('#mby').value.trim();
function fail(m, el) { err.hidden = false; err.innerHTML = '<span aria-hidden="true">■</span>' + esc(m); if (el) el.focus(); }
if (!read) { fail('근거표를 읽지 않았다. 일치 묶음의 근거를 읽어야 확정할 수 있다. 근거표를 끝까지 내린다', sc); return; }
if (!by) { fail('확정자 이름이 비어 있다. 확정은 사람의 기록이다. 이름을 입력한다', $('#mby')); return; }
S.by = by;
targets.forEach(function (r) { r.human = { by: by, at: '14:41', why: '' }; });
S.flash = targets.length + '개를 ' + by + '이(가) 확정했다. 확정 기록에는 입력한 이름만 남았다.';
closeModal(); route();
};
document.addEventListener('keydown', trap, true); $('#mx').focus();
}
/* ---------- S02 new pier fit (sample: Ungok P1 drawings uploaded) ---------- */
var QS = [['foundation', '기초 형식(말뚝 유무)'], ['soffit', '코핑 하면 형상'], ['bearings', '받침 개수']];
var OPTS = { foundation: ['기초판+말뚝', '기초판만', '말뚝만'], soffit: ['곡선', '직선 경사', '나팔형'], bearings: ['2', '4', '10'] };
var TPLS = [
{ n: '운곡교P1형', p: { foundation: '기초판+말뚝', soffit: '곡선', bearings: '10' } },
{ n: '옥성교P1형', p: { foundation: '기초판+말뚝', soffit: '직선 경사', bearings: '10' } },
{ n: '새재1교P1형', p: { foundation: '기초판만', soffit: '직선 경사', bearings: '10' } },
{ n: 'Ramp-A교P1형', p: { foundation: '말뚝만', soffit: '나팔형', bearings: '2' } },
{ n: '산외교울산P1형', p: { foundation: '기초판만', soffit: '직선 경사', bearings: '4' } }
];
var EST = {
foundation: ['기초판+말뚝', "'말뚝' 문자 7개: D1 CS-TEXT (복합말뚝)", '보통'],
soffit: ['곡선', "R 12000 mm: D1 CS-DIMT (8257, 16513) 'R=12.000'", '보통'],
bearings: ['10', 'ELEVATION TABLE 행 10개: D2 (26179, 9342)', '보통']
};
var N = { up: false, ans: null, tpl: '운곡교P1형', name: '', rs: '미확인', rm: '미확인', skew: '', brot: '', frot: '', err: '' };
function nMatch(t) { return QS.filter(function (q) { return t.p[q[0]] === EST[q[0]][0]; }).length; }
function nBad() {
var t = TPLS.filter(function (x) { return x.n === N.tpl; })[0], bad = [];
QS.forEach(function (q) { if (t.p[q[0]] !== N.ans[q[0]]) bad.push(q[1]); });
if (N.rs === '아니오') bad.push('철근 형상 종류');
if (N.rm === '아니오') bad.push('철근 생성 방식');
return bad;
}
function viewNew() {
setWhere('S02 · 새 교각 적합성 판별');
var html = '<div class="head"><span class="eyebrow">새 교각</span><h1>도면으로 템플릿 추천</h1><p class="lead">도면을 먼저 올리면 앱이 가장 비슷한 템플릿을 추천한다. 추천은 제안이고, 6문항은 사람이 확인한다.</p></div>';
if (!N.up) {
html += '<section class="card" style="max-width:38em"><div class="field"><label for="dw">교각 도면(DWG, 여러 장)</label><input id="dw" type="file" accept=".dwg" multiple><span class="help">시안에서는 운곡교 P1 도면 3장을 올린 것으로 본다</span></div><div class="row"><button class="btn primary" type="button" id="up">도면 읽고 추천 받기</button></div></section>';
main.innerHTML = html;
$('#up').onclick = function () { N.up = true; N.ans = {}; QS.forEach(function (q) { N.ans[q[0]] = EST[q[0]][0]; }); viewNew(); };
return;
}
var ranked = TPLS.slice().sort(function (a, b) { return nMatch(b) - nMatch(a); });
html += '<section class="strip"><p class="concl">도면 3장, 문자 2,366개를 읽었다 — 1위 ' + ranked[0].n + ' (3 / 3 일치)</p></section>';
html += '<h2>1 추천 순위</h2><div class="tw"><table class="t"><caption>추정한 3문항과 일치한 수</caption><thead><tr><th scope="col">순위</th><th scope="col">템플릿</th><th class="num" scope="col">일치</th>' + QS.map(function (q) { return '<th scope="col">' + q[1] + '</th>'; }).join('') + '</tr></thead><tbody>';
ranked.forEach(function (t, i) {
html += '<tr><td class="num">' + (i + 1) + '</td><td><b>' + t.n + '</b></td><td class="num">' + nMatch(t) + ' / 3</td>' + QS.map(function (q) {
var ok = t.p[q[0]] === EST[q[0]][0];
return '<td>' + esc(t.p[q[0]]) + ' ' + (ok ? badge('good', '●', '일치') : badge('bad', '■', '다름')) + '</td>';
}).join('') + '</tr>';
});
html += '</tbody></table></div>';
html += '<h2>2 추정 근거</h2><div class="tw"><table class="t"><thead><tr><th scope="col">문항</th><th scope="col">추정</th><th scope="col">근거(도면 위치)</th><th scope="col">신뢰</th></tr></thead><tbody>';
QS.forEach(function (q) { var e = EST[q[0]]; html += '<tr><td>' + q[1] + '</td><td><b>' + e[0] + '</b></td><td><span class="chip">' + esc(e[1]) + '</span></td><td>' + e[2] + '</td></tr>'; });
html += '<tr><td>기둥 단면·철근 형상·철근 생성 방식</td><td>' + badge('dash', '◇', '판단 불가') + '</td><td>도면 문자로 판별할 근거가 없다. 사람이 답한다</td><td>-</td></tr></tbody></table></div>';
html += '<div class="banner tip"><span class="m" aria-hidden="true">◆</span><div>코핑 하면 형상은 정면도의 큰 R 문자(10 m 이상)만 본다. 평면 모서리의 작은 R은 제외한다. 평면의 사각·직각과 각도는 문항이 아니라 아래 값으로 입력한다.</div></div>';
html += '<h2>3 6문항 확인 후 교각 만들기</h2><div style="max-width:38em;display:flex;flex-direction:column;gap:var(--prism-space-200)">';
QS.forEach(function (q) {
html += '<div class="field"><label for="a_' + q[0] + '">' + q[1] + '</label><select id="a_' + q[0] + '" data-a="' + q[0] + '">' + OPTS[q[0]].map(function (o) { return '<option' + (N.ans[q[0]] === o ? ' selected' : '') + '>' + o + '</option>'; }).join('') + '</select></div>';
});
[['rs', '철근 형상 종류가 템플릿의 재료표 형식 열과 같은가'], ['rm', '철근 생성 방식(규칙 값 / 철근별 중심선)이 같은가']].forEach(function (q) {
html += '<div class="field"><label for="' + q[0] + '">' + q[1] + '</label><select id="' + q[0] + '" data-r="' + q[0] + '">' + ['미확인', '예', '아니오'].map(function (o) { return '<option' + (N[q[0]] === o ? ' selected' : '') + '>' + o + '</option>'; }).join('') + '</select></div>';
});
html += '<fieldset class="card sunken"><legend class="lbl">각도 값 확인 <span class="muted">템플릿을 가르지 않는 값. 비워도 된다</span></legend><div class="grid3">' +
[['skew', '코핑 평면 skew (°)'], ['brot', '받침 회전 (°)'], ['frot', '기초 회전 (°)']].map(function (q) { return '<div class="field"><label for="g_' + q[0] + '">' + q[1] + '</label><input id="g_' + q[0] + '" type="text" inputmode="decimal" data-g="' + q[0] + '" value="' + esc(N[q[0]]) + '"></div>'; }).join('') + '</div></fieldset>';
var bad = nBad();
html += '<section class="strip" role="status" aria-live="polite"><h3 style="margin:0 0 .25rem">결과 판정</h3><p class="concl">' + (bad.length ? badge('bad', '■', '같은 템플릿으로 만들 수 없음') + ' 다른 문항: ' + esc(bad.join(', ')) : badge('good', '●', '이 템플릿으로 만들 수 있음')) + '</p>' +
(bad.length ? '<p class="muted" style="margin:.25rem 0 0">가장 가까운 템플릿에 생성기·검사기를 추가해야 한다. 그래도 진행하면 교각 이름 옆과 모든 화면 상단에 형식 불일치 경고가 계속 표시된다(닫을 수 없다).</p>' : '') + '</section>';
html += '<div class="field' + (N.err ? ' bad' : '') + '"><label for="nm">교각 이름<span class="req">필수</span></label><input id="nm" type="text" value="' + esc(N.name) + '" style="max-width:20rem">' + (N.err ? '<span class="err"><span aria-hidden="true">■</span>' + esc(N.err) + '</span>' : '') + '</div>';
html += '<div class="field"><label for="tp">템플릿</label><select id="tp" style="max-width:20rem">' + ranked.map(function (t, i) { return '<option value="' + t.n + '"' + (N.tpl === t.n ? ' selected' : '') + '>' + (i + 1) + '위 ' + t.n + '</option>'; }).join('') + '</select></div>';
html += '<div class="row"><button class="btn primary" type="button" id="mk">' + (bad.length ? '그래도 만들기' : '교각 만들기') + '</button></div></div>';
main.innerHTML = html;
var y = window.scrollY;
main.querySelectorAll('[data-a]').forEach(function (e) { e.onchange = function () { N.ans[e.getAttribute('data-a')] = e.value; viewNew(); window.scrollTo(0, y); $('#' + e.id).focus(); }; });
main.querySelectorAll('[data-r]').forEach(function (e) { e.onchange = function () { N[e.getAttribute('data-r')] = e.value; viewNew(); window.scrollTo(0, y); $('#' + e.id).focus(); }; });
main.querySelectorAll('[data-g]').forEach(function (e) { e.oninput = function () { N[e.getAttribute('data-g')] = e.value; }; });
$('#nm').oninput = function () { N.name = this.value; };
$('#tp').onchange = function () { N.tpl = this.value; viewNew(); window.scrollTo(0, y); $('#tp').focus(); };
$('#mk').onclick = function () {
N.name = $('#nm').value.trim();
if (!N.name) { N.err = '교각 이름이 비어 있다. 폴더를 만들 이름이 필요하다. 이름을 입력한다'; viewNew(); $('#nm').focus(); return; }
N.err = '';
toast("교각 '" + N.name + "'을(를) 만들었다. 올린 도면이 교각 폴더로 옮겨졌고 6문항 답과 각도 값이 저장되었다." + (bad.length ? ' 형식 불일치 경고가 표시된다.' : ''));
viewNew();
};
}
/* ---------- router ---------- */
function route() {
var h = location.hash || '#/bundles', m;
$('#nav-bundles').classList.toggle('cur', h.indexOf('#/bundles') === 0 || h.indexOf('#/evidence') === 0);
$('#nav-cand').style.fontWeight = h.indexOf('#/candidates') === 0 ? '700' : '500';
$('#nav-new').style.fontWeight = h.indexOf('#/new') === 0 ? '700' : '500';
if ((m = h.match(/^#\/evidence\/(\d+)/))) viewEvidence(+m[1]);
else if (h.indexOf('#/candidates') === 0) viewCandidates();
else if (h.indexOf('#/new') === 0) viewNew();
else viewBundles();
if (document.activeElement === document.body) main.focus();
}
window.addEventListener('hashchange', function () { window.scrollTo(0, 0); route(); });
route();
})();
</script>
</body>
</html>
@@ -0,0 +1,182 @@
/* PierProof — Prism app profile tokens (design.md 3.2 / 3.3) and components. Literals live only in :root. */
:root {
--prism-bg-canvas: #F4F3F0; --prism-bg-card: #FFFFFF;
--prism-text-body: #474235; --prism-text-heading: #1F524B; --prism-text-subtle: #474235; --prism-text-accent: #D05C26;
--prism-border-default: #474235; --prism-border-strong: #1F524B;
--prism-status-do: #005A4E; --prism-status-do-bg: #DCEAE6;
--prism-status-dont: #8F2A11; --prism-status-dont-bg: #F6DFDA;
--prism-status-caution: #6B4A00; --prism-status-caution-bg: #F5E7CC;
--prism-status-tip: #0A4E86; --prism-status-tip-bg: #DDE8F3;
--prism-space-025: 0.125rem; --prism-space-050: 0.25rem; --prism-space-100: 0.5rem; --prism-space-150: 0.75rem;
--prism-space-200: 1rem; --prism-space-300: 1.5rem; --prism-space-400: 2rem; --prism-space-600: 3rem; --prism-space-800: 4rem;
--prism-type-caption: 0.8125rem; --prism-lh-caption: 1.55; --prism-type-body-sm: 0.875rem; --prism-lh-body: 1.7;
--prism-type-body: 1rem; --prism-type-body-lg: 1.125rem; --prism-type-title-sm: 1.25rem; --prism-lh-title: 1.5;
--prism-type-title: 1.625rem; --prism-type-title-lg: 2rem;
--prism-radius-sm: 4px; --prism-radius-md: 8px; --prism-radius-lg: 12px; --prism-radius-pill: 9999px;
--prism-stroke-hairline: 1px; --prism-stroke-regular: 2px; --prism-stroke-bold: 3px;
--prism-z-raised: 100; --prism-z-sticky: 200; --prism-z-scrim: 400; --prism-z-modal: 500;
--prism-scrim: rgba(0, 0, 0, 0.32);
--prism-motion-fast: 100ms; --prism-ease-standard: cubic-bezier(0.2, 0, 0, 1);
--prism-focus-ring: 3px; --prism-focus-offset: 2px; --prism-measure-max: 38em;
--prism-font: Pretendard, -apple-system, BlinkMacSystemFont, "Noto Sans KR", "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;
--prism-comp-line: var(--prism-border-default); --prism-comp-line-strong: var(--prism-border-strong);
--prism-comp-state-good-fg: var(--prism-status-do); --prism-comp-state-good-bg: var(--prism-status-do-bg);
--prism-comp-state-bad-fg: var(--prism-status-dont); --prism-comp-state-bad-bg: var(--prism-status-dont-bg);
--prism-comp-state-warn-fg: var(--prism-status-caution); --prism-comp-state-warn-bg: var(--prism-status-caution-bg);
--prism-comp-state-info-fg: var(--prism-status-tip); --prism-comp-state-info-bg: var(--prism-status-tip-bg);
--prism-comp-rail-width: 15rem; --prism-comp-header-height: 3.5rem; --prism-comp-hit-min: 2.75rem;
--prism-comp-table-head-bg: var(--prism-bg-canvas);
--prism-comp-on-emphasis: var(--prism-bg-card);
}
:root[data-theme="dark"] {
--prism-bg-canvas: #111111; --prism-bg-card: #111111; --prism-text-body: #F4F3F0; --prism-text-heading: #F4F3F0;
--prism-text-subtle: #D3D3D3; --prism-border-default: #D3D3D3; --prism-border-strong: #F4F3F0;
--prism-status-do: #DCEAE6; --prism-status-do-bg: #005A4E; --prism-status-dont: #F6DFDA; --prism-status-dont-bg: #8F2A11;
--prism-status-caution: #F5E7CC; --prism-status-caution-bg: #6B4A00; --prism-status-tip: #DDE8F3; --prism-status-tip-bg: #0A4E86;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--prism-bg-canvas); color: var(--prism-text-body); font-family: var(--prism-font);
font-size: var(--prism-type-body-sm); line-height: var(--prism-lh-body); font-variant-numeric: tabular-nums; }
a { color: var(--prism-text-heading); }
h1, h2, h3, p, dl, dd, ol, ul { margin: 0; padding: 0; }
:focus-visible { outline: var(--prism-focus-ring) solid var(--prism-comp-line-strong); outline-offset: var(--prism-focus-offset); }
.skip { position: absolute; left: -999rem; }
/* shell */
.app { min-height: 100%; display: flex; flex-direction: column; position: relative; }
.top { height: var(--prism-comp-header-height); background: var(--prism-bg-card); border-bottom: var(--prism-stroke-hairline) solid var(--prism-comp-line);
display: flex; align-items: center; gap: var(--prism-space-300); padding-inline: var(--prism-space-300); position: sticky; top: 0; z-index: var(--prism-z-sticky); }
.brand { display: flex; align-items: center; gap: var(--prism-space-100); font-weight: 700; font-size: var(--prism-type-body-lg); color: var(--prism-text-heading); letter-spacing: -0.01em; }
.brand i { width: 0.7rem; height: 0.7rem; background: var(--prism-text-accent); transform: rotate(45deg); display: block; }
.top .where { font-size: var(--prism-type-caption); font-weight: 500; }
.top .meta { margin-left: auto; display: flex; gap: var(--prism-space-300); font-size: var(--prism-type-caption); }
.body { display: flex; flex: 1; min-height: 0; }
.rail { width: var(--prism-comp-rail-width); flex: none; background: var(--prism-bg-card); border-right: var(--prism-stroke-hairline) solid var(--prism-comp-line);
padding: var(--prism-space-300) var(--prism-space-200); display: flex; flex-direction: column; gap: var(--prism-space-200); }
.rail .cap { font-size: var(--prism-type-caption); font-weight: 500; letter-spacing: 0.06em; }
.rail .pier { font-weight: 700; font-size: var(--prism-type-body); color: var(--prism-text-heading); }
.rail ol { list-style: none; display: flex; flex-direction: column; gap: var(--prism-space-050); }
.step { display: flex; align-items: center; gap: var(--prism-space-150); min-height: var(--prism-comp-hit-min); padding: 0 var(--prism-space-100);
border-radius: var(--prism-radius-sm); color: var(--prism-text-body); text-decoration: none; }
.step .n { width: 1.5rem; height: 1.5rem; flex: none; border: var(--prism-stroke-regular) dashed var(--prism-comp-line); border-radius: var(--prism-radius-pill);
display: grid; place-items: center; font-size: var(--prism-type-caption); font-weight: 700; }
.step.done .n { background: var(--prism-text-heading); color: var(--prism-comp-on-emphasis); border: var(--prism-stroke-regular) solid var(--prism-text-heading); }
.step.cur { background: var(--prism-bg-canvas); font-weight: 700; }
.step.cur .n { border: var(--prism-stroke-bold) solid var(--prism-comp-line-strong); }
.rail .sub { display: flex; flex-direction: column; margin-left: 2.1rem; }
.rail .sub a { min-height: 2rem; display: flex; align-items: center; font-size: var(--prism-type-caption); color: var(--prism-text-body); }
.rail hr { border: 0; border-top: var(--prism-stroke-hairline) solid var(--prism-comp-line); width: 100%; margin: 0; }
.main { flex: 1; min-width: 0; padding: var(--prism-space-400); display: flex; flex-direction: column; gap: var(--prism-space-300); }
.main > .head { display: flex; flex-direction: column; gap: var(--prism-space-100); }
h1 { font-size: var(--prism-type-title); line-height: var(--prism-lh-title); color: var(--prism-text-heading); font-weight: 700; }
h2 { font-size: var(--prism-type-title-sm); line-height: var(--prism-lh-title); font-weight: 500; color: var(--prism-text-heading); }
h3 { font-size: var(--prism-type-body-lg); font-weight: 700; color: var(--prism-text-heading); }
.eyebrow { font-size: var(--prism-type-caption); font-weight: 500; letter-spacing: 0.06em; }
.lead { max-width: var(--prism-measure-max); font-size: var(--prism-type-body); }
.cap { font-size: var(--prism-type-caption); }
.muted { color: var(--prism-text-subtle); }
.row { display: flex; gap: var(--prism-space-200); align-items: center; flex-wrap: wrap; }
.row.end { justify-content: flex-end; }
.stack { display: flex; flex-direction: column; gap: var(--prism-space-200); }
.grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--prism-space-300); }
.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--prism-space-300); }
.split { display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); gap: var(--prism-space-300); align-items: start; }
.grow { flex: 1; }
/* scope, card, strip, banner */
.scope { background: var(--prism-bg-card); border: var(--prism-stroke-hairline) solid var(--prism-comp-line); padding: var(--prism-space-150) var(--prism-space-200);
display: flex; gap: var(--prism-space-400); font-size: var(--prism-type-body-sm); border-radius: var(--prism-radius-sm); }
.scope b { font-weight: 700; }
.card { background: var(--prism-bg-card); border: var(--prism-stroke-hairline) solid var(--prism-comp-line); border-radius: var(--prism-radius-md); padding: var(--prism-space-200);
display: flex; flex-direction: column; gap: var(--prism-space-150); }
.card.sunken { background: var(--prism-bg-canvas); }
.strip { background: var(--prism-bg-card); border: var(--prism-stroke-regular) solid var(--prism-comp-line-strong); border-radius: var(--prism-radius-md);
padding: var(--prism-space-200) var(--prism-space-300); display: flex; flex-direction: column; gap: var(--prism-space-150); }
.strip .concl { font-size: var(--prism-type-body-lg); font-weight: 700; color: var(--prism-text-heading); max-width: none; }
.counts { display: flex; gap: var(--prism-space-200); flex-wrap: wrap; align-items: stretch; }
.cnt { display: flex; align-items: baseline; gap: var(--prism-space-100); min-height: var(--prism-comp-hit-min); padding: 0 var(--prism-space-200);
background: var(--prism-bg-card); border: var(--prism-stroke-regular) solid var(--prism-comp-line); border-radius: var(--prism-radius-sm); font: inherit; color: inherit; cursor: pointer; }
.cnt b { font-size: var(--prism-type-title); font-weight: 700; line-height: 1.2; }
.cnt.sel { background: var(--prism-text-heading); color: var(--prism-comp-on-emphasis); border-color: var(--prism-text-heading); }
.banner { border: var(--prism-stroke-regular) solid var(--prism-comp-state-warn-fg); background: var(--prism-comp-state-warn-bg); color: var(--prism-comp-state-warn-fg);
border-radius: var(--prism-radius-sm); padding: var(--prism-space-150) var(--prism-space-200); display: flex; gap: var(--prism-space-150); align-items: flex-start; }
.banner b { font-weight: 700; }
.banner.tip { border-color: var(--prism-comp-state-info-fg); background: var(--prism-comp-state-info-bg); color: var(--prism-comp-state-info-fg); }
.banner.dont { border-color: var(--prism-comp-state-bad-fg); background: var(--prism-comp-state-bad-bg); color: var(--prism-comp-state-bad-fg); }
.banner.good { border-color: var(--prism-comp-state-good-fg); background: var(--prism-comp-state-good-bg); color: var(--prism-comp-state-good-fg); }
.banner .m { flex: none; }
/* badge */
.b { display: inline-flex; align-items: center; gap: var(--prism-space-050); padding: var(--prism-space-025) var(--prism-space-100); white-space: nowrap;
font-size: var(--prism-type-caption); font-weight: 500; line-height: 1.5; border: var(--prism-stroke-regular) solid var(--prism-comp-line);
border-radius: var(--prism-radius-sm); color: var(--prism-text-body); background: transparent; }
.b.good { color: var(--prism-comp-state-good-fg); background: var(--prism-comp-state-good-bg); border-color: var(--prism-comp-state-good-fg); }
.b.warn { color: var(--prism-comp-state-warn-fg); background: var(--prism-comp-state-warn-bg); border-color: var(--prism-comp-state-warn-fg); }
.b.info { color: var(--prism-comp-state-info-fg); background: var(--prism-comp-state-info-bg); border-color: var(--prism-comp-state-info-fg); }
.b.bad { color: var(--prism-comp-state-bad-fg); background: var(--prism-comp-state-bad-bg); border-color: var(--prism-comp-state-bad-fg); border-width: var(--prism-stroke-bold); }
.b.dash { border-style: dashed; }
.b.fixed { color: var(--prism-comp-on-emphasis); background: var(--prism-text-heading); border-color: var(--prism-text-heading); }
.b .m { font-size: 0.7em; }
/* buttons, fields */
.btn { min-height: var(--prism-comp-hit-min); padding: 0 var(--prism-space-200); border-radius: var(--prism-radius-sm); font: inherit; font-weight: 500;
cursor: pointer; border: var(--prism-stroke-regular) solid var(--prism-text-heading); background: var(--prism-bg-card); color: var(--prism-text-heading);
display: inline-flex; align-items: center; justify-content: center; gap: var(--prism-space-100); text-decoration: none; white-space: nowrap; }
.btn.primary { background: var(--prism-text-heading); color: var(--prism-comp-on-emphasis); }
.btn.quiet { border-color: transparent; background: transparent; text-decoration: underline; }
.btn.danger { border-color: var(--prism-comp-state-bad-fg); color: var(--prism-comp-state-bad-fg); }
.btn.sm { min-height: var(--prism-comp-hit-min); padding: 0 var(--prism-space-150); font-size: var(--prism-type-caption); }
.field { display: flex; flex-direction: column; gap: var(--prism-space-050); }
.field label, .lbl { font-weight: 700; font-size: var(--prism-type-body-sm); }
.req { font-weight: 500; font-size: var(--prism-type-caption); margin-left: var(--prism-space-100); }
.field input, .field select, .field textarea { min-height: var(--prism-comp-hit-min); padding: var(--prism-space-100) var(--prism-space-150); font: inherit;
color: var(--prism-text-body); background: var(--prism-bg-card); border: var(--prism-stroke-regular) solid var(--prism-comp-line); border-radius: var(--prism-radius-sm); }
.field textarea { font-size: var(--prism-type-body-sm); line-height: 1.6; resize: none; }
.field .help { font-size: var(--prism-type-caption); }
.field.bad input, .field.bad textarea { border-color: var(--prism-comp-state-bad-fg); border-width: var(--prism-stroke-bold); }
.err { color: var(--prism-comp-state-bad-fg); font-size: var(--prism-type-caption); font-weight: 500; display: flex; gap: var(--prism-space-050); }
.choice { display: flex; gap: var(--prism-space-300); align-items: center; }
.choice label { display: inline-flex; align-items: center; gap: var(--prism-space-100); min-height: var(--prism-comp-hit-min); font-weight: 500; }
.choice input { width: 1.25rem; height: 1.25rem; accent-color: var(--prism-text-heading); }
/* table */
.tw { overflow-x: auto; background: var(--prism-bg-card); border: var(--prism-stroke-hairline) solid var(--prism-comp-line); border-radius: var(--prism-radius-sm); }
table.t { width: 100%; border-collapse: collapse; font-size: var(--prism-type-body-sm); }
table.t caption { text-align: left; font-weight: 700; padding: var(--prism-space-100) var(--prism-space-150); color: var(--prism-text-heading); }
table.t th, table.t td { padding: var(--prism-space-100) var(--prism-space-150); border-bottom: var(--prism-stroke-hairline) solid var(--prism-comp-line); text-align: left; vertical-align: middle; }
table.t thead th { position: sticky; top: 0; background: var(--prism-comp-table-head-bg); font-weight: 700; white-space: nowrap; border-bottom: var(--prism-stroke-regular) solid var(--prism-comp-line); }
table.t td.num, table.t th.num { text-align: right; }
table.t tr.sel td { background: var(--prism-comp-table-head-bg); }
table.t tr.sep td { background: var(--prism-comp-table-head-bg); font-weight: 700; color: var(--prism-text-heading); }
table.t td a { font-weight: 700; }
.chip { display: inline-block; border: var(--prism-stroke-hairline) solid var(--prism-comp-line); border-radius: var(--prism-radius-sm);
padding: 0 var(--prism-space-100); font-size: var(--prism-type-caption); white-space: nowrap; background: var(--prism-bg-card); }
/* key-value, evidence, diff */
dl.kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--prism-space-100) var(--prism-space-300); font-size: var(--prism-type-body-sm); }
dl.kv dt { font-weight: 700; }
.pbar { height: 0.75rem; border: var(--prism-stroke-regular) solid var(--prism-comp-line); border-radius: var(--prism-radius-pill); overflow: hidden; background: var(--prism-bg-card); }
.pbar > i { display: block; height: 100%; background: var(--prism-text-heading); }
.list { list-style: none; display: flex; flex-direction: column; }
.list li { display: flex; align-items: center; gap: var(--prism-space-100); min-height: var(--prism-comp-hit-min); padding: 0 var(--prism-space-150); border-bottom: var(--prism-stroke-hairline) solid var(--prism-comp-line); }
.list li a { flex: 1; font-weight: 500; text-decoration: none; }
.list li.cur { background: var(--prism-bg-canvas); }
.list li.cur a { font-weight: 700; }
/* modal */
.scrim { position: absolute; inset: 0; background: var(--prism-scrim); z-index: var(--prism-z-scrim); display: flex; align-items: center; justify-content: center; padding: var(--prism-space-400); }
.modal { background: var(--prism-bg-card); border: var(--prism-stroke-regular) solid var(--prism-comp-line-strong); border-radius: var(--prism-radius-lg);
width: 100%; max-width: 62rem; padding: var(--prism-space-300); display: flex; flex-direction: column; gap: var(--prism-space-200); z-index: var(--prism-z-modal); }
.modal header { display: flex; align-items: center; gap: var(--prism-space-200); }
.modal header h2 { flex: 1; }
.scroll { max-height: 18rem; overflow-y: auto; border: var(--prism-stroke-hairline) solid var(--prism-comp-line); border-radius: var(--prism-radius-sm); }
.drawing { background: var(--prism-bg-card); border: var(--prism-stroke-regular) solid var(--prism-comp-line); border-radius: var(--prism-radius-sm); display: block; width: 100%; height: auto; }
.toc a { display: flex; align-items: center; min-height: var(--prism-comp-hit-min); padding: 0 var(--prism-space-150); text-decoration: none; font-weight: 500; border-radius: var(--prism-radius-sm); }
.toc a.cur { background: var(--prism-bg-canvas); font-weight: 700; }
.toc ol { list-style: none; }
.num { font-variant-numeric: tabular-nums; }
pre.code { margin: 0; background: var(--prism-bg-canvas); border: var(--prism-stroke-hairline) solid var(--prism-comp-line); border-radius: var(--prism-radius-sm);
padding: var(--prism-space-150); font-family: var(--prism-font); font-size: var(--prism-type-caption); line-height: 1.6; white-space: pre-wrap; }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }