공용 파일(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>
57 lines
8.9 KiB
TypeScript
57 lines
8.9 KiB
TypeScript
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}));}
|