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>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-09 18:01:02 +09:00
1 parent b2bebe47eb
commit fc520ec03d
47 files changed
+2848 -149

No files matched your search

+18 -9
View File
@@ -1,5 +1,7 @@
import './ground-popup.css';
const API='/__pierzainer/ground-link';
let importGround: ((text: string, name: string) => Promise<void>) | undefined;
export function registerGroundImporter(importer: (text: string, name: string) => Promise<void>) { importGround = importer; }
let pending:{file:File;folder?:string}|undefined,staged:Promise<string|undefined>=Promise.resolve(undefined),version=0;
const request=async(action:string,body:object)=>{const r=await fetch(`${API}/${action}`,{method:'POST',headers:{'Content-Type':'application/json','X-Pierzainer-Action':'ground-link'},body:JSON.stringify(body)});const value=await r.json();if(!r.ok)throw new Error(value.error??'지반 프로그램 연동 실패');return value;};
const base64=(file:File)=>new Promise<string>((resolve,reject)=>{const reader=new FileReader();reader.onerror=()=>reject(new Error('Excel 파일을 읽지 못했습니다.'));reader.onload=()=>resolve(String(reader.result).split(',')[1]);reader.readAsDataURL(file);});
@@ -9,6 +11,10 @@ document.addEventListener('click',event=>{if(!(event.target as Element).closest(
let opening=false,running=false,launchSerial=0;
let popup:HTMLDialogElement;
window.addEventListener('pierzainer:project-changed',()=>{
version++;launchSerial++;running=false;opening=false;
if(popup){popup.close();popup.querySelector<HTMLElement>('[data-status]')!.textContent='프로젝트가 변경되어 이전 작업의 결과를 연결하지 않습니다.';}
});
function getPopup(){
if(popup)return popup;
popup=document.createElement('dialog');popup.className='ground-popup';popup.setAttribute('aria-labelledby','ground-popup-title');
@@ -28,13 +34,13 @@ window.addEventListener('pierzainer:open-ground',async()=>{
const drawingInput=dialog.querySelector<HTMLInputElement>('[data-drawing]')!,excelInput=dialog.querySelector<HTMLInputElement>('[data-excel]')!;
drawingInput.value='';excelInput.value='';drawingInput.disabled=excelInput.disabled=false;dialog.querySelector('[data-drawing-name]')!.textContent='파일 확인 중…';dialog.querySelector('[data-excel-name]')!.textContent='파일 확인 중…';run.disabled=true;status.textContent='입력 파일 확인 중…';logs.textContent='';dialog.showModal();
let stageId:string|undefined,drawing='';
try{stageId=await staged;const prepared=await request('prepare',{stageId});drawing=prepared.drawing;
try{stageId=await staged;const prepared=await request('prepare',{stageId});if(snapshot!==version||ticket!==launchSerial)return;drawing=prepared.drawing;
const display=(kind:string,name:string,connected:boolean)=>{dialog.querySelector(`[data-${kind}-name]`)!.textContent=name;dialog.querySelector(`[data-pick="${kind}"]`)!.textContent=connected?'파일 변경':kind==='drawing'?'종평면도 선택':'Excel 선택';};
const drawingName=drawing.split(/[\\/]/).pop()||'종평면도를 선택하세요.';const excelName=prepared.excelName||'교각 Excel을 선택하세요.';
display('drawing',drawingName,!!drawing);display('excel',excelName,prepared.excelSelected);
drawingInput.onchange=()=>display('drawing',drawingInput.files?.[0]?.name||drawingName,!!drawingInput.files?.[0]||!!drawing);
excelInput.onchange=()=>display('excel',excelInput.files?.[0]?.name||excelName,!!excelInput.files?.[0]||prepared.excelSelected);status.textContent='입력을 확인하고 지반 생성을 누르세요.';run.disabled=false;
}catch(error){status.textContent=(error as Error).message;opening=false;return;}
}catch(error){if(snapshot!==version||ticket!==launchSerial)return;status.textContent=(error as Error).message;opening=false;return;}
run.onclick=async()=>{
if(snapshot!==version){status.textContent='프로젝트가 변경되었습니다. 닫고 다시 열어 주세요.';return;}
run.disabled=true;drawingInput.disabled=excelInput.disabled=true;
@@ -43,23 +49,26 @@ window.addEventListener('pierzainer:open-ground',async()=>{
status.textContent='입력 파일 전달 중…';const chosen=drawingInput.files?.[0],excel=excelInput.files?.[0];
if(chosen)drawing=await upload(chosen);if(!drawing)throw new Error('종평면도를 선택하세요.');
const excelPath=excel?await upload(excel):undefined;
if(snapshot!==version||ticket!==launchSerial)return;
if(!excelPath&&!stageId)throw new Error('교각 Excel을 선택하세요.');
const job=await request('launch',{stageId,drawing,excel:excelPath});running=true;status.textContent='지반·지도 생성 중… 닫아도 생성은 계속됩니다.';
const job=await request('launch',{stageId,drawing,excel:excelPath});if(snapshot!==version||ticket!==launchSerial)return;running=true;status.textContent='지반·지도 생성 중… 닫아도 생성은 계속됩니다.';
const deadline=Date.now()+30*60*1000;
const poll=async()=>{
if(snapshot!==version||ticket!==launchSerial){running=false;return;}
if(snapshot!==version||ticket!==launchSerial)return;
try{
const result=await request('result',{id:job.id});logs.textContent=result.logs??'';logs.scrollTop=logs.scrollHeight;
if(snapshot!==version||ticket!==launchSerial)return;
if(result.state==='error')throw new Error('지반 생성 실패. 진행 로그를 확인하세요.');
if(result.ready){
const input=document.querySelector<HTMLInputElement>('#ground-import');
status.textContent=input?'지반 생성 완료. 프로젝트에 불러왔습니다.':'지반 생성 완료. 지반·지층 화면으로 돌아오면 불러옵니다.';
if(input){const transfer=new DataTransfer();transfer.items.add(new File([result.text],result.filename,{type:'application/json'}));input.files=transfer.files;input.dispatchEvent(new Event('change',{bubbles:true}));running=false;run.disabled=false;drawingInput.disabled=excelInput.disabled=false;return;}
if(!importGround)throw new Error('프로젝트 지반모델 불러오기를 사용할 수 없습니다.');
await importGround(result.text,result.filename);
status.textContent='지반 생성 완료. 프로젝트 공통 모델로 불러왔습니다. 관리창에서 비교 후 적용하세요.';
running=false;run.disabled=false;drawingInput.disabled=excelInput.disabled=false;return;
}
if(Date.now()>deadline)throw new Error('응답 대기 시간이 지났습니다. 결과 폴더를 확인하세요.');setTimeout(poll,2000);
}catch(error){status.textContent=(error as Error).message;running=false;run.disabled=false;drawingInput.disabled=excelInput.disabled=false;}
}catch(error){if(snapshot!==version||ticket!==launchSerial)return;status.textContent=(error as Error).message;running=false;run.disabled=false;drawingInput.disabled=excelInput.disabled=false;}
};setTimeout(poll,1000);
}catch(error){status.textContent=(error as Error).message;running=false;run.disabled=false;drawingInput.disabled=excelInput.disabled=false;}
}catch(error){if(snapshot!==version||ticket!==launchSerial)return;status.textContent=(error as Error).message;running=false;run.disabled=false;drawingInput.disabled=excelInput.disabled=false;}
};
});
// Closing hides the panel; the current job and its polling continue.