Files
ParaWiki/cimery/crates/app/frontend/index.html
minsung 824c18610b
All checks were successful
Publish ParaWiki / build-and-deploy (push) Successful in 34s
Sprint 23/24 — Tauri v2 앱 래핑 + salsa 0.16 증분 쿼리 백엔드
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>
2026-04-15 09:09:47 +09:00

341 lines
11 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>