Files
Pier_v1/수량산출/model.ts
T
ypshimandClaude Haiku 5.5 fc520ec03d feat: 프로젝트 공통 지반모델·교대 지반 통합·재료표/수량 진입·옥성교 랜딩 히어로 (11-06~11-09 작업분)
공용 파일(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>
2026-11-09 18:01:02 +09:00

57 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string,number|string|null>;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<number,[number|null,string]>={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<typeof calculate>[]){return quantityRows.map((r,i)=>({...r,value:results.every(x=>x.complete)?results.reduce((s,x)=>s+x.results[i].value!,0):null}));}