39 lines
7.1 KiB
TypeScript
39 lines
7.1 KiB
TypeScript
import {extraNumbers,specifications} from './schema';
|
|
import {template} from './template-data';
|
|
import {cellKey} from './formula';
|
|
import {node,type TakeoffResult,type PierContext,type DashboardState} from './dashboard-types';
|
|
import type {QuantityInputs} from './model';
|
|
type Field={key:string;name:string;unit:string;value:string;kind:'auto'|'manual'|'template';source:string;required:boolean;numeric:boolean;missing:boolean};
|
|
export function inputFields(result:TakeoffResult,stored:QuantityInputs,pier:PierContext):Field[]{
|
|
const needed=new Set(result.results.flatMap(r=>r.dependencies));
|
|
const fields:Field[]=result.mapped.map(v=>{const raw=v.automatic?v.value:stored.values[v.key];return {key:v.key,name:v.name+' ('+v.symbol+')',unit:v.unit,value:v.notApplicable?'직선 · 해당 없음':raw==null?'':String(typeof raw==='number'?Number(raw.toFixed(6)):raw),kind:v.automatic?'auto':'manual',source:v.source,required:needed.has(cellKey('3.2 관련변수','N'+v.key.slice(1))),numeric:!v.automatic,missing:!v.automatic&&(raw==null||raw==='')};});
|
|
for(const v of extraNumbers){const auto=v.key==='rebar'&&pier.rebarTon!=null,raw=auto?pier.rebarTon:stored.values[v.key];fields.push({key:v.key,name:v.name,unit:v.unit,value:raw==null?'':String(raw),kind:auto?'auto':v.key==='rebar'?'manual':'template',source:auto?'기존 철근재료표 · 할증 전 / 길이 누락 없음':`원본 ${v.cell}${v.key==='rebar'?' 직접값':' 기본값'}`,numeric:!auto,required:true,missing:raw==null||raw===''});}
|
|
for(const v of specifications){const raw=stored.values[v.key];fields.push({key:v.key,name:v.name,unit:'규격',value:String(raw??''),kind:'template',source:`원본 Excel ${v.cell} · ${String(template['3.3 수량산출_P1'][v.cell].value)}`,numeric:false,required:true,missing:raw==null||String(raw).trim()===''});}
|
|
return fields;
|
|
}
|
|
export function createInputPanel(options:{result:TakeoffResult;stored:QuantityInputs;pier:PierContext;project:{name:string;bridge:string};state:DashboardState;onChange:(data:QuantityInputs)=>void}){
|
|
const {result,stored,pier,state}=options,fields=inputFields(result,stored,pier),required=fields.filter(f=>f.required),done=required.filter(f=>!f.missing).length;
|
|
const panel=node('section',undefined,'tq-panel tq-input-panel takeoff-inputs');panel.append(node('h3','입력 정보'));
|
|
const progress=node('div',undefined,'tq-progress');progress.append(node('span','필수 입력 진행률'),node('b',`${done} / ${required.length} (${Math.round(done/Math.max(1,required.length)*100)}%)`));const bar=node('progress');bar.max=required.length;bar.value=done;progress.append(bar);panel.append(progress);
|
|
const scroll=node('div',undefined,'tq-input-scroll');panel.append(scroll);scroll.onscroll=()=>state.inputScroll=scroll.scrollTop;
|
|
const groups=[{id:'basic',name:'기본정보 / 두부보·기둥',accept:(f:Field)=>['r18','r24','r25','r26','r27','r28','r29','r30','r31'].includes(f.key)},{id:'ground',name:'지반 / 기초 / 말뚝',accept:(f:Field)=>/^r(19|2[0-3]|3[2-9])$/.test(f.key)},{id:'material',name:'받침 / 재료',accept:(f:Field)=>/^r4/.test(f.key)||f.key==='rebar'||f.key.startsWith('spec')},{id:'conditions',name:'공제 / 시공조건',accept:(f:Field)=>['eoid','restrike','inspection'].includes(f.key)}];
|
|
const locations=new Map<string,{group:HTMLDetailsElement;input:HTMLElement}>();
|
|
const jump=(key:string)=>{const target=locations.get(key);if(!target)return;target.group.open=true;state.openInputs.add(target.group.dataset.group!);requestAnimationFrame(()=>{scroll.scrollTo({top:target.input.getBoundingClientRect().top-scroll.getBoundingClientRect().top+scroll.scrollTop-60,behavior:'smooth'});target.input.focus({preventScroll:true});});};
|
|
const missing=required.filter(f=>f.missing);
|
|
if(missing.length){const alert=node('details',undefined,'tq-missing-box');alert.append(node('summary',`⚠ 누락 입력 ${missing.length}개`),node('p','필수 항목을 입력하면 관련 수량이 산출됩니다.'));for(const f of missing){const b=node('button',f.name);b.type='button';b.onclick=()=>jump(f.key);alert.append(b);}scroll.append(alert);}
|
|
for(const g of groups){const subset=fields.filter(g.accept),req=subset.filter(f=>f.required),filled=req.filter(f=>!f.missing).length,details=node('details',undefined,'tq-input-group');details.dataset.group=g.id;details.open=state.openInputs.has(g.id);const summary=node('summary');summary.append(node('strong',g.name),node('span',`${filled}/${req.length}`,filled===req.length?'tq-count-done':'tq-count-missing'));details.append(summary);details.ontoggle=()=>{if(details.open)state.openInputs.add(g.id);else state.openInputs.delete(g.id);};
|
|
if(g.id==='basic'){const dl=node('dl',undefined,'tq-project-identity');for(const [key,v]of [['프로젝트',options.project.name],['교량',options.project.bridge],['교각 / 형식',pier.input.name+' · '+pier.input.type],['기초 형식',pier.input.foundationType]])dl.append(node('dt',key),node('dd',v));details.append(dl);}
|
|
for(const f of subset){const row=node('label',undefined,'takeoff-variable tq-field'+(f.required&&f.missing?' is-missing':'')),title=node('span',f.name+(f.unit?' · '+f.unit:''),'tq-field-name'),tag=node('span',f.kind==='auto'?'자동':f.kind==='template'?'원본 Excel':'직접입력','tq-input-tag '+f.kind);row.append(title,tag);row.title=f.source;
|
|
let control:HTMLElement;
|
|
if(f.kind==='auto'){const out=node('output',f.value);out.setAttribute('aria-label',f.name+' 자동 입력');out.tabIndex=0;row.append(out);control=out;}
|
|
else{const n=node('input');n.type=f.numeric?'number':'text';if(f.numeric)n.step='any';n.value=f.value;n.placeholder=f.required?'필수 입력':'참고 입력';n.dataset.variable=f.key;n.setAttribute('aria-label',f.name);n.setAttribute('aria-invalid',String(f.required&&f.missing));n.onchange=()=>options.onChange({...stored,values:{...stored.values,[f.key]:n.value===''?null:f.numeric?Number(n.value):n.value},lastSignature:undefined});row.append(n);control=n;}
|
|
if(f.required&&f.missing){const warning=node('span','!', 'tq-field-warning');warning.title='필수 입력 누락';row.append(warning);}row.append(node('small',f.source));details.append(row);locations.set(f.key,{group:details,input:control});
|
|
}
|
|
if(g.id==='conditions'){const fixed=node('details',undefined,'tq-fixed-conditions');fixed.append(node('summary','원본 고정 공제·여유 기준'));for(const note of ['터파기 여유: 각 1m','버림: 두께 0.1m / 치수여유 0.2m','몰탈: 계산 두께 0.05m','솔플레이트: 치수여유 0.1m','원본에 없는 공제조건은 추가하지 않음'])fixed.append(node('p',note));details.append(fixed);}
|
|
scroll.append(details);
|
|
}
|
|
const review=node('label',undefined,'takeoff-review tq-approval');const checkbox=node('input');checkbox.type='checkbox';checkbox.checked=stored.reviewed;checkbox.onchange=()=>options.onChange({...stored,reviewed:checkbox.checked,lastSignature:undefined});review.append(checkbox,document.createTextNode('원본 고정 기준·설명식 불일치 및 모델 적용 기준을 확인했습니다.'));scroll.append(review);
|
|
requestAnimationFrame(()=>scroll.scrollTop=state.inputScroll);
|
|
return {panel,fields,done,total:required.length,jump,focusMissing:()=>jump(missing[0]?.key??fields[0].key)};
|
|
}
|