|
38 | 38 | .mbtn:hover{border-color:var(--cyan);color:var(--txt);} |
39 | 39 | .mbtn.active{background:#0a2030;border-color:var(--cyan);color:var(--cyan);} |
40 | 40 | #run-btn{ |
41 | | - margin-left:auto;padding:5px 16px; |
| 41 | + padding:5px 16px; |
42 | 42 | background:linear-gradient(135deg,#003828,#002a20); |
43 | 43 | border:1px solid #00e89055;color:var(--green);border-radius:4px; |
44 | 44 | font-size:11.5px;font-weight:700;transition:.15s; |
|
47 | 47 | #run-btn:hover{background:linear-gradient(135deg,#004838,#003428);box-shadow:0 0 14px #00e89035;} |
48 | 48 | #run-btn:disabled{opacity:.5;cursor:default;} |
49 | 49 | #run-btn.running{color:var(--orange);border-color:#ff6b3555;text-shadow:0 0 8px var(--orange);} |
| 50 | +#file-btns{display:flex;gap:4px;margin-left:auto;} |
| 51 | +.fbtn{padding:4px 10px;background:var(--bg3);border:1px solid var(--b2);color:var(--mid);border-radius:3px;font-size:11px;transition:.15s;white-space:nowrap;} |
| 52 | +.fbtn:hover{border-color:var(--cyan);color:var(--cyan);} |
| 53 | + |
50 | 54 | #wasm-badge{ |
51 | 55 | font-size:10px;color:var(--dim);padding:2px 9px; |
52 | 56 | border:1px solid var(--b1);border-radius:10px;display:flex;align-items:center;gap:5px; |
|
117 | 121 | /* ── 리사이즈 핸들 ── */ |
118 | 122 | .rv{width:4px;background:var(--b1);cursor:col-resize;flex-shrink:0;transition:.15s;} |
119 | 123 | .rv:hover,.rv.drag{background:var(--cyan);} |
120 | | -#bot-resize{height:4px;background:var(--b1);cursor:row-resize;flex-shrink:0;transition:.15s;} |
121 | | -#bot-resize:hover,#bot-resize.drag{background:var(--cyan);} |
122 | 124 |
|
123 | 125 | /* ── 아웃라인 ── */ |
124 | 126 | #outline{ |
|
154 | 156 | border-radius:3px;padding:4px 7px;font-size:11px;color:var(--txt); |
155 | 157 | outline:none;font-family:monospace;transition:.15s; |
156 | 158 | } |
157 | | -.prp-div{border-top:1px solid var(--b1);margin:8px 0;} |
| 159 | +.prp-del{width:100%;padding:5px;background:#1a0808;border:1px solid var(--red)44;color:var(--red);border-radius:3px;font-size:11px;margin-top:6px;transition:.15s;} |
| 160 | +.prp-del:hover{background:var(--red);color:#fff;border-color:var(--red);} |
158 | 161 | .prp-uid{display:flex;justify-content:space-between;font-size:9px;color:var(--dim);padding:2px 0;} |
159 | 162 |
|
160 | 163 | /* ── AI 패널 ── */ |
|
293 | 296 |
|
294 | 297 | /* ── 하단 ── */ |
295 | 298 | #bottom{ |
296 | | - height:var(--botH);min-height:60px;background:var(--panel); |
| 299 | + height:var(--botH);min-height:60px;max-height:70vh;background:var(--panel); |
297 | 300 | border-top:1px solid var(--b1);display:flex;flex-direction:column;flex-shrink:0; |
| 301 | + overflow:hidden; |
| 302 | +} |
| 303 | +#bot-resize{ |
| 304 | + height:5px;background:var(--b1);cursor:row-resize;flex-shrink:0; |
| 305 | + transition:.15s;border-top:1px solid var(--b1); |
298 | 306 | } |
| 307 | +#bot-resize:hover,#bot-resize.drag{background:var(--cyan);box-shadow:0 0 6px var(--cyan)44;} |
| 308 | + |
299 | 309 | #tabs{ |
300 | 310 | display:flex;align-items:center;background:#060a14; |
301 | 311 | border-bottom:1px solid var(--b1);padding:0 8px;gap:2px;flex-shrink:0; |
|
361 | 371 | <button id="mt" class="mbtn" onclick="setMode('text')">≡ 텍스트</button> |
362 | 372 | </div> |
363 | 373 | <button id="run-btn" onclick="handleRun()">▶ 실행</button> |
| 374 | + <div id="file-btns"> |
| 375 | + <button class="fbtn" onclick="newFile()" title="새 파일 (Ctrl+N)">□ 새 파일</button> |
| 376 | + <button class="fbtn" onclick="saveFile()" title="저장 (Ctrl+S)">↓ 저장</button> |
| 377 | + <button class="fbtn" onclick="document.getElementById('file-open-inp').click()" title="열기 (Ctrl+O)">↑ 열기</button> |
| 378 | + </div> |
| 379 | + <input id="file-open-inp" type="file" accept=".han,.txt,.kcode" style="display:none" onchange="openFile(event)"> |
364 | 380 | <div id="wasm-badge"> |
365 | 381 | <span id="wasm-dot"></span> |
366 | 382 | <span id="wasm-txt">WASM 로딩...</span> |
@@ -979,8 +995,85 @@ <h3>🤖 AI 어시스턴트 선택</h3> |
979 | 995 | } |
980 | 996 |
|
981 | 997 | /* ================================================================ |
982 | | - 렌더 함수들 |
| 998 | + 파일 관리 — 새 파일 / 저장 / 열기 |
| 999 | + ================================================================ */ |
| 1000 | +function newFile(){ |
| 1001 | + if(ws.length>0){ |
| 1002 | + if(!confirm('현재 작업을 모두 지우고 새 파일을 만드시겠습니까?'))return; |
| 1003 | + } |
| 1004 | + ws=[];selUid=null;consoleLog=[];errLog=[]; |
| 1005 | + const ta=document.getElementById('text-area'); |
| 1006 | + if(ta)ta.value=''; |
| 1007 | + renderAll(); |
| 1008 | + consoleLog.push({t:'info',s:'□ 새 파일'}); |
| 1009 | + renderTabContent(); |
| 1010 | +} |
| 1011 | + |
| 1012 | +function saveFile(){ |
| 1013 | + let content=''; |
| 1014 | + let filename='main.han'; |
| 1015 | + if(editorMode==='text'){ |
| 1016 | + content=document.getElementById('text-area').value||''; |
| 1017 | + }else{ |
| 1018 | + content=ws.map(b=>blockToHan(b,0)).join(''); |
| 1019 | + } |
| 1020 | + const blob=new Blob([content],{type:'text/plain;charset=utf-8'}); |
| 1021 | + const a=document.createElement('a'); |
| 1022 | + a.href=URL.createObjectURL(blob); |
| 1023 | + a.download=filename; |
| 1024 | + a.click(); |
| 1025 | + URL.revokeObjectURL(a.href); |
| 1026 | + consoleLog.push({t:'info',s:`↓ 저장: ${filename}`}); |
| 1027 | + renderTabContent(); |
| 1028 | +} |
| 1029 | + |
| 1030 | +function openFile(e){ |
| 1031 | + const file=e.target.files?.[0];if(!file)return; |
| 1032 | + const reader=new FileReader(); |
| 1033 | + reader.onload=ev=>{ |
| 1034 | + const content=ev.target.result||''; |
| 1035 | + /* 텍스트 모드로 전환하여 내용 표시 */ |
| 1036 | + setMode('text'); |
| 1037 | + document.getElementById('text-area').value=content; |
| 1038 | + consoleLog.push({t:'info',s:`↑ 열기: ${file.name}`}); |
| 1039 | + renderTabContent(); |
| 1040 | + }; |
| 1041 | + reader.readAsText(file,'UTF-8'); |
| 1042 | + e.target.value=''; /* 같은 파일 재선택 가능하도록 초기화 */ |
| 1043 | +} |
| 1044 | + |
| 1045 | +/* ================================================================ |
| 1046 | + 블록 삭제 |
| 1047 | + ================================================================ */ |
| 1048 | +function deleteBlock(uid){ |
| 1049 | + function remove(bls){return bls.filter(b=>b.uid!==uid).map(b=>({...b,children:remove(b.children||[])}));} |
| 1050 | + ws=remove(ws); |
| 1051 | + selUid=null; |
| 1052 | + renderAll(); |
| 1053 | +} |
| 1054 | + |
| 1055 | +/* ================================================================ |
| 1056 | + 키보드 단축키 |
983 | 1057 | ================================================================ */ |
| 1058 | +document.addEventListener('keydown',e=>{ |
| 1059 | + const tag=document.activeElement?.tagName; |
| 1060 | + const isInput=tag==='INPUT'||tag==='TEXTAREA'; |
| 1061 | + |
| 1062 | + /* 블록 삭제: Delete / Backspace (입력창 제외) */ |
| 1063 | + if(!isInput&&(e.key==='Delete'||e.key==='Backspace')&&selUid!=null){ |
| 1064 | + e.preventDefault(); |
| 1065 | + deleteBlock(selUid); |
| 1066 | + return; |
| 1067 | + } |
| 1068 | + /* Ctrl+N: 새 파일 */ |
| 1069 | + if(e.ctrlKey&&e.key==='n'){e.preventDefault();newFile();return;} |
| 1070 | + /* Ctrl+S: 저장 */ |
| 1071 | + if(e.ctrlKey&&e.key==='s'){e.preventDefault();saveFile();return;} |
| 1072 | + /* Ctrl+O: 열기 */ |
| 1073 | + if(e.ctrlKey&&e.key==='o'){e.preventDefault();document.getElementById('file-open-inp').click();return;} |
| 1074 | + /* ESC: 선택 해제 */ |
| 1075 | + if(!isInput&&e.key==='Escape'){selUid=null;renderAll();return;} |
| 1076 | +}); |
984 | 1077 | function renderMenus(){ |
985 | 1078 | const el=document.getElementById('menus'); |
986 | 1079 | el.style.cssText='display:flex;align-items:center;';el.innerHTML=''; |
@@ -1103,6 +1196,11 @@ <h3>🤖 AI 어시스턴트 선택</h3> |
1103 | 1196 | const dv=document.createElement('div');dv.className='prp-div';el.appendChild(dv); |
1104 | 1197 | const uid=document.createElement('div');uid.className='prp-uid'; |
1105 | 1198 | uid.innerHTML=`<span>uid</span><span style="color:var(--dim);">#${sb.uid}</span>`;el.appendChild(uid); |
| 1199 | + /* 삭제 버튼 */ |
| 1200 | + const del=document.createElement('button');del.className='prp-del'; |
| 1201 | + del.textContent='✕ 블록 삭제'; |
| 1202 | + del.onclick=()=>deleteBlock(sb.uid); |
| 1203 | + el.appendChild(del); |
1106 | 1204 | } |
1107 | 1205 |
|
1108 | 1206 | function renderAI(){ |
@@ -1448,13 +1546,31 @@ <h3>🤖 AI 어시스턴트 선택</h3> |
1448 | 1546 | const bh=document.getElementById('bot-resize'); |
1449 | 1547 | if(bh){ |
1450 | 1548 | let startY,startH; |
1451 | | - bh.addEventListener('mousedown',e=>{ |
1452 | | - startY=e.clientY;startH=document.getElementById('bottom').getBoundingClientRect().height; |
1453 | | - bh.classList.add('drag');document.body.style.cursor='row-resize';document.body.style.userSelect='none'; |
1454 | | - const onMove=e=>{const delta=startY-e.clientY;const newH=Math.max(60,Math.min(window.innerHeight*0.65,startH+delta));document.getElementById('bottom').style.height=newH+'px';}; |
1455 | | - const onUp=()=>{bh.classList.remove('drag');document.body.style.cursor='';document.body.style.userSelect='';document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);}; |
1456 | | - document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp); |
1457 | | - }); |
| 1549 | + const startDrag=e=>{ |
| 1550 | + e.preventDefault(); |
| 1551 | + startY=e.clientY??(e.touches?.[0]?.clientY); |
| 1552 | + startH=document.getElementById('bottom').getBoundingClientRect().height; |
| 1553 | + bh.classList.add('drag'); |
| 1554 | + document.body.style.cursor='row-resize'; |
| 1555 | + document.body.style.userSelect='none'; |
| 1556 | + }; |
| 1557 | + const onMove=e=>{ |
| 1558 | + const y=e.clientY??(e.touches?.[0]?.clientY); |
| 1559 | + const delta=startY-y; |
| 1560 | + const newH=Math.max(60,Math.min(window.innerHeight*0.70,startH+delta)); |
| 1561 | + document.getElementById('bottom').style.height=newH+'px'; |
| 1562 | + }; |
| 1563 | + const onUp=()=>{ |
| 1564 | + bh.classList.remove('drag'); |
| 1565 | + document.body.style.cursor=''; |
| 1566 | + document.body.style.userSelect=''; |
| 1567 | + document.removeEventListener('mousemove',onMove); |
| 1568 | + document.removeEventListener('mouseup',onUp); |
| 1569 | + document.removeEventListener('touchmove',onMove); |
| 1570 | + document.removeEventListener('touchend',onUp); |
| 1571 | + }; |
| 1572 | + bh.addEventListener('mousedown',e=>{startDrag(e);document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}); |
| 1573 | + bh.addEventListener('touchstart',e=>{startDrag(e);document.addEventListener('touchmove',onMove,{passive:false});document.addEventListener('touchend',onUp);},{passive:false}); |
1458 | 1574 | } |
1459 | 1575 | } |
1460 | 1576 |
|
|
0 commit comments