Preserve local abutment, cast-in-place rebar and compact header features alongside remote drawing, ground-link and quantity workspaces. Adapt drawing and takeoff consumers to generated solplate plans, retain current P1 regression coverage, and resolve bearing-detail annotation collisions. Validation: 122 test files / 975 tests passed; production build passed; 10 browser integration checks passed.
62 lines
7.2 KiB
TypeScript
62 lines
7.2 KiB
TypeScript
import {it,expect} from 'vitest';
|
|
import {mkdirSync,readFileSync,writeFileSync} from 'node:fs';
|
|
import {resolve} from 'node:path';
|
|
import {buildPier} from '../src/core/geometry';
|
|
import {parseProject,serializeProject} from '../src/core/project';
|
|
import {calculate,withTemplateDefaults,automaticInputs,geometrySignature,extraNumbers,specifications,DETAIL,INPUTS,type QuantityInputs} from './model';
|
|
import {exportEdits} from './export';
|
|
import {template,templateHash,quantityRows} from './template-data';
|
|
import {FormulaBook,cellKey} from './formula';
|
|
const sample=JSON.parse(readFileSync(resolve(import.meta.dirname,'../랜딩페이지/assets/samples/sample02.pzr.json'),'utf8'));
|
|
const input=sample.piers[0].input,model=buildPier(input);
|
|
mkdirSync(resolve(import.meta.dirname,'.cache'),{recursive:true});
|
|
const manual=():QuantityInputs=>{const values:QuantityInputs['values']={};for(let r=18;r<=46;r++)values['r'+r]=Number(template[INPUTS]['N'+r].cached);for(const v of extraNumbers)values[v.key]=Number(template[DETAIL][v.cell].cached);for(const v of specifications)values[v.key]=String(template[DETAIL][v.cell].value);return {values,reviewed:true};};
|
|
it('선택 교각의 자동값이 원본 예시값이나 직접입력으로 덮어써지지 않는다',()=>{
|
|
const s=manual(),r=calculate(input,model,s);expect(r.overrides[cellKey(INPUTS,'N24')]).toBe(model.copingL);expect(r.overrides[cellKey(INPUTS,'N39')]).toBe(model.piles.length);expect(r.overrides[cellKey(INPUTS,'N33')]).toBe(model.footings[0].length);expect(r.results).toHaveLength(quantityRows.length);
|
|
const lines=['# 변수 자동/수동 매핑 (운곡교 P1 현재 모델 기준)','','|변수|기호|단위|원본 입력셀|자동 추출 가능 여부|출처|','|---|---|---|---|---|---|'];
|
|
for(const v of r.mapped)lines.push(`|${v.name}|${v.symbol}|${v.unit}|3.2 관련변수!N${v.key.slice(1)}|${v.automatic?'자동':'직접 입력/검토'}|${v.source}|`);
|
|
lines.push('','## 수량 항목별 필요한 변수와 추적 위치','','|수량 항목|필요 입력변수|계산식 참조 위치|자동/수동|','|---|---|---|---|');
|
|
for(const row of r.results){const needed=r.mapped.filter(v=>row.dependencies.includes(cellKey(INPUTS,'N'+v.key.slice(1))));lines.push(`|${row.name}|${needed.map(v=>v.name+'('+v.symbol+')').join(', ')}|${DETAIL}!${row.cell} · 원식은 ANALYSIS.md|자동 ${needed.filter(v=>v.automatic).length} / 직접 ${needed.filter(v=>!v.automatic).length}|`);}
|
|
writeFileSync(resolve(import.meta.dirname,'docs/MAPPING.md'),lines.join('\n'));
|
|
|
|
const p=JSON.stringify(input);calculate(input,model,s);expect(JSON.stringify(input)).toBe(p);
|
|
});
|
|
it('입력누락은 0으로 계산되지 않고 의존 수량만 미산정이 된다',()=>{const r=calculate(input,model,{values:{},reviewed:false});expect(r.complete).toBe(false);expect(r.results.find(r=>r.cell==='W67')!.value).toBeNull();expect(r.results.find(r=>r.cell==='W57')!.value).not.toBeNull();});
|
|
it('형상 갱신·저장복원은 직접입력과 독립적이다',()=>{
|
|
const stored=manual(),i={...input,quantityTakeoff:stored};sample.piers[0].input=i;const serialized=serializeProject(sample);expect(parseProject(serialized).piers[0].input.quantityTakeoff).toEqual(stored);
|
|
const changed={...i,copingLengthMm:i.copingLengthMm+500};expect(geometrySignature(changed,buildPier(changed))).not.toBe(geometrySignature(i,buildPier(i)));expect(changed.quantityTakeoff.values).toEqual(stored.values);
|
|
});
|
|
it('다기둥은 원본 산식 적용범위 밖으로 분류된다',()=>{const i={...input,type:'2주형' as const};expect(calculate(i,buildPier(i),manual()).issues.some(s=>s.includes('기둥 1개'))).toBe(true);});
|
|
it('원본 P1의 출력 캐시는 수식과 일치하고 미사용 교각 수량을 합산하지 않는다',()=>{
|
|
const overrides:Record<string,number>={};for(let r=18;r<=46;r++)overrides[cellKey(INPUTS,'N'+r)]=Number(template[INPUTS]['N'+r].cached);for(const v of extraNumbers)overrides[cellKey(DETAIL,v.cell)]=Number(template[DETAIL][v.cell].cached);
|
|
const payload=exportEdits({project:{name:'원본 산식 검증',bridge:'운곡교'},pier:'P1',overrides,specs:manual().values,reviewed:true,templateHash});
|
|
for(const r of quantityRows)if(typeof template[DETAIL][r.cell].value==='string'&&String(template[DETAIL][r.cell].value).startsWith('='))expect(payload.edits[DETAIL][r.cell].cached).toBeCloseTo(Number(template[DETAIL][r.cell].cached),8);
|
|
expect(payload.edits['3.3 수량산출_P2'].W57.value).toBeNull();expect(payload.edits['1. 수량집계'].K57.cached).toBeCloseTo(Number(template[DETAIL].W57.cached),8);
|
|
writeFileSync(resolve(import.meta.dirname,'.cache/baseline-edits.json'),JSON.stringify(payload));
|
|
});
|
|
|
|
it('시험·시설·규격은 원본 Excel을 사용하고 모델 제원이 있으면 토층 4개만 남는다',()=>{
|
|
const i={...input,groundWaterOn:false,sole:{h3Mm:181,marginMm:100,tByRow:{전열:35,후열:35},slopePct:Object.fromEntries(input.bearings.map((b:{no:number})=>[String(b.no),0]))}};
|
|
const m=buildPier(i),empty={values:{},reviewed:false},r=calculate(i,m,empty,1.25);
|
|
expect(r.missing.sort()).toEqual(['지하수위 WL','토사 깊이','풍화암 깊이','연암 깊이'].sort());
|
|
expect(r.overrides[cellKey(DETAIL,'K68')]).toBe(2);expect(r.overrides[cellKey(DETAIL,'K69')]).toBe(2);expect(r.overrides[cellKey(DETAIL,'K74')]).toBe(1);
|
|
expect(withTemplateDefaults(empty).values.specFooting).toBe(template[DETAIL].E57.value);
|
|
expect(r.mapped.find(x=>x.key==='r46')!.value).toBe(.035);expect(r.mapped.find(x=>x.key==='r29')!.value).toBe(0);expect(r.mapped.find(x=>x.key==='r30')!.notApplicable).toBe(true);
|
|
const full={values:{r20:0,r21:2.8,r22:0,r23:0},reviewed:true},done=calculate(i,m,full,1.25);expect(done.complete).toBe(true);
|
|
const payload=exportEdits({project:sample.project,pier:i.name,overrides:done.overrides,specs:withTemplateDefaults(full).values,reviewed:true,copingMode:done.copingMode,templateHash});
|
|
expect(payload.edits[DETAIL].W50.formula).not.toContain('ASIN');expect(payload.edits[INPUTS].N30.value).toBeNull();
|
|
expect(payload.edits[DETAIL].W58.cached).toBeCloseTo(done.results.find(x=>x.cell==='W58')!.value!,8);
|
|
writeFileSync(resolve(import.meta.dirname,'.cache/straight-edits.json'),JSON.stringify(payload));
|
|
});
|
|
it('직선식은 원본 곡률식의 R→무한대 극한과 일치한다',()=>{
|
|
const o:Record<string,number>={};for(let r=18;r<=46;r++)o[cellKey(INPUTS,'N'+r)]=Number(template[INPUTS]['N'+r].cached);o[cellKey(INPUTS,'N30')]=1e6;
|
|
const curved=new FormulaBook(o),straight=new FormulaBook(o,template,calculate(input,model,manual()).formulas);
|
|
for(const c of ['W50','W58'])expect(Math.abs(Number(curved.cell(DETAIL,c))-Number(straight.cell(DETAIL,c)))).toBeLessThan(.001);
|
|
});
|
|
|
|
it('원본 기본값보다 사용자 지정값(0 포함)을 우선하고 없는 받침 치수는 추정하지 않는다',()=>{
|
|
const x=withTemplateDefaults({values:{eoid:0,specBody:'사용자 규격'},reviewed:false});expect(x.values.eoid).toBe(0);expect(x.values.specBody).toBe('사용자 규격');
|
|
const r=calculate(input,model,{values:{},reviewed:false},1.2);expect(r.mapped.find(v=>v.key==='r40')!.value).toBeCloseTo(.6);expect(r.mapped.find(v=>v.key==='r41')!.value).toBeCloseTo(.54);
|
|
expect(r.missing).not.toContain('E.O.I.D 시험 횟수');expect(r.missing).not.toContain('본체 콘크리트 규격');
|
|
});
|