공용 파일(main.ts·index.html·style.css)에서 서로 얽혀 있어 한 커밋으로 묶음. - 지반: ① 형상 프로젝트 지반모델 관리창(생성·JSON 불러오기·교각별 비교·선택 적용), 프로젝트 JSON 내장 groundModel 추출, 교대 8행 통합 적용·공통지형 중복격자 제외 - 재료표·수량: 상단 ⑤ 메뉴·배근 바로가기, 결과 교각/탭 기억, 수량 워크북·정합 보정 - 랜딩: 옥성교 6교각 히어로 이미지·투영 강조점·좌→우 광선·지형 라운드/도로 연결 - 검증 스크립트(verify-project-ground/ground-abutments/okseong-user-files/quantity-*/landing-hero 등) 정본: DOC/04_실행/실행_*_2026-11-09_v0.1.00.md Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
49 lines
6.2 KiB
TypeScript
49 lines
6.2 KiB
TypeScript
import type {PierInput} from '../src/core/types';
|
||
import type {PierModel} from '../src/core/geometry';
|
||
import {calculate,withTemplateDefaults,type QuantityInputs} from './model';
|
||
import {inputFields} from './input-panel';
|
||
import {node} from './dashboard-types';
|
||
export type QuantitySnapshot={project:{name:string;bridge:string};selected:string|null;piers:{input:PierInput;model:PierModel|null;dirty:boolean;rebarTon?:number|null;rebarIssues?:string[]}[]};
|
||
export function projectRecords(s:QuantitySnapshot){return s.piers.map(p=>{
|
||
const stored=withTemplateDefaults(p.input.quantityTakeoff??{values:{},reviewed:false});
|
||
const calculated=p.model?calculate(p.input,p.model,stored,p.rebarTon):null;
|
||
const result=calculated?{...calculated,issues:[...calculated.issues,...(p.rebarIssues??[]).map(m=>'철근재료표 일부 계산: '+m)],complete:calculated.complete&&!p.rebarIssues?.length}:null;
|
||
return {pier:p,stored,result};
|
||
});}
|
||
export function openProjectInputs(read:()=>QuantitySnapshot,save:(name:string,data:QuantityInputs)=>void,onClose:()=>void,onExport:()=>Promise<void>){
|
||
const dialog=node('dialog');dialog.className='tq-project-dialog';dialog.dataset.projectInputs='true';
|
||
const head=node('header'),title=node('h2','전체 교각 입력 · 수량산출'),close=node('button','닫기 ×');close.type='button';close.onclick=()=>dialog.close();head.append(title,close);dialog.append(head);
|
||
const status=node('p'),tools=node('div');tools.className='tq-project-tools';
|
||
const filter=node('label'),missingOnly=node('input');missingOnly.type='checkbox';missingOnly.checked=true;filter.append(missingOnly,document.createTextNode('미입력 항목만'));tools.append(filter);
|
||
const recalc=node('button','전체 재계산'),exporter=node('button','전체 교각 Excel 출력');recalc.type=exporter.type='button';tools.append(recalc,exporter);dialog.append(tools,status);
|
||
dialog.append(node('p','자동값은 모델에서 가져옵니다. 직접 입력값은 교각별 저장됩니다. 표에 Excel 셀 범위를 붙여넣을 수 있습니다. 받침 형상이 없거나 서로 다른 치수이면 직접 확인이 필요합니다.'));
|
||
const scroll=node('div');scroll.className='tq-project-table';dialog.append(scroll);
|
||
const draw=()=>{
|
||
const records=projectRecords(read());const rows=records.map(r=>({...r,fields:r.result?inputFields(r.result,r.stored,r.pier):[]}));
|
||
const columns=new Map<string,(typeof rows)[number]['fields'][number]>();
|
||
for(const r of rows)for(const f of r.fields)if(f.required&&(!missingOnly.checked||f.missing))columns.set(f.key,f);
|
||
// Show real bearing geometry even while reviewing only missing conditions.
|
||
for(const r of rows)for(const f of r.fields)if(['r40','r41'].includes(f.key))columns.set(f.key,f);
|
||
const keys=[...columns.keys()],table=node('table'),thead=node('thead'),tr=node('tr');tr.append(node('th','교각 / 상태'));
|
||
for(const f of columns.values()){const th=node('th',f.name+(f.unit?' · '+f.unit:''));th.title=f.source;tr.append(th);}tr.append(node('th','원본 기준 검토'));thead.append(tr);table.append(thead);
|
||
const body=node('tbody');table.append(body);let missing=0;
|
||
for(const [ri,r]of rows.entries()){
|
||
const row=node('tr');row.dataset.projectPier=r.pier.input.name;
|
||
const label=node('th',r.pier.input.name+' · '+r.pier.input.type);const issues=r.pier.dirty?['형상 재생성 필요']:!r.result?['형상 없음']:r.result.issues;
|
||
label.append(node('small',issues.length?issues.join(' / '):r.result?.missing.length?'누락 '+r.result.missing.length+'개':r.stored.reviewed?'계산 가능':'기준 검토 필요'));row.append(label);missing+=r.result?.missing.length??1;
|
||
for(const key of keys){const f=r.fields.find(f=>f.key===key),td=node('td');if(!f){td.textContent='—';row.append(td);continue;}
|
||
if(f.kind==='auto'){td.append(node('output',f.value),node('small','자동'));td.title=f.source;}
|
||
else{const control=node('input');control.type=f.numeric?'number':'text';if(f.numeric)control.step='any';control.value=f.value;control.dataset.batchKey=key;control.setAttribute('aria-label',r.pier.input.name+' '+f.name);control.classList.toggle('is-missing',f.missing);control.onchange=()=>{const live=projectRecords(read()).find(x=>x.pier.input.name===r.pier.input.name)!;save(r.pier.input.name,{...live.stored,values:{...live.stored.values,[key]:control.value===''?null:f.numeric?Number(control.value):control.value},lastSignature:undefined});control.classList.toggle('is-missing',control.value==='');status.textContent='입력값 저장됨 · 전체 재계산으로 누락 및 적용 범위를 확인하세요.';};
|
||
control.onpaste=event=>{const raw=event.clipboardData?.getData('text/plain');if(!raw||!/[\t\n]/.test(raw))return;event.preventDefault();for(const [dy,line]of raw.trimEnd().split(/\r?\n/).entries())for(const [dx,value]of line.split('\t').entries()){const target=body.rows[ri+dy]?.querySelector<HTMLInputElement>(`[data-batch-key="${keys[keys.indexOf(key)+dx]}"]`);if(target){target.value=value.trim();target.dispatchEvent(new Event('change'));}}};td.append(control,node('small',f.kind==='template'?'원본 Excel':'직접 입력'));
|
||
}row.append(td);
|
||
}
|
||
const td=node('td'),review=node('input');review.type='checkbox';review.checked=r.stored.reviewed;review.setAttribute('aria-label',r.pier.input.name+' 원본 기준 확인');review.onchange=()=>{const live=projectRecords(read()).find(x=>x.pier.input.name===r.pier.input.name)!;save(r.pier.input.name,{...live.stored,reviewed:review.checked,lastSignature:undefined});};td.append(review);row.append(td);body.append(row);
|
||
}
|
||
scroll.replaceChildren(table);status.textContent=`전체 ${rows.length}개 교각 · 미입력 ${missing}개 · 교각별 산출서 합산 방식`;
|
||
};
|
||
missingOnly.onchange=draw;
|
||
recalc.onclick=()=>{for(const r of projectRecords(read()))if(r.result)save(r.pier.input.name,{...r.stored,lastSignature:r.result.signature});draw();};
|
||
exporter.onclick=async()=>{exporter.disabled=true;try{await onExport();status.textContent='전체 교각 Excel 출력 완료';}catch(error){status.textContent=(error as Error).message;status.classList.add('tq-project-error');}finally{exporter.disabled=false;}};
|
||
dialog.addEventListener('close',()=>{dialog.remove();onClose();},{once:true});document.body.append(dialog);draw();dialog.showModal();
|
||
}
|