claude-ai-office 0.25.6 โ†’ 0.29.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -61,7 +61,7 @@ node bin/ai-office.js # or: npm run install-hooks && npm start
61
61
 
62
62
  | Area | Contents |
63
63
  |---|---|
64
- | **Left: Floors** | One floor per git project (repository root). Worktrees share the floor and show their branch (โއ). Opening a floor lists its sessions with `waiting` and `viewing` badges. ๐ŸŽจ on the selected floor picks that floor's interior preset (Warm Wood ยท Cool Gray ยท Mint ยท Lavender ยท Sand ยท Pastel; assigned automatically by default). "Follow the latest session" (`A`) moves the selection to the most recently active session; when it is off, the last floor and session you looked at are restored next time |
64
+ | **Left: Floors** | One floor per git project (repository root). Worktrees share the floor and show their branch (โއ). Opening a floor lists its sessions with `waiting` and `viewing` badges. By default only the selected floor is open; **Expand all โ†” Collapse all** in the header opens or closes every floor's session list at once (clicking a floor while all are collapsed opens that floor; remembered in the browser). ๐ŸŽจ on the selected floor picks that floor's interior preset (Warm Wood ยท Cool Gray ยท Mint ยท Lavender ยท Sand ยท Pastel; assigned automatically by default). "Follow the latest session" (`A`) moves the selection to the most recently active session; when it is off, the last floor and session you looked at are restored next time |
65
65
  | **Center: Office** | Drag to pan, wheel or buttons to zoom, `โŸฒ` to reset, pinch on touch screens. Click a character to select its session; hover for a tooltip (current action, target file, prompt, branch, path). ๐Ÿ“ **Follow** (`F`) keeps the camera on the selected session's Claude. ๐Ÿข **Building view** (`O`) appears when there are two or more floors |
66
66
  | **Right panel** | Session card (project path ยท session id ยท character name ยท copy the resume command ยท ๐Ÿ—‘ dismiss ended sessions), people in the office (room ยท speech bubble ยท elapsed time), activity log (search with `/`, selected session only), legend (click the title to collapse) |
67
67
 
@@ -157,9 +157,9 @@ With two or more floors, ๐Ÿข **Building view** (`O`) appears in the header. Flo
157
157
 
158
158
  ## Menu bar app (Tauri)
159
159
 
160
- A resident app that shows the web UI in a window. It starts the Node server itself (or reuses one that is already running) and lives in the menu bar / system tray with Open, Hide, Always on top, Reinstall hooks, Check for updates and Quit. When sessions are waiting for you, the menu bar shows `๐Ÿ™‹ n`. Closing the window hides it; Quit stops the server too. Node is bundled: on first launch the app copies the server files to `~/.ai-office/app` and the Node binary to `~/.ai-office/node`, then registers the hooks, so moving or deleting the app does not break them.
160
+ A resident app that shows the web UI in a window. It starts the Node server itself (or reuses one that is already running) and lives in the menu bar / system tray; left- or right-click the icon for the menu: Open, Hide, Always on top, Launch at login, Reinstall hooks, Check for updates, Apply update and Quit. When a session is waiting for you or an update is ready, a **red dot** appears on the menu bar icon and on the Dock (Windows: taskbar) icon, like a chat app; there is no count, and the tooltip says what it is. **Launch at login** is turned on at first run; when started at login the app stays in the menu bar without a window. You can turn it off from the menu. Closing the window hides it; Quit stops the server too. Node is bundled: on first launch the app copies the server files to `~/.ai-office/app` and the Node binary to `~/.ai-office/node`, then registers the hooks, so moving or deleting the app does not break them.
161
161
 
162
- **Updates**: the app checks for a new version 10 seconds after launch and every 6 hours, downloads it in the background, then shows โฌ† in the menu bar and enables "Apply update (restart)". Releases are built by GitHub Actions for macOS (Apple Silicon) and Windows (x64) and signed for the updater.
162
+ **Updates**: the app checks for a new version 10 seconds after launch and every 6 hours and only **downloads** it in the background (signature verified; a failed check is retried after 5 minutes), then shows the red dot and enables "Apply vX update (restart)". Applying stops the server, installs and restarts; nothing is installed without asking and the running app is never replaced underneath you. "Check for updates" in the menu shows whether you are up to date or why a check failed. Releases are built by GitHub Actions for macOS (Apple Silicon) and Windows (x64) and signed for the updater.
163
163
 
