Sprint 23/24 — Tauri v2 앱 래핑 + salsa 0.16 증분 쿼리 백엔드
All checks were successful
Publish ParaWiki / build-and-deploy (push) Successful in 34s
All checks were successful
Publish ParaWiki / build-and-deploy (push) Successful in 34s
Sprint 23: cimery-app을 Tauri v2 앱으로 전환. - tauri.conf.json, capabilities/default.json, frontend/index.html 추가 - src/commands.rs: 7개 IPC 커맨드 (launch_viewer, 프로젝트 관리, USD/CSV 익스포트) - 뷰어 사이드카: std::process::Command 방식 (PATH + exe-dir 탐색) - release.yml: 3단계 멀티플랫폼 릴리스 워크플로로 교체 Sprint 24: cimery-incremental에 salsa 0.16 백엔드 추가. - salsa_db.rs: BridgeQueryGroup + SalsaIncrementalDb<K> - --features salsa-backend 로 활성화 (기본값: 수동 tracking, WASM 안전) - IR 전 구조체 + Mesh + KernelError에 PartialEq/Eq 추가 - 테스트 20개 전부 통과 (수동 12 + salsa 8) - cargo check --workspace 0 errors/warnings 기타: viewer/dsl 컴파일 경고 제거, wiki 실행 가이드 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
340
cimery/crates/app/frontend/index.html
Normal file
340
cimery/crates/app/frontend/index.html
Normal file
@@ -0,0 +1,340 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>cimery</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Malgun Gothic', sans-serif;
|
||||
background: #1a1d23;
|
||||
color: #d4d8e0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── Titlebar ─────────────────────────────────────────────── */
|
||||
.titlebar {
|
||||
background: #13161b;
|
||||
border-bottom: 1px solid #2c3040;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
.titlebar .logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #5b9bd5;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.titlebar .subtitle {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* ── Main layout ──────────────────────────────────────────── */
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Sidebar ──────────────────────────────────────────────── */
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
background: #13161b;
|
||||
border-right: 1px solid #2c3040;
|
||||
padding: 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.sidebar-item {
|
||||
padding: 9px 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
border-left: 3px solid transparent;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.sidebar-item:hover { background: #1f2330; }
|
||||
.sidebar-item.active {
|
||||
background: #1f2330;
|
||||
border-left-color: #5b9bd5;
|
||||
color: #fff;
|
||||
}
|
||||
.sidebar-section {
|
||||
padding: 16px 20px 6px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
/* ── Content ──────────────────────────────────────────────── */
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 32px 40px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #e5e7eb;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* ── Card grid ────────────────────────────────────────────── */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.card {
|
||||
background: #1f2330;
|
||||
border: 1px solid #2c3040;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, transform 0.1s;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: #5b9bd5;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.card h3 { font-size: 14px; color: #e5e7eb; margin-bottom: 6px; }
|
||||
.card p { font-size: 12px; color: #6b7280; line-height: 1.5; }
|
||||
.card .icon { font-size: 24px; margin-bottom: 10px; }
|
||||
|
||||
/* ── Action buttons ───────────────────────────────────────── */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 18px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-primary { background: #2563eb; color: #fff; }
|
||||
.btn-primary:hover { background: #1d4ed8; }
|
||||
.btn-secondary { background: #1f2330; color: #d4d8e0; border: 1px solid #2c3040; }
|
||||
.btn-secondary:hover { background: #2c3040; }
|
||||
.btn-group { display: flex; gap: 10px; margin-bottom: 28px; }
|
||||
|
||||
/* ── Status bar ───────────────────────────────────────────── */
|
||||
.statusbar {
|
||||
background: #13161b;
|
||||
border-top: 1px solid #2c3040;
|
||||
padding: 6px 20px;
|
||||
font-size: 11px;
|
||||
color: #4b5563;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
.status-ok { color: #22c55e; }
|
||||
.status-warn { color: #f59e0b; }
|
||||
|
||||
/* ── Recent projects table ────────────────────────────────── */
|
||||
.recent-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
.recent-table th {
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
color: #6b7280;
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #2c3040;
|
||||
}
|
||||
.recent-table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #1f2330;
|
||||
color: #d4d8e0;
|
||||
}
|
||||
.recent-table tr:hover td { background: #1f2330; cursor: pointer; }
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
background: #1e3a5f;
|
||||
color: #5b9bd5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Titlebar -->
|
||||
<div class="titlebar">
|
||||
<span class="logo">cimery</span>
|
||||
<span class="subtitle">Civil Parametric BIM v0.1.0</span>
|
||||
</div>
|
||||
|
||||
<!-- Main layout -->
|
||||
<div class="main">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-section">작업공간</div>
|
||||
<div class="sidebar-item active" onclick="showPage('home')">홈</div>
|
||||
<div class="sidebar-item" onclick="showPage('projects')">프로젝트</div>
|
||||
<div class="sidebar-item" onclick="launchViewer()">3D 뷰어 열기</div>
|
||||
|
||||
<div class="sidebar-section">내보내기</div>
|
||||
<div class="sidebar-item" onclick="exportUSD()">USD 익스포트</div>
|
||||
<div class="sidebar-item" onclick="exportCSV()">CSV 템플릿</div>
|
||||
|
||||
<div class="sidebar-section">도움말</div>
|
||||
<div class="sidebar-item" onclick="showPage('about')">정보</div>
|
||||
</nav>
|
||||
|
||||
<!-- Content area -->
|
||||
<div class="content" id="content">
|
||||
<!-- Populated by JS showPage() -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status bar -->
|
||||
<div class="statusbar">
|
||||
<span id="status-kernel" class="status-ok">커널: PureRust</span>
|
||||
<span id="status-version">v0.1.0</span>
|
||||
<span id="status-msg"></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Tauri IPC helper ─────────────────────────────────────────
|
||||
async function invoke(cmd, args = {}) {
|
||||
try {
|
||||
const { invoke: tauriInvoke } = await import('https://unpkg.com/@tauri-apps/api@2/core');
|
||||
return await tauriInvoke(cmd, args);
|
||||
} catch (e) {
|
||||
// Dev fallback when running outside Tauri (browser preview)
|
||||
console.warn('Tauri IPC not available — dev fallback', cmd, args);
|
||||
return { ok: false, error: 'dev_mode' };
|
||||
}
|
||||
}
|
||||
|
||||
async function launchViewer() {
|
||||
setStatus('3D 뷰어 실행 중…');
|
||||
const result = await invoke('launch_viewer');
|
||||
if (result && result.ok) {
|
||||
setStatus('뷰어 실행됨');
|
||||
} else {
|
||||
setStatus('뷰어 실행 실패: ' + (result?.error ?? '알 수 없는 오류'), true);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportUSD() {
|
||||
setStatus('USD 익스포트 중…');
|
||||
const result = await invoke('export_usd_default');
|
||||
setStatus(result?.ok ? 'USD 익스포트 완료' : ('USD 실패: ' + result?.error), !result?.ok);
|
||||
}
|
||||
|
||||
async function exportCSV() {
|
||||
setStatus('CSV 템플릿 생성 중…');
|
||||
const result = await invoke('export_csv_template');
|
||||
setStatus(result?.ok ? 'CSV 템플릿 생성 완료' : ('CSV 실패: ' + result?.error), !result?.ok);
|
||||
}
|
||||
|
||||
function setStatus(msg, warn = false) {
|
||||
const el = document.getElementById('status-msg');
|
||||
el.textContent = msg;
|
||||
el.className = warn ? 'status-warn' : 'status-ok';
|
||||
}
|
||||
|
||||
// ── Pages ────────────────────────────────────────────────────
|
||||
const pages = {
|
||||
home: `
|
||||
<div class="section-title">시작하기</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="launchViewer()">3D 뷰어 열기</button>
|
||||
<button class="btn btn-secondary" onclick="newProject()">새 프로젝트</button>
|
||||
<button class="btn btn-secondary" onclick="openProject()">프로젝트 열기</button>
|
||||
</div>
|
||||
<div class="card-grid">
|
||||
<div class="card" onclick="launchViewer()">
|
||||
<div class="icon">🏗️</div>
|
||||
<h3>거더교 3D 뷰어</h3>
|
||||
<p>egui+wgpu 기반 실시간 파라메트릭 뷰어</p>
|
||||
</div>
|
||||
<div class="card" onclick="exportUSD()">
|
||||
<div class="icon">📦</div>
|
||||
<h3>USD 익스포트</h3>
|
||||
<p>교량 씬 전체를 USD 텍스트 포맷으로 저장</p>
|
||||
</div>
|
||||
<div class="card" onclick="exportCSV()">
|
||||
<div class="icon">📋</div>
|
||||
<h3>CSV 템플릿</h3>
|
||||
<p>거더 파라미터 템플릿을 CSV로 생성</p>
|
||||
</div>
|
||||
<div class="card" onclick="showPage('about')">
|
||||
<div class="icon">ℹ️</div>
|
||||
<h3>cimery 정보</h3>
|
||||
<p>버전·기술 스택·라이선스</p>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
projects: `
|
||||
<div class="section-title">프로젝트</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="newProject()">새 프로젝트</button>
|
||||
<button class="btn btn-secondary" onclick="openProject()">열기</button>
|
||||
</div>
|
||||
<table class="recent-table">
|
||||
<thead>
|
||||
<tr><th>이름</th><th>마지막 수정</th><th>유형</th></tr>
|
||||
</thead>
|
||||
<tbody id="project-list">
|
||||
<tr><td colspan="3" style="color:#4b5563;padding:20px 12px;">저장된 프로젝트 없음</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
about: `
|
||||
<div class="section-title">cimery 정보</div>
|
||||
<div class="card" style="max-width:480px">
|
||||
<h3 style="font-size:16px;margin-bottom:12px">cimery v0.1.0</h3>
|
||||
<p style="margin-bottom:8px">Civil + BIM + -ery</p>
|
||||
<p style="margin-bottom:16px;line-height:1.8">
|
||||
토목 엔지니어링 특성을 반영한 파라메트릭 모델링 도구.<br>
|
||||
MVP: 거더교 (PSC-I 거더, 교각, 교대, 받침, 교량받침).
|
||||
</p>
|
||||
<p style="font-size:11px;color:#4b5563">
|
||||
Tauri v2 · egui 0.29 · wgpu 22 · Rust 2021<br>
|
||||
License: MIT OR Apache-2.0
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
function showPage(name) {
|
||||
document.querySelectorAll('.sidebar-item').forEach(el => el.classList.remove('active'));
|
||||
event?.target?.classList.add('active');
|
||||
document.getElementById('content').innerHTML = pages[name] ?? pages.home;
|
||||
}
|
||||
|
||||
async function newProject() {
|
||||
const result = await invoke('new_project');
|
||||
setStatus(result?.ok ? '새 프로젝트 생성됨' : '새 프로젝트 실패');
|
||||
}
|
||||
|
||||
async function openProject() {
|
||||
const result = await invoke('open_project_dialog');
|
||||
if (result?.ok) setStatus('프로젝트 열림: ' + result.path);
|
||||
}
|
||||
|
||||
// Init
|
||||
showPage('home');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user