import {bearingDimensions} from './bearing-dimensions'; import {straightCopingFormulas} from './coping-formulas'; import type {PierInput} from '../src/core/types'; import {DIM,type PierModel} from '../src/core/geometry'; import {solplateGeometry} from '../src/core/solplate'; import {variables,quantityRows,template,templateHash} from './template-data'; import {FormulaBook,cellKey,type Overrides} from './formula'; import {DETAIL,INPUTS,extraNumbers,specifications} from './schema'; export {DETAIL,INPUTS,extraNumbers,specifications} from './schema'; export type QuantityInputs={values:Record;reviewed:boolean;lastSignature?:string}; declare module '../src/core/types'{interface PierInput{quantityTakeoff?:QuantityInputs}} export type Mapped={key:string;name:string;symbol:string;unit:string;value:number|null;source:string;automatic:boolean;notApplicable?:boolean}; export function withTemplateDefaults(stored:QuantityInputs):QuantityInputs{ const values={...stored.values}; for(const v of extraNumbers.filter(x=>x.key!=='rebar'))if(values[v.key]==null||values[v.key]==='')values[v.key]=Number(template[DETAIL][v.cell].cached); for(const v of specifications)if(values[v.key]==null||values[v.key]==='')values[v.key]=String(template[DETAIL][v.cell].value); return {...stored,values}; } export function automaticInputs(input:PierInput,m:PierModel):Mapped[]{ const uniform=(v:(number|undefined)[])=>v.length&&v.every(x=>x!==undefined&&Number.isFinite(x)&&Math.abs(x!-v[0]!)<1e-7)?v[0]!:null; const footing=m.footings[0],nopiles=m.piles.length===0,bearing=bearingDimensions(m); const slope=m.copingL>0&&m.segments.length?((m.segments[m.segments.length-1].top-m.segments[0].top)/m.copingL)*100:null; const auto:Record={18:[m.copingRefTop,'model.copingRefTop (원본 Zb 의미 검토)'],19:[input.groundEl,'input.groundEl'],20:[input.groundWaterOn===true&&Number.isFinite(input.groundWaterEl)?input.groundWaterEl!:null,'input.groundWaterEl / 지하수 미지정은 직접 확인'],24:[m.copingL,'model.copingL'],25:[m.copingB,'model.copingB'],26:[m.h2,'model.h2 · 하부 테이퍼 높이'],27:[m.h1,'model.h1 · 상부 높이'],28:[slope,'모델 양단 상면 높이차/Lc ×100 · 단차 포함 등가경사'],29:[0,'현재 코핑 모델은 수직 끝단 · 별도 돌출부 없음'],30:[null,'직선 모델: 곡률 없음 · 원본 곡률항의 직선 극한 사용'],31:[uniform(m.columns.map(c=>c.diameter)),'model.columns[].diameter'],32:[footing?.bottom??null,'model.footings[0].bottom'],33:[footing?.length??null,'model.footings[0].length'],34:[footing?.width??null,'model.footings[0].width'],35:[footing?footing.top-footing.bottom:null,'기초 상면-하면'],36:[nopiles?0:uniform(m.piles.map(p=>p.diameter)),'model.piles[].diameter'],37:[nopiles?0:input.pileThicknessMm!=null?input.pileThicknessMm/1000:null,'입력 말뚝두께 mm→m'],38:[nopiles?0:uniform(m.piles.map(p=>p.top-p.bottom)),'말뚝 상면-하면'],39:[m.piles.length,'model.piles.length'],40:[bearing.length,'3D 받침 본체: plan.L/1000−0.2 m'],41:[bearing.width,'3D 받침 본체: plan.W/1000−0.2 m'],42:[m.blocks.length,'model.blocks.length'],43:[m.integratedBlocks.length?null:DIM.blockSize,'모델 분리블럭 크기'],44:[m.integratedBlocks.length?null:DIM.blockSize,'모델 분리블럭 크기'],45:[m.blocks.length?m.blocks.reduce((s,b)=>s+b.top-b.bottom,0)/m.blocks.length:null,'동일 평면 블럭 높이 합/Nb (원식의 등가 높이)'],46:[uniform(m.blocks.map(b=>b.plan?.T!==undefined?b.plan.T/1000:undefined))??null,'실제 솔플레이트 plan.T mm→m · 받침별 두께가 같을 때만 자동 적용']}; return variables.map(v=>{const a=auto[v.row];return {...v,value:a?.[0]??null,source:a?.[1]??`${INPUTS}!N${v.row} · 모델에 없는 조건`,automatic:a?.[0]!=null||v.row===30,notApplicable:v.row===30};}); } export function geometrySignature(input:PierInput,m:PierModel):string{const {quantityTakeoff,...raw}=input;return JSON.stringify({raw,auto:automaticInputs(input,m),version:templateHash});} export function supportIssues(input:PierInput,m:PierModel):string[]{const issues:string[]=[];if(m.columns.length!==1)issues.push('원본은 기둥 1개 산식입니다. 다기둥 수량 규칙이 없어 자동 완료할 수 없습니다.');if(m.footings.length!==1)issues.push('원본은 기초 1개 산식입니다. 기초 수/형식을 검토해야 합니다.');if(m.stretches.length!==1)issues.push('꺾인 교각의 원본 수량 규칙이 없습니다.');if(m.integratedBlocks.length)issues.push('일체 교좌면을 개별 블럭 Nb배로 계산할 수 없습니다.');if(input.pileKind==='PHC')issues.push('원본 복합말뚝 산식은 PHC 시공수량에 적용하지 않습니다.');const p=m.piles;if(p.length&&p.some(x=>Math.abs(x.diameter-p[0].diameter)>1e-7||Math.abs(x.top-x.bottom-p[0].top+p[0].bottom)>1e-7))issues.push('원본은 말뚝 직경·길이 단일값을 전제로 합니다.');return issues;} export function overridesFor(mapped:Mapped[],stored:QuantityInputs):Overrides{ stored=withTemplateDefaults(stored); const result:Overrides={};for(const v of mapped){const raw=v.notApplicable?0:v.automatic?v.value:stored.values[v.key];result[cellKey(INPUTS,'N'+v.key.slice(1))]=typeof raw==='number'&&Number.isFinite(raw)?raw:null;} for(const v of extraNumbers){const raw=stored.values[v.key];result[cellKey(DETAIL,v.cell)]=typeof raw==='number'&&Number.isFinite(raw)?raw:null;} return result; } export function calculate(input:PierInput,m:PierModel,stored:QuantityInputs,rebarTon?:number|null){ stored=withTemplateDefaults(stored); const mapped=automaticInputs(input,m),overrides=overridesFor(mapped,stored),copingMode='straight' as const,formulas=straightCopingFormulas,book=new FormulaBook(overrides,template,formulas); if(rebarTon!=null&&Number.isFinite(rebarTon))overrides[cellKey(DETAIL,'W67')]=rebarTon; const solplateMode='model' as const; const solplateTon=m.blocks.reduce((sum,b)=>sum+solplateGeometry(b,b.plan).volume*7.85,0); overrides[cellKey(DETAIL,'W64')]=solplateTon; overrides[cellKey(DETAIL,'K64')]='3D 모델 판별 체적 합계 × 7.85 (앱 재생성 값)'; const results=quantityRows.map(row=>{const dependencies=book.references(DETAIL,row.cell),missing=dependencies.filter(k=>Object.hasOwn(overrides,k)&&overrides[k]===null);let value:number|null=null,error=''; try{if(!missing.length)value=Number(book.cell(DETAIL,row.cell));}catch(e){error=(e as Error).message;} return {...row,value,error,missing,dependencies,description:row.cell==='W64'?'3D 모델 판별 체적 합계 × 7.85':row.description,formula:row.cell==='W64'?'3D 모델 솔플레이트 체적 합계 × 7.85 t/m³':formulas[cellKey(DETAIL,row.cell)]??template[DETAIL][row.cell]?.value,sourceFormula:template[DETAIL][row.cell]?.value};}); const needed=new Set(results.flatMap(r=>r.dependencies)); const missing=mapped.filter(v=>!v.automatic&&needed.has(cellKey(INPUTS,'N'+v.key.slice(1)))&&overrides[cellKey(INPUTS,'N'+v.key.slice(1))]===null).map(v=>v.name); for(const v of extraNumbers)if(overrides[cellKey(DETAIL,v.cell)]===null)missing.push(v.name); for(const v of specifications)if(typeof stored.values[v.key]!=='string'||!String(stored.values[v.key]).trim())missing.push(v.name); const issues=supportIssues(input,m);const totalDepth=[21,22,23].map(r=>overrides[cellKey(INPUTS,'N'+r)]);if(totalDepth.every(v=>typeof v==='number')&&totalDepth.reduce((a,b)=>Number(a)+Number(b),0)!>8)issues.push('원본 토공 심도구간은 8m까지입니다. 초과 수량을 완료 처리하지 않습니다.'); for(const [key,v]of Object.entries(overrides))if(typeof v==='number'&&(/!N(21|22|23|24|25|26|27|29|30|31|33|34|35|36|37|38|39|40|41|42|43|44|45|46)$/.test(key)||key.startsWith(DETAIL+'!'))&&v<0)issues.push(`${key}: 음수 입력을 확인하세요.`); const warnings=['원본 W51의 실제 기둥 거푸집식은 설명 Hcol과 다릅니다(Zb-H1-H2-Zs+Zf). 원식을 유지합니다.','원본 W50 코핑 거푸집식의 계수와 W63 몰탈 0.05m는 표시 설명/규격과 다릅니다.','현재 코핑은 직선/단차 형상: a=0, R 미적용이며 W50/W58의 원본 원호항에 직선 극한(R→∞)을 적용합니다. s는 단차를 포함한 양단 등가경사입니다.','원본 고정 기준: 터파기 여유 각 1m, 버림 두께 0.1m·치수여유 0.2m, 몰탈 두께 0.05m, 솔플레이트 여유 0.1m, 강재비중 7.85.']; warnings.push('W64 솔플레이트는 생성 모델의 판별 체적 합계×7.85t/m³를 사용합니다. 원본 Excel 식은 비교 근거로 보존하며 출력 W64는 모델 계산값입니다.'); return {mapped,overrides,book,results,copingMode,solplateMode,formulas,missing,issues,warnings,complete:!missing.length&&!issues.length&&!results.some(r=>r.value===null||r.error)&&stored.reviewed,signature:geometrySignature(input,m)+'|rebar:'+String(rebarTon??'manual')+'|solplate-model:'+solplateTon} ; } export function aggregate(results:ReturnType[]){return quantityRows.map((r,i)=>({...r,value:results.every(x=>x.complete)?results.reduce((s,x)=>s+x.results[i].value!,0):null}));}