164
164
  > **Download**: [macOS AI-Office-mac-arm64.dmg](https://github.com/cozyuj/AI-office/releases/latest/download/AI-Office-mac-arm64.dmg) ยท [Windows AI-Office-windows-x64-setup.exe](https://github.com/cozyuj/AI-office/releases/latest/download/AI-Office-windows-x64-setup.exe) (always the latest; older versions in [Releases](https://github.com/cozyuj/AI-office/releases)). The macOS app is not notarized: **right-click โ†’ Open** the first time; on macOS 15+ use System Settings โ†’ Privacy & Security โ†’ **Open Anyway**, and if it still says "damaged", run `xattr -dr com.apple.quarantine "/Applications/AI Office.app"` once. To build it yourself:
165
165
 
package/README.md CHANGED
@@ -63,7 +63,7 @@ node bin/ai-office.js # ๋˜๋Š” npm run install-hooks && npm start
63
63
 
64
64
  | ์˜์—ญ | ๋‚ด์šฉ |
65
65
  |---|---|
66
- | **์ขŒ์ธก ์ธต๋ณ„ ์•ˆ๋‚ด** | git ํ”„๋กœ์ ํŠธ(์ €์žฅ์†Œ ๋ฃจํŠธ) = ์ธต. worktree ๋Š” ๊ฐ™์€ ์ธต์œผ๋กœ ๋ฌถ์ด๊ณ  ๋ธŒ๋žœ์น˜(โއ)๊ฐ€ ํ•จ๊ป˜ ๋ณด์ธ๋‹ค. ์ธต์„ ์—ด๋ฉด ์„ธ์…˜ ๋ชฉ๋ก โ€” `๋‹ต ๊ธฐ๋‹ค๋ฆผ`, `์ง€๊ธˆ ๋ณด๋Š” ์ค‘` ํ‘œ์‹œ. ์„ ํƒํ•œ ์ธต์˜ ๐ŸŽจ ๋กœ ๊ทธ ์ธต ์ธํ…Œ๋ฆฌ์–ด ์Šคํƒ€์ผ(๋”ฐ๋œปํ•œ ์šฐ๋“œยท์ฟจ ๊ทธ๋ ˆ์ดยท๋ฏผํŠธยท๋ผ๋ฒค๋”ยท์ƒŒ๋“œยทํŒŒ์Šคํ…”, ๊ธฐ๋ณธ์€ ์ธต๋งˆ๋‹ค ์ž๋™ ๋ฐฐ์ •)์„ ๊ณ ๋ฅธ๋‹ค. "์ตœ๊ทผ ์„ธ์…˜ ์ž๋™์œผ๋กœ ๋”ฐ๋ผ๊ฐ€๊ธฐ"(`A`)๋ฅผ ์ผœ๋ฉด ์ตœ์‹  ํ™œ๋™ ์„ธ์…˜์œผ๋กœ ์„ ํƒ์ด ์˜ฎ๊ฒจ๊ฐ€๊ณ , ๋„๋ฉด ๋งˆ์ง€๋ง‰์— ๋ณด๋˜ ์ธตยท์„ธ์…˜์„ ๋‹ค์Œ์— ์—ด ๋•Œ ๊ธฐ์–ตํ•œ๋‹ค |
66
+ | **์ขŒ์ธก ์ธต๋ณ„ ์•ˆ๋‚ด** | git ํ”„๋กœ์ ํŠธ(์ €์žฅ์†Œ ๋ฃจํŠธ) = ์ธต. worktree ๋Š” ๊ฐ™์€ ์ธต์œผ๋กœ ๋ฌถ์ด๊ณ  ๋ธŒ๋žœ์น˜(โއ)๊ฐ€ ํ•จ๊ป˜ ๋ณด์ธ๋‹ค. ์ธต์„ ์—ด๋ฉด ์„ธ์…˜ ๋ชฉ๋ก โ€” `๋‹ต ๊ธฐ๋‹ค๋ฆผ`, `์ง€๊ธˆ ๋ณด๋Š” ์ค‘` ํ‘œ์‹œ. ๊ธฐ๋ณธ์€ ์„ ํƒํ•œ ์ธต๋งŒ ์—ด๋ฆฌ๊ณ , ๋จธ๋ฆฌ์˜ **๋ชจ๋‘ ํŽผ์น˜๊ธฐ โ†” ๋ชจ๋‘ ์ ‘๊ธฐ** ๋กœ ๋ชจ๋“  ์ธต์˜ ์„ธ์…˜ ๋ชฉ๋ก์„ ํ•œ๊บผ๋ฒˆ์— ์—ด๊ณ  ๋‹ซ๋Š”๋‹ค(๋ชจ๋‘ ์ ‘ํžŒ ์ƒํƒœ์—์„œ ์ธต์„ ๋ˆ„๋ฅด๋ฉด ๊ทธ ์ธต์ด ์—ด๋ฆผ, ๋ธŒ๋ผ์šฐ์ €์— ๊ธฐ์–ต). ์„ ํƒํ•œ ์ธต์˜ ๐ŸŽจ ๋กœ ๊ทธ ์ธต ์ธํ…Œ๋ฆฌ์–ด ์Šคํƒ€์ผ(๋”ฐ๋œปํ•œ ์šฐ๋“œยท์ฟจ ๊ทธ๋ ˆ์ดยท๋ฏผํŠธยท๋ผ๋ฒค๋”ยท์ƒŒ๋“œยทํŒŒ์Šคํ…”, ๊ธฐ๋ณธ์€ ์ธต๋งˆ๋‹ค ์ž๋™ ๋ฐฐ์ •)์„ ๊ณ ๋ฅธ๋‹ค. "์ตœ๊ทผ ์„ธ์…˜ ์ž๋™์œผ๋กœ ๋”ฐ๋ผ๊ฐ€๊ธฐ"(`A`)๋ฅผ ์ผœ๋ฉด ์ตœ์‹  ํ™œ๋™ ์„ธ์…˜์œผ๋กœ ์„ ํƒ์ด ์˜ฎ๊ฒจ๊ฐ€๊ณ , ๋„๋ฉด ๋งˆ์ง€๋ง‰์— ๋ณด๋˜ ์ธตยท์„ธ์…˜์„ ๋‹ค์Œ์— ์—ด ๋•Œ ๊ธฐ์–ตํ•œ๋‹ค |
67
67
  | **์ค‘์•™ ์‚ฌ๋ฌด์‹ค** | ๋“œ๋ž˜๊ทธ ์ด๋™, ํœ /๋ฒ„ํŠผ ์คŒ, `โŸฒ` ์ดˆ๊ธฐ ์œ„์น˜. ์บ๋ฆญํ„ฐ ํด๋ฆญ โ†’ ์„ธ์…˜ ์„ ํƒ, ์บ๋ฆญํ„ฐ์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ํˆดํŒ(์ง€๊ธˆ ํ•˜๋Š” ์ผยท๋Œ€์ƒ ํŒŒ์ผยท์š”์ฒญยท๋ธŒ๋žœ์น˜ยท๊ฒฝ๋กœ). ๐Ÿ“ **๋”ฐ๋ผ๊ฐ€๊ธฐ**(`F`) = ์นด๋ฉ”๋ผ๊ฐ€ ์„ ํƒ ์„ธ์…˜์˜ Claude ๋ฅผ ์ซ“์•„๊ฐ. ๐Ÿข **์ „์ฒด ๋ณด๊ธฐ**(`O`)๋Š” ์ธต์ด ๋‘˜ ์ด์ƒ์ผ ๋•Œ ๋‚˜ํƒ€๋‚œ๋‹ค |
68
68
  | **์šฐ์ธก ํŒจ๋„** | ์„ธ์…˜ ์นด๋“œ(ํ”„๋กœ์ ํŠธ ๊ฒฝ๋กœ ยท ์„ธ์…˜ id ยท ์บ๋ฆญํ„ฐ ์ด๋ฆ„ ยท ์ด์–ด๊ฐ€๊ธฐ ๋ช…๋ น ๋ณต์‚ฌ ยท ๋๋‚œ ์„ธ์…˜ ๐Ÿ—‘ ์น˜์šฐ๊ธฐ), ์‚ฌ๋ฌด์‹ค ์‚ฌ๋žŒ๋“ค(๋ฐฉยท๋งํ’์„ ยท๊ฒฝ๊ณผ), ํ™œ๋™ ๋กœ๊ทธ(๊ฒ€์ƒ‰ `/`, ์„ ํƒ ์„ธ์…˜๋งŒ), ๋ฒ”๋ก€(์ œ๋ชฉ ํด๋ฆญ์œผ๋กœ ์ ‘๊ธฐ) |
69
69
 
@@ -159,9 +159,9 @@ node bin/ai-office.js # ๋˜๋Š” npm run install-hooks && npm start
159
159
 
160
160
  ## ๋ฉ”๋‰ด๋ฐ” ์•ฑ (Tauri)
161
161
 
162
- ์›น ๋ฒ„์ „์„ ๊ทธ๋Œ€๋กœ ์ฐฝ์— ๋‹ด์€ ์ƒ์ฃผ ์•ฑ์ž…๋‹ˆ๋‹ค(๋งฅ ๋ฉ”๋‰ด๋ฐ” ยท Windows ํŠธ๋ ˆ์ด). ์•ฑ์ด Node ์„œ๋ฒ„๋ฅผ ์ง์ ‘ ๋„์šฐ๊ณ (์ด๋ฏธ ๋–  ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ) ์•„์ด์ฝ˜ ๋ฉ”๋‰ด์—์„œ ์—ด๊ธฐ/์ˆจ๊ธฐ๊ธฐ/ํ•ญ์ƒ ์œ„/ํ›… ๋‹ค์‹œ ์„ค์น˜/์—…๋ฐ์ดํŠธ ํ™•์ธ/์ข…๋ฃŒ๋ฅผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์„ธ์…˜์ด ์žˆ์œผ๋ฉด ๋ฉ”๋‰ด๋ฐ”์— `๐Ÿ™‹ n` ์ด ๋œน๋‹ˆ๋‹ค. ์ฐฝ์„ ๋‹ซ์•„๋„ ๋ฉ”๋‰ด๋ฐ”์— ๋‚จ๊ณ , ์ข…๋ฃŒํ•˜๋ฉด ์„œ๋ฒ„๋„ ํ•จ๊ป˜ ๋‚ด๋ ค๊ฐ‘๋‹ˆ๋‹ค(์•ฑ์ด ๊ฐ•์ œ ์ข…๋ฃŒ๋ผ๋„ ์„œ๋ฒ„๊ฐ€ ์Šค์Šค๋กœ ๋๋‚ฉ๋‹ˆ๋‹ค). ์•ฑ์—๋Š” Node ๊ฐ€ ๋‚ด์žฅ๋˜์–ด ์žˆ๊ณ , ์ฒซ ์‹คํ–‰ ๋•Œ ์„œ๋ฒ„ ํŒŒ์ผ์„ `~/.ai-office/app` ์—, Node ๋ฅผ `~/.ai-office/node` ์— ๋ณต์‚ฌํ•˜๊ณ  ๊ทธ ๊ฒฝ๋กœ๋กœ ํ›…์„ ๋“ฑ๋กํ•˜๋ฏ€๋กœ ์•ฑ์„ ์˜ฎ๊ธฐ๊ฑฐ๋‚˜ ์ง€์›Œ๋„ ํ›…์ด ๊นจ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
162
+ ์›น ๋ฒ„์ „์„ ๊ทธ๋Œ€๋กœ ์ฐฝ์— ๋‹ด์€ ์ƒ์ฃผ ์•ฑ์ž…๋‹ˆ๋‹ค(๋งฅ ๋ฉ”๋‰ด๋ฐ” ยท Windows ํŠธ๋ ˆ์ด). ์•ฑ์ด Node ์„œ๋ฒ„๋ฅผ ์ง์ ‘ ๋„์šฐ๊ณ (์ด๋ฏธ ๋–  ์žˆ์œผ๋ฉด ์žฌ์‚ฌ์šฉ) ์•„์ด์ฝ˜์„ ์™ผ์ชฝ์ด๋‚˜ ์˜ค๋ฅธ์ชฝ ํด๋ฆญํ•˜๋ฉด ๋ฉ”๋‰ด๊ฐ€ ๋œน๋‹ˆ๋‹ค. ๋ฉ”๋‰ด๋Š” ์—ด๊ธฐ ยท ์ˆจ๊ธฐ๊ธฐ ยท ํ•ญ์ƒ ์œ„ ยท ๋กœ๊ทธ์ธ ์‹œ ์ž๋™ ์‹คํ–‰ ยท ํ›… ๋‹ค์‹œ ์„ค์น˜ ยท ์—…๋ฐ์ดํŠธ ํ™•์ธ ยท ์—…๋ฐ์ดํŠธ ์ ์šฉ ยท ์ข…๋ฃŒ์ž…๋‹ˆ๋‹ค. ๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์„ธ์…˜์ด ์žˆ๊ฑฐ๋‚˜ ๋ฐ›์•„ ๋‘” ์—…๋ฐ์ดํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ์นด์นด์˜คํ†ก์ฒ˜๋Ÿผ ๋ฉ”๋‰ด๋ฐ” ์•„์ด์ฝ˜๊ณผ Dock(Windows ๋Š” ์ž‘์—… ํ‘œ์‹œ์ค„) ์•„์ด์ฝ˜์— **๋นจ๊ฐ„ ์ **์ด ๋œน๋‹ˆ๋‹ค. ์ˆซ์ž๋Š” ์—†๊ณ , ๋ฌด์—‡์ธ์ง€๋Š” ์•„์ด์ฝ˜์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ํˆดํŒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. **๋กœ๊ทธ์ธ ์‹œ ์ž๋™ ์‹คํ–‰**์€ ์ฒ˜์Œ ์‹คํ–‰ ๋•Œ ์ผœ์ง€๋ฉฐ, ์ž๋™์œผ๋กœ ์ผœ์งˆ ๋• ์ฐฝ ์—†์ด ๋ฉ”๋‰ด๋ฐ”์—๋งŒ ๋œน๋‹ˆ๋‹ค. ๋ฉ”๋‰ด์—์„œ ๋Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฐฝ์„ ๋‹ซ์•„๋„ ๋ฉ”๋‰ด๋ฐ”์— ๋‚จ๊ณ , ์ข…๋ฃŒํ•˜๋ฉด ์„œ๋ฒ„๋„ ํ•จ๊ป˜ ๋‚ด๋ ค๊ฐ‘๋‹ˆ๋‹ค(์•ฑ์ด ๊ฐ•์ œ ์ข…๋ฃŒ๋ผ๋„ ์„œ๋ฒ„๊ฐ€ ์Šค์Šค๋กœ ๋๋‚ฉ๋‹ˆ๋‹ค). ์•ฑ์—๋Š” Node ๊ฐ€ ๋‚ด์žฅ๋˜์–ด ์žˆ๊ณ , ์ฒซ ์‹คํ–‰ ๋•Œ ์„œ๋ฒ„ ํŒŒ์ผ์„ `~/.ai-office/app` ์—, Node ๋ฅผ `~/.ai-office/node` ์— ๋ณต์‚ฌํ•˜๊ณ  ๊ทธ ๊ฒฝ๋กœ๋กœ ํ›…์„ ๋“ฑ๋กํ•˜๋ฏ€๋กœ ์•ฑ์„ ์˜ฎ๊ธฐ๊ฑฐ๋‚˜ ์ง€์›Œ๋„ ํ›…์ด ๊นจ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
163
163
 
164
- **์ž๋™ ์—…๋ฐ์ดํŠธ**: ์•ฑ์ด ์‹œ์ž‘ 10์ดˆ ๋’ค์™€ 6์‹œ๊ฐ„๋งˆ๋‹ค ์ƒˆ ๋ฒ„์ „์„ ํ™•์ธํ•ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๋ฐ›์•„ ๋‘๊ณ , ๋ฉ”๋‰ด๋ฐ”์— โฌ† ๋ฅผ ๋„์šฐ๋ฉฐ ๋ฉ”๋‰ด์˜ "์—…๋ฐ์ดํŠธ ์ ์šฉ (๋‹ค์‹œ ์‹œ์ž‘)" ์ด ์ผœ์ง‘๋‹ˆ๋‹ค. ๋ฆด๋ฆฌ์Šค๋Š” ํƒœ๊ทธ๋ฅผ ์˜ฌ๋ฆฌ๋ฉด GitHub Actions ๊ฐ€ ๋งฅ(Apple Silicon)ยทWindows(x64) ๋กœ ๋นŒ๋“œํ•˜๊ณ  ์„œ๋ช…ํ•ด ์˜ฌ๋ฆฝ๋‹ˆ๋‹ค.
164
+ **์ž๋™ ์—…๋ฐ์ดํŠธ**: ์•ฑ์ด ์‹œ์ž‘ 10์ดˆ ๋’ค์™€ 6์‹œ๊ฐ„๋งˆ๋‹ค ์ƒˆ ๋ฒ„์ „์„ ํ™•์ธํ•ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ **๋ฐ›์•„ ๋‘๊ธฐ๋งŒ** ํ•˜๊ณ (์„œ๋ช… ๊ฒ€์ฆ๊นŒ์ง€, ํ™•์ธ์— ์‹คํŒจํ•˜๋ฉด 5๋ถ„ ๋’ค ๋‹ค์‹œ), ๋นจ๊ฐ„ ์ ์„ ๋„์šฐ๋ฉฐ ๋ฉ”๋‰ด์˜ "vX ์—…๋ฐ์ดํŠธ ์ ์šฉ (๋‹ค์‹œ ์‹œ์ž‘)" ์ด ์ผœ์ง‘๋‹ˆ๋‹ค. ๋ˆ„๋ฅด๋ฉด ์„œ๋ฒ„๋ฅผ ๋‚ด๋ฆฌ๊ณ  ์„ค์น˜ํ•œ ๋’ค ๋‹ค์‹œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. ๋ฌป์ง€ ์•Š๊ณ  ์„ค์น˜ํ•˜๊ฑฐ๋‚˜ ์“ฐ๋˜ ์ค‘์— ์•ฑ ํŒŒ์ผ์„ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฉ”๋‰ด์˜ "์—…๋ฐ์ดํŠธ ํ™•์ธ" ์— ์ตœ์‹  ์—ฌ๋ถ€๋‚˜ ์‹คํŒจ ์ด์œ ๊ฐ€ ๋ณด์ž…๋‹ˆ๋‹ค. ๋ฆด๋ฆฌ์Šค๋Š” ํƒœ๊ทธ๋ฅผ ์˜ฌ๋ฆฌ๋ฉด GitHub Actions ๊ฐ€ ๋งฅ(Apple Silicon)ยทWindows(x64) ๋กœ ๋นŒ๋“œํ•˜๊ณ  ์„œ๋ช…ํ•ด ์˜ฌ๋ฆฝ๋‹ˆ๋‹ค.
165
165
 
166
166
  > **๋‹ค์šด๋กœ๋“œ**: โฌ‡๏ธ **[๋งฅ AI-Office-mac-arm64.dmg](https://github.com/cozyuj/AI-office/releases/latest/download/AI-Office-mac-arm64.dmg)** ยท **[Windows AI-Office-windows-x64-setup.exe](https://github.com/cozyuj/AI-office/releases/latest/download/AI-Office-windows-x64-setup.exe)** (ํ•ญ์ƒ ์ตœ์‹ ) ยท ์ด์ „ ๋ฒ„์ „์€ [Releases](https://github.com/cozyuj/AI-office/releases). DMG ๋ฅผ ์—ด์–ด `AI Office.app` ์„ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ ํด๋”์— ๋„ฃ์œผ์„ธ์š”. ์ฝ”๋“œ ์„œ๋ช…์ด ์—†์–ด ์ฒ˜์Œ์—” "ํ™•์ธ๋˜์ง€ ์•Š์€ ๊ฐœ๋ฐœ์ž" ๊ฒฝ๊ณ ๊ฐ€ ๋œจ๋‹ˆ ์•ฑ์„ **์šฐํด๋ฆญ โ†’ ์—ด๊ธฐ** ๋กœ ํ•œ ๋ฒˆ ํ—ˆ์šฉํ•˜๊ณ , macOS 15 ์ด์ƒ์—์„œ ์•ˆ ์—ด๋ฆฌ๋ฉด ์‹œ์Šคํ…œ ์„ค์ • โ†’ ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ ๋ฐ ๋ณด์•ˆ โ†’ **๊ทธ๋ž˜๋„ ์—ด๊ธฐ**, ๊ทธ๋ž˜๋„ "์†์ƒ๋จ" ์ด๋ผ๊ณ  ๋‚˜์˜ค๋ฉด ํ„ฐ๋ฏธ๋„์—์„œ `xattr -dr com.apple.quarantine "/Applications/AI Office.app"` ๋ฅผ ํ•œ ๋ฒˆ ์‹คํ–‰ํ•˜์„ธ์š”. Intel ๋งฅยทLinux ๋Š” ์œ„์˜ `npx claude-ai-office` ํ•œ ์ค„๋กœ ์›น ๋ฒ„์ „์„ ์“ฐ์„ธ์š”. ์ง์ ‘ ๋นŒ๋“œํ•˜๋ ค๋ฉด:
167
167
 
@@ -1,72 +1,88 @@
1
- # AI Office ๊ตฌ์กฐ ์„ค๊ณ„ (์›น ๋ฒ„์ „)
1
+ # AI Office ๊ตฌ์กฐ ์„ค๊ณ„
2
2
 
3
- Claude Code ์„ธ์…˜์ด ์ง€๊ธˆ ๋ฌด์—‡์„ ํ•˜๋Š”์ง€ ์•„์ด์†Œ๋ฉ”ํŠธ๋ฆญ ์‚ฌ๋ฌด์‹ค ํ™”๋ฉด์œผ๋กœ ์‹ค์‹œ๊ฐ„ ๋ณด์—ฌ์ฃผ๋Š” ๋กœ์ปฌ ์›น ์•ฑ.
3
+ Claude Code ์„ธ์…˜์ด ์ง€๊ธˆ ๋ฌด์—‡์„ ํ•˜๋Š”์ง€ ์•„์ด์†Œ๋ฉ”ํŠธ๋ฆญ ์‚ฌ๋ฌด์‹ค ํ™”๋ฉด์œผ๋กœ ์‹ค์‹œ๊ฐ„ ๋ณด์—ฌ์ฃผ๋Š” ๋กœ์ปฌ ์•ฑ (์›น + macOS/Windows ๋ฐ์Šคํฌํ†ฑ).
4
4
  ํŒŒ์ผ์„ ์ฝ์œผ๋ฉด ์ž๋ฃŒ์‹ค, ๋ช…๋ น์„ ๋Œ๋ฆฌ๋ฉด ์„œ๋ฒ„์‹ค, ๋ง‰ํžˆ๋ฉด ๊ทธ ์ž๋ฆฌ์—์„œ ๊ณ ๋ฏผ, ์„œ๋ธŒ์—์ด์ „ํŠธ๋Š” ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ๋กœ ์ถœ๊ทผ.
5
5
 
6
+ > ์ด ๋ฌธ์„œ๋Š” v0.29 ๊ธฐ์ค€์ด๋‹ค. ๊ธฐ๋Šฅ ์†Œ๊ฐœยท์Šคํฌ๋ฆฐ์ƒท์€ `docs/AI-Office-์†Œ๊ฐœ.html`.
7
+
6
8
  ## 1. ์ „์ฒด ํ๋ฆ„
7
9
 
8
10
  ```
9
- Claude Code ์„ธ์…˜ (์—ฌ๋Ÿฌ ํ„ฐ๋ฏธ๋„, ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ)
10
- โ”‚ settings.json hooks (PreToolUse, PostToolUse, SubagentStart, Stop, ...)
11
+ Claude Code ์„ธ์…˜ (์—ฌ๋Ÿฌ ํ„ฐ๋ฏธ๋„, ์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ, ์›ŒํฌํŠธ๋ฆฌ)
12
+ โ”‚ ~/.claude/settings.json hooks (11๊ฐœ ์ด๋ฒคํŠธ)
11
13
  โ–ผ
12
- hooks/emit.sh stdin JSON โ†’ curl POST (1์ดˆ ํƒ€์ž„์•„์›ƒ, ๋ฐฑ๊ทธ๋ผ์šด๋“œ, ํ•ญ์ƒ exit 0)
14
+ hooks/emit.js stdin JSON โ†’ POST 127.0.0.1:<port>/hook (Node ๋งŒ ์‚ฌ์šฉ, 1์ดˆ ํƒ€์ž„์•„์›ƒ, ํ•ญ์ƒ exit 0)
13
15
  โ”‚
14
16
  โ–ผ POST /hook
15
- server.js (Node 18+, ์™ธ๋ถ€ ์˜์กด์„ฑ 0)
16
- โ”œโ”€ normalize ํ›… payload โ†’ ๋‚ด๋ถ€ Event (๋„๊ตฌ๋ช…/๋Œ€์ƒ ํŒŒ์ผ/์„œ๋ธŒ์—์ด์ „ํŠธ id ์ถ”์ถœ, ๋Œ€์šฉ๋Ÿ‰ ํ•„๋“œ ์ œ๊ฑฐ)
17
- โ”œโ”€ reduce Event โ†’ Floor/Session/Agent ์ƒํƒœ ๊ฐฑ์‹  (์ƒํƒœ ๋จธ์‹ )
18
- โ”œโ”€ log ์ตœ๊ทผ 500๊ฑด ring buffer
19
- โ””โ”€ broadcast SSE /events ๋กœ {snapshot | update} ์ „์†ก
17
+ server.js (Node 18+, ์™ธ๋ถ€ ์˜์กด์„ฑ 0, 127.0.0.1 ์—๋งŒ listen)
18
+ โ”œโ”€ ์ ‘๊ทผ ๊ฒ€์‚ฌ Host ๋ฃจํ”„๋ฐฑ ยท Origin ํ—ˆ์šฉ ๋ชฉ๋ก (9์ ˆ)
19
+ โ”œโ”€ journal payload ๋ฅผ ~/.ai-office/events.jsonl ์— append โ†’ ๊ธฐ๋™ ์‹œ ์ตœ๊ทผ 2์‹œ๊ฐ„ ์žฌ์ƒ
20
+ โ”œโ”€ normalize payload โ†’ ์ž‘์€ Event (๋„๊ตฌโ†’๋ฐฉยท๋งํ’์„ , ํฐ ํ•„๋“œ ์ž๋ฅด๊ธฐ) lib/normalize.js
21
+ โ”œโ”€ state Event โ†’ ์ธต/์„ธ์…˜/์บ๋ฆญํ„ฐ ์ƒํƒœ ๋จธ์‹ , ํ™œ๋™ ๋กœ๊ทธ 500๊ฑด, sweep lib/state.js
22
+ โ””โ”€ broadcast SSE /events ๋กœ snapshot ยท update ยท remove ยท floor ยท demo
20
23
  โ”‚
21
24
  โ–ผ GET /events (SSE), GET /api/state
22
25
  public/ (๋ธŒ๋ผ์šฐ์ €, ๋ฒˆ๋“ค๋Ÿฌ ์—†์Œ, ES module)
23
- โ”œโ”€ store ์„œ๋ฒ„ ์ƒํƒœ ๋ฏธ๋Ÿฌ + ๊ตฌ๋…
24
- โ”œโ”€ scene canvas ์•„์ด์†Œ๋ฉ”ํŠธ๋ฆญ ์‚ฌ๋ฌด์‹ค: ๋งต, ๊ฐ€๊ตฌ, ์บ๋ฆญํ„ฐ ์ด๋™(A*), ๋งํ’์„ , ์นด๋ฉ”๋ผ
25
- โ””โ”€ panels ์ขŒ: ์ธต(ํ”„๋กœ์ ํŠธ)ยท์„ธ์…˜ ๋ชฉ๋ก / ์šฐ: ์‚ฌ๋ฌด์‹ค ์‚ฌ๋žŒ๋“คยทํ™œ๋™ ๋กœ๊ทธยท๋ฒ”๋ก€
26
+ โ”œโ”€ store ์„œ๋ฒ„ ์ƒํƒœ ๋ฏธ๋Ÿฌ + ์„ ํƒยท์ ‘๊ธฐ ์ƒํƒœ + ๊ตฌ๋…
27
+ โ”œโ”€ scene canvas ์•„์ด์†Œ๋ฉ”ํŠธ๋ฆญ ์‚ฌ๋ฌด์‹ค: ์ธต ๋ ˆ์ด์•„์›ƒ, ๊ฐ€๊ตฌ, ์บ๋ฆญํ„ฐ ์ด๋™(A*), ๋งํ’์„ , ์นด๋ฉ”๋ผ, ์‹œ๊ฐ„๋Œ€ ์กฐ๋ช…
28
+ โ”œโ”€ panels ์ขŒ: ์ธต๋ณ„ ์•ˆ๋‚ด(์ธตยท์„ธ์…˜ ๋ชฉ๋ก) / ์šฐ: ์‚ฌ๋ฌด์‹ค ์‚ฌ๋žŒ๋“คยทํ™œ๋™ ๋กœ๊ทธยท๋ฒ”๋ก€ / ์ „์ฒด ๋ณด๊ธฐ(๊ฑด๋ฌผ ๋‹จ๋ฉด)
29
+ โ””โ”€ i18n ํ•œ๊ตญ์–ด ์›๋ฌธ + ์˜์–ด ๋ฒˆ์—ญ (DOM ์ž๋™, ์บ”๋ฒ„์Šค๋Š” tr())
26
30
  ```
27
31
 
28
32
  - ํ›…์€ Claude ์‹คํ–‰์„ ์ ˆ๋Œ€ ๋ง‰์ง€ ์•Š๋Š”๋‹ค. ์„œ๋ฒ„๊ฐ€ ๊บผ์ ธ ์žˆ์–ด๋„ 1์ดˆ ์•ˆ์— ์กฐ์šฉํžˆ ํฌ๊ธฐํ•œ๋‹ค.
29
33
  - ์„œ๋ฒ„๋Š” "์ง€๊ธˆ ์ƒํƒœ"์˜ ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›์ด๋‹ค. ๋ธŒ๋ผ์šฐ์ €๋Š” ์ƒํƒœ๋ฅผ ๋ฐ›์•„ ์• ๋‹ˆ๋ฉ”์ด์…˜๋งŒ ๋‹ด๋‹นํ•œ๋‹ค.
30
- - ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋ช‡ ๊ฐœ ์—ด์–ด๋„, ์ƒˆ๋กœ ์—ด์–ด๋„ ๊ฐ™์€ ํ™”๋ฉด์ด๋‹ค (์ ‘์† ์‹œ snapshot ์ˆ˜์‹ ).
34
+ - ๋ธŒ๋ผ์šฐ์ €๋ฅผ ๋ช‡ ๊ฐœ ์—ด์–ด๋„, ์ƒˆ๋กœ ์—ด์–ด๋„ ๊ฐ™์€ ํ™”๋ฉด์ด๋‹ค (์ ‘์† ์‹œ snapshot ์ˆ˜์‹ ). ์„œ๋ฒ„๋ฅผ ๋‹ค์‹œ ์ผœ๋„ ์ €๋„ ์žฌ์ƒ์œผ๋กœ ๊ฐ™์€ ํ™”๋ฉด์ด ๋Œ์•„์˜จ๋‹ค.
35
+ - ๋ฐ์Šคํฌํ†ฑ ์•ฑ์€ ๊ฐ™์€ ์„œ๋ฒ„๋ฅผ ์‚ฌ์ด๋“œ์นด Node ๋กœ ๋„์šฐ๊ณ  ์ฐฝ์— ๊ฐ™์€ ํ™”๋ฉด์„ ์—ฐ๋‹ค (10์ ˆ).
31
36
 
32
37
  ## 2. ๋ฐ์ดํ„ฐ ๋ชจ๋ธ
33
38
 
34
39
  ```ts
35
- Floor { // ํ”„๋กœ์ ํŠธ = ์ธต. key = cwd ์ ˆ๋Œ€๊ฒฝ๋กœ
36
- id: string // cwd
37
- name: string // basename(cwd) ์˜ˆ: "nccx"
38
- no: number // ์ถœํ˜„ ์ˆœ์„œ๋กœ 1F, 2F, ... ๋ถ€์—ฌ
39
- sessions: Map<sessionId, Session>
40
- lastActivity: number // epoch ms
40
+ Floor { // ํ”„๋กœ์ ํŠธ = ์ธต. id = git ์ €์žฅ์†Œ ๋ฃจํŠธ (lib/project.js ๊ฐ€ .git ์„ ์œ„๋กœ ์ฐพ๋Š”๋‹ค. ์—†์œผ๋ฉด cwd)
41
+ id: string // ์›ŒํฌํŠธ๋ฆฌ๋„ ์›๋ž˜ ์ €์žฅ์†Œ์˜ ์ธต์œผ๋กœ ๋ชจ์ธ๋‹ค
42
+ name: string // basename(id). ์ด๋ฆ„์ด ๊ฒน์น˜๋ฉด ์ƒ์œ„ ํด๋”๋ฅผ ๋ถ™์—ฌ ๊ตฌ๋ถ„
43
+ no: number // ์ถœํ˜„ ์ˆœ์„œ๋กœ 1F, 2F, ...
44
+ style: string // ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ id (์ง์ ‘ ์„ ํƒ > ์ธต id ํ•ด์‹œ). ~/.ai-office/floor-styles.json
45
+ sessions: Set<sessionId>
46
+ lastActivity: number
41
47
  }
42
48
 
43
49
  Session {
44
50
  id: string // hook.session_id
45
- floorId: string
46
- title: string // ์ฒซ UserPromptSubmit ์˜ prompt ์•ž 40์ž (session_title ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ)
51
+ floorId: string // ์ฒ˜์Œ ์ •ํ•ด์ง„ ์ธต ์œ ์ง€ (cwd ๊ฐ€ ํ•˜์œ„ ํด๋”๋กœ ๋ฐ”๋€Œ์–ด๋„)
52
+ title: string // ํ›… session_title > transcript ์˜ ๋งˆ์ง€๋ง‰ custom-title > ์ฒซ ์š”์ฒญ ์•ž๋ถ€๋ถ„
53
+ firstPrompt, lastPrompt // ์ฒซ ์š”์ฒญ(transcript ์•ž 1MB ์—์„œ ๋ณต์›), ์ตœ๊ทผ ์š”์ฒญ
47
54
  status: 'thinking' | 'working' | 'waiting' | 'idle' | 'ended'
55
+ cwd, branch, worktree // ํ—ค๋”ยทํˆดํŒ ํ‘œ์‹œ์šฉ
56
+ transcriptPath // ์ œ๋ชฉ ๊ฐฑ์‹ ยท์ƒ์กด ํ™•์ธ(stat)์šฉ
48
57
  startedAt, lastActivity: number
49
- agents: Map<agentId, Agent> // 'main' + ์„œ๋ธŒ์—์ด์ „ํŠธ๋“ค
50
- pendingSubagents: string[] // Agent ํˆด PreToolUse ์˜ description ํ (SubagentStart ์™€ ์ง์ง€์Œ)
58
+ agents: { [agentId]: Agent } // 'main' + ์„œ๋ธŒ์—์ด์ „ํŠธ๋“ค
59
+ pendingSubagents: string[] // Agent ํˆด PreToolUse ์˜ description ํ (SubagentStart ์™€ ์ง์ง€์Œ)
51
60
  }
52
61
 
53
62
  Agent { // ํ™”๋ฉด์˜ ์บ๋ฆญํ„ฐ ํ•œ ๋ช…
54
63
  id: 'main' | agent_id
55
- sessionId: string
56
64
  kind: 'main' | 'sub'
57
- name: string // "Claude" | agent_type ("Explore") | Agent ํˆด description
65
+ name: string // "Claude" | Agent ํˆด description | agent_type. ํ™”๋ฉด ์ด๋ฆ„์€ ํด๋ผ์ด์–ธํŠธ names.js ๊ฐ€ ๋ณ„๋ช…์œผ๋กœ ๋ฐ”๊พผ๋‹ค
58
66
  room: RoomId // ์ง€๊ธˆ ์žˆ์–ด์•ผ ํ•  ๋ฐฉ
59
- activity: ActivityId // 'think' | 'read' | 'search' | 'run' | 'edit' | 'web' | 'delegate' | 'ask' | 'wait' | 'rest' | 'compact' | 'leave'
60
- bubble: string // ๋งํ’์„  ํ…์ŠคํŠธ ์˜ˆ: "server.js ์ฝ๋Š” ์ค‘"
61
- tool?: { name, useId, target }
67
+ activity: ActivityId // 'arrive' | 'think' | 'read' | 'search' | 'run' | 'edit' | 'web' | 'delegate' | 'plan' | 'ask' | 'rest' | 'compact' | 'work' | 'leave'
68
+ icon, bubble: string // ๋งํ’์„  ์˜ˆ: "๐Ÿ“– server.js ์ฝ๋Š” ์ค‘"
69
+ tool?: { name, useId, target, detail }
62
70
  since: number // ํ˜„์žฌ activity ์‹œ์ž‘ ์‹œ๊ฐ (๊ฒฝ๊ณผ ์‹œ๊ฐ„ ํ‘œ์‹œ์šฉ)
63
71
  }
64
72
 
65
73
  LogEntry { t, floorId, sessionId, agentId, agentName, icon, text }
66
74
 
67
- RoomId = 'library' | 'dev' | 'research' | 'server' | 'meeting' | 'lounge' | 'cafe' | 'terrace' | 'mydesk' | 'lobby' | 'elevator' | 'corridor'
75
+ RoomId = 'library' | 'dev' | 'research' | 'server' | 'meeting' | 'lounge' | 'cafe' | 'terrace' | 'mydesk' | 'lobby' | 'elevator'
76
+ // cafe/terrace ๋Š” ์„œ๋ฒ„์˜ "ํœด์‹ ๋ฐฉ" id. ์ธต ๋ ˆ์ด์•„์›ƒ์ด ์‹ค์ œ ํŽธ์˜์‹œ์„ค(ํ—ฌ์Šค์žฅยท๊ตฌ๋‚ด์‹๋‹น โ€ฆ)๋กœ ๋ณ„์นญํ•œ๋‹ค (6์ ˆ)
68
77
  ```
69
78
 
79
+ ์„ธ์…˜ ์ƒ๋ช…์ฃผ๊ธฐ (sweep ์€ 1์ดˆ๋งˆ๋‹ค, transcript stat ์€ ์„ธ์…˜๋‹น 1๋ถ„์— ํ•œ ๋ฒˆ)
80
+ - 30๋ถ„ ๋ฌดํ™œ๋™ โ†’ ํœด์‹ ๋ฐฉ์œผ๋กœ "์ž๋ฆฌ ๋น„์›€"
81
+ - 10๋ถ„ ๋ฌดํ™œ๋™๋ถ€ํ„ฐ transcript ๋ฅผ ํ™•์ธํ•ด 60๋ถ„ ๋™์•ˆ ๊ฐฑ์‹ ์ด ์—†์œผ๋ฉด ํ‡ด๊ทผ ์ฒ˜๋ฆฌ (ํ„ฐ๋ฏธ๋„์„ ๊ทธ๋ƒฅ ๋‹ซ์€ ๊ฒฝ์šฐ)
82
+ - 2์‹œ๊ฐ„ ๋ฌดํ™œ๋™ โ†’ ํ‡ด๊ทผ. SessionEnd ํ›„ 10์ดˆ ๋’ค ์ œ๊ฑฐ
83
+ - ์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์—์„œ ์น˜์šด ์„ธ์…˜(DELETE)์€ ์ €๋„์— `SessionEnd(reason: dismissed)` ๋กœ ๋‚จ๊ฒจ ์žฌ์‹œ์ž‘ํ•ด๋„ ๋˜์‚ด์•„๋‚˜์ง€ ์•Š๋Š”๋‹ค
84
+ - ์ œ๋ชฉ: ํ›…์˜ session_title ์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ๋”ฐ๋ฅด๊ณ (๋ถ„๋ฆฌ๋œ ์„ธ์…˜), transcript ๋ 64KB ์˜ custom-title ์„ mtime ์ด ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ๋งŒ 1๋ถ„์— ํ•œ ๋ฒˆ ์ฝ๋Š”๋‹ค. ๋ฐ”๋€Œ๋ฉด ๋กœ๊ทธ "๐Ÿท๏ธ ์ œ๋ชฉ: โ€ฆ"
85
+
70
86
  ## 3. ํ›… ์ด๋ฒคํŠธ โ†’ ์ƒํƒœ ์ „์ด
71
87
 
72
88
  Claude Code 2.1.x ํ›… payload ๊ธฐ์ค€. ์„œ๋ธŒ์—์ด์ „ํŠธ๊ฐ€ ๋‚ธ ๋„๊ตฌ ํ˜ธ์ถœ์€ `agent_id` ๊ฐ€ ์‹ค๋ ค ์˜ค๋ฏ€๋กœ ๊ทธ ๊ฐ’์œผ๋กœ ์บ๋ฆญํ„ฐ๋ฅผ ์ฐพ๋Š”๋‹ค. ์—†์œผ๋ฉด `main`.
@@ -74,7 +90,7 @@ Claude Code 2.1.x ํ›… payload ๊ธฐ์ค€. ์„œ๋ธŒ์—์ด์ „ํŠธ๊ฐ€ ๋‚ธ ๋„๊ตฌ ํ˜ธ์ถœ์€
74
90
  | ํ›… ์ด๋ฒคํŠธ | ๋Œ€์ƒ | ๋ฐฉ | activity | ๋งํ’์„  | ๋น„๊ณ  |
75
91
  |---|---|---|---|---|---|
76
92
  | SessionStart | main ์ƒ์„ฑ | elevator โ†’ mydesk | wait | "์ถœ๊ทผ" | source=resume/compact ๋Š” ๊ธฐ์กด ์„ธ์…˜ ์œ ์ง€ |
77
- | UserPromptSubmit | main | ํ˜„์žฌ ์œ„์น˜ | think | "๊ณ ๋ฏผ ์ค‘โ€ฆ" | title ๋ฏธ์„ค์ •์ด๋ฉด prompt ๋กœ ์„ค์ •. status=thinking |
93
+ | UserPromptSubmit | main | ํ˜„์žฌ ์œ„์น˜ | think | "๊ณ ๋ฏผ ์ค‘โ€ฆ" | firstPrompt(์—†์„ ๋•Œ)ยทlastPrompt ๊ฐฑ์‹ . status=thinking |
78
94
  | PreToolUse | agent_id ๋˜๋Š” main | ๋„๊ตฌ๋ณ„ ๋ฐฉ (4์ ˆ) | ๋„๊ตฌ๋ณ„ | ๋„๊ตฌ๋ณ„ ๋ฌธ๊ตฌ | status=working |
79
95
  | PostToolUse | ๋™์ผ | ๊ทธ ์ž๋ฆฌ | think | "๊ณ ๋ฏผ ์ค‘โ€ฆ" | tool ํ•ด์ œ. duration_ms ๋กœ๊ทธ |
80
96
  | PostToolUseFailure | ๋™์ผ | ๊ทธ ์ž๋ฆฌ | think | "โŒ ์‹คํŒจ, ๋‹ค์‹œ ์ƒ๊ฐ ์ค‘" | error ์•ž 80์ž ๋กœ๊ทธ |
@@ -112,94 +128,151 @@ Claude Code 2.1.x ํ›… payload ๊ธฐ์ค€. ์„œ๋ธŒ์—์ด์ „ํŠธ๊ฐ€ ๋‚ธ ๋„๊ตฌ ํ˜ธ์ถœ์€
112
128
  | AskUserQuestion | mydesk | ask | `์งˆ๋ฌธ ์žˆ์–ด์š”` |
113
129
  | ๊ทธ ์™ธ | dev | work | `{tool} ์‚ฌ์šฉ ์ค‘` |
114
130
 
131
+
115
132
  ## 5. ์„œ๋ฒ„ API
116
133
 
117
134
  | ๋ฉ”์„œ๋“œ | ๊ฒฝ๋กœ | ์„ค๋ช… |
118
135
  |---|---|---|
119
- | POST | `/hook` | ํ›… payload ์ˆ˜์‹ . body 2MB ์ œํ•œ. ํ•ญ์ƒ 204 |
120
- | GET | `/events` | SSE. ์ ‘์† ์ฆ‰์‹œ `snapshot` 1ํšŒ, ์ดํ›„ `update` (๊ฐฑ์‹ ๋œ Session ์ „์ฒด + ์ƒˆ LogEntry) |
121
- | GET | `/api/state` | ํ˜„์žฌ ์ „์ฒด ์ƒํƒœ JSON (๋””๋ฒ„๊ทธ์šฉ) |
136
+ | POST | `/hook` | ํ›… payload ์ˆ˜์‹ . body 2MB ์ œํ•œ. ๋จผ์ € 204 ๋ฅผ ๋Œ๋ ค์ฃผ๊ณ  ์ฒ˜๋ฆฌ. ๋ฐ๋ชจ๊ฐ€ ๊บผ์ ธ ์žˆ์œผ๋ฉด `/demo/` ์ด๋ฒคํŠธ๋Š” ๋ฌด์‹œ |
137
+ | GET | `/events` | SSE. ์ ‘์† ์ฆ‰์‹œ `snapshot` 1ํšŒ, 25์ดˆ๋งˆ๋‹ค ping |
138
+ | GET | `/api/state` | ํ˜„์žฌ ์ „์ฒด ์ƒํƒœ JSON (snapshot ๊ณผ ๊ฐ™์€ ๋ชจ์–‘) |
139
+ | POST | `/api/floors/style` | `{ floorId, style }` ์ธต ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ ์ง€์ • (null ์ด๋ฉด ์ž๋™) |
140
+ | DELETE | `/api/sessions/:id` | ๋๋‚œ/์ž๋ฆฌ ๋น„์šด ์„ธ์…˜์„ ๋ชฉ๋ก์—์„œ ์น˜์šด๋‹ค |
141
+ | POST | `/api/demo/start` ยท `/api/demo/stop` | ๋ฐ๋ชจ ์„ธ์…˜ ์‹œ์ž‘ / ์ค‘์ง€(๋ฐ๋ชจ ์„ธ์…˜ ์ „๋ถ€ ์ œ๊ฑฐ) |
122
142
  | GET | `/` | `public/` ์ •์  ํŒŒ์ผ |
123
143
 
124
144
  SSE ๋ฉ”์‹œ์ง€
125
145
  ```
126
- event: snapshot data: { me: "jinyu", floors: Floor[], log: LogEntry[] }
127
- event: update data: { session: Session, floor: {id,name,no,lastActivity}, log?: LogEntry }
146
+ event: snapshot data: { me, floors, sessions, log, serverPort, hookPort, version, demo, journal }
147
+ event: update data: { session, floor, log? }
128
148
  event: remove data: { sessionId, floorId? }
149
+ event: floor data: Floor // ์ด๋ฆ„ ๊ตฌ๋ถ„ยทํ”„๋ฆฌ์…‹ ๋ณ€๊ฒฝ
150
+ event: demo data: { on }
129
151
  ```
130
152
 
131
- ํฌํŠธ ๊ธฐ๋ณธ 4777, ํ™˜๊ฒฝ๋ณ€์ˆ˜ `AI_OFFICE_PORT` ๋กœ ๋ณ€๊ฒฝ. ์„œ๋ฒ„/ํ›… ์–‘์ชฝ์ด ๊ฐ™์€ ๋ณ€์ˆ˜๋ฅผ ๋ณธ๋‹ค.
153
+ - ํฌํŠธ ๊ธฐ๋ณธ 4777, `AI_OFFICE_PORT` ๋กœ ๋ณ€๊ฒฝ. ํ›… ๋ช…๋ น์— ํฌํŠธ๊ฐ€ ๋ฐ•ํ˜€ ์žˆ์–ด(`lib/hookport.js` ๋กœ ์ฝ์Œ) ์„œ๋ฒ„ ํฌํŠธ์™€ ๋‹ค๋ฅด๋ฉด ํ™”๋ฉด์— ๊ฒฝ๊ณ ํ•œ๋‹ค.
154
+ - ํ™ˆ ํด๋” `~/.ai-office` (`AI_OFFICE_HOME`). `--no-journal` ๋กœ ์ €๋„ ๋„๊ธฐ.
155
+ - `AI_OFFICE_WATCH_PARENT=1` ์ด๋ฉด ๋ถ€๋ชจ ํ”„๋กœ์„ธ์Šค(์•ฑยทCLI)๊ฐ€ ๊ฐ•์ œ ์ข…๋ฃŒ๋  ๋•Œ ์„œ๋ฒ„๋„ ์Šค์Šค๋กœ ๋๋‚œ๋‹ค.
156
+
157
+ ### ์ €๋„ (lib/journal.js)
158
+
159
+ - ํ›… payload ๋ฅผ ํฐ ํ•„๋“œ๋ฅผ ์ž๋ฅธ ์ฑ„ ํ•œ ์ค„์”ฉ append (๋ฐ๋ชจ ์ด๋ฒคํŠธ๋Š” ๋‚จ๊ธฐ์ง€ ์•Š์Œ).
160
+ - ๊ธฐ๋™ ์‹œ ์ตœ๊ทผ 2์‹œ๊ฐ„(SESSION_TTL)๋งŒ ์‹œ๊ฐ„ ์ˆœ์„œ๋Œ€๋กœ `state.apply` ์— ๋‹ค์‹œ ํ˜๋ ค ์ƒํƒœ๋ฅผ ๋ณต์›ํ•œ๋‹ค.
161
+ - ๋งค์‹œ๊ฐ„ + 20MB ์ดˆ๊ณผ ์‹œ ์˜ค๋ž˜๋œ ์ค„์„ ๋ฒ„๋ฆฌ๊ณ  ๋‹ค์‹œ ์“ด๋‹ค(์••์ถ•). `ai-office compact` ๋กœ ์ฆ‰์‹œ ์••์ถ•, `ai-office doctor` ๊ฐ€ ํฌ๊ธฐ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค.
132
162
 
133
163
  ## 6. ํ”„๋ก ํŠธ์—”๋“œ ๋ชจ๋“ˆ
134
164
 
135
165
  ```
136
166
  public/
137
- index.html 3๋‹จ ๋ ˆ์ด์•„์›ƒ (์ขŒ 260px / ์ค‘์•™ canvas / ์šฐ 300px), ์ƒ๋‹จ ํ—ค๋”(์ธต ์ด๋ฆ„ยท์„ธ์…˜ ์ œ๋ชฉยทLIVE)
138
- style.css ๋ผ์ดํŠธ ํ†ค, ํŒจ๋„ ์นด๋“œ ์Šคํƒ€์ผ
167
+ index.html 3๋‹จ ๋ ˆ์ด์•„์›ƒ (์ขŒ ์ธต๋ณ„ ์•ˆ๋‚ด / ์ค‘์•™ canvas / ์šฐ ์‚ฌ๋žŒ๋“คยท๋กœ๊ทธ), ์ƒ๋‹จ ํ—ค๋”(์ธตยท์„ธ์…˜ ์ œ๋ชฉยท๋ธŒ๋žœ์น˜ยทLIVE)
168
+ style.css ๋ผ์ดํŠธ/๋‹คํฌ ํ…Œ๋งˆ ํ† ํฐ, ํŒจ๋„ ์นด๋“œ
169
+ fonts/ ์„œ๋ธŒ์…‹ ์›นํฐํŠธ (scripts/subset-fonts.sh)
139
170
  js/
140
- main.js SSE ์—ฐ๊ฒฐ, store ๊ฐฑ์‹ , requestAnimationFrame ๋ฃจํ”„
141
- store.js floors/sessions/log ๋ฏธ๋Ÿฌ, ์„ ํƒ๋œ floor/session, subscribe()
142
- panels.js ์ขŒยท์šฐ ํŒจ๋„ DOM ๋ Œ๋” (์ธต ๋ชฉ๋ก, ์„ธ์…˜ ๋ชฉ๋ก, ์‚ฌ๋žŒ๋“ค, ํ™œ๋™ ๋กœ๊ทธ, ๋ฒ”๋ก€)
171
+ main.js SSE ์—ฐ๊ฒฐยท์žฌ์ ‘์†, store ๊ฐฑ์‹ , ์•Œ๋ฆผ(์†Œ๋ฆฌยท๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ), ์„ค์ •, ํ‚ค๋ณด๋“œ(1~9 ์ธต ์ด๋™)
172
+ store.js floors/sessions/log ๋ฏธ๋Ÿฌ, ์„ ํƒ๋œ ์ธต/์„ธ์…˜, ์ธต ์ ‘๊ธฐ(auto/all/none), subscribe()
173
+ panels.js ์ขŒยท์šฐ ํŒจ๋„ DOM (์ธต ๋ชฉ๋ก + ๋ชจ๋‘ ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ, ์„ธ์…˜ ๋ชฉ๋ก, ์‚ฌ๋žŒ๋“ค, ํ™œ๋™ ๋กœ๊ทธ, ๋ฒ”๋ก€)
174
+ overview.js ์ „์ฒด ๋ณด๊ธฐ: ์ธต์„ ๊ฑด๋ฌผ์ฒ˜๋Ÿผ ์Œ“์•„ ๋ฏธ๋‹ˆ ์‚ฌ๋ฌด์‹ค + ์บ๋ฆญํ„ฐ ์ 
175
+ i18n.js ์–ธ์–ด: ํ•œ๊ตญ์–ด ์›๋ฌธ, ์˜์–ด๋Š” ์‚ฌ์ „ โ†’ ํŒจํ„ด ๊ทœ์น™ โ†’ ๊ตฌ์ ˆ ์น˜ํ™˜. ์‚ฌ์šฉ์ž ์š”์ฒญยท์ œ๋ชฉ์€ ๋ฒˆ์—ญํ•˜์ง€ ์•Š๋Š”๋‹ค
176
+ names.js ์„ธ์…˜๋ณ„ ์บ๋ฆญํ„ฐ ๋ณ„๋ช… (localStorage)
177
+ customize.js, avatarStore.js ์บ๋ฆญํ„ฐ ์™ธํ˜• ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆ (๋ชธ ์ข…๋ฅ˜ยทํŒŒ์ธ ยท์ƒ‰ยท์•…์„ธ์‚ฌ๋ฆฌ)
178
+ floorStylePicker.js ์ธต ๐ŸŽจ โ†’ ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ ์„ ํƒ (์„œ๋ฒ„์— ์ €์žฅ)
179
+ tooltip.js, texttip.js ์บ๋ฆญํ„ฐ ํˆดํŒ, ์ž˜๋ฆฐ ๊ธ€์ž ํˆดํŒ
143
180
  scene/
144
- map.js 28x22 ํƒ€์ผ ๊ทธ๋ฆฌ๋“œ, ๋ฐฉ ์˜์—ญ, ๊ฐ€๊ตฌ footprint, ๋ฒฝ, ์„œ ์žˆ๋Š” ์ž๋ฆฌ(slot), ํ†ตํ–‰ ๊ฐ€๋Šฅ ๋งต
145
- pathfind.js A* (4๋ฐฉํ–ฅ), ํƒ€์ผ ๋‹จ์œ„
181
+ layouts.js ์ธต ๋ ˆ์ด์•„์›ƒ ์ƒ์„ฑ๊ธฐ: 4์—ด ร— ๋  ๊ฒฉ์ž์— ๋ฐฉ ๋ฐฐ์น˜, ๋ฐฉ ํฌ๊ธฐ์— ๋งž๋Š” ๊ฐ€๊ตฌยท์„œ๋Š” ์ž๋ฆฌยทํœด์‹ ๋ช…์†Œ
182
+ floorStyle.js ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹(์ƒ‰) โ†” ๋ ˆ์ด์•„์›ƒ ์—ฐ๊ฒฐ
183
+ map.js ํƒ€์ผ ๊ฒฉ์ž(x 1..35, y 1..21), ๋ฒฝยท๋ฌธ, ํ†ตํ–‰ ๋งต, ํˆฌ์˜. setLayout() ์œผ๋กœ ์ธต๋งˆ๋‹ค ๊ต์ฒด
184
+ pathfind.js A* (4๋ฐฉํ–ฅ), ๋ฒฝ edge ์™€ solid ๊ฐ€๊ตฌ ํšŒํ”ผ
146
185
  actors.js Agent โ†’ Actor(์œ„์น˜, ๊ฒฝ๋กœ, ๊ฑท๊ธฐ ์• ๋‹ˆ, ๋งํ’์„ , ์Šฌ๋กฏ ์ ์œ /ํ•ด์ œ, ํœด์‹ ๋ฐฉ ์•ˆ ๋ฐฐํšŒ)
147
- spots.js ํœด์‹ ๋ฐฉ(ํœด๊ฒŒ์‹คยท์นดํŽ˜ยทํ…Œ๋ผ์Šค) ์•ˆ ๋ช…์†Œ ๋ชฉ๋ก: ํƒ€์ผ, ๋งํ’์„ , ๋จธ๋ฌด๋Š” ์‹œ๊ฐ„
148
- render.js canvas 2D ์•„์ด์†Œ๋ฉ”ํŠธ๋ฆญ: ๋ฐ”๋‹ฅ โ†’ ๊นŠ์ด ์ •๋ ฌ๋œ ๊ฐ€๊ตฌ/๋ฒฝ/์บ๋ฆญํ„ฐ โ†’ ๋งํ’์„ /์ด๋ฆ„ํ‘œ โ†’ ์„ ํƒ ํ•˜์ด๋ผ์ดํŠธ
149
- camera.js pan(๋“œ๋ž˜๊ทธ), zoom(ํœ /๋ฒ„ํŠผ), ์ดˆ๊ธฐ fit, ์„ ํƒ ์„ธ์…˜ ๋”ฐ๋ผ๊ฐ€๊ธฐ
150
- sprites.js ๊ฐ€๊ตฌ/์บ๋ฆญํ„ฐ๋ฅผ ์ฝ”๋“œ๋กœ ๊ทธ๋ฆฌ๋Š” ํ•จ์ˆ˜๋“ค (์™ธ๋ถ€ ์ด๋ฏธ์ง€ ์—†์Œ, ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ)
151
- daylight.js ์‹œ๊ฐ„๋Œ€ ์กฐ๋ช…: ์‹ค์ œ ์‹œ๊ฐ โ†’ ์•„์นจ/๋‚ฎ/์ €๋…/๋ฐค, ์ฐฝ๋ฌธยทํ…Œ๋ผ์Šคยท์‹ค๋‚ด ํ‹ดํŠธ ํŒ”๋ ˆํŠธ (์„ค์ • '์‹œ๊ฐ„๋Œ€ ์กฐ๋ช…', aiOffice.daylight)
186
+ spots.js ํœด์‹ ๋ฐฉ ๋ช…์†Œ (๋ ˆ์ด์•„์›ƒ์ด ์ฑ„์šฐ๋Š” live ๊ฐ์ฒด)
187
+ avatar.js ์น˜๋น„ ์บ๋ฆญํ„ฐ ๊ทธ๋ฆฌ๊ธฐ (์‚ฌ๋žŒยท๋กœ๋ด‡ยท๊ณ ์–‘์ดยท๊ณฐ)
188
+ render.js ๋ฐ”๋‹ฅ โ†’ 3D AABB ๊นŠ์ด ์ •๋ ฌ๋œ ๊ฐ€๊ตฌ/๋ฒฝ/์บ๋ฆญํ„ฐ โ†’ ๋ฐฉ ์ด๋ฆ„ํ‘œยท๋งํ’์„ 
189
+ sprites.js ๊ฐ€๊ตฌยท๋ฒฝ์„ ์ฝ”๋“œ๋กœ ๊ทธ๋ฆฌ๋Š” ํ•จ์ˆ˜๋“ค (์™ธ๋ถ€ ์ด๋ฏธ์ง€ ์—†์Œ)
190
+ camera.js ํŒฌยท์คŒยท๋งต ๋งž์ถคยทํด๋ฆญ ํŒ์ •, ์„ ํƒ ์„ธ์…˜ ๋”ฐ๋ผ๊ฐ€๊ธฐ
191
+ daylight.js ์‹ค์ œ ์‹œ๊ฐ โ†’ ์•„์นจ/๋‚ฎ/์ €๋…/๋ฐค ์ฐฝ๋ฌธยท์‹ค๋‚ด ํ‹ดํŠธ
152
192
  ```
153
193
 
154
- ๋งต ๋ ˆ์ด์•„์›ƒ (ํƒ€์ผ ์ขŒํ‘œ, ๊ทธ๋ฆผ ์ฐธ๊ณ )
194
+ ์ธต ๋ ˆ์ด์•„์›ƒ๊ณผ ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹
155
195
 
156
- ```
157
- x 1-8 x 10-17 x 19-26 x 28-35
158
- y 1-7 ์ž๋ฃŒ์‹ค(์ฑ…์žฅ) ๊ฐœ๋ฐœ์‹ค(์ฑ…์ƒ6) ์ •๋ณด๊ฒ€์ƒ‰์‹ค(๋Œ€ํ˜• ํ™”๋ฉด, ์ง€๊ตฌ๋ณธ) ์นดํŽ˜(๋ถ์ชฝ ์œ ๋ฆฌ์ฐฝ, ์นด์šดํ„ฐยท์ปคํ”ผ๋จธ์‹ ยทํ…Œ์ด๋ธ”)
159
- y 8-9 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๋ณต๋„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
160
- y 10-15 ํšŒ์˜์‹ค(ํ…Œ์ด๋ธ”) ํœด๊ฒŒ์‹ค(์†ŒํŒŒ) ์„œ๋ฒ„์‹ค(๋ž™ 2์—ด) ํ…Œ๋ผ์Šค(๋ฐํฌ ๋ฐ”๋‹ฅ, ๋‚œ๊ฐ„, ํŒŒ๋ผ์†”ยท๋ฒค์น˜ยท๋‚˜๋ฌด)
161
- y 16-17 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๋ณต๋„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ (ํ…Œ๋ผ์Šค๋Š” y 10-21 ๊นŒ์ง€, ๋ถ์ชฝ ๋ฒฝ ๋ฌธ์œผ๋กœ ์ถœ์ž…)
162
- y 18-21 ๋‚ด ์ž๋ฆฌ(์‚ฌ์šฉ์ž) ๋กœ๋น„(์•ˆ๋‚ด, ํ™”๋ถ„) ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ(๋ฌธ 2๊ฐœ)
163
- ```
164
-
165
- - x 27 ์€ ์„ธ๋กœ ๋ณต๋„. ์นดํŽ˜๋Š” ๋‚จ์ชฝ ๋ฒฝ ๋ฌธ, ํ…Œ๋ผ์Šค๋Š” ๋ถ์ชฝ ๋ฒฝ ๋ฌธ์œผ๋กœ ๋“œ๋‚˜๋“ ๋‹ค. ์นดํŽ˜ยทํ…Œ๋ผ์Šค๋Š” ๋„๊ตฌ ๋งคํ•‘ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋ผ ํœด๊ฒŒ์‹ค๊ณผ ํ•จ๊ป˜ ํœด์‹ ๋ฐฉ(์„ธ์…˜๋งˆ๋‹ค ํ•˜๋‚˜)์ด๋ฉฐ, ์‰ฌ๋Š” ์บ๋ฆญํ„ฐ๊ฐ€ ๋ฐฉ ์•ˆ ๋ช…์†Œ๋ฅผ ์˜ฎ๊ฒจ ๋‹ค๋‹Œ๋‹ค. "์ƒ๋™๊ฐ" ์ž์œจ ํ–‰๋™(๋Œ€๊ธฐ ์ค‘ ์‚ฐ์ฑ…)์˜ ๋ชฉ์ ์ง€์ด๊ธฐ๋„ ํ•˜๋‹ค.
166
- - ํ…Œ๋ผ์Šค ๋‚œ๊ฐ„์€ ๊ฒฉ์ž ๋ฐ–(x 36, y 22)์— ๋†“์ธ ๋‚ฎ์€ ๋ฒฝ์ด๋ผ ํ†ตํ–‰์—๋Š” ์˜ํ–ฅ์ด ์—†๋‹ค.
196
+ | ํ”„๋ฆฌ์…‹ | ๋ ˆ์ด์•„์›ƒ | ํœด์‹ ๋ฐฉ cafe / terrace ๊ฐ€ ๋˜๋Š” ๊ณณ |
197
+ |---|---|---|
198
+ | ๋”ฐ๋œปํ•œ ์šฐ๋“œ ยท ํŒŒ์Šคํ…” | classic | ์นดํŽ˜ / ํ…Œ๋ผ์Šค |
199
+ | ์ฟจ ๊ทธ๋ ˆ์ด | studio | ์ŠคํŠœ๋””์˜ค / ์„ธ๋ฏธ๋‚˜์‹ค |
200
+ | ๋ฏผํŠธ | fitness | ํ—ฌ์Šค์žฅ / ์˜ฅ์ƒ์ •์› |
201
+ | ๋ผ๋ฒค๋” | play | ๋ถ ๋ผ์šด์ง€ / ๊ฒŒ์ž„๋ฃธ |
202
+ | ์ƒŒ๋“œ | canteen | ๊ตฌ๋‚ด์‹๋‹น / ์ˆ˜๋ฉด์‹ค |
167
203
 
168
- - ์บ๋ฆญํ„ฐ๋Š” ๋ฐฉ๋งˆ๋‹ค ์ •ํ•ด์ง„ "์„œ๋Š” ์ž๋ฆฌ(slot)" ์ค‘ ๋นˆ ๊ณณ์„ ์ฐจ์ง€ํ•œ๋‹ค. ๊ฐ™์€ ๋ฐฉ์— ์—ฌ๋Ÿฟ์ด๋ฉด ์˜† ์ž๋ฆฌ๋กœ.
169
- - ์ด๋™์€ A* ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ผ ์ดˆ๋‹น 4ํƒ€์ผ. ๊ฑท๋Š” ๋™์•ˆ ์ƒํ•˜ ๋ฐ”์šด์Šค, ๋ฐœ๋ฐ‘ ๊ทธ๋ฆผ์ž.
170
- - ์„œ๋ธŒ์—์ด์ „ํŠธ๋Š” ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ํƒ€์ผ์—์„œ ์ƒ์„ฑ๋˜์–ด ํšŒ์˜์‹ค๋กœ ๊ฑธ์–ด์˜ค๊ณ , ๋๋‚˜๋ฉด ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ๋กœ ๊ฑธ์–ด๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค.
171
- - ๋งํ’์„ : `{์•„์ด์ฝ˜} {bubble} ยท {๊ฒฝ๊ณผ์‹œ๊ฐ„}`. ์„ ํƒ๋œ ์„ธ์…˜์˜ ์บ๋ฆญํ„ฐ๋Š” ํ…Œ๋‘๋ฆฌ ๊ฐ•์กฐ, ๋‚˜๋จธ์ง€๋Š” ๋ฐ˜ํˆฌ๋ช….
172
- - ์‚ฌ์šฉ์ž ์บ๋ฆญํ„ฐ("{me} (๋‚˜)")๋Š” ๋‚ด ์ž๋ฆฌ์— ํ•ญ์ƒ ์•‰์•„ ์žˆ๋‹ค. Claude ๊ฐ€ `ask` ์ƒํƒœ๋ฉด ๊ทธ ์˜†์— ์™€์„œ ์„ ๋‹ค.
173
- - ํ•œ ์ธต = ํ•œ ํ”„๋กœ์ ํŠธ. ๊ทธ ํ”„๋กœ์ ํŠธ์˜ ๋ชจ๋“  ์„ธ์…˜ ์บ๋ฆญํ„ฐ๊ฐ€ ๊ฐ™์€ ์ธต์— ๋ณด์ธ๋‹ค. ์ขŒ์ธก์—์„œ ์ธต์„ ๋ฐ”๊พธ๋ฉด ๋งต์ด ํ†ต์งธ๋กœ ๋ฐ”๋€๋‹ค.
204
+ - ์—…๋ฌด ๋ฐฉ 9๊ฐœ(libraryยทdevยทresearchยทmeetingยทloungeยทserverยทmydeskยทlobbyยทelevator)๋Š” ๋ชจ๋“  ๋ ˆ์ด์•„์›ƒ์— ์žˆ๋‹ค. ์œ„์น˜๋งŒ ๋‹ค๋ฅด๋‹ค.
205
+ - ์„œ๋ฒ„(`lib/floorStyle.js`)๊ฐ€ ๊ฐ™์€ ํ‘œ๋ฅผ CommonJS ๋กœ ๋“ค๊ณ  ์žˆ์–ด ํœด์‹ ๋งํ’์„ ์„ ๊ทธ ์ธต ํŽธ์˜์‹œ์„ค์— ๋งž์ถ˜๋‹ค (`test/floorStyleShared.test.js` ๊ฐ€ ๋‘ ํ‘œ ์ผ์น˜๋ฅผ ๊ณ ์ •).
206
+ - ์บ๋ฆญํ„ฐ๋Š” ๋ฐฉ๋งˆ๋‹ค ์ •ํ•ด์ง„ "์„œ๋Š” ์ž๋ฆฌ(slot)" ์ค‘ ๋นˆ ๊ณณ์„ ์ฐจ์ง€ํ•œ๋‹ค. ์ด๋™์€ A* ๊ฒฝ๋กœ, ์ดˆ๋‹น 4ํƒ€์ผ.
207
+ - ํœด์‹ ์ค‘์ธ ์บ๋ฆญํ„ฐ๋Š” ๊ทธ ๋ฐฉ ๋ช…์†Œ๋ฅผ 10~30์ดˆ๋งˆ๋‹ค ์˜ฎ๊ฒจ ๋‹ค๋‹Œ๋‹ค. ์ƒˆ ์ด๋ฒคํŠธ๊ฐ€ ์˜ค๋ฉด ์ฆ‰์‹œ ํ•ด๋‹น ๋ฐฉ์œผ๋กœ ๊ฐ„๋‹ค.
208
+ - ์„œ๋ธŒ์—์ด์ „ํŠธ๋Š” ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ์—์„œ ์ƒ์„ฑ๋˜์–ด ํšŒ์˜์‹ค๋กœ ๊ฑธ์–ด์˜ค๊ณ , ๋๋‚˜๋ฉด ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ๋กœ ๊ฑธ์–ด๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค.
209
+ - ์‚ฌ์šฉ์ž ์บ๋ฆญํ„ฐ("{me} (๋‚˜)")๋Š” ๋‚ด ์ž๋ฆฌ์— ์•‰์•„ ์žˆ๋‹ค. Claude ๊ฐ€ `ask` ์ƒํƒœ๋ฉด ๊ทธ ์˜†์— ์™€์„œ ์„ ๋‹ค.
174
210
 
175
- ## 7. ์„ค์น˜ยท์šด์˜ ์Šคํฌ๋ฆฝํŠธ
211
+ ## 7. CLI ยท ์„ค์น˜ ์Šคํฌ๋ฆฝํŠธ
176
212
 
177
213
  | ํŒŒ์ผ | ์—ญํ•  |
178
214
  |---|---|
179
- | `hooks/emit.sh` | ํ›… ๋ช…๋ น. stdin โ†’ POST. ์‹คํŒจ ๋ฌด์‹œ |
180
- | `scripts/install-hooks.js` | `~/.claude/settings.json` ๋ฐฑ์—… ํ›„ 11๊ฐœ ์ด๋ฒคํŠธ์— emit.sh ํ•ญ๋ชฉ์„ ๋ณ‘ํ•ฉ. ์ด๋ฏธ ์žˆ์œผ๋ฉด ๊ฑด๋„ˆ๋œ€. `--remove` ๋กœ ์ œ๊ฑฐ |
181
- | `scripts/demo.js` | ๊ฐ€์งœ ์„ธ์…˜ 2๊ฐœ(ํ”„๋กœ์ ํŠธ 2๊ฐœ)๊ฐ€ ์ฝ๊ธฐโ†’๊ฒ€์ƒ‰โ†’์‹คํ–‰โ†’์ˆ˜์ •โ†’์„œ๋ธŒ์—์ด์ „ํŠธโ†’์งˆ๋ฌธโ†’ํœด์‹ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋ฐ˜๋ณต POST. UI ๊ฐœ๋ฐœยท์‹œ์—ฐ์šฉ |
182
- | `npm start` | ์„œ๋ฒ„ ๊ธฐ๋™. ์ฝ˜์†”์— ์ ‘์† URL ์ถœ๋ ฅ |
215
+ | `bin/ai-office.js` | `ai-office` (์„ค์น˜ + ํ›… ๋“ฑ๋ก + ์„œ๋ฒ„ + ๋ธŒ๋ผ์šฐ์ €), `install [--force]`, `install-hooks`, `uninstall-hooks`, `start`, `demo`, `doctor`, `compact` |
216
+ | `hooks/emit.js` | ํ›… ๋ช…๋ น. stdin โ†’ POST. ์‹คํŒจ ๋ฌด์‹œ (`emit.sh` ๋Š” ์˜ˆ์ „ curl ๋ฒ„์ „. ๋“ฑ๋ก๋ผ ์žˆ์œผ๋ฉด install-hooks ๊ฐ€ emit.js ๋กœ ๋ฐ”๊พผ๋‹ค) |
217
+ | `scripts/install-hooks.js` | `settings.json` ๋ฐฑ์—… ํ›„ 11๊ฐœ ์ด๋ฒคํŠธ์— emit.js ํ•ญ๋ชฉ ๋ณ‘ํ•ฉ, ์ด๋ฏธ ์žˆ์œผ๋ฉด ๊ฑด๋„ˆ๋œ€, `--remove` ๋กœ ์ œ๊ฑฐ |
218
+ | `lib/nodepath.js` | ํ›… ๋ช…๋ น์— ๋„ฃ์„ Node ๊ฒฝ๋กœ: `~/.ai-office/node` > Homebrew Cellar ๋ฒ„์ „ ํด๋” โ†’ ๋ฒ„์ „ ์—†๋Š” opt ๋งํฌ > ํ˜„์žฌ Node. Node ๋ฅผ ์—…๊ทธ๋ ˆ์ด๋“œํ•ด๋„ ํ›…์ด ์•ˆ ๊นจ์ง€๊ฒŒ |
219
+ | `scripts/demo.js` | ๊ฐ€์งœ ์„ธ์…˜๋“ค์ด ์ฝ๊ธฐโ†’๊ฒ€์ƒ‰โ†’์‹คํ–‰โ†’์ˆ˜์ •โ†’์„œ๋ธŒ์—์ด์ „ํŠธโ†’์งˆ๋ฌธโ†’ํœด์‹ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋ฐ˜๋ณต POST |
220
+ | `scripts/bump-version.js` | ๋ฒ„์ „์„ package.json ยท app/package.json ยท tauri.conf.json ยท Cargo.toml ์— ๋™์‹œ ๋ฐ˜์˜ (`npm run bump:patch|minor`) |
221
+ | `scripts/fetch-node.js` | ๋ฐ์Šคํฌํ†ฑ ์•ฑ ์‚ฌ์ด๋“œ์นด์šฉ Node ๋ฐ”์ด๋„ˆ๋ฆฌ ๋‹ค์šด๋กœ๋“œ (์ปค๋ฐ‹ํ•˜์ง€ ์•Š์Œ) |
222
+ | `scripts/update-homebrew.js` | ๋ฆด๋ฆฌ์Šค ๋’ค Homebrew tap ์˜ caskยทformula ๋ฒ„์ „๊ณผ sha256 ๊ฐฑ์‹  |
223
+ | `scripts/make-tray-icon.js` | ๋ฉ”๋‰ด๋ฐ” ํ…œํ”Œ๋ฆฟ ์•„์ด์ฝ˜(๋นจ๊ฐ„ ์  ๋ณ€ํ˜• ํฌํ•จ) ์ƒ์„ฑ |
224
+
225
+ - CLI ๋Š” ์•ฑ ํŒŒ์ผ์„ `~/.ai-office/app` ์— ๋ณต์‚ฌํ•ด ๋‘๊ณ  ๊ฑฐ๊ธฐ์„œ ์‹คํ–‰ํ•œ๋‹ค(npx ์บ์‹œ๊ฐ€ ์ง€์›Œ์ ธ๋„ ํ›…์ด ์‚ด์•„ ์žˆ๊ฒŒ). ๋ฐ์Šคํฌํ†ฑ ์•ฑ์ด ๊น”์•„ ๋‘” ๊ฒƒ(`.app-version`)์€ `--force` ์—†์ด๋Š” ๋ฎ์–ด์“ฐ์ง€ ์•Š๋Š”๋‹ค.
226
+
183
227
 
184
228
  ## 8. ์„ค๊ณ„์ƒ ๊ฒฐ์ •
185
229
 
186
- - **์˜์กด์„ฑ 0**: ์„ค์น˜ ๋งˆ์ฐฐ์„ ์—†์• ๊ณ  ๋‚˜์ค‘์— Electron/Tauri ๋กœ ๊ฐ์Œ€ ๋•Œ ๊ทธ๋Œ€๋กœ ์˜ฎ๊ธฐ๊ธฐ ์œ„ํ•ด Node ๋‚ด์žฅ ๋ชจ๋“ˆ๊ณผ ๋ฐ”๋‹๋ผ JS ๋งŒ ์“ด๋‹ค.
230
+ - **์˜์กด์„ฑ 0**: ์„ค์น˜ ๋งˆ์ฐฐ์„ ์—†์• ๊ณ  ๋ฐ์Šคํฌํ†ฑ ์•ฑ(์‚ฌ์ด๋“œ์นด Node)์œผ๋กœ ๊ทธ๋Œ€๋กœ ์˜ฎ๊ธฐ๊ธฐ ์œ„ํ•ด Node ๋‚ด์žฅ ๋ชจ๋“ˆ๊ณผ ๋ฐ”๋‹๋ผ JS ๋งŒ ์“ด๋‹ค.
187
231
  - **SSE, WebSocket ์•„๋‹˜**: ์„œ๋ฒ„โ†’๋ธŒ๋ผ์šฐ์ € ๋‹จ๋ฐฉํ–ฅ์ด๋ฉด ์ถฉ๋ถ„ํ•˜๊ณ  ์žฌ์ ‘์†์ด ๋ธŒ๋ผ์šฐ์ €์— ๋‚ด์žฅ๋ผ ์žˆ๋‹ค.
188
232
  - **์ƒํƒœ๋Š” ์„œ๋ฒ„์—์„œ ๊ณ„์‚ฐ**: ํ›… ์ด๋ฒคํŠธ ํ•ด์„ ๊ทœ์น™์ด ํ•œ ๊ณณ์— ์žˆ์–ด์•ผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฝ๊ณ , ๋ธŒ๋ผ์šฐ์ €๋Š” "์–ด๋””๋กœ ๊ฐ€์„œ ๋ฌด์Šจ ๋งํ’์„ " ๋งŒ ๋ฐ›๋Š”๋‹ค.
189
- - **๊ทธ๋ฆผ ์ž์‚ฐ ์—†์Œ**: ๊ฐ€๊ตฌยท์บ๋ฆญํ„ฐ๋ฅผ canvas ์ฝ”๋“œ๋กœ ๊ทธ๋ฆฐ๋‹ค. ์ฒ˜์Œ์—” ๋‹จ์ˆœํ•œ ์ƒ์ž ์กฐํ•ฉ์œผ๋กœ ์‹œ์ž‘ํ•˜๊ณ , ๋‚˜์ค‘์— ์Šคํ”„๋ผ์ดํŠธ ์‹œํŠธ๋กœ ๊ต์ฒด ๊ฐ€๋Šฅํ•˜๋„๋ก sprites.js ์— ๊ฒฉ๋ฆฌํ•œ๋‹ค.
190
- - **๋ฉ”๋ชจ๋ฆฌ ์ƒํƒœ**: ์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ์‹œ ์ƒํƒœ๋Š” ์‚ฌ๋ผ์ง„๋‹ค. ๋‹ค์Œ ํ›…๋ถ€ํ„ฐ ๋‹ค์‹œ ์ฑ„์›Œ์ง„๋‹ค. ํ•„์š”ํ•ด์ง€๋ฉด `~/.ai-office/events.jsonl` ๋กœ append ํ›„ ์žฌ์ƒํ•˜๋Š” ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•œ๋‹ค.
233
+ - **๊ทธ๋ฆผ ์ž์‚ฐ ์—†์Œ**: ๊ฐ€๊ตฌยท์บ๋ฆญํ„ฐ๋ฅผ canvas ์ฝ”๋“œ๋กœ ๊ทธ๋ฆฐ๋‹ค (sprites.js ยท avatar.js).
234
+ - **์žฌ์ƒ ๊ฐ€๋Šฅํ•œ ์ €๋„**: ์ƒํƒœ๋ฅผ ์ง์ ‘ ์ €์žฅํ•˜์ง€ ์•Š๊ณ  ์›๋ณธ ์ด๋ฒคํŠธ๋ฅผ ๋‚จ๊ฒจ ์žฌ์ƒํ•œ๋‹ค. ์ƒํƒœ ๋จธ์‹ ์ด ๋ฐ”๋€Œ์–ด๋„ ์ €๋„์€ ๊ทธ๋Œ€๋กœ ์“ธ ์ˆ˜ ์žˆ๋‹ค.
235
+ - **๋ทฐ์–ด ์„ค์ •์€ ๋ธŒ๋ผ์šฐ์ €์—**: ๋ณ„๋ช…ยท์™ธํ˜•ยท์–ธ์–ดยทํ…Œ๋งˆ๋Š” localStorage. ์ธต ์ธํ…Œ๋ฆฌ์–ด์ฒ˜๋Ÿผ ๋ชจ๋‘๊ฐ€ ๊ฐ™์ด ๋ด์•ผ ํ•˜๋Š” ๊ฒƒ๋งŒ ์„œ๋ฒ„์— ๋‘”๋‹ค.
236
+ - **๋ฒ”์œ„**: Claude Code ํ›…๋งŒ ๋ฐ›๋Š”๋‹ค. claude.ai ์ฑ„ํŒ…ยทํด๋ผ์šฐ๋“œ ์„ธ์…˜์€ ํ›…์ด ์—†์–ด ๋‹ค๋ฃจ์ง€ ์•Š๋Š”๋‹ค.
237
+
238
+ ## 9. ๋ณด์•ˆ (BE-21)
239
+
240
+ ์„œ๋ฒ„๋Š” `127.0.0.1` ์—๋งŒ ์—ด๋ฆฌ์ง€๋งŒ, ๋ธŒ๋ผ์šฐ์ €์— ์—ด๋ฆฐ ๋‹ค๋ฅธ ์›น์‚ฌ์ดํŠธ๋„ ๊ทธ ์ฃผ์†Œ๋กœ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋‹ค. ์„ธ์…˜ ์ œ๋ชฉยท์š”์ฒญยท๊ฒฝ๋กœ๋ฅผ ์ฝ๊ฑฐ๋‚˜ ๊ฐ€์งœ ์ด๋ฒคํŠธ๋ฅผ ๋„ฃ์ง€ ๋ชปํ•˜๊ฒŒ:
241
+
242
+ - `Host` ์ด๋ฆ„์ด `127.0.0.1` / `localhost` / `[::1]` ์ด ์•„๋‹ˆ๋ฉด 403 โ†’ DNS ๋ฆฌ๋ฐ”์ธ๋”ฉ ์ฐจ๋‹จ
243
+ - `Origin` ์ด ์žˆ์œผ๋ฉด ์ด ์„œ๋ฒ„ ํ™”๋ฉด(๊ฐ™์€ ํฌํŠธ), ๋ฐ์Šคํฌํ†ฑ ์•ฑ ์ฐฝ(`tauri://localhost`, `http(s)://tauri.localhost`)๋งŒ ํ—ˆ์šฉ, ๋‚˜๋จธ์ง€(`null` ํฌํ•จ) 403
244
+ - `Access-Control-Allow-Origin` ์€ ํ—ˆ์šฉ๋œ Origin ๋งŒ ๊ทธ๋Œ€๋กœ ๋Œ๋ ค์ค€๋‹ค (`*` ์“ฐ์ง€ ์•Š์Œ)
245
+ - ํ›…(emit.js)ยทCLIยทcurl ์€ Origin ์„ ๋ณด๋‚ด์ง€ ์•Š์œผ๋ฏ€๋กœ ์˜ํ–ฅ ์—†์Œ
246
+
247
+ ## 10. ๋ฐ์Šคํฌํ†ฑ ์•ฑ (app/, Tauri 2)
191
248
 
192
- ## 9. ํ‹ฐ์ผ“ ๋ถ„ํ• 
249
+ ```
250
+ AI Office.app / AI Office.exe (app/src-tauri/src/lib.rs)
251
+ โ”œโ”€ ๋ฆฌ์†Œ์Šค(server.jsยทlibยทpublicยทhooksยทscripts) โ†’ ~/.ai-office/app ์— ๋ณต์‚ฌ (.app-version ์ด ๋‹ค๋ฅผ ๋•Œ๋งŒ)
252
+ โ”œโ”€ ์‚ฌ์ด๋“œ์นด Node โ†’ ~/.ai-office/node ์— ๋ณต์‚ฌ โ†’ ํ›… ๋ช…๋ น๋„ ์ด Node ๋ฅผ ์“ด๋‹ค
253
+ โ”œโ”€ ํ›… ๋“ฑ๋ก (install-hooks.js --app-dir โ€ฆ)
254
+ โ”œโ”€ ์„œ๋ฒ„ ์ž์‹ ํ”„๋กœ์„ธ์Šค (AI_OFFICE_WATCH_PARENT=1, Windows ๋Š” CREATE_NO_WINDOW). ํฌํŠธ๊ฐ€ ์ด๋ฏธ ์—ด๋ ค ์žˆ์œผ๋ฉด ๊ทธ ์„œ๋ฒ„๋ฅผ ์“ด๋‹ค
255
+ โ”œโ”€ ์ฐฝ: app/dist/index.html ๋กœ๋”๊ฐ€ /api/state ๊ฐ€ ๋œฐ ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค http://127.0.0.1:<port>/ ๋กœ ์ด๋™
256
+ โ”œโ”€ ํŠธ๋ ˆ์ด(๋ฉ”๋‰ด๋ฐ”): ์™ผ์ชฝยท์˜ค๋ฅธ์ชฝ ํด๋ฆญ ๋ชจ๋‘ ๋ฉ”๋‰ด. ๊ธฐ๋‹ค๋ฆฌ๋Š” ์„ธ์…˜์ด๋‚˜ ๋ฐ›์•„ ๋‘” ์—…๋ฐ์ดํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ๋นจ๊ฐ„ ์ 
257
+ โ”‚ (macOS ๋Š” status item ์˜ effectiveAppearance ๋กœ ๊ธ€์ž ์ƒ‰ ๊ฒฐ์ •), Dock ๋ฐฐ์ง€ / Windows ์˜ค๋ฒ„๋ ˆ์ด ์•„์ด์ฝ˜๋„ ๊ฐ™์€ ๊ทœ์น™
258
+ โ”œโ”€ ์—…๋ฐ์ดํ„ฐ (tauri-plugin-updater): ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋‚ด๋ ค๋ฐ›๊ธฐ๋งŒ โ†’ ๋ฉ”๋‰ด "์—…๋ฐ์ดํŠธ ์ ์šฉ" ๋•Œ ์„œ๋ฒ„ ์ข…๋ฃŒ โ†’ ์„ค์น˜ โ†’ ์žฌ์‹œ์ž‘.
259
+ โ”‚ ์„ฑ๊ณต ์ „์—” 5๋ถ„๋งˆ๋‹ค, ์„ฑ๊ณต ๋’ค์—” 6์‹œ๊ฐ„๋งˆ๋‹ค ํ™•์ธ
260
+ โ””โ”€ ๋กœ๊ทธ์ธ ์‹œ ์ž๋™ ์‹คํ–‰ (tauri-plugin-autostart, LaunchAgent / Run ํ‚ค, --hidden). ์ฒซ ์‹คํ–‰์— ํ•œ ๋ฒˆ ์ผœ๊ณ  ๋ฉ”๋‰ด ์ฒดํฌ๋ฐ•์Šค๋กœ ๋”
261
+ ```
262
+
263
+ ## 11. ๋ฐฐํฌ ยท CI
264
+
265
+ | ๊ฒฝ๋กœ | ๋‚ด์šฉ |
266
+ |---|---|
267
+ | GitHub Release | ํƒœ๊ทธ `v*` โ†’ `.github/workflows/release.yml`: macOS arm64 dmg + `.app.tar.gz(.sig)`, Windows NSIS(.sig), `latest.json`(์—…๋ฐ์ดํ„ฐ), ๊ณ ์ • ์ด๋ฆ„ ์‚ฌ๋ณธ(`AI-Office-mac-arm64.dmg`, `AI-Office-windows-x64-setup.exe`), Windows ์„ค์น˜ ์Šค๋ชจํฌ ํ…Œ์ŠคํŠธ |
268
+ | npm | `claude-ai-office` (bin `ai-office`). 2FA ๋•Œ๋ฌธ์— ์‚ฌ๋žŒ์ด `npm publish` |
269
+ | Homebrew | `cozyuj/homebrew-tap`: `Casks/ai-office.rb`(dmg, ๊ฒฉ๋ฆฌ ์†์„ฑ ์ œ๊ฑฐ), `Formula/claude-ai-office.rb`(npm tarball). release ๊ฐ€ ์„ฑ๊ณตํ•˜๋ฉด `homebrew.yml` ์ด ๋‘˜์„ ๊ฐฑ์‹ ํ•ด ํ‘ธ์‹œ(formula ๋Š” npm ์— ๊ทธ ๋ฒ„์ „์ด ์žˆ์„ ๋•Œ). npm publish ๋’ค์—” `gh workflow run homebrew.yml -f version=x.y.z` |
193
270
 
194
- BE
195
- 1. `BE-01` ํ›… ์ˆ˜์‹  ์„œ๋ฒ„ + emit.sh + install-hooks (POST /hook, SSE, ์ •์  ์„œ๋น™)
196
- 2. `BE-02` ์ด๋ฒคํŠธ ์ •๊ทœํ™” + ์ƒํƒœ ๋จธ์‹  (3ยท4์ ˆ ํ‘œ ๊ตฌํ˜„, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋Š” `node --test`)
197
- 3. `BE-03` ๋ฐ๋ชจ ์‹œ๋‚˜๋ฆฌ์˜ค ์Šคํฌ๋ฆฝํŠธ
271
+ - `test.yml`: `npm test` (node --test) ๋ฅผ ubuntu ยท windows ยท macos ์—์„œ
272
+ - `app-check.yml`: `app/**` ๊ฐ€ ๋ฐ”๋€Œ๋ฉด macOS ยท Windows ์—์„œ `cargo check --locked`
273
+ - ์•„์ง ์„œ๋ช…ยท๊ณต์ฆ ์ „์ด๋ผ macOS ๋Š” ์ฒ˜์Œ ์—ด ๋•Œ "ํ™•์ธ๋˜์ง€ ์•Š์€ ๊ฐœ๋ฐœ์ž" ๊ฒฝ๊ณ ๊ฐ€ ๋œฌ๋‹ค (Homebrew cask ๋Š” ๊ฒฉ๋ฆฌ ์†์„ฑ์„ ์ง€์›Œ ์šฐํšŒ).
198
274
 
199
- FE
200
- 4. `FE-01` 3๋‹จ ๋ ˆ์ด์•„์›ƒ + ์ขŒยท์šฐ ํŒจ๋„ (์ธต/์„ธ์…˜ ๋ชฉ๋ก, ์‚ฌ๋žŒ๋“ค, ํ™œ๋™ ๋กœ๊ทธ, ๋ฒ”๋ก€), SSE ์—ฐ๊ฒฐยทstore
201
- 5. `FE-02` ์•„์ด์†Œ๋ฉ”ํŠธ๋ฆญ ๋งต + ๊ฐ€๊ตฌ + ์นด๋ฉ”๋ผ (์ •์  ์‚ฌ๋ฌด์‹ค์ด ๊ทธ๋ ค์ง€๋Š” ๋‹จ๊ณ„)
202
- 6. `FE-03` ์บ๋ฆญํ„ฐ ์ด๋™(A*)ยท๋งํ’์„ ยท์Šฌ๋กฏยท์—˜๋ฆฌ๋ฒ ์ดํ„ฐ ์ถœํ‡ด๊ทผ
203
- 7. `FE-04` ์„ ํƒ/ํ•˜์ด๋ผ์ดํŠธ/๋”ฐ๋ผ๊ฐ€๊ธฐ, ๊ฒฝ๊ณผ ์‹œ๊ฐ„, ๋งˆ๋ฌด๋ฆฌ ๋‹ค๋“ฌ๊ธฐ
275
+ ## 12. ์ž‘์—… ๊ทœ์น™
204
276
 
205
- ์ˆœ์„œ: BE-01 โ†’ BE-02 โ†’ BE-03 โ†’ FE-01 โ†’ FE-02 โ†’ FE-03 โ†’ FE-04. ๋ธŒ๋žœ์น˜๋Š” `feature/BE-01`, `fix/FE-02`, `test/BE-02` ์ฒ˜๋Ÿผ `<type>/<ํ‹ฐ์ผ“ID>`. FE ๋Š” BE-03 ๋ฐ๋ชจ๋กœ ์‹ค์ œ ํ›… ์—†์ด๋„ ๊ฐœ๋ฐœยท๊ฒ€์ฆํ•œ๋‹ค.
277
+ - ์ž‘์—…๋งˆ๋‹ค GitHub ์ด์Šˆ `[FE-nn]` / `[BE-nn]` / `[APP-nn]` โ†’ main ์—์„œ `feature|fix|test|docs/<ID>` ๋ธŒ๋žœ์น˜ โ†’ PR โ†’ CI(test, app-check) ํ†ต๊ณผ ํ›„ ๋จธ์ง€ โ†’ ์ด์Šˆ์— ๊ฒฐ๊ณผยทํ…Œ์ŠคํŠธยท์บก์ฒ˜ ๋Œ“๊ธ€.
278
+ - ๋ณ€๊ฒฝ๋งˆ๋‹ค ๋ฒ„์ „์„ ์˜ฌ๋ฆฐ๋‹ค: ๋ฒ„๊ทธยท๋ฌธ์„œ `npm run bump:patch`, ๊ธฐ๋Šฅ `npm run bump:minor`.
package/lib/state.js CHANGED
@@ -240,6 +240,7 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
240
240
  function apply(payload, now = Date.now()) {
241
241
  if (!payload || typeof payload !== 'object') return;
242
242
  const ev = normalize(payload);
243
+ if (ev.event === 'SessionEnd' && !sessions.has(ev.sessionId)) return; // ๋ชจ๋ฅด๋Š” ์„ธ์…˜์˜ ์ข…๋ฃŒ(์น˜์šด ์„ธ์…˜ ์žฌ์ƒ ๋“ฑ)๋กœ ๋นˆ ์ธต์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค
243
244
  const { session: s, floor } = sessionFor(ev, now);
244
245
  let entry = null;
245
246
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-ai-office",
3
- "version": "0.25.6",
3
+ "version": "0.29.5",
4
4
  "description": "Claude Code ์„ธ์…˜ ํ™œ๋™์„ ์•„์ด์†Œ๋ฉ”ํŠธ๋ฆญ ์‚ฌ๋ฌด์‹ค๋กœ ์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™”",
5
5
  "keywords": [
6
6
  "claude",
package/public/index.html CHANGED
@@ -14,6 +14,7 @@
14
14
  <div class="panel-head">
15
15
  <span class="panel-title">์ธต๋ณ„ ์•ˆ๋‚ด</span>
16
16
  <span class="panel-sub">FLOOR GUIDE</span>
17
+ <button id="floors-fold" class="tool fold-btn" title="๋ชจ๋“  ์ธต์˜ ์„ธ์…˜ ๋ชฉ๋ก ์—ด๊ธฐ">๋ชจ๋‘ ํŽผ์น˜๊ธฐ</button>
17
18
  </div>
18
19
  <div id="floors" class="scroll"></div>
19
20
  <div class="panel-foot" id="left-foot"></div>
package/public/js/i18n.js CHANGED
@@ -73,6 +73,7 @@ const DICT = {
73
73
  '์ง€๊ธˆ ๋ณด๋Š” ์ธต์˜ ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ (์ธต๋ณ„ ์•ˆ๋‚ด์˜ ๐ŸŽจ ์™€ ๊ฐ™์Œ) โ€” ์ƒ‰๊ณผ ์‚ฌ๋ฌด์‹ค ๊ตฌ์กฐ๊ฐ€ ๋ฐ”๋€๋‹ˆ๋‹ค': 'Interior preset of the floor you are viewing (same as ๐ŸŽจ in Floors) โ€” changes colors and the office layout',
74
74
  '์ธต์„ ๊ฑด๋ฌผ์ฒ˜๋Ÿผ ์Œ“์•„ ํ•œ๋ˆˆ์— (O)': 'Stack the floors like a building (O)', 'ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค ํ‘œ': 'Keyboard shortcut table', 'ํ„ฐ๋ฏธ๋„์—์„œ ์ด ์„ธ์…˜์„ ์ด์–ด๊ฐ€๋Š” ๋ช…๋ น์„ ๋ณต์‚ฌ': 'Copy the command that resumes this session in a terminal', '์ด ์ธต ์ธํ…Œ๋ฆฌ์–ด ์Šคํƒ€์ผ ๊ณ ๋ฅด๊ธฐ': 'Pick this floor\'s interior style',
75
75
  '์ด ๋ธŒ๋ผ์šฐ์ €๋Š” ์•Œ๋ฆผ์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค': 'This browser does not support notifications', '๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์žˆ์–ด์š”': 'waiting for your answer',
76
+ '๋ชจ๋‘ ํŽผ์น˜๊ธฐ': 'Expand all', '๋ชจ๋‘ ์ ‘๊ธฐ': 'Collapse all', '๋ชจ๋“  ์ธต์˜ ์„ธ์…˜ ๋ชฉ๋ก ์—ด๊ธฐ': 'Open the session list of every floor', '๋ชจ๋“  ์ธต์˜ ์„ธ์…˜ ๋ชฉ๋ก ๋‹ซ๊ธฐ': 'Close the session list of every floor', '์„ธ์…˜ ์—†์Œ': 'No sessions',
76
77
  'ํ•œ๊ตญ์–ด / English': 'ํ•œ๊ตญ์–ด / English', 'ํ™”๋ฉด ์–ธ์–ด': 'UI language',
77
78
  '๋“œ๋ž˜๊ทธ๋กœ ์ด๋™': 'Drag to pan', 'ํœ ๋กœ ํ™•๋Œ€': 'wheel to zoom', '์บ๋ฆญํ„ฐ๋ฅผ ํด๋ฆญํ•˜๋ฉด ์„ธ์…˜ ์„ ํƒ': 'click a character to select its session',
78
79
  '์ˆซ์ž ์ธต ๋ฒˆํ˜ธ๋กœ ์ด๋™ (12F ๋Š” 1,2 ์—ฐํƒ€)': 'Digits go to a floor number (12F: press 1, 2)', 'โ†/โ†’ ์ด์ „ยท๋‹ค์Œ ์ธต': 'โ†/โ†’ previous ยท next floor', 'F ๋”ฐ๋ผ๊ฐ€๊ธฐ': 'F follow', 'A ์ž๋™ ๋”ฐ๋ผ๊ฐ€๊ธฐ': 'A auto-follow', 'D ๋ฐ๋ชจ': 'D demo', 'O ์ „์ฒด ๋ณด๊ธฐ': 'O building view', 'B ๋งํ’์„ ': 'B bubbles', 'S ์†Œ๋ฆฌ': 'S sound', 'T ํ…Œ๋งˆ': 'T theme', '/ ๋กœ๊ทธ ๊ฒ€์ƒ‰': '/ log search', 'Esc ๋‹ซ๊ธฐ': 'Esc close', '? ์ด ๋„์›€๋ง': '? this help',
@@ -87,7 +87,8 @@ export function renderFloors() {
87
87
  ${selected ? '<button class="floor-style" title="์ด ์ธต ์ธํ…Œ๋ฆฌ์–ด ์Šคํƒ€์ผ ๊ณ ๋ฅด๊ธฐ">๐ŸŽจ</button>' : ''}
88
88
  <span class="dot ${status}"></span>
89
89
  </div>`;
90
- if (!selected) return `<div class="floor">${head}</div>`;
90
+ const open = store.isFloorOpen(f.id);
91
+ if (!open) return `<div class="floor ${selected ? 'selected' : ''}">${head}</div>`;
91
92
  const list = sessions.map((s) => `
92
93
  <div class="session ${s.id === store.selectedSessionId ? 'selected' : ''} ${s.title ? '' : 'untitled'} ${s.status === 'away' || s.status === 'ended' ? 'away' : ''}" data-session="${esc(s.id)}" data-tip="${esc(s.title || s.id)}">
93
94
  <span class="dot ${s.status}"></span>
@@ -103,12 +104,14 @@ export function renderFloors() {
103
104
  </span>` : ''}
104
105
  </span>
105
106
  </div>`).join('');
106
- return `<div class="floor selected">${head}
107
+ return `<div class="floor open ${selected ? 'selected' : ''}">${head}
107
108
  <div class="floor-sessions">
108
- <label class="follow"><input type="checkbox" id="auto-follow" ${store.autoFollow ? 'checked' : ''}> ์ตœ๊ทผ ์„ธ์…˜ ์ž๋™์œผ๋กœ ๋”ฐ๋ผ๊ฐ€๊ธฐ</label>
109
- ${list}
109
+ ${selected ? `<label class="follow"><input type="checkbox" id="auto-follow" ${store.autoFollow ? 'checked' : ''}> ์ตœ๊ทผ ์„ธ์…˜ ์ž๋™์œผ๋กœ ๋”ฐ๋ผ๊ฐ€๊ธฐ</label>` : ''}
110
+ ${list || '<div class="muted" style="padding:4px 8px">์„ธ์…˜ ์—†์Œ</div>'}
110
111
  </div></div>`;
111
112
  }).join('');
113
+ const fold = $('floors-fold');
114
+ if (fold) { const all = store.allFloorsOpen(); fold.textContent = all ? '๋ชจ๋‘ ์ ‘๊ธฐ' : '๋ชจ๋‘ ํŽผ์น˜๊ธฐ'; fold.title = all ? '๋ชจ๋“  ์ธต์˜ ์„ธ์…˜ ๋ชฉ๋ก ๋‹ซ๊ธฐ' : '๋ชจ๋“  ์ธต์˜ ์„ธ์…˜ ๋ชฉ๋ก ์—ด๊ธฐ'; fold.disabled = floors.length < 1; }
112
115
  $('left-foot').textContent = `ํ”„๋กœ์ ํŠธ ${floors.length}๊ฐœ ยท ์„ธ์…˜ ${store.sessions.size}๊ฐœ${store.version ? ` ยท v${store.version}` : ''}`;
113
116
  }
114
117
 
@@ -181,6 +184,7 @@ export function bindPanelEvents() {
181
184
  const floor = e.target.closest('[data-floor]');
182
185
  if (floor) store.selectFloor(floor.dataset.floor);
183
186
  });
187
+ $('floors-fold').addEventListener('click', () => store.toggleAllFloors());
184
188
  $('log-search').addEventListener('input', (e) => store.setLogFilter({ q: e.target.value }));
185
189
  $('log-mine').addEventListener('click', () => store.setLogFilter({ mine: !store.logFilter.mine }));
186
190
  // ์„ธ์…˜ ์บ๋ฆญํ„ฐ ์ด๋ฆ„: ์ž…๋ ฅ(๋นˆ ๊ฐ’์ด๋ฉด ์ž๋™ ๋ณ„๋ช…์œผ๋กœ) ยท ๐ŸŽฒ ๋‹ค๋ฅธ ๋ณ„๋ช…
@@ -39,6 +39,7 @@ class Store {
39
39
  this.booted = false; // ์ฒซ ์Šค๋ƒ…์ƒท์—์„œ๋งŒ ๋งˆ์ง€๋ง‰ ์„ ํƒ์„ ๋ณต์›
40
40
  this.followCamera = false; // ์„ ํƒ ์„ธ์…˜ ์บ๋ฆญํ„ฐ๋ฅผ ์นด๋ฉ”๋ผ๊ฐ€ ๋”ฐ๋ผ๊ฐ
41
41
  this.bubbleMode = 'all'; // 'all' | 'selected'
42
+ this.floorFold = (() => { const v = ls.get('aiOffice.floorFold'); return v === 'all' || v === 'none' ? v : 'auto'; })(); // ์ธต๋ณ„ ์•ˆ๋‚ด: auto(์„ ํƒํ•œ ์ธต๋งŒ) ยท all ยท none
42
43
  this.sound = (() => { try { return localStorage.getItem('aiOffice.sound') === 'on'; } catch { return false; } })(); // ๋Œ€๊ธฐ ์•Œ๋ฆผ ์†Œ๋ฆฌ
43
44
  this.soundScope = ls.get('aiOffice.soundScope') === 'all' ? 'all' : 'waiting'; // 'waiting' ๋‹ต ๊ธฐ๋‹ค๋ฆผ๋งŒ | 'all' ์‹คํŒจยท์ข…๋ฃŒ๋„
44
45
  this.ambient = (() => { try { return localStorage.getItem('aiOffice.ambient') !== 'off'; } catch { return true; } })(); // ์‰ฌ๋Š” ์บ๋ฆญํ„ฐ ์ž์œจ ํ–‰๋™
@@ -142,7 +143,15 @@ class Store {
142
143
  }
143
144
  }
144
145
 
146
+ /** ์ธต ๋ชฉ๋ก์—์„œ ์ด ์ธต์˜ ์„ธ์…˜ ๋ชฉ๋ก์„ ํŽผ์น ์ง€ (FE-47) */
147
+ isFloorOpen(floorId) { return this.floorFold === 'all' || (this.floorFold === 'auto' && floorId === this.selectedFloorId); }
148
+ allFloorsOpen() { const fs = [...this.floors.keys()]; return fs.length > 0 && fs.every((id) => this.isFloorOpen(id)); }
149
+ setFloorFold(v) { this.floorFold = v === 'all' || v === 'none' ? v : 'auto'; ls.set('aiOffice.floorFold', this.floorFold === 'auto' ? null : this.floorFold); this.emit('option'); }
150
+ /** ๋ชจ๋‘ ํŽผ์น˜๊ธฐ โ†” ๋ชจ๋‘ ์ ‘๊ธฐ */
151
+ toggleAllFloors() { this.setFloorFold(this.allFloorsOpen() ? 'none' : 'all'); }
152
+
145
153
  selectFloor(id) {
154
+ if (this.floorFold === 'none') this.setFloorFold('auto'); // ๋ชจ๋‘ ์ ‘ํžŒ ์ƒํƒœ์—์„œ ์ธต์„ ๊ณ ๋ฅด๋ฉด ์ง€๊ธˆ์ฒ˜๋Ÿผ ๊ทธ ์ธต์ด ์—ด๋ฆฐ๋‹ค
146
155
  this.setAutoFollowFlag(false);
147
156
  this.selectedFloorId = id;
148
157
  this.selectedSessionId = null;
package/public/style.css CHANGED
@@ -38,12 +38,15 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background:
38
38
  #floors { padding: 8px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
39
39
  .floor { border: 1px solid var(--line); border-radius: 10px; background: #fafbfc; overflow: hidden; }
40
40
  .floor.selected { border-color: var(--accent); background: var(--accent-soft); }
41
+ .panel-head .fold-btn { margin-left: auto; align-self: center; padding: 2px 9px; font-size: 11px; }
42
+ .floor.open:not(.selected) .floor-sessions { background: var(--panel); }
41
43
  .floor-row { display: flex; align-items: center; gap: 10px; padding: 9px 10px; cursor: pointer; }
42
44
  .floor-row:hover { background: rgba(0, 0, 0, .03); }
43
45
  .floor-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 30px; height: 22px; padding: 0 6px; border-radius: 6px; background: #e8ebef; color: #4a515a; font-weight: 700; font-size: 11px; }
44
46
  .floor.selected .floor-badge, #office-head .floor-badge { background: var(--accent); color: #fff; }
45
- .floor-name { font-weight: 600; font-size: 13px; }
46
- .floor-meta { font-size: 11px; color: var(--muted); }
47
+ .floor-name { font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
48
+ .floor-meta { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* ๋ฐฐ์ง€ยท๐ŸŽจ ์— ๋ฐ€๋ ค ๋‘ ์ค„๋กœ ๊บพ์ด์ง€ ์•Š๊ฒŒ (FE-49) */
49
+ .floor-row > .badge-wait, .floor-row > .floor-style, .floor-row > .dot { flex: none; }
47
50
  .floor-text { flex: 1; min-width: 0; }
48
51
  .floor-style { border: 1px solid var(--line); background: var(--panel); cursor: pointer; font-size: 13px; line-height: 1; padding: 3px 5px; border-radius: 6px; opacity: .95; }
49
52
  .floor-style:hover { opacity: 1; background: rgba(0, 0, 0, .06); }
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  // ๋ฒ„์ „์„ ๋„ค ํŒŒ์ผ์— ๋™์‹œ์— ๋ฐ˜์˜ํ•œ๋‹ค: package.json, app/package.json, app/src-tauri/tauri.conf.json, app/src-tauri/Cargo.toml
4
+ // (+ Cargo.lock ์˜ ์ด ํฌ๋ ˆ์ดํŠธ ํ•ญ๋ชฉ. ์•ˆ ๋งž์œผ๋ฉด CI ์˜ cargo check --locked ๊ฐ€ ์‹คํŒจํ•œ๋‹ค)
4
5
  // node scripts/bump-version.js patch|minor|major|1.2.3 (๋ฒ„๊ทธ ์ˆ˜์ • = patch, ๊ธฐ๋Šฅ = minor, ํ˜ธํ™˜ ๊นจ์ง = major)
5
6
  const fs = require('fs');
6
7
  const path = require('path');
@@ -11,6 +12,7 @@ const FILES = {
11
12
  appPkg: path.join(ROOT, 'app', 'package.json'),
12
13
  tauri: path.join(ROOT, 'app', 'src-tauri', 'tauri.conf.json'),
13
14
  cargo: path.join(ROOT, 'app', 'src-tauri', 'Cargo.toml'),
15
+ lock: path.join(ROOT, 'app', 'src-tauri', 'Cargo.lock'),
14
16
  };
15
17
 
16
18
  function next(current, how) {
@@ -40,9 +42,16 @@ function bump(how) {
40
42
  changed.push(FILES.tauri);
41
43
  }
42
44
  if (fs.existsSync(FILES.cargo)) {
43
- const t = fs.readFileSync(FILES.cargo, 'utf8').replace(/^version = "[^"]+"/m, `version = "${to}"`);
44
- fs.writeFileSync(FILES.cargo, t);
45
+ const toml = fs.readFileSync(FILES.cargo, 'utf8');
46
+ fs.writeFileSync(FILES.cargo, toml.replace(/^version = "[^"]+"/m, `version = "${to}"`));
45
47
  changed.push(FILES.cargo);
48
+ const name = (/^name = "([^"]+)"/m.exec(toml) || [])[1];
49
+ if (name && fs.existsSync(FILES.lock)) {
50
+ const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51
+ const lock = fs.readFileSync(FILES.lock, 'utf8');
52
+ const t = lock.replace(new RegExp(`(\\[\\[package\\]\\]\\r?\\nname = "${esc}"\\r?\\nversion = )"[^"]+"`), `$1"${to}"`);
53
+ if (t !== lock) { fs.writeFileSync(FILES.lock, t); changed.push(FILES.lock); }
54
+ }
46
55
  }
47
56
  return { from: pkg.version, to, changed };
48
57
  }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- // ๋ฆด๋ฆฌ์Šค ๋’ค Homebrew tap(cozyuj/homebrew-tap)์˜ caskยทformula ๋ฒ„์ „๊ณผ sha256 ์„ ๊ฐฑ์‹ ํ•œ๋‹ค.
3
+ // ๋ฆด๋ฆฌ์Šค ๋’ค Homebrew tap(cozyuj/homebrew-tap)์˜ caskยทformula ๋ฒ„์ „๊ณผ sha256 ์„ ๊ฐฑ์‹ ํ•œ๋‹ค. CI ์—์„  .github/workflows/homebrew.yml ์ด ๋Œ๋ฆฐ๋‹ค.
4
4
  // node scripts/update-homebrew.js [--tap ../homebrew-tap] [--version 0.25.0]
5
5
  // cask: GitHub Release ์˜ AI.Office_<v>_aarch64.dmg, formula: npm ์˜ claude-ai-office-<v>.tgz (npm ์— ๊ทธ ๋ฒ„์ „์ด ์žˆ์„ ๋•Œ๋งŒ)
6
6
  const fs = require('fs');
package/server.js CHANGED
@@ -96,7 +96,7 @@ async function handleFloorStyle(req, res) {
96
96
  const id = typeof body.id === 'string' ? body.id : '';
97
97
  if (!id) { res.writeHead(400).end(); return; }
98
98
  const floor = state.setFloorStyle(id, typeof body.style === 'string' ? body.style : null);
99
- res.writeHead(200, { 'Content-Type': MIME['.json'], 'Access-Control-Allow-Origin': '*' });
99
+ res.writeHead(200, { 'Content-Type': MIME['.json'], ...cors(req) });
100
100
  res.end(JSON.stringify({ floor }));
101
101
  }
102
102
 
@@ -125,7 +125,7 @@ async function handleHook(req, res) {
125
125
 
126
126
  function handleEvents(req, res) {
127
127
  res.writeHead(200, {
128
- 'Access-Control-Allow-Origin': '*',
128
+ ...cors(req),
129
129
  'Content-Type': 'text/event-stream',
130
130
  'Cache-Control': 'no-cache',
131
131
  Connection: 'keep-alive',
@@ -150,25 +150,59 @@ function serveStatic(req, res) {
150
150
  });
151
151
  }
152
152
 
153
+ // ---------- ์ ‘๊ทผ ๊ฒ€์‚ฌ (BE-21) ----------
154
+ // ๋กœ์ปฌ ์„œ๋ฒ„๋ผ๋„ ๋ธŒ๋ผ์šฐ์ €์— ์—ด๋ฆฐ ๋‹ค๋ฅธ ์›น์‚ฌ์ดํŠธ๊ฐ€ fetchยทEventSource ๋กœ ์„ธ์…˜ ๊ธฐ๋ก(์ œ๋ชฉยท์š”์ฒญยท๊ฒฝ๋กœ)์„ ์ฝ๊ฑฐ๋‚˜ ๊ฐ€์งœ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.
155
+ // - Host ๊ฐ€ ๋ฃจํ”„๋ฐฑ ์ด๋ฆ„์ด ์•„๋‹ˆ๋ฉด ๊ฑฐ์ ˆ (DNS ๋ฆฌ๋ฐ”์ธ๋”ฉ ์ฐจ๋‹จ)
156
+ // - Origin ์ด ์žˆ์œผ๋ฉด AI Office ์ž์‹ ๊ณผ ๋ฐ์Šคํฌํ†ฑ ์•ฑ ์ฐฝ๋งŒ ํ—ˆ์šฉ. ํ›…(emit.js)ยทCLIยทcurl ์€ Origin ์ด ์—†์–ด ๊ทธ๋Œ€๋กœ ํ†ต๊ณผ
157
+ const LOOPBACK = new Set(['127.0.0.1', 'localhost', '[::1]', '::1']);
158
+ function hostAllowed(req) {
159
+ const host = String(req.headers.host || '').toLowerCase();
160
+ const name = host.startsWith('[') ? host.slice(0, host.indexOf(']') + 1) : host.replace(/:\d+$/, '');
161
+ return LOOPBACK.has(name);
162
+ }
163
+ function originAllowed(origin) {
164
+ if (!origin) return true;
165
+ let u;
166
+ try { u = new URL(origin); } catch { return false; }
167
+ const host = u.hostname.toLowerCase();
168
+ if (u.protocol === 'tauri:' && host === 'localhost') return true; // macOS ์•ฑ ์ฐฝ
169
+ if ((u.protocol === 'http:' || u.protocol === 'https:') && host === 'tauri.localhost') return true; // Windows ์•ฑ ์ฐฝ
170
+ return u.protocol === 'http:' && LOOPBACK.has(host) && Number(u.port || 80) === Number(PORT); // ์ด ์„œ๋ฒ„๊ฐ€ ๋„์šด ์›น ํ™”๋ฉด
171
+ }
172
+ /** ํ—ˆ์šฉ๋œ Origin ์ด๋ฉด ๊ทธ ๊ฐ’์„ ๋Œ๋ ค์ฃผ๋Š” CORS ํ—ค๋” (* ๋Š” ์“ฐ์ง€ ์•Š๋Š”๋‹ค) */
173
+ function cors(req) {
174
+ const origin = req.headers.origin;
175
+ return origin && originAllowed(origin) ? { 'Access-Control-Allow-Origin': origin, Vary: 'Origin' } : {};
176
+ }
177
+
153
178
  const server = http.createServer((req, res) => {
179
+ if (!hostAllowed(req) || !originAllowed(req.headers.origin)) {
180
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
181
+ return res.end('forbidden: AI Office ๋Š” ์ด ์ปดํ“จํ„ฐ์˜ AI Office ํ™”๋ฉด์—์„œ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค\n');
182
+ }
154
183
  const { pathname } = new URL(req.url, 'http://x');
155
184
  if (req.method === 'POST' && pathname === '/hook') return handleHook(req, res);
156
185
  if (req.method === 'GET' && pathname === '/events') return handleEvents(req, res);
157
186
  if (req.method === 'POST' && pathname === '/api/floors/style') return handleFloorStyle(req, res);
158
- if (req.method === 'POST' && pathname === '/api/demo/start') { startDemo(); res.writeHead(200, { 'Content-Type': MIME['.json'], 'Access-Control-Allow-Origin': '*' }); return res.end(JSON.stringify({ demo: true })); }
159
- if (req.method === 'POST' && pathname === '/api/demo/stop') { const removed = stopDemo(); res.writeHead(200, { 'Content-Type': MIME['.json'], 'Access-Control-Allow-Origin': '*' }); return res.end(JSON.stringify({ demo: false, removed })); }
187
+ if (req.method === 'POST' && pathname === '/api/demo/start') { startDemo(); res.writeHead(200, { 'Content-Type': MIME['.json'], ...cors(req) }); return res.end(JSON.stringify({ demo: true })); }
188
+ if (req.method === 'POST' && pathname === '/api/demo/stop') { const removed = stopDemo(); res.writeHead(200, { 'Content-Type': MIME['.json'], ...cors(req) }); return res.end(JSON.stringify({ demo: false, removed })); }
160
189
  // ๋๋‚œ/์ž๋ฆฌ ๋น„์šด ์„ธ์…˜์„ ๋ชฉ๋ก์—์„œ ์น˜์šด๋‹ค (TTL ์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ ). state.remove โ†’ SSE 'remove' ๋กœ ๋ชจ๋“  ๋ธŒ๋ผ์šฐ์ €์— ์ „ํŒŒ
161
190
  const del = /^\/api\/sessions\/([^/]+)$/.exec(pathname);
162
191
  if (del && req.method === 'DELETE') {
163
192
  const id = decodeURIComponent(del[1]);
164
193
  const found = !!state.get(id);
165
- if (found) state.remove(id);
166
- res.writeHead(found ? 200 : 404, { 'Content-Type': MIME['.json'], 'Access-Control-Allow-Origin': '*' });
194
+ if (found) {
195
+ state.remove(id);
196
+ // ์ €๋„์—๋„ ๋‚จ๊ฒจ ์„œ๋ฒ„๋ฅผ ๋‹ค์‹œ ์ผœ๋„ ์žฌ์ƒ์œผ๋กœ ๋˜์‚ด์•„๋‚˜์ง€ ์•Š๊ฒŒ (FE-49). ์žฌ์ƒํ•˜๋ฉด SessionEnd โ†’ 10์ดˆ ๋’ค ์ œ๊ฑฐ
197
+ const s0 = { session_id: id, hook_event_name: 'SessionEnd', reason: 'dismissed' };
198
+ if (!String(id).startsWith('demo')) journal.append(Date.now(), s0);
199
+ }
200
+ res.writeHead(found ? 200 : 404, { 'Content-Type': MIME['.json'], ...cors(req) });
167
201
  return res.end(JSON.stringify({ removed: found, sessionId: id }));
168
202
  }
169
203
  if (req.method === 'GET' && pathname === '/api/state') {
170
204
  // ๋ฐ์Šคํฌํƒ‘ ์•ฑ(tauri://) ๋กœ๋”๊ฐ€ ์„œ๋ฒ„ ์ค€๋น„ ์—ฌ๋ถ€๋ฅผ ๋ฌป๋Š”๋‹ค โ€” ๋กœ์ปฌ ์ „์šฉ ์„œ๋ฒ„๋ผ ์ „์ฒด ํ—ˆ์šฉ
171
- res.writeHead(200, { 'Content-Type': MIME['.json'], 'Access-Control-Allow-Origin': '*' });
205
+ res.writeHead(200, { 'Content-Type': MIME['.json'], ...cors(req) });
172
206
  return res.end(JSON.stringify({ ...withPorts(state.snapshot()), installDir: __dirname }));
173
207
  }
174
208
  if (req.method === 'GET') return serveStatic(req, res);