feat: 1차 과제 프로젝트 코드 업로드

This commit is contained in:
eglee committed 2026-10-27 11:01:12 +09:00
1 parent 9cee9bf791
commit 9df1bcfc2a
12 files changed
+129 -6

No files matched your search

+5 -1
View File
@@ -4,6 +4,8 @@
## 실행 및 로컬 웹 게시
Windows 탐색기에서는 프로젝트 폴더의 **`뷰어 실행.cmd`를 더블클릭**하세요. WSL 서버가 꺼져 있으면 시작하고, 응답을 확인한 다음 브라우저를 엽니다. 서버 창은 사용하는 동안 열어 두세요. `index.html` 자체는 서버를 시작하지 않으므로 클릭 실행에는 `뷰어 실행.cmd`를 사용하세요.
WSL 터미널에서 다음 명령을 실행합니다.
```bash
@@ -11,7 +13,9 @@ cd /home/b19203/3d-drawing-web
python3 r2c.py serve
```
브라우저에서 **http://localhost:8000** 을 엽니다. 서버 종료는 `Ctrl+C`입니다. 실행할 때마다 DXF에서 모델과 웹페이지를 다시 생성합니다. 생성된 루트 `index.html`을 직접 열어 기본 도면의 규격별 모델을 볼 수도 있습니다. **DXF 파일 첨부 변환은 `serve`로 실행한 웹사이트에서 사용합니다.** 프로그램을 수정한 경우 기존 서버를 종료하고 다시 실행하세요.
브라우저에서 **http://localhost:8000** 을 엽니다. 서버 종료는 `Ctrl+C`입니다. 실행할 때마다 DXF에서 모델과 웹페이지를 다시 생성합니다. 생성된 루트 `index.html`을 더블클릭해도 서버 뷰어로 자동 이동합니다. **DXF 파일 첨부 변환은 `serve`로 실행한 웹사이트에서 사용합니다.** 프로그램을 수정한 경우 기존 서버를 종료하고 다시 실행하세요.
HTML 파일을 더블클릭하면 `file://`에서 `http://localhost:8000/`로 자동 이동합니다. `--port`를 지정해 빌드하거나 서버를 실행하면 해당 포트로 이동하도록 생성됩니다. HTTP로 접속한 페이지에서는 이동하지 않습니다. HTML은 서버 프로그램을 직접 시작할 수 없으므로 먼저 위 명령으로 서버를 실행하세요.
다른 포트 또는 로컬 네트워크에 게시하려면 다음처럼 실행합니다.
+24
View File
@@ -0,0 +1,24 @@
param(
[Parameter(Mandatory=$true)][string]$WslAddress,
[Parameter(Mandatory=$true)][string]$ListenAddress
)
$ErrorActionPreference = 'Stop'
$log = Join-Path $env:TEMP 'r2c-network-setup.log'
try {
$wslIp = [System.Net.IPAddress]::Parse($WslAddress)
$listenIp = [System.Net.IPAddress]::Parse($ListenAddress)
if ($wslIp.AddressFamily -ne 'InterNetwork' -or $listenIp.AddressFamily -ne 'InterNetwork') {
throw 'IPv4 addresses are required.'
}
& netsh.exe interface portproxy add v4tov4 listenaddress=$ListenAddress listenport=8000 connectaddress=$WslAddress connectport=8000
if ($LASTEXITCODE -ne 0) { throw 'Could not configure port forwarding.' }
$ruleName = 'R2C-Viewer-8000-Private10'
if (Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue) {
Remove-NetFirewallRule -Name $ruleName
}
New-NetFirewallRule -Name $ruleName -DisplayName 'R2C viewer (10.0.0.0/8)' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000 -LocalAddress $ListenAddress -RemoteAddress '10.0.0.0/8' -Profile Any | Out-Null
"OK: http://${ListenAddress}:8000/ -> ${WslAddress}:8000; allowed clients: 10.0.0.0/8" | Set-Content $log
} catch {
$_.Exception.Message | Set-Content $log
exit 1
}
+26
View File
@@ -0,0 +1,26 @@
Git 커밋 기록
작성 시간: 2026-10-23 09:29:16+09:00
commit 9cee9bf79126f9d87220dd45d6a6038611d750a7 (HEAD -> master)
Author: lek <eglee@samaneng.com>
Date: Thu Oct 22 11:28:16 2026
Validate DXF geometry and splines and improve viewer maintenance
commit 11fd3badf6f6e46d20f010ffa151c46fe87dc7ad
Author: lek <eglee@samaneng.com>
Date: Tue Oct 20 21:23:57 2026
Build DXF-based R2C 3D viewer and local web server
commit 277c7005aba2c7b8fb15ca3268516c1a687765bc
Author: lek <eglee@samaneng.com>
Date: Tue Oct 20 20:45:56 2026
Add R2C drawing 3D viewer
commit b59196feb3bf98764f118b13c52fdf3a3f2fadff
Author: lek <eglee@samaneng.com>
Date: Tue Oct 20 12:06:14 2026
Start 3D practice
+7
View File
@@ -3,6 +3,11 @@
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#111c29">
<title>R2C · DXF 도면 3D 뷰어</title>
<script>
if (location.protocol === 'file:') {
location.replace('http://localhost:8000/' + location.search + location.hash);
}
</script>
<style>
:root{--bg:#101b28;--panel:#172635;--border:#2b4052;--fg:#edf3f6;--muted:#9caebb;--mint:#8de1cf;--gold:#edc28a}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}button,a,select{font:inherit}button,select{cursor:pointer}button:focus-visible,a:focus-visible,select:focus-visible{outline:2px solid var(--mint);outline-offset:3px}button{color:inherit}a{color:var(--mint)}.wrap{max-width:1600px;margin:auto;padding:0 28px 30px}header{display:flex;justify-content:space-between;align-items:center;gap:16px;height:82px;border-bottom:1px solid var(--border)}.brand{display:flex;align-items:center;gap:12px}.symbol{width:39px;height:39px;border:1px solid #428379;color:var(--mint);border-radius:11px;display:grid;place-items:center;font-weight:800}.brand strong{display:block;font-size:15px}.brand small{color:var(--muted);font-size:11px}.actions{display:flex;gap:8px;align-items:center}.badge{font-size:10px;color:var(--mint);padding:5px 10px;border:1px solid #345f5b;border-radius:20px}.link,.btn{border:1px solid var(--border);background:var(--panel);padding:8px 12px;border-radius:7px;text-decoration:none;font-size:12px;color:var(--fg);white-space:nowrap}.btn:hover,.link:hover{background:#24394a}.btn.active{background:#204a49;border-color:#4a8b7f;color:#bcf4e6}h1{font-size:25px;font-weight:650;letter-spacing:-.04em;margin:4px 0 6px}.intro{display:flex;justify-content:space-between;align-items:end;margin:25px 0 19px;gap:20px}.eyebrow{color:var(--mint);font-size:10px;font-weight:700;letter-spacing:.15em}.subtitle{color:var(--muted);font-size:12px;margin:0}.intro-note{text-align:right;color:var(--muted);font-size:11px}.workspace{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:18px}.main{min-width:0}.toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}.seg{display:flex;gap:5px;flex-wrap:wrap}.seg .btn{padding:6px 10px;font-size:11px}.stage{position:relative;isolation:isolate;height:clamp(420px,61vh,760px);border:1px solid #465662;border-radius:12px;overflow:hidden;background:radial-gradient(ellipse at 45% 38%,#f5f7f6,#dfe7e9)}#scene{display:block;position:absolute;inset:0;width:100%;height:100%;touch-action:none;cursor:grab}#scene:active{cursor:grabbing}.stage-label{position:absolute;left:17px;top:15px;pointer-events:none;font-size:10px;color:#526673;letter-spacing:.12em;z-index:2}.stage-label strong{display:block;font-size:12px;color:#283e49;letter-spacing:.03em;margin-top:3px}.stage-bottom{position:absolute;left:17px;bottom:14px;color:#526673;font-size:10px;pointer-events:none}.stage-status{position:absolute;right:14px;top:14px;border:1px solid #b7c9cc;background:#f9fcfaca;padding:5px 9px;border-radius:7px;color:#35635f;font-size:10px;pointer-events:none}.zoom{position:absolute;right:13px;bottom:13px;display:flex;gap:5px}.zoom button{background:#f5f8f8e8;border:1px solid #b5c6cd;border-radius:6px;color:#334b59;width:31px;height:30px;font-weight:650}.viewbar{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:10px}.help{font-size:10px;color:var(--muted)}.error{position:absolute;inset:55px 15px auto;z-index:5;padding:12px;color:#7b2f22;background:#ffefdf;border-radius:7px;font-size:12px}.error[hidden]{display:none}.cards{display:flex;flex-direction:column;gap:12px}.card{padding:16px;border:1px solid var(--border);border-radius:10px;background:var(--panel)}.card h2{font-size:12px;margin:0 0 13px;display:flex;justify-content:space-between;align-items:center}.small-tag{font-size:9px;font-weight:400;letter-spacing:.05em;color:var(--mint)}.dims{margin:0;display:grid;grid-template-columns:1fr auto;gap:8px;font-size:12px}.dims dt{color:var(--muted)}.dims dd{margin:0;font-variant-numeric:tabular-nums}.dims dd.main-value{color:var(--mint);font-weight:650}.rule{border:0;border-top:1px solid var(--border);margin:13px 0}.micro{font-size:11px;color:var(--muted);margin:0;line-height:1.7}.legend{display:flex;gap:8px;align-items:center;font-size:11px;margin-bottom:7px}.swatch{width:10px;height:10px;border-radius:3px;background:#bcc8c9}.swatch.bottom{background:#81979d}.swatch.inner{background:#6b8189}.card .btn{width:100%;margin-top:12px;text-align:center}.limits{color:var(--muted);font-size:10px;line-height:1.7;margin-top:10px}.drawing-card{border:1px solid var(--border);border-radius:10px;margin-top:22px;background:var(--panel);overflow:hidden}.drawing-card>summary{cursor:pointer;list-style:none;display:flex;justify-content:space-between;gap:12px;padding:16px;font-size:13px;font-weight:600}.drawing-card>summary::after{content:'+';color:var(--mint)}.drawing-card[open]>summary::after{content:'−'}.drawing-controls{display:flex;gap:7px;flex-wrap:wrap;align-items:center;padding:0 16px 12px}.drawing-controls select{background:var(--bg);border:1px solid var(--border);color:var(--fg);border-radius:6px;padding:6px 9px;font-size:11px}.drawing-viewport{height:440px;background:#f7fafbLine truncated
@media(max-width:1050px){.workspace{grid-template-columns:minmax(0,1fr) 270px}.wrap{padding:0 17px 25px}.intro-note{display:none}.viewbar{align-items:start;flex-direction:column}.stage{height:540px}}@media(max-width:740px){header{height:72px}.badge{display:none}.wrap{padding:0 12px 20px}.workspace{grid-template-columns:1fr}h1{font-size:21px}.intro{margin-top:20px}.stage{height:440px}.cards{display:grid;grid-template-columns:1fr 1fr}.card:last-child{grid-column:1/-1}.drawing-viewport{height:350px}.toolbar{align-items:start}.brand small{display:none}}@media(max-width:440px){.cards{grid-template-columns:1fr}.stage{height:390px}.toolbar{flex-direction:column}.actions .link{font-size:10px;padding:7px}.help{display:none}}
@@ -12,6 +17,7 @@
</style>
</head>
<body><div class="wrap">
<p id="localFileNotice" class="upload-message" hidden role="status">파일로 연 뷰어입니다. 3D 회전과 규격 선택은 가능하며, DXF 첨부 변환은 서버 뷰어에서 사용할 수 있습니다. <a href="http://localhost:8000/">서버 뷰어 열기 →</a><br>서버가 꺼져 있다면 <code>python3 r2c.py serve</code>를 실행하세요. 다른 포트를 사용한다면 해당 서버 주소로 접속하세요.</p>
<header><div class="brand"><div class="symbol">R2</div><div><strong>R2C 도면 스튜디오</strong><small>DXF → 단면 재구성 → 웹 3D</small></div></div><div class="actions"><span class="badge">DXF 엔티티 추출</span><a class="link" id="sourceLink" href="./drawing/R2C_일반도_20260601.dxf" download>원본 DXF ↓</a><button class="btn" id="download">3D 모델 ↓</button></div></header>
<div class="intro"><div><div class="eyebrow">DRAWING / RECONSTRUCTION</div><h1>사각 입구에서 원형 관로까지</h1><p class="subtitle">R2C관의 전환부와 소켓을 원본 단면으로 확인하세요.</p></div><div class="intro-note">모든 치수는 mm 기준<br>원본 A-A / B-B 단면 및 변화치수표 반영</div></div>
<section class="upload-zone" id="dropZone" aria-label="DXF 첨부 영역"><div><strong>DXF 파일을 첨부하거나 여기에 놓으세요</strong><p>R2C 일반도의 단면·변화치수표를 읽어 바로 3D로 변환합니다. · 최대 10 MB</p><p>첨부 파일은 변환 후 서버에 저장하지 않습니다.</p></div><div class="upload-controls"><input type="file" id="dxfFile" accept=".dxf" hidden><button class="btn" id="uploadButton">DXF 파일 첨부</button><button class="btn" id="sampleReset">기본 도면으로</button></div></section><p class="upload-message" id="uploadMessage" role="status" aria-live="polite" hidden></p>
@@ -34,6 +40,7 @@ let sourceDocument=initialDocument,data=initialDocument,selectedDiameter=initial
const initialDrawing=document.querySelector('.drawing-viewport').innerHTML,initialSource=document.getElementById('sourceLink').getAttribute('href');
let sourceUrl=null,uploadBusy=false;
const specification=document.getElementById('specification');
document.getElementById('localFileNotice').hidden=location.protocol!=='file:';
const tableKeys=['D','D1','D2','L','L1','L2','L3','B','b1','b2','b3','b4','H','h1','h2','h3','h4','h5','h6','h7'];
let model='r2c',mode='closed',yaw=.87,pitch=.40,zoom=1,wire=false,drag=null,pending=false;
const fmt=n=>Math.round(n).toLocaleString('ko-KR');
+41
View File
@@ -0,0 +1,41 @@
$ErrorActionPreference = 'Stop'
$url = 'http://127.0.0.1:8000/'
$serverOutput = Join-Path $env:TEMP 'r2c-viewer-server.log'
$serverError = Join-Path $env:TEMP 'r2c-viewer-server-error.log'
function Test-Viewer {
try {
$model = Invoke-RestMethod -Uri ($url + 'model.json') -TimeoutSec 10
return ($null -ne $model.meshes -and $null -ne $model.variants)
} catch { return $false }
}
try {
if (-not (Test-Viewer)) {
# Explorer exposes WSL folders as either of these UNC paths.
if ($PSScriptRoot -match '^\\\\(?:wsl\$|wsl\.localhost)\\([^\\]+)\\(.*)$') {
$distro = $Matches[1]
$linuxPath = '/' + $Matches[2].Replace('\', '/')
$wslArgs = @('-d', $distro, '--cd', ('"' + $linuxPath + '"'), '--', 'python3', 'r2c.py', 'serve')
} else {
$linuxPath = & wsl.exe -- wslpath -u $PSScriptRoot
if ($LASTEXITCODE -ne 0) { throw 'Cannot locate the project in WSL.' }
$wslArgs = @('--cd', ('"' + $linuxPath.Trim() + '"'), '--', 'python3', 'r2c.py', 'serve')
}
$server = Start-Process -FilePath 'wsl.exe' -ArgumentList $wslArgs -PassThru -RedirectStandardOutput $serverOutput -RedirectStandardError $serverError
$ready = $false
for ($attempt = 0; $attempt -lt 30; $attempt++) {
if (Test-Viewer) { $ready = $true; break }
if ($server.HasExited) {
$details = Get-Content $serverError -Raw -ErrorAction SilentlyContinue
throw ('The viewer server exited: ' + $details)
}
Start-Sleep -Milliseconds 500
}
if (-not $ready) { throw 'The viewer did not respond at http://localhost:8000/. Check WSL and port 8000.' }
}
Start-Process $url
} catch {
Write-Host $_.Exception.Message -ForegroundColor Red
exit 1
}
+6 -3
View File
@@ -516,7 +516,9 @@ def initial_scene(model):
return ''.join(result)
def build(dxf=DEFAULT_DXF, output=ROOT / 'dist', update_entry=True):
def build(dxf=DEFAULT_DXF, output=ROOT / 'dist', update_entry=True, port=8000):
if not isinstance(port, int) or not 1 <= port <= 65535:
raise ValueError('포트는 1~65535 사이의 정수여야 합니다.')
dxf, output = Path(dxf).resolve(), Path(output).resolve()
if output == ROOT or output == ROOT / 'drawing':
raise ValueError('출력 폴더는 dist 같은 별도 폴더로 지정해 주세요.')
@@ -524,6 +526,7 @@ def build(dxf=DEFAULT_DXF, output=ROOT / 'dist', update_entry=True):
model, svg = payload['model'], payload['drawingSvg']
output.mkdir(parents=True, exist_ok=True)
template = (ROOT / 'web' / 'viewer.html').read_text(encoding='utf-8')
template = template.replace('__SERVER_PORT__', str(port)).replace('http://localhost:8000/', f'http://localhost:{port}/')
template = template.replace('__VIEWER_JS__', (ROOT / 'web' / 'viewer.js').read_text(encoding='utf-8'))
template = template.replace('__INITIAL_SCENE__', initial_scene(model)).replace('__DXF_HREF__', './drawing/' + html.escape(dxf.name, quote=True))
page = template.replace('__MODEL_JSON__', json.dumps(model, ensure_ascii=False, separators=(',', ':')).replace('<', '\\u003c')).replace('__DRAWING_SVG__', svg)
@@ -545,11 +548,11 @@ def main():
parser.add_argument('command', choices=['build', 'serve'], nargs='?', default='serve')
parser.add_argument('--dxf', type=Path, default=DEFAULT_DXF)
parser.add_argument('--output', type=Path, default=ROOT / 'dist')
parser.add_argument('--host', default='127.0.0.1', help='다른 기기에서 접속하려면 0.0.0.0')
parser.add_argument('--host', default='0.0.0.0', help='수신 주소 (기본: 0.0.0.0, 로컬 전용: 127.0.0.1)')
parser.add_argument('--port', type=int, default=8000)
args = parser.parse_args()
try:
build(args.dxf, args.output)
build(args.dxf, args.output, port=args.port)
if args.command == 'serve':
handler = functools.partial(ViewerHandler, directory=str(args.output.resolve()))
with http.server.ThreadingHTTPServer((args.host, args.port), handler) as server:
+10 -2
View File
@@ -80,12 +80,20 @@ class DrawingReconstructionTests(unittest.TestCase):
self.assertNotIn('__DRAWING_SVG__', source)
self.assertNotIn('__VIEWER_JS__', source)
self.assertIn('function drawingRegions()', source)
before_script = source.split('<script')[0]
self.assertGreater(before_script.count('<polygon'), 900)
scene = source.split('id="scene"', 1)[1].split('</svg>', 1)[0]
self.assertGreater(scene.count('<polygon'), 900)
self.assertTrue((out / 'drawing' / r2c.DEFAULT_DXF.name).is_file())
self.assertEqual(json.loads((out / 'model.json').read_text())['sha256'], m['sha256'])
self.assertTrue((out / 'model.obj').read_text().startswith('# R2C'))
def test_file_entry_redirects_to_configured_server_port(self):
with tempfile.TemporaryDirectory() as directory:
r2c.build(output=directory, update_entry=False, port=8080)
source = (Path(directory) / 'index.html').read_text()
self.assertNotIn('__SERVER_PORT__', source)
self.assertIn("if (location.protocol === 'file:')", source)
self.assertIn("location.replace('http://localhost:8080/' + location.search + location.hash)", source)
def test_web_handler_serves_generated_files_only(self):
class Handler(http.server.SimpleHTTPRequestHandler):
def log_message(self, *args):
+6
View File
@@ -3,6 +3,11 @@
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#111c29">
<title>R2C · DXF 도면 3D 뷰어</title>
<script>
if (location.protocol === 'file:') {
location.replace('http://localhost:__SERVER_PORT__/' + location.search + location.hash);
}
</script>
<style>
:root{--bg:#101b28;--panel:#172635;--border:#2b4052;--fg:#edf3f6;--muted:#9caebb;--mint:#8de1cf;--gold:#edc28a}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}button,a,select{font:inherit}button,select{cursor:pointer}button:focus-visible,a:focus-visible,select:focus-visible{outline:2px solid var(--mint);outline-offset:3px}button{color:inherit}a{color:var(--mint)}.wrap{max-width:1600px;margin:auto;padding:0 28px 30px}header{display:flex;justify-content:space-between;align-items:center;gap:16px;height:82px;border-bottom:1px solid var(--border)}.brand{display:flex;align-items:center;gap:12px}.symbol{width:39px;height:39px;border:1px solid #428379;color:var(--mint);border-radius:11px;display:grid;place-items:center;font-weight:800}.brand strong{display:block;font-size:15px}.brand small{color:var(--muted);font-size:11px}.actions{display:flex;gap:8px;align-items:center}.badge{font-size:10px;color:var(--mint);padding:5px 10px;border:1px solid #345f5b;border-radius:20px}.link,.btn{border:1px solid var(--border);background:var(--panel);padding:8px 12px;border-radius:7px;text-decoration:none;font-size:12px;color:var(--fg);white-space:nowrap}.btn:hover,.link:hover{background:#24394a}.btn.active{background:#204a49;border-color:#4a8b7f;color:#bcf4e6}h1{font-size:25px;font-weight:650;letter-spacing:-.04em;margin:4px 0 6px}.intro{display:flex;justify-content:space-between;align-items:end;margin:25px 0 19px;gap:20px}.eyebrow{color:var(--mint);font-size:10px;font-weight:700;letter-spacing:.15em}.subtitle{color:var(--muted);font-size:12px;margin:0}.intro-note{text-align:right;color:var(--muted);font-size:11px}.workspace{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:18px}.main{min-width:0}.toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}.seg{display:flex;gap:5px;flex-wrap:wrap}.seg .btn{padding:6px 10px;font-size:11px}.stage{position:relative;isolation:isolate;height:clamp(420px,61vh,760px);border:1px solid #465662;border-radius:12px;overflow:hidden;background:radial-gradient(ellipse at 45% 38%,#f5f7f6,#dfe7e9)}#scene{display:block;position:absolute;inset:0;width:100%;height:100%;touch-action:none;cursor:grab}#scene:active{cursor:grabbing}.stage-label{position:absolute;left:17px;top:15px;pointer-events:none;font-size:10px;color:#526673;letter-spacing:.12em;z-index:2}.stage-label strong{display:block;font-size:12px;color:#283e49;letter-spacing:.03em;margin-top:3px}.stage-bottom{position:absolute;left:17px;bottom:14px;color:#526673;font-size:10px;pointer-events:none}.stage-status{position:absolute;right:14px;top:14px;border:1px solid #b7c9cc;background:#f9fcfaca;padding:5px 9px;border-radius:7px;color:#35635f;font-size:10px;pointer-events:none}.zoom{position:absolute;right:13px;bottom:13px;display:flex;gap:5px}.zoom button{background:#f5f8f8e8;border:1px solid #b5c6cd;border-radius:6px;color:#334b59;width:31px;height:30px;font-weight:650}.viewbar{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:10px}.help{font-size:10px;color:var(--muted)}.error{position:absolute;inset:55px 15px auto;z-index:5;padding:12px;color:#7b2f22;background:#ffefdf;border-radius:7px;font-size:12px}.error[hidden]{display:none}.cards{display:flex;flex-direction:column;gap:12px}.card{padding:16px;border:1px solid var(--border);border-radius:10px;background:var(--panel)}.card h2{font-size:12px;margin:0 0 13px;display:flex;justify-content:space-between;align-items:center}.small-tag{font-size:9px;font-weight:400;letter-spacing:.05em;color:var(--mint)}.dims{margin:0;display:grid;grid-template-columns:1fr auto;gap:8px;font-size:12px}.dims dt{color:var(--muted)}.dims dd{margin:0;font-variant-numeric:tabular-nums}.dims dd.main-value{color:var(--mint);font-weight:650}.rule{border:0;border-top:1px solid var(--border);margin:13px 0}.micro{font-size:11px;color:var(--muted);margin:0;line-height:1.7}.legend{display:flex;gap:8px;align-items:center;font-size:11px;margin-bottom:7px}.swatch{width:10px;height:10px;border-radius:3px;background:#bcc8c9}.swatch.bottom{background:#81979d}.swatch.inner{background:#6b8189}.card .btn{width:100%;margin-top:12px;text-align:center}.limits{color:var(--muted);font-size:10px;line-height:1.7;margin-top:10px}.drawing-card{border:1px solid var(--border);border-radius:10px;margin-top:22px;background:var(--panel);overflow:hidden}.drawing-card>summary{cursor:pointer;list-style:none;display:flex;justify-content:space-between;gap:12px;padding:16px;font-size:13px;font-weight:600}.drawing-card>summary::after{content:'+';color:var(--mint)}.drawing-card[open]>summary::after{content:'−'}.drawing-controls{display:flex;gap:7px;flex-wrap:wrap;align-items:center;padding:0 16px 12px}.drawing-controls select{background:var(--bg);border:1px solid var(--border);color:var(--fg);border-radius:6px;padding:6px 9px;font-size:11px}.drawing-viewport{height:440px;background:#f7fafbLine truncated
@media(max-width:1050px){.workspace{grid-template-columns:minmax(0,1fr) 270px}.wrap{padding:0 17px 25px}.intro-note{display:none}.viewbar{align-items:start;flex-direction:column}.stage{height:540px}}@media(max-width:740px){header{height:72px}.badge{display:none}.wrap{padding:0 12px 20px}.workspace{grid-template-columns:1fr}h1{font-size:21px}.intro{margin-top:20px}.stage{height:440px}.cards{display:grid;grid-template-columns:1fr 1fr}.card:last-child{grid-column:1/-1}.drawing-viewport{height:350px}.toolbar{align-items:start}.brand small{display:none}}@media(max-width:440px){.cards{grid-template-columns:1fr}.stage{height:390px}.toolbar{flex-direction:column}.actions .link{font-size:10px;padding:7px}.help{display:none}}
@@ -12,6 +17,7 @@
</style>
</head>
<body><div class="wrap">
<p id="localFileNotice" class="upload-message" hidden role="status">파일로 연 뷰어입니다. 3D 회전과 규격 선택은 가능하며, DXF 첨부 변환은 서버 뷰어에서 사용할 수 있습니다. <a href="http://localhost:8000/">서버 뷰어 열기 →</a><br>서버가 꺼져 있다면 <code>python3 r2c.py serve</code>를 실행하세요. 다른 포트를 사용한다면 해당 서버 주소로 접속하세요.</p>
<header><div class="brand"><div class="symbol">R2</div><div><strong>R2C 도면 스튜디오</strong><small>DXF → 단면 재구성 → 웹 3D</small></div></div><div class="actions"><span class="badge">DXF 엔티티 추출</span><a class="link" id="sourceLink" href="__DXF_HREF__" download>원본 DXF ↓</a><button class="btn" id="download">3D 모델 ↓</button></div></header>
<div class="intro"><div><div class="eyebrow">DRAWING / RECONSTRUCTION</div><h1>사각 입구에서 원형 관로까지</h1><p class="subtitle">R2C관의 전환부와 소켓을 원본 단면으로 확인하세요.</p></div><div class="intro-note">모든 치수는 mm 기준<br>원본 A-A / B-B 단면 및 변화치수표 반영</div></div>
<section class="upload-zone" id="dropZone" aria-label="DXF 첨부 영역"><div><strong>DXF 파일을 첨부하거나 여기에 놓으세요</strong><p>R2C 일반도의 단면·변화치수표를 읽어 바로 3D로 변환합니다. · 최대 10 MB</p><p>첨부 파일은 변환 후 서버에 저장하지 않습니다.</p></div><div class="upload-controls"><input type="file" id="dxfFile" accept=".dxf" hidden><button class="btn" id="uploadButton">DXF 파일 첨부</button><button class="btn" id="sampleReset">기본 도면으로</button></div></section><p class="upload-message" id="uploadMessage" role="status" aria-live="polite" hidden></p>
+1
View File
@@ -6,6 +6,7 @@ let sourceDocument=initialDocument,data=initialDocument,selectedDiameter=initial
const initialDrawing=document.querySelector('.drawing-viewport').innerHTML,initialSource=document.getElementById('sourceLink').getAttribute('href');
let sourceUrl=null,uploadBusy=false;
const specification=document.getElementById('specification');
document.getElementById('localFileNotice').hidden=location.protocol!=='file:';
const tableKeys=['D','D1','D2','L','L1','L2','L3','B','b1','b2','b3','b4','H','h1','h2','h3','h4','h5','h6','h7'];
let model='r2c',mode='closed',yaw=.87,pitch=.40,zoom=1,wire=false,drag=null,pending=false;
const fmt=n=>Math.round(n).toLocaleString('ko-KR');
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
@echo off
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0launch-viewer.ps1"
if errorlevel 1 pause
BIN
View File
Binary file not shown.