공용 파일(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>
71 lines
4.7 KiB
TypeScript
71 lines
4.7 KiB
TypeScript
import {describe,it,expect} from 'vitest';
|
||
import {buildPier} from '../src/core/geometry';
|
||
import {solplateGeometry} from '../src/core/solplate';
|
||
import {loadP1} from '../tests/helpers';
|
||
import {calculate,withTemplateDefaults,DETAIL,type QuantityInputs} from './model';
|
||
import {exportEdits} from './export';
|
||
import {template,templateHash} from './template-data';
|
||
import {cellKey} from './formula';
|
||
import {exportRecords,finishQuantityExport} from './export-state';
|
||
import type {QuantitySnapshot} from './project-inputs';
|
||
|
||
const base=loadP1();
|
||
const input={...base,sole:{h3Mm:181,marginMm:100,tByRow:{전열:35,후열:35},slopePct:Object.fromEntries(base.bearings.map(b=>[String(b.no),0]))}};
|
||
const model=buildPier(input);
|
||
const stored:QuantityInputs={values:{r20:0,r21:2.8,r22:0,r23:0},reviewed:true};
|
||
const raw=()=>calculate(input,model,stored,1.25);
|
||
const snapshot=():QuantitySnapshot=>({project:{name:'프로젝트',bridge:'교량'},selected:input.name,piers:[{input:{...input,quantityTakeoff:stored},model,dirty:false,rebarTon:1.25}]});
|
||
|
||
describe('W64 모델 기준 수량·Excel',()=>{
|
||
it('800×740×35mm 10개는 1.62652t이며 Excel에도 같은 숫자를 전달한다',()=>{
|
||
const result=raw(),row=result.results.find(r=>r.cell==='W64')!;
|
||
expect(model.blocks).toHaveLength(10);
|
||
expect(row.value).toBeCloseTo(1.62652,10);
|
||
expect(row.sourceFormula).toBe(template[DETAIL].W64.value);
|
||
expect(row.formula).toContain('3D 모델');
|
||
const payload=exportEdits({project:{name:'',bridge:''},pier:input.name,overrides:result.overrides,specs:withTemplateDefaults(stored).values,reviewed:true,copingMode:result.copingMode,solplateMode:result.solplateMode,templateHash});
|
||
expect(payload.edits[DETAIL].W64.value).toBeCloseTo(row.value!,12);
|
||
expect(payload.edits[DETAIL].K64.value).toContain('3D 모델');
|
||
expect(template[DETAIL].W64.value).toBe('=(BG28+0.1)*(BG29+0.1)*BG34*BG30*7.85');
|
||
});
|
||
it('구형 요청은 모델 값을 원식의 캐시로 섞지 않고 기존 계산을 유지한다',()=>{
|
||
const result=raw();
|
||
const payload=exportEdits({project:{name:'',bridge:''},pier:input.name,overrides:result.overrides,specs:withTemplateDefaults(stored).values,reviewed:true,copingMode:result.copingMode,templateHash});
|
||
expect(payload.edits[DETAIL].W64.cached).toBeCloseTo(1.23088,10);
|
||
expect(payload.edits[DETAIL].W64.value).toBeUndefined();
|
||
});
|
||
it('받침별 크기·경사·두께가 달라도 개별 모델 체적으로 합산한다',()=>{
|
||
const next={...input,sole:{...input.sole,plates:{[String(base.bearings[0].no)]:{aMm:900,bMm:800}},slopePct:{[String(base.bearings[0].no)]:2.1}}};
|
||
const m=buildPier(next),r=calculate(next,m,{...stored,values:{...stored.values,r40:999,r41:999,r46:999}},1.25);
|
||
const actual=m.blocks.reduce((sum,b)=>sum+solplateGeometry(b,b.plan).volume*7.85,0);
|
||
expect(r.results.find(x=>x.cell==='W64')!.value).toBeCloseTo(actual,12);
|
||
expect(r.results.find(x=>x.cell==='W64')!.dependencies).toEqual([cellKey(DETAIL,'W64')]);
|
||
});
|
||
});
|
||
|
||
describe('Excel 비동기 생성 후 입력 보존·범위',()=>{
|
||
it('선택 출력은 해당 교각만, 전체 출력은 미생성 교각도 검사 대상으로 반환한다',()=>{
|
||
const s=snapshot();s.piers.push({...s.piers[0],input:{...input,name:'P2'},model:null});
|
||
expect(exportRecords(s,'selected').map(r=>r.pier.input.name)).toEqual([input.name]);
|
||
expect(exportRecords(s,'all').map(r=>r.pier.input.name)).toEqual([input.name,'P2']);
|
||
});
|
||
it('생성 중 수정한 입력·검토 체크를 이전 스냅샷으로 저장하지 않는다',()=>{
|
||
const before=snapshot(),live=snapshot(),saved:unknown[]=[];
|
||
live.piers[0].input={...live.piers[0].input,quantityTakeoff:{values:{...stored.values,r21:6},reviewed:false}};
|
||
expect(finishQuantityExport(before,'all',live,(...args)=>saved.push(args))).toEqual([input.name]);
|
||
expect(saved).toEqual([]);
|
||
expect(live.piers[0].input.quantityTakeoff).toMatchObject({values:{r21:6},reviewed:false});
|
||
});
|
||
it('변경이 없으면 현재 입력에 계산 서명만 기록한다',()=>{
|
||
const s=snapshot(),saved:{name:string;data:QuantityInputs}[]=[];
|
||
expect(finishQuantityExport(s,'selected',s,(name,data)=>saved.push({name,data}))).toEqual([]);
|
||
expect(saved[0].data).toMatchObject({values:stored.values,reviewed:true,lastSignature:raw().signature});
|
||
});
|
||
it('다른 프로젝트로 바뀌거나 모델이 재생성되면 이전 출력의 서명을 저장하지 않는다',()=>{
|
||
const before=snapshot(),live=snapshot();live.project={...live.project,name:'다른 프로젝트'};
|
||
expect(finishQuantityExport(before,'all',live,()=>{throw new Error('과거 입력 저장');})).toEqual([input.name]);
|
||
live.project=before.project;live.piers[0].model=buildPier(input);
|
||
expect(finishQuantityExport(before,'all',live,()=>{throw new Error('과거 입력 저장');})).toEqual([input.name]);
|
||
});
|
||
});
|