diff --git a/.astra/project-context/architecture.md b/.astra/project-context/architecture.md index ce02838..edc1194 100644 --- a/.astra/project-context/architecture.md +++ b/.astra/project-context/architecture.md @@ -3,15 +3,15 @@ ## Snapshot -- **Workspace**: `ConnectAI` `v2.2.12` _(absolute path varies by environment; resolved from the active VS Code workspace)_ +- **Workspace**: `ConnectAI` `v2.2.13` _(absolute path varies by environment; resolved from the active VS Code workspace)_ - **Description**: The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making. - **Stack**: TypeScript, Node.js, VS Code Extension, LM Studio SDK, Test runner -- **Stats**: 224 source files, ~45,925 lines across 5 top-level modules. +- **Stats**: 224 source files, ~46,311 lines across 5 top-level modules. ## Last Refresh -- **Time**: 2026-05-16T03:37:49.405Z -- **Files newly analysed**: 0 -- **Files reused from cache**: 224 +- **Time**: 2026-05-16T04:18:55.379Z +- **Files newly analysed**: 2 +- **Files reused from cache**: 222 ## Directory Map ```mermaid @@ -64,7 +64,7 @@ flowchart LR ## Modules -### `src/` — 110 files, ~30,926 lines +### `src/` — 110 files, ~31,312 lines **Sub-directories** - `src/features/` (37) — 기본 에이전트 로스터 — 1인 기업 모드의 출고 디폴트. 설계 의도: 소프트웨어/게임 개발 IT 회사의 1인 기업 운영을 가정. 한 사람이 기획 → 디자인 → 개발 → QA → 출시 → 운영/마케팅을 모두 책임질 때 @@ -87,7 +87,7 @@ flowchart LR - `src/core/services.ts` (164 lines) - `src/lib/paths.ts` (151 lines) - `src/features/company/companyConfig.ts` (896 lines) — State + config plumbing for 1인 기업 모드. Two surfaces: - CompanyState (runtime data: enabled flag, company name, which agents are active, per-agent model overrides). Persisted in VS Code's globalState so -- `src/sidebarProvider.ts` (5119 lines) +- `src/sidebarProvider.ts` (5505 lines) - `src/memory/types.ts` (126 lines) — Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic - `src/retrieval/scoring.ts` (518 lines) — Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다. - `src/skills/agentKnowledgeMap.ts` (374 lines) @@ -319,7 +319,7 @@ Astra는 대표님의 명시적인 승인 하에 로컬 시스템의 강력한 **Designed for High-Performance Decision Making.** Copyright (C) **g1nation**. All rights reserved. -_Last auto-scan: 2026-05-16T03:37:49.405Z · signature `2e3db319`_ +_Last auto-scan: 2026-05-16T04:18:55.379Z · signature `b1025fa`_ ## Purpose diff --git a/.astra/project-context/scan-cache.json b/.astra/project-context/scan-cache.json index 7e1017d..497782c 100644 --- a/.astra/project-context/scan-cache.json +++ b/.astra/project-context/scan-cache.json @@ -1,6 +1,6 @@ { "version": 1, - "generatedAt": "2026-05-16T03:37:49.410Z", + "generatedAt": "2026-05-16T04:18:55.389Z", "files": { "src/agent.ts": { "mtimeMs": 1778902489000, @@ -1030,9 +1030,9 @@ ] }, "src/sidebarProvider.ts": { - "mtimeMs": 1778902489000, - "size": 228676, - "lines": 5119, + "mtimeMs": 1778904191000, + "size": 245949, + "lines": 5505, "role": "", "imports": [ "src/utils", @@ -1638,8 +1638,8 @@ "imports": [] }, "docs/records/ConnectAI/chronicle.config.json": { - "mtimeMs": 1778902507000, - "size": 371, + "mtimeMs": 1778902789000, + "size": 416, "lines": 11, "role": "JSON configuration", "imports": [] diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index cd52e52..fee384f 100644 --- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json +++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1778902731275, + "createdAt": 1778905142810, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json index 959f4e0..383041a 100644 --- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json +++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1778902731267, + "createdAt": 1778905142809, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json index d5611f2..f189851 100644 --- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json +++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json @@ -1,5 +1,5 @@ { "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", - "createdAt": 1778902731263, + "createdAt": 1778905142808, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json index 284e58e..826a7bd 100644 --- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json +++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json @@ -1,5 +1,5 @@ { - "result": "---\nid: stress_conflict_1778902731247\ndate: 2026-05-16T03:38:51.279Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (5ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (8ms)\n", - "createdAt": 1778902731279, + "result": "---\nid: stress_conflict_1778905142797\ndate: 2026-05-16T04:19:02.810Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (1ms)\n", + "createdAt": 1778905142810, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1778902731247.json b/.astra/tests/stress/.astra/missions/stress_conflict_1778905142797.json similarity index 80% rename from .astra/tests/stress/.astra/missions/stress_conflict_1778902731247.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1778905142797.json index b3f038f..0fd7956 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1778902731247.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1778905142797.json @@ -1,8 +1,8 @@ { - "missionId": "stress_conflict_1778902731247", + "missionId": "stress_conflict_1778905142797", "status": "completed", - "startTime": "2026-05-16T03:38:51.247Z", - "totalElapsedMs": 33, + "startTime": "2026-05-16T04:19:02.797Z", + "totalElapsedMs": 13, "results": { "planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", "researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", @@ -18,28 +18,28 @@ "to": "planner", "durationMs": 11, "message": "전략 수립 중...", - "ts": "2026-05-16T03:38:51.258Z" + "ts": "2026-05-16T04:19:02.808Z" }, { "from": "planner", "to": "researcher", - "durationMs": 5, + "durationMs": 1, "message": "핵심 정보 수집 및 분석 중...", - "ts": "2026-05-16T03:38:51.263Z" + "ts": "2026-05-16T04:19:02.809Z" }, { "from": "researcher", "to": "writer", - "durationMs": 8, + "durationMs": 1, "message": "최종 리포트 작성 및 편집 중...", - "ts": "2026-05-16T03:38:51.271Z" + "ts": "2026-05-16T04:19:02.810Z" }, { "from": "writer", "to": "completed", - "durationMs": 9, + "durationMs": 0, "message": "미션 완료", - "ts": "2026-05-16T03:38:51.280Z" + "ts": "2026-05-16T04:19:02.810Z" } ], "resilienceMetrics": { diff --git a/PATCHNOTES.md b/PATCHNOTES.md index 3803dd1..bdaedb1 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,5 +1,17 @@ # Astra Patch Notes +## v2.2.14 (2026-05-16) +### 🎭 Advanced Pixel Office Customization & Face Directions +- **캐릭터 방향성 고도화:** 기존 좌우(Left/Right) 방향에 더해 상하(Up/Down) 방향 스프라이트 지원을 추가하여 더욱 다채로운 사무실 연출이 가능해졌습니다. +- **캐릭터-책상 독립 제어:** 책상은 유지하면서 캐릭터만 삭제하거나, 캐릭터가 없는 책상에 새 팀원을 다시 추가할 수 있는 기능을 도입했습니다. +- **레이아웃 스키마 V2 도입:** 캐릭터 존재 여부 및 정밀한 위치/회전 정보를 보존하는 최신 레이아웃 스냅샷 엔진을 적용했습니다. +- **UI/UX 편의성 개선:** 속성 패널에서 방향 전환과 캐릭터 추가/삭제가 즉시 반영되도록 인터랙션을 강화했습니다. +- **신규 패키징:** `astra-2.2.14.vsix` 패키지를 통해 더욱 유연해진 사무실 모델링 환경을 제공합니다. + +--- + + + ## v2.2.13 (2026-05-16) ### 🏗️ Pixel Office Interactive Editor & Core Refinement - **픽셀 오피스 편집 모드 도입:** 책상 추가/삭제, 에이전트 매핑 변경, 가구(프랍) 배치 및 크기 조절이 가능한 인터랙티브 편집 기능을 추가했습니다. diff --git a/docs/records/ConnectAI/chronicle.config.json b/docs/records/ConnectAI/chronicle.config.json index 32e4e44..c8ea6fc 100644 --- a/docs/records/ConnectAI/chronicle.config.json +++ b/docs/records/ConnectAI/chronicle.config.json @@ -7,5 +7,5 @@ "corePurpose": "", "detailLevel": "standard", "createdAt": "2026-05-13T13:09:33.788Z", - "updatedAt": "2026-05-16T03:39:49.319Z" + "updatedAt": "2026-05-16T04:20:09.223Z" } diff --git a/package.json b/package.json index bb3f957..6e73078 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.2.13", + "version": "2.2.14", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 36eea21..d07b9f0 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -4091,12 +4091,17 @@ function addChar(st){ chars[st.key]=ch; anim[st.key]={row:st.charRow,frame:0,dir:0,mode:'sit',face:st.face,route:0}; const img=ch.querySelector('img'); - img.src=png('idle-r'+st.charRow+'-f0'); - img.style.transform=st.face==='R'?'scaleX(-1)':'none'; + if(st.face === 'U' || st.face === 'D'){ + img.src=png('walk-r'+st.charRow+'-d'+_faceToWalkDir(st.face)+'-f0'); + img.style.transform='none'; + } else { + img.src=png('idle-r'+st.charRow+'-f0'); + img.style.transform=st.face==='R'?'scaleX(-1)':'none'; + } return ch; } -function buildStation(st){ addDesk(st); addChar(st); } +function buildStation(st){ addDesk(st); if(!st.noChar) addChar(st); } function _renderDefaultStations(){ // default 8 stations + default props. @@ -4114,8 +4119,17 @@ const DIR_DOWN = 0; // 정면 (카메라/사용자 쪽) const DIR_LEFT = 1; const DIR_RIGHT = 2; const DIR_UP = 3; // 뒤 +// idle/work 의 정면(D) / 후면(U) sprite 는 별도로 없으므로 walk 의 같은 방향 +// frame 0 (정지 포즈) 를 그대로 빌려쓴다. +function _faceToWalkDir(face){ + if(face === 'D') return DIR_DOWN; + if(face === 'U') return DIR_UP; + if(face === 'L') return DIR_LEFT; + return DIR_RIGHT; +} function setSprite(role,mode,frame=0,dir=0){ - const ch=chars[role],a=anim[role]; + const ch=chars[role]; if(!ch) return; + const a=anim[role]; if(!a) return; a.mode=mode;a.frame=frame;a.dir=dir; ch.classList.toggle('walking',mode==='walk'); const img=ch.querySelector('img'); @@ -4123,6 +4137,10 @@ function setSprite(role,mode,frame=0,dir=0){ // 4방향 walk sprite — 좌우 sprite를 따로 제공하므로 scaleX 반전은 *안* 한다. img.src=png('walk-r'+a.row+'-d'+dir+'-f'+frame); img.style.transform='none'; + } else if(a.face === 'U' || a.face === 'D'){ + // 위/아래 face 는 idle/work sprite 가 없으므로 walk 의 같은 방향 정지 포즈로. + img.src=png('walk-r'+a.row+'-d'+_faceToWalkDir(a.face)+'-f0'); + img.style.transform='none'; } else if(mode==='work'){ img.src=png('work-r'+a.row+'-f'+frame); img.style.transform=(a.face==='R'?'scaleX(-1)':'none'); @@ -4132,8 +4150,9 @@ function setSprite(role,mode,frame=0,dir=0){ } } function move(role,x,y){ - const ch=chars[role],a=anim[role], - cx=parseFloat(ch.style.left),cy=parseFloat(ch.style.top), + const ch=chars[role]; if(!ch) return; // 캐릭터 삭제된 자리면 무시. + const a=anim[role]; if(!a) return; + const cx=parseFloat(ch.style.left),cy=parseFloat(ch.style.top), dx=x-cx,dy=y-cy; // 주축 결정: 큰 쪽을 우선해 4방향 중 하나로 매핑. 동률이면 가로 우선. let dir; @@ -4232,8 +4251,22 @@ function walkPath(role,points,done,route){ const tail=planned.slice(1).concat(rest); setTimeout(()=>walkPath(role,tail,done,token),950); } -function sendHome(role,mode='sit'){const st=stationByKey[role],ch=chars[role],hx=st.seatX,hy=st.seatY,cx=parseFloat(ch.style.left),cy=parseFloat(ch.style.top);anim[role].route++;if(Math.abs(cx-hx)<1&&Math.abs(cy-hy)<1){setSprite(role,mode);return;}walkPath(role,[st.dock,[hx,hy]],()=>setSprite(role,mode))} -setInterval(()=>{const free=Object.keys(chars).filter(k=>anim[k].mode==='sit'&&!chars[k].classList.contains('active'));if(!free.length)return;const k=free[Math.floor(Math.random()*free.length)],st=stationByKey[k],pt=st.roam[Math.floor(Math.random()*st.roam.length)];walkPath(k,[st.dock,pt,st.dock,[st.seatX,st.seatY]],()=>setSprite(k,'sit'))},5600) +function sendHome(role,mode='sit'){ + const st=stationByKey[role],ch=chars[role]; + if(!st || !ch) return; // 책상/캐릭터 부재 시 no-op. + const hx=st.seatX,hy=st.seatY,cx=parseFloat(ch.style.left),cy=parseFloat(ch.style.top); + anim[role].route++; + if(Math.abs(cx-hx)<1&&Math.abs(cy-hy)<1){setSprite(role,mode);return;} + walkPath(role,[st.dock,[hx,hy]],()=>setSprite(role,mode)); +} +setInterval(()=>{ + const free=Object.keys(chars).filter(k=>anim[k]?.mode==='sit'&&!chars[k].classList.contains('active')); + if(!free.length)return; + const k=free[Math.floor(Math.random()*free.length)],st=stationByKey[k]; + if(!st || !Array.isArray(st.roam) || !st.roam.length || !Array.isArray(st.dock)) return; + const pt=st.roam[Math.floor(Math.random()*st.roam.length)]; + walkPath(k,[st.dock,pt,st.dock,[st.seatX,st.seatY]],()=>setSprite(k,'sit')); +},5600); function activate(role){Object.keys(chars).forEach(k=>chars[k].classList.toggle('active',k===role))} function bubble(role,text){const ch=chars[role];if(!ch||!text)return;const b=document.createElement('div');b.className='bubble';b.textContent=text;b.style.left=(parseFloat(ch.style.left)+28)+'px';b.style.top=(parseFloat(ch.style.top)-6)+'px';stage.appendChild(b);setTimeout(()=>b.remove(),1600)} // ── A. 상태 계층화 ── @@ -4491,26 +4524,30 @@ function _snapshotLayout(){ // _restoreLayout 이 양쪽 모두 처리. return { schema: 2, - cells: stations.map(st=>({ - roleKey: st.key, - agentKey: st.agentKey || '', - label: st.label || '', - charRow: st.charRow ?? 0, - deskSprite: st.deskSprite || 'desk-main', - face: st.face || 'R', - boss: !!st.boss, - dock: st.dock, - roam: st.roam, - deskX: parseFloat(__deskWrap[st.key].style.left), - deskY: parseFloat(__deskWrap[st.key].style.top), - deskW: parseFloat(__deskWrap[st.key].style.width), - deskRot: parseFloat(__deskWrap[st.key].dataset.rot || '0'), - deskZ: parseFloat(__deskWrap[st.key].dataset.z || '0'), - seatX: parseFloat(chars[st.key].style.left), - seatY: parseFloat(chars[st.key].style.top), - charRot: parseFloat(chars[st.key].dataset.rot || '0'), - charZ: parseFloat(chars[st.key].dataset.z || '0'), - })), + cells: stations.map(st=>{ + const ch = chars[st.key]; + return { + roleKey: st.key, + agentKey: st.agentKey || '', + label: st.label || '', + charRow: st.charRow ?? 0, + deskSprite: st.deskSprite || 'desk-main', + face: st.face || 'R', + boss: !!st.boss, + noChar: !!st.noChar, + dock: st.dock, + roam: st.roam, + deskX: parseFloat(__deskWrap[st.key].style.left), + deskY: parseFloat(__deskWrap[st.key].style.top), + deskW: parseFloat(__deskWrap[st.key].style.width), + deskRot: parseFloat(__deskWrap[st.key].dataset.rot || '0'), + deskZ: parseFloat(__deskWrap[st.key].dataset.z || '0'), + seatX: ch ? parseFloat(ch.style.left) : (st.seatX ?? 0), + seatY: ch ? parseFloat(ch.style.top) : (st.seatY ?? 0), + charRot: ch ? parseFloat(ch.dataset.rot || '0') : 0, + charZ: ch ? parseFloat(ch.dataset.z || '0') : 0, + }; + }), objs: Array.from(stage.querySelectorAll('img.obj')).map(el=>({ id: el.dataset.objId, name: el.dataset.objName, @@ -4564,6 +4601,7 @@ function _restoreLayout(snap){ deskSprite: c.deskSprite || 'desk-main', face: c.face || 'R', boss: !!c.boss, + noChar: !!c.noChar, dock: Array.isArray(c.dock) ? c.dock : [c.deskX+32, c.deskY+80], roam: Array.isArray(c.roam) ? c.roam : [[c.deskX, c.deskY+120],[c.deskX+60, c.deskY+100]], deskX: c.deskX, deskY: c.deskY, deskW: c.deskW || 112, @@ -4684,13 +4722,20 @@ function _renderDeskProps(deskEl){ for(let r=0;r<8;r++){ thumbs += '
'; } + const hasChar = !!chars[role]; panel.innerHTML = '

책상 속성

'+ '
'+ '
'+ '
'+ + (hasChar ? '' : '
')+ '
'+thumbs+'
'+ - '
'; + '
'; // 핸들러 panel.querySelector('#ppLabel').oninput = (ev)=>{ st.label = ev.target.value; @@ -4718,8 +4763,17 @@ function _renderDeskProps(deskEl){ panel.querySelector('#ppFace').onchange = (ev)=>{ st.face = ev.target.value; if(anim[role]) anim[role].face = st.face; - const ch = chars[role]; if(ch){ const img=ch.querySelector('img'); if(img) img.style.transform = st.face==='R'?'scaleX(-1)':'none'; } + // 현재 mode 기준 sprite 즉시 다시 그리기 — U/D 도 한 번에 반영. + const a = anim[role]; if(a) setSprite(role, a.mode || 'sit', 0, 0); }; + const addCharBtn = panel.querySelector('#ppAddChar'); + if(addCharBtn){ + addCharBtn.onclick = ()=>{ + st.noChar = false; + addChar(st); + _renderDeskProps(deskEl); // 패널 재렌더 — "캐릭터 추가" 버튼 제거. + }; + } } function _renderObjProps(el){ @@ -4795,30 +4849,43 @@ function _openPropPicker(){ } // ── 선택 항목 삭제 ── +// 캐릭터 / 책상 / 프랍 각각 분리해서 처리: +// · 캐릭터 선택 → 캐릭터만 삭제 (책상은 유지). 속성 패널에서 "+ 캐릭터" 로 재추가 가능. +// · 책상 선택 → 책상 + 캐릭터 모두 삭제 (station 자체 제거). +// · 프랍 선택 → 프랍 삭제. function _deleteSelected(){ if(!_editMode || !_selected) return; - // char 가 선택돼 있으면 그 desk 도 함께 묶어서 처리. - let target = _selected; - if(target.classList.contains('char')){ - const role = Object.keys(chars).find(k=>chars[k]===target); - if(role && __deskWrap[role]) target = __deskWrap[role]; + if(_selected.classList.contains('char')){ + const role = Object.keys(chars).find(k=>chars[k]===_selected); + if(!role) return; + const st = stationByKey[role]; + if(!confirm('"'+(st?.label||role)+'" 책상의 캐릭터를 삭제할까요? 책상은 그대로 남습니다.')) return; + _selected.remove(); + delete chars[role]; delete anim[role]; + if(st) st.noChar = true; + _selected = null; + _onSelectionChanged(); + return; } - if(target.classList.contains('desk')){ - const role = target.dataset.role; - if(!confirm('"'+(stationByKey[role]?.label||role)+'" 책상을 삭제할까요? 이 자리에 매핑된 에이전트도 자리가 사라집니다.')) return; - target.remove(); + if(_selected.classList.contains('desk')){ + const role = _selected.dataset.role; + if(!confirm('"'+(stationByKey[role]?.label||role)+'" 책상을 삭제할까요? 캐릭터도 함께 사라집니다.')) return; + _selected.remove(); if(chars[role]) chars[role].remove(); delete __deskWrap[role]; delete chars[role]; delete anim[role]; const idx = stations.findIndex(s=>s.key===role); if(idx>=0) stations.splice(idx,1); _rebuildStationIndex(); - } else if(target.classList.contains('obj')){ - target.remove(); - } else { + _selected = null; + _onSelectionChanged(); + return; + } + if(_selected.classList.contains('obj')){ + _selected.remove(); + _selected = null; + _onSelectionChanged(); return; } - _selected = null; - _onSelectionChanged(); } function _findDraggable(el){