claude-ai-office 0.34.2 โ†’ 0.37.1

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
@@ -96,6 +96,12 @@ Each floor (= project) is assigned an **interior preset** automatically (hash of
96
96
 
97
97
  A session's rest room (cafรฉ or terrace) becomes that floor's facility: on a Mint floor the character goes to the Gym (`๐Ÿƒ working out`) and the log reads `โœ… โ€ฆ โ†’ ๐Ÿƒ Gym`. Press **๐ŸŽจ** on the selected floor to pick a preset by hand; the choice is stored on the server (`~/.ai-office/floor-styles.json`) so every browser sees the same floor (`Auto` restores the automatic choice). Building view shows the different layouts too.
98
98
 
99
+ **Theme floors**: the bottom of the ๐ŸŽจ menu has **๐ŸŽƒ Halloween ยท ๐ŸŽ„ Christmas ยท ๐Ÿ–๏ธ Summer vacation**. They keep the floor's layout and facilities and change the floor, walls, colors and decorations โ€” pumpkins, a bubbling cauldron, gravestones, floating ghosts and bats; a tree with twinkling lights, a fireplace with stockings, presents, snowmen and wreaths; parasols, loungers, a pool, beach balls and surfboards. Large pieces block the way only when every seat stays reachable. โš™๏ธ Settings โ†’ **Theme floors** โ†’ `๐Ÿ“… Auto by date` puts the season's theme on every floor you did not pick by hand (Halloween 10/15โ€“11/1, Christmas 12/1โ€“12/26, Summer 7/15โ€“8/31).
100
+
101
+ ![Christmas floor](docs/screenshots/fe-63-christmas.jpg)
102
+
103
+ The whole floor changes: the sky outside the office (a purple night with a big moon and bats / a snowy night / a blue sky with sun and clouds), garlands on every wall including partitions, a rug in every room plus low fog for Halloween, and pumpkins, lanterns or tiki torches along the corridors.
104
+
99
105
  ![Different office on every floor](docs/screenshots/fe-35-overview.jpg)
100
106
 
101
107
  Subagents wear blue hard hats and are named after the `description` given to the Agent tool. You (`me`) sit at My Desk, and Claude comes over when it has a question or needs permission.
@@ -115,7 +121,9 @@ Open **โš™๏ธ Settings** in the header. Everything here is stored in this browse
115
121
  | Demo | Four fake sessions on separate `(demo)` floors | `D` |
116
122
  | Character looks | Opens the customizer (below) | |
117
123
  | Daylight | Window, terrace and room lighting follow the real clock: morning (06โ€“10), day (10โ€“17), evening (17โ€“20), night (20โ€“06). Can be fixed to one period or turned off | |
118
- | Floor interior | Interior preset of the floor you are viewing | |
124
+ | Outdoor weather | Rain or snow over outdoor rooms (terrace, rooftop garden): `๐ŸŽ„ Match theme` (default; snow on Christmas floors) ยท `๐ŸŒค๏ธ Real weather` (type a city โ†’ current weather from [Open-Meteo](https://open-meteo.com) every 30 minutes; only the city name and coordinates are sent) ยท `โ˜€๏ธ Clear` ยท `๐ŸŒง๏ธ Rain` ยท `โ„๏ธ Snow` | |
125
+ | Theme floors | `๐ŸŽจ Pick manually` (default) or `๐Ÿ“… Auto by date` | |
126
+ | Floor interior | Interior preset or theme of the floor you are viewing | |
119
127
  | Language | ํ•œ๊ตญ์–ด / English | |
120
128
  | Shortcuts | Keyboard shortcut panel | `?` |
121
129
 
@@ -148,6 +156,8 @@ Characters are the 12 [Kenney Mini Characters](https://kenney.nl/assets/mini-cha
148
156
 
149
157
  ![Mix parts](docs/screenshots/fe-57-customize.jpg)
150
158
 
159
+ **Theme characters**: a row of ๐Ÿง› Vampire ยท ๐ŸงŸ Zombie ยท ๐Ÿ’€ Skeleton ยท ๐Ÿ‘ป Ghost ([Kenney Graveyard Kit](https://kenney.nl/assets/graveyard-kit), CC0), โ›„ Snowman ยท ๐ŸฆŒ Rudolph ([Kenney Holiday Kit](https://kenney.nl/assets/holiday-kit), CC0) and ready-made ๐ŸŽƒ Pumpkin head ยท ๐ŸŽ… Santa. Vampire, zombie and skeleton walk, sit, type and nod like the others and mix with other heads and bodies; snowman and Rudolph are still models that move as a whole. The accessory row adds hand-drawn **Santa hat ยท reindeer antlers ยท pumpkin mask** for anyone. Auto still uses only the 12 default characters.
160
+
151
161
  **My characters (your own pictures)**: click **๏ผ‹ Upload PNG** or drop a file anywhere on the customizer to use a picture with a transparent background.
152
162
 
153
163
  - The transparent margin is trimmed and the long side scaled to 256 px, then stored in this browser's localStorage (several pictures, up to 24 / about 2.5 MB). Hover a tile and press **ร—** twice to delete it; targets using it go back to their default.
package/README.md CHANGED
@@ -100,6 +100,26 @@ node bin/ai-office.js # ๋˜๋Š” npm run install-hooks && npm start
100
100
 
101
101
  ![์ธต๋งˆ๋‹ค ๋‹ค๋ฅธ ์‚ฌ๋ฌด์‹ค](docs/screenshots/fe-35-overview.jpg)
102
102
 
103
+ **ํ…Œ๋งˆ ์ธต**: ๐ŸŽจ ๋ฉ”๋‰ด ์•„๋ž˜์ชฝ์˜ **๐ŸŽƒ ํ• ๋กœ์œˆ ยท ๐ŸŽ„ ํฌ๋ฆฌ์Šค๋งˆ์Šค ยท ๐Ÿ–๏ธ ์—ฌ๋ฆ„ํœด๊ฐ€** ๋Š” ์ธต ๊ตฌ์กฐ(ํŽธ์˜์‹œ์„ค)๋Š” ๊ทธ๋Œ€๋กœ ๋‘๊ณ  ๋ฐ”๋‹ฅยท๋ฒฝยท์ƒ‰๊ณผ ์†Œํ’ˆ๋งŒ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค.
104
+
105
+ | ํ…Œ๋งˆ | ๋ฐ”๋‹ฅ ยท ๋ฒฝ | ์†Œํ’ˆ |
106
+ |---|---|---|
107
+ | ๐ŸŽƒ ํ• ๋กœ์œˆ | ๋ณด๋ผ ์ฒด์ปค ยท ์ค„๋ฌด๋Šฌ ๋ฒฝ์ง€, ๋ณด๋ž๋น› ํ‹ดํŠธ | ํ˜ธ๋ฐ• ๋“ฑ๋ถˆ, ๋“๋Š” ์†ฅ, ๋ฌ˜๋น„, ์ด›๋ถˆ, ๋‘ฅ์‹ค ์œ ๋ น, ๋‚ ์•„๋‹ค๋‹ˆ๋Š” ๋ฐ•์ฅ, ๊ฑฐ๋ฏธ์ค„, ์ฃผํ™ฉยท๋ณด๋ผ ๊นƒ๋ฐœ |
108
+ | ๐ŸŽ„ ํฌ๋ฆฌ์Šค๋งˆ์Šค | ๋‚˜๋ฌด ๋ฐ”๋‹ฅ ยท ํ†ต๋‚˜๋ฌด ๋ฒฝ, ๋”ฐ๋œปํ•œ ๋น› | ํŠธ๋ฆฌ(๋ฐ˜์ง์ด๋Š” ์ „๊ตฌ), ๋ฒฝ๋‚œ๋กœ(๋ถˆ๊ฝƒยท์–‘๋ง), ์„ ๋ฌผ, ๋ˆˆ์‚ฌ๋žŒ, ๋ฆฌ์Šค, ์ „๊ตฌ ์ค„ |
109
+ | ๐Ÿ–๏ธ ์—ฌ๋ฆ„ํœด๊ฐ€ | ๋ฐํฌ ยท ํฐ ํŒ์ž ๋ฒฝ(๋ฏผํŠธ ๋ ) | ํŒŒ๋ผ์†”, ์„ ๋ฒ ๋“œ, ํ’€์žฅ, ํŠœ๋ธŒ, ๋น„์น˜๋ณผ, ์•„์ด์Šค๋ฐ•์Šค, ์„œํ•‘๋ณด๋“œ, ๊ตฌ๋ช… ํŠœ๋ธŒ, ๊นƒ๋ฐœ ์ค„ |
110
+
111
+ ![ํฌ๋ฆฌ์Šค๋งˆ์Šค ์ธต](docs/screenshots/fe-63-christmas.jpg)
112
+
113
+ ํ…Œ๋งˆ ์ธต์€ ์ธต ์ „์ฒด๊ฐ€ ๋ฐ”๋€๋‹ˆ๋‹ค โ€” ์‚ฌ๋ฌด์‹ค ๋ฐ– ํ•˜๋Š˜(ํ• ๋กœ์œˆ ๋ณด๋ผ ๋ฐคํ•˜๋Š˜ยทํฐ ๋‹ฌยท๋ฐ•์ฅ ๋–ผ / ํฌ๋ฆฌ์Šค๋งˆ์Šค ๋ˆˆ ๋‚ด๋ฆฌ๋Š” ๋ฐค / ์—ฌ๋ฆ„ ํŒŒ๋ž€ ํ•˜๋Š˜ยทํ•ดยท๊ตฌ๋ฆ„), ์นธ๋ง‰์ด๊นŒ์ง€ ๋ชจ๋“  ๋ฒฝ์˜ ๊นƒ๋ฐœยท์ „๊ตฌ ๊ฐ€๋žœ๋“œ, ๋ฐฉ๋งˆ๋‹ค ๋Ÿฌ๊ทธ(๊ฑฐ๋ฏธ์ค„ ๋Ÿฌ๊ทธยท์ฒดํฌ ๋Ÿฌ๊ทธยท๋น„์น˜ ํƒ€์›”)์™€ ํ• ๋กœ์œˆ ๋ฐ”๋‹ฅ ์•ˆ๊ฐœ, ๋ณต๋„์˜ ํ˜ธ๋ฐ•ยท์ด›๋ถˆ / ๋žœํ„ด / ํšƒ๋ถˆ.
114
+
115
+ ![ํ• ๋กœ์œˆ ์ธต](docs/screenshots/fe-63-halloween.jpg)
116
+
117
+ **์•ผ์™ธ ๋‚ ์”จ**: ์•ผ์™ธ ๋ฐฉ์ด ์žˆ๋Š” ๊ตฌ์กฐ(ํ…Œ๋ผ์Šคยท์˜ฅ์ƒ์ •์›)์—์„œ๋Š” ๋น„๊ฐ€ ์˜ค๋ฉด ๋ฐ”๋‹ฅ์ด ์ –์€ ํ†ค์ด ๋˜๊ณ  ๋น—์ค„๊ธฐ๊ฐ€, ๋ˆˆ์ด ์˜ค๋ฉด ๋ฐ”๋‹ฅ์ด ํ•˜์–—๊ฒŒ ๋ฎ์ด๊ณ  ๋ˆˆ์†ก์ด๊ฐ€ ๋‚ด๋ฆฝ๋‹ˆ๋‹ค. โš™๏ธ ์„ค์ • **์•ผ์™ธ ๋‚ ์”จ** ๋ฅผ `๐ŸŒค๏ธ ์‹ค์ œ ๋‚ ์”จ` ๋กœ ๋‘๊ณ  ๋„์‹œ ์ด๋ฆ„(`์„œ์šธ`, `Tokyo` โ€ฆ)์„ ์ ์œผ๋ฉด [Open-Meteo](https://open-meteo.com)(ํ‚ค ์—†์Œ, CC BY 4.0)์—์„œ ๊ทธ ๋„์‹œ์˜ ํ˜„์žฌ ๋‚ ์”จ๋ฅผ 30๋ถ„๋งˆ๋‹ค ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค โ€” ๋„์‹œ ์ด๋ฆ„๊ณผ ์ขŒํ‘œ๋งŒ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
118
+
119
+ ![์˜ฅ์ƒ์ •์›์˜ ๋ˆˆ (ํฌ๋ฆฌ์Šค๋งˆ์Šค ์ธต)](docs/screenshots/fe-61-snow.jpg)
120
+
121
+ ํŠธ๋ฆฌยท๋ฒฝ๋‚œ๋กœยท์†ฅยทํ’€์žฅยท์„ ๋ฒ ๋“œ์ฒ˜๋Ÿผ ํฐ ์†Œํ’ˆ์€ ๊ธธ์„ ๋ง‰์ง€ ์•Š์„ ๋•Œ๋งŒ ์ง€๋‚˜๊ฐˆ ์ˆ˜ ์—†๊ฒŒ ๋‘ก๋‹ˆ๋‹ค. โš™๏ธ ์„ค์ •์˜ **ํ…Œ๋งˆ ์ธต** ์„ `๐Ÿ“… ๋‚ ์งœ์— ๋งž์ถฐ ์ž๋™` ์œผ๋กœ ๋ฐ”๊พธ๋ฉด ์ง์ ‘ ๊ณ ๋ฅด์ง€ ์•Š์€ ์ธต์— ๊ทธ ์ฒ ์˜ ํ…Œ๋งˆ๊ฐ€ ๋ฎ์ž…๋‹ˆ๋‹ค (10/15~11/1 ํ• ๋กœ์œˆ ยท 12/1~12/26 ํฌ๋ฆฌ์Šค๋งˆ์Šค ยท 7/15~8/31 ์—ฌ๋ฆ„ํœด๊ฐ€). ๊ธฐ๋ณธ์€ `๐ŸŽจ ์ง์ ‘ ๊ณ ๋ฅด๊ธฐ`.
122
+
103
123
  ์„œ๋ธŒ์—์ด์ „ํŠธ๋Š” ํŒŒ๋ž€ ์•ˆ์ „๋ชจ๋ฅผ ์“ฐ๊ณ , Agent ๋„๊ตฌ์— ์ ์€ description ์ด ์ด๋ฆ„์ด ๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ๋ณธ์ธ(`๋‚˜`)์€ ๋‚ด ์ž๋ฆฌ์— ์•‰์•„ ์žˆ๊ณ , Claude ๊ฐ€ ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜ ํ—ˆ๋ฝ์„ ๊ธฐ๋‹ค๋ฆฌ๋ฉด ๊ทธ ์˜†์œผ๋กœ ์ฐพ์•„์˜ต๋‹ˆ๋‹ค.
104
124
 
105
125
  ## ์„ค์ • ๋ฉ”๋‰ด์™€ ๋‹จ์ถ•ํ‚ค
@@ -116,7 +136,9 @@ node bin/ai-office.js # ๋˜๋Š” npm run install-hooks && npm start
116
136
  | ํ…Œ๋งˆ | ๋ผ์ดํŠธ / ๋‹คํฌ | `T` |
117
137
  | ๋ฐ๋ชจ | ๊ฐ€์งœ ์„ธ์…˜ 4๊ฐœ๋ฅผ ๋„์›Œ ์‚ฌ๋ฌด์‹ค์„ ๋ถ์ ์ด๊ฒŒ. ์‹ค์ œ ์„ธ์…˜๊ณผ ๋ณ„๋„ ์ธต์ด๊ณ  `(๋ฐ๋ชจ)` ๊ฐ€ ๋ถ™๋Š”๋‹ค | `D` |
118
138
  | ์บ๋ฆญํ„ฐ ์™ธํ˜• | ์™ธํ˜• ๋ชจ๋‹ฌ ์—ด๊ธฐ (์•„๋ž˜ ์ฐธ๊ณ ) | |
119
- | ์ธต ์ธํ…Œ๋ฆฌ์–ด | ์ง€๊ธˆ ๋ณด๋Š” ์ธต์˜ ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ (์ธต ํ–‰์˜ ๐ŸŽจ ์™€ ๊ฐ™์Œ) | |
139
+ | ์•ผ์™ธ ๋‚ ์”จ | ํ…Œ๋ผ์Šคยท์˜ฅ์ƒ์ •์› ๊ฐ™์€ ์•ผ์™ธ ๋ฐฉ์— ๋น„ยท๋ˆˆ. `๐ŸŽ„ ํ…Œ๋งˆ์— ๋งž์ถฐ`(๊ธฐ๋ณธ, ํฌ๋ฆฌ์Šค๋งˆ์Šค ์ธต์€ ๋ˆˆ) ยท `๐ŸŒค๏ธ ์‹ค์ œ ๋‚ ์”จ`(์•„๋ž˜ ์นธ์— ๋„์‹œ ์ด๋ฆ„ โ†’ [Open-Meteo](https://open-meteo.com) ํ˜„์žฌ ๋‚ ์”จ, 30๋ถ„๋งˆ๋‹ค) ยท `โ˜€๏ธ ๋ง‘์Œ` ยท `๐ŸŒง๏ธ ๋น„` ยท `โ„๏ธ ๋ˆˆ` | |
140
+ | ํ…Œ๋งˆ ์ธต | `๐ŸŽจ ์ง์ ‘ ๊ณ ๋ฅด๊ธฐ`(๊ธฐ๋ณธ) ๋˜๋Š” `๐Ÿ“… ๋‚ ์งœ์— ๋งž์ถฐ ์ž๋™` โ€” ํ• ๋กœ์œˆยทํฌ๋ฆฌ์Šค๋งˆ์Šคยท์—ฌ๋ฆ„ํœด๊ฐ€ ์ฒ ์— ์ง์ ‘ ๊ณ ๋ฅด์ง€ ์•Š์€ ์ธต์„ ๊ทธ ํ…Œ๋งˆ๋กœ | |
141
+ | ์ธต ์ธํ…Œ๋ฆฌ์–ด | ์ง€๊ธˆ ๋ณด๋Š” ์ธต์˜ ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ยทํ…Œ๋งˆ (์ธต ํ–‰์˜ ๐ŸŽจ ์™€ ๊ฐ™์Œ) | |
120
142
  | ์–ธ์–ด | ํ•œ๊ตญ์–ด / English. ํ™”๋ฉด ๋ฌธ๊ตฌยท์บ”๋ฒ„์Šค ๊ธ€์žยท์•Œ๋ฆผ์ด ๋ฐ”๋€๋‹ค. ์„ธ์…˜ ์ œ๋ชฉยทํ”„๋กฌํ”„ํŠธยทClaude ๋ฉ”์‹œ์ง€๋Š” ์“ด ๊ทธ๋Œ€๋กœ | |
121
143
  | ์‹œ๊ฐ„๋Œ€ ์กฐ๋ช… | ์‹ค์ œ ์‹œ๊ฐ์— ๋”ฐ๋ผ ์ฐฝ๋ฌธยทํ…Œ๋ผ์Šคยท์‹ค๋‚ด ๋น›์ด ์•„์นจ(06~10)ยท๋‚ฎ(10~17)ยท์ €๋…(17~20)ยท๋ฐค(20~06)์œผ๋กœ ๋ฐ”๋€๋‹ค. `์ž๋™` ์™ธ์— `๋”` ์ด๋‚˜ ์‹œ๊ฐ„๋Œ€ ํ•˜๋‚˜๋กœ ๊ณ ์ • ๊ฐ€๋Šฅ | |
122
144
  | ๋‹จ์ถ•ํ‚ค | ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค ํ‘œ ํŒจ๋„ | `?` |
@@ -162,6 +184,10 @@ node bin/ai-office.js # ๋˜๋Š” npm run install-hooks && npm start
162
184
 
163
185
  ![ํŒŒํŠธ ์กฐํ•ฉ ร— ๋™์ž‘ โ€” ๋งจ ์˜ค๋ฅธ์ชฝ์€ ์„œ๋ธŒ์—์ด์ „ํŠธ ์•ˆ์ „๋ชจ](docs/screenshots/fe-57-parts.jpg)
164
186
 
187
+ **ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ**: ๊ฐ™์€ ์ฐฝ์˜ ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ ์ค„์—์„œ ๐Ÿง› ๋ฑ€ํŒŒ์ด์–ด ยท ๐ŸงŸ ์ข€๋น„ ยท ๐Ÿ’€ ํ•ด๊ณจ ยท ๐Ÿ‘ป ์œ ๋ น([Kenney Graveyard Kit](https://kenney.nl/assets/graveyard-kit), CC0)๊ณผ โ›„ ๋ˆˆ์‚ฌ๋žŒ ยท ๐ŸฆŒ ๋ฃจ๋Œํ”„([Kenney Holiday Kit](https://kenney.nl/assets/holiday-kit), CC0), ์ถ”์ฒœ ์กฐํ•ฉ ๐ŸŽƒ ํ˜ธ๋ฐ• ๋จธ๋ฆฌ ยท ๐ŸŽ… ์‚ฐํƒ€๋ฅผ ๊ณ ๋ฆ…๋‹ˆ๋‹ค. ๋ฑ€ํŒŒ์ด์–ดยท์ข€๋น„ยทํ•ด๊ณจ์€ ๊ฑท๊ธฐยท์•‰๊ธฐยทํƒ€์ดํ•‘ยท๋„๋•์ž„์ด ๊ทธ๋Œ€๋กœ ๋˜๊ณ  ๋จธ๋ฆฌยท๋ชธ์„ ๋‹ค๋ฅธ ์บ๋ฆญํ„ฐ์™€ ์„ž์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ˆˆ์‚ฌ๋žŒยท๋ฃจ๋Œํ”„๋Š” ์ •์ง€ ๋ชจ๋ธ์ด๋ผ ๋ชธ ์ „์ฒด๋ฅผ ์›€์ง์—ฌ ๋ณด์ž…๋‹ˆ๋‹ค. ์†Œํ’ˆ ์ค„์—๋Š” ์ง์ ‘ ๊ทธ๋ฆฐ **์‚ฐํƒ€ ๋ชจ์ž ยท ๋ฃจ๋Œํ”„ ๋ฟ” ยท ํ˜ธ๋ฐ• ํƒˆ** ์ด ์žˆ์–ด ๋ˆ„๊ตฌ์—๊ฒŒ๋‚˜ ์”Œ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž๋™ ๋ฐฐ์ •์€ ์ง€๊ธˆ์ฒ˜๋Ÿผ ๊ธฐ๋ณธ 12๋ช…๋งŒ ์”๋‹ˆ๋‹ค.
188
+
189
+ ![ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ ร— ๋™์ž‘](docs/screenshots/fe-62-characters.jpg)
190
+
165
191
  **๋‚ด ์บ๋ฆญํ„ฐ (์ง์ ‘ ์˜ฌ๋ฆฐ ๊ทธ๋ฆผ)**: ๊ฐ™์€ ์ฐฝ์˜ **๏ผ‹ PNG ์˜ฌ๋ฆฌ๊ธฐ** ํƒ€์ผ์„ ๋ˆ„๋ฅด๊ฑฐ๋‚˜ ๊ทธ ์œ„(์ฐฝ ์–ด๋””๋“ )์— ํŒŒ์ผ์„ ๋Œ์–ด๋‹ค ๋†“์œผ๋ฉด, ๋ฐฐ๊ฒฝ์ด ํˆฌ๋ช…ํ•œ ๊ทธ๋ฆผ์„ ์บ๋ฆญํ„ฐ๋กœ ์”๋‹ˆ๋‹ค.
166
192
 
167
193
  ![PNG ์˜ฌ๋ฆฌ๊ธฐ โ€” ๊ณ ๋ฅด๊ธฐ ์ฐฝ](docs/screenshots/fe-56-customize.jpg)
@@ -229,6 +255,6 @@ npm run bump:minor # 0.19.0 โ†’ 0.20.0
229
255
 
230
256
  ๋ฆด๋ฆฌ์Šค: `git tag vX.Y.Z && git push origin vX.Y.Z` ๋ฅผ ์˜ฌ๋ฆฌ๋ฉด GitHub Actions ๊ฐ€ ๋งฅยทWindows ์•ฑ์„ ๋นŒ๋“œํ•ด Release ์— ์˜ฌ๋ฆฌ๊ณ (updater ์šฉ `latest.json` ํฌํ•จ), Windows ๋Ÿฌ๋„ˆ์—์„œ ์„ค์น˜ โ†’ ์‹คํ–‰ โ†’ ํ›…๊นŒ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋Š” pushยทPR ๋งˆ๋‹ค UbuntuยทWindowsยทmacOS ์—์„œ ๋•๋‹ˆ๋‹ค.
231
257
 
232
- ์บ๋ฆญํ„ฐ ์Šคํ”„๋ผ์ดํŠธ ๋‹ค์‹œ ๊ตฝ๊ธฐ: [Kenney Mini Characters](https://kenney.nl/assets/mini-characters) ํŒฉ์„ ๋ฐ›์•„ ํ’€๊ณ  `node scripts/bake-characters/bake.js <ํŒฉ ํด๋”>` (ํ—ค๋“œ๋ฆฌ์Šค ํฌ๋กฌ + three.js CDN, ๊ฒฐ๊ณผ๋Š” `public/assets/characters/*.webp`). ์บ๋ฆญํ„ฐ๋งˆ๋‹ค ์ƒ‰ ์‹œํŠธ(๋จธ๋ฆฌ ๋ ˆ์ด์–ด + ๋ชธํ†ต ๋ ˆ์ด์–ด)์™€ ๋งˆ์Šคํฌ ์‹œํŠธ(๋จธ๋ฆฌ์นด๋ฝยท์ƒ์˜ยทํ•˜์˜ ์Œ์˜), ์†Œํ’ˆ๋งˆ๋‹ค ์‹œํŠธ ํ•˜๋‚˜๋ฅผ ๊ตฝ๋Š”๋‹ค. ์‹œํŠธ ๊ตฌ์„ฑ(ํ”„๋ ˆ์ž„ ํฌ๊ธฐยท๋™์ž‘ ํ–‰ยท๋ ˆ์ด์–ด)๊ณผ ์†Œํ’ˆ ์œ„์น˜๋Š” `characters.js` ์˜ `SHEET`ยท`ACCESSORIES` ์—์„œ ์ •ํ•œ๋‹ค.
258
+ ์บ๋ฆญํ„ฐ ์Šคํ”„๋ผ์ดํŠธ ๋‹ค์‹œ ๊ตฝ๊ธฐ: [Kenney Mini Characters](https://kenney.nl/assets/mini-characters) ํŒฉ์„ ๋ฐ›์•„ ํ’€๊ณ  `node scripts/bake-characters/bake.js <ํŒฉ ํด๋”>` (ํ—ค๋“œ๋ฆฌ์Šค ํฌ๋กฌ + three.js CDN, ๊ฒฐ๊ณผ๋Š” `public/assets/characters/*.webp`). ์บ๋ฆญํ„ฐ๋งˆ๋‹ค ์ƒ‰ ์‹œํŠธ(๋จธ๋ฆฌ ๋ ˆ์ด์–ด + ๋ชธํ†ต ๋ ˆ์ด์–ด)์™€ ๋งˆ์Šคํฌ ์‹œํŠธ(๋จธ๋ฆฌ์นด๋ฝยท์ƒ์˜ยทํ•˜์˜ ์Œ์˜), ์†Œํ’ˆ๋งˆ๋‹ค ์‹œํŠธ ํ•˜๋‚˜๋ฅผ ๊ตฝ๋Š”๋‹ค. ์‹œํŠธ ๊ตฌ์„ฑ(ํ”„๋ ˆ์ž„ ํฌ๊ธฐยท๋™์ž‘ ํ–‰ยท๋ ˆ์ด์–ด)๊ณผ ์†Œํ’ˆ ์œ„์น˜๋Š” `characters.js` ์˜ `SHEET`ยท`ACCESSORIES` ์—์„œ ์ •ํ•œ๋‹ค. ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ๋Š” `--graveyard <Graveyard Kit ํด๋”> --holiday <Holiday Kit ํด๋”>` ๋ฅผ ๋”ํ•œ๋‹ค.
233
259
 
234
260
  ์ž‘์—… ํ๋ฆ„: GitHub ์ด์Šˆ๋ฅผ `[FE-30]`, `[BE-13]`, `[APP-03]` ์ฒ˜๋Ÿผ FE/BE/APP ์œผ๋กœ ๋‚˜๋ˆ  ๋งŒ๋“ค๊ณ , `main` ์—์„œ `feature/FE-30` ์‹ ๋ธŒ๋žœ์น˜๋ฅผ ๋ถ„๋ฆฌํ•ด ์ž‘์—…ํ•œ ๋’ค ๊ฒฐ๊ณผยทํ…Œ์ŠคํŠธยท์บก์ฒ˜๋ฅผ ์ด์Šˆ ๋Œ“๊ธ€๋กœ ๋‚จ๊ธด๋‹ค.
@@ -168,8 +168,9 @@ public/
168
168
  index.html 3๋‹จ ๋ ˆ์ด์•„์›ƒ (์ขŒ ์ธต๋ณ„ ์•ˆ๋‚ด / ์ค‘์•™ canvas / ์šฐ ์‚ฌ๋žŒ๋“คยท๋กœ๊ทธ), ์ƒ๋‹จ ํ—ค๋”(์ธตยท์„ธ์…˜ ์ œ๋ชฉยท๋ธŒ๋žœ์น˜ยทLIVE)
169
169
  style.css ๋ผ์ดํŠธ/๋‹คํฌ ํ…Œ๋งˆ ํ† ํฐ, ํŒจ๋„ ์นด๋“œ
170
170
  fonts/ ์„œ๋ธŒ์…‹ ์›นํฐํŠธ (scripts/subset-fonts.sh)
171
- assets/characters/ ์บ๋ฆญํ„ฐ ์Šคํ”„๋ผ์ดํŠธ webp: 12๋ช… ์ƒ‰ ์‹œํŠธ(๋จธ๋ฆฌยท๋ชธํ†ต ๋ ˆ์ด์–ด) + ๋งˆ์Šคํฌ ์‹œํŠธ(๋จธ๋ฆฌ์นด๋ฝยท์ƒ์˜ยทํ•˜์˜ ์Œ์˜) + ์†Œํ’ˆ 4์žฅ (scripts/bake-characters ๋กœ ๊ตฝ๋Š”๋‹ค)
171
+ assets/characters/ ์บ๋ฆญํ„ฐ ์Šคํ”„๋ผ์ดํŠธ webp: 12๋ช… ์ƒ‰ ์‹œํŠธ(๋จธ๋ฆฌยท๋ชธํ†ต ๋ ˆ์ด์–ด) + ๋งˆ์Šคํฌ ์‹œํŠธ(๋จธ๋ฆฌ์นด๋ฝยท์ƒ์˜ยทํ•˜์˜ ์Œ์˜) + ์†Œํ’ˆ 4์žฅ + ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ 6์žฅ (scripts/bake-characters ๋กœ ๊ตฝ๋Š”๋‹ค)
172
172
  js/
173
+ weather.js ์•ผ์™ธ ๋‚ ์”จ (FE-61): WMO ์ฝ”๋“œ โ†’ ๋น„ยท๋ˆˆ, Open-Meteo ์ง€์˜ค์ฝ”๋”ฉยทํ˜„์žฌ ๋‚ ์”จ
173
174
  main.js SSE ์—ฐ๊ฒฐยท์žฌ์ ‘์†, store ๊ฐฑ์‹ , ์•Œ๋ฆผ(์†Œ๋ฆฌยท๋ธŒ๋ผ์šฐ์ € ์•Œ๋ฆผ), ์„ค์ •, ํ‚ค๋ณด๋“œ(1~9 ์ธต ์ด๋™)
174
175
  store.js floors/sessions/log ๋ฏธ๋Ÿฌ, ์„ ํƒ๋œ ์ธต/์„ธ์…˜, ์ธต ์ ‘๊ธฐ(auto/all/none), subscribe()
175
176
  panels.js ์ขŒยท์šฐ ํŒจ๋„ DOM (์ธต ๋ชฉ๋ก + ๋ชจ๋‘ ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ, ์„ธ์…˜ ๋ชฉ๋ก, ์‚ฌ๋žŒ๋“ค, ํ™œ๋™ ๋กœ๊ทธ, ๋ฒ”๋ก€)
@@ -187,6 +188,8 @@ public/
187
188
  pathfind.js A* (4๋ฐฉํ–ฅ), ๋ฒฝ edge ์™€ solid ๊ฐ€๊ตฌ ํšŒํ”ผ
188
189
  actors.js Agent โ†’ Actor(์œ„์น˜, ๊ฒฝ๋กœ, ๊ฑท๊ธฐ ์• ๋‹ˆ, ๋งํ’์„ , ์Šฌ๋กฏ ์ ์œ /ํ•ด์ œ, ํœด์‹ ๋ฐฉ ์•ˆ ๋ฐฐํšŒ)
189
190
  spots.js ํœด์‹ ๋ฐฉ ๋ช…์†Œ (๋ ˆ์ด์•„์›ƒ์ด ์ฑ„์šฐ๋Š” live ๊ฐ์ฒด)
191
+ decorPlan.js ํ…Œ๋งˆ ์ธต ์†Œํ’ˆ ๋ฐฐ์น˜ (์ˆœ์ˆ˜ ํ•จ์ˆ˜, FE-60)
192
+ decor.js ํ…Œ๋งˆ ์ธต ์†Œํ’ˆ ๊ทธ๋ฆฌ๊ธฐ (ํ˜ธ๋ฐ•ยท์†ฅยท์œ ๋ นยท๋ฐ•์ฅยทํŠธ๋ฆฌยท๋ฒฝ๋‚œ๋กœยท๋ˆˆ์‚ฌ๋žŒยทํŒŒ๋ผ์†”ยท์„ ๋ฒ ๋“œยทํ’€์žฅ โ€ฆ)
190
193
  characters.js ์บ๋ฆญํ„ฐ ์Šคํ”„๋ผ์ดํŠธ (Kenney Mini Characters, CC0) ๋™์ž‘ ํ”„๋ ˆ์ž„ยท์•ˆ์ „๋ชจ, ํŒŒํŠธ ์กฐํ•ฉ('mix:...') ๋ ˆ์ด์–ดยท์ƒ‰ ์ž…ํžˆ๊ธฐยท์†Œํ’ˆ, ์ปค์Šคํ…€ ๊ทธ๋ฆผ('custom:<id>') ๊ทธ๋ฆฌ๊ธฐ
191
194
  render.js ๋ฐ”๋‹ฅ โ†’ 3D AABB ๊นŠ์ด ์ •๋ ฌ๋œ ๊ฐ€๊ตฌ/๋ฒฝ/์บ๋ฆญํ„ฐ โ†’ ๋ฐฉ ์ด๋ฆ„ํ‘œยท๋งํ’์„ 
192
195
  sprites.js ๊ฐ€๊ตฌยท๋ฒฝ์„ ์ฝ”๋“œ๋กœ ๊ทธ๋ฆฌ๋Š” ํ•จ์ˆ˜๋“ค (์™ธ๋ถ€ ์ด๋ฏธ์ง€ ์—†์Œ)
@@ -238,6 +241,9 @@ public/
238
241
  - **๋ทฐ์–ด ์„ค์ •์€ ๋ธŒ๋ผ์šฐ์ €์—**: ๋ณ„๋ช…ยท์™ธํ˜•ยท์–ธ์–ดยทํ…Œ๋งˆ๋Š” localStorage. ์ธต ์ธํ…Œ๋ฆฌ์–ด์ฒ˜๋Ÿผ ๋ชจ๋‘๊ฐ€ ๊ฐ™์ด ๋ด์•ผ ํ•˜๋Š” ๊ฒƒ๋งŒ ์„œ๋ฒ„์— ๋‘”๋‹ค.
239
242
  - **์ปค์Šคํ…€ ์บ๋ฆญํ„ฐ๋„ ๋ธŒ๋ผ์šฐ์ €์— (FE-56)**: ์˜ฌ๋ฆฐ PNG ๋Š” ์„œ๋ฒ„๋กœ ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค. ํˆฌ๋ช… ์—ฌ๋ฐฑ์„ ์ž๋ฅด๊ณ  ๊ธด ๋ณ€ 256px ๋กœ ์ค„์ธ dataURL ์„ `aiOffice.customCharacters.v1` ์— ๋‘๊ณ (์ตœ๋Œ€ 24์žฅยท2.5MB, ์‹คํŒจํ•˜๋ฉด ๋˜๋Œ๋ฆผ), ๊ณ ๋ฅธ ๊ฐ’์€ `custom:<id>` ๋กœ `aiOffice.characters.v1` ์— ์„ž์ธ๋‹ค. ํ‚ค๋ฅผ ๋‚˜๋ˆ  ๊ทธ๋ฆผ ์ €์žฅ์ด ์‹คํŒจํ•ด๋„ ๊ณ ๋ฅด๊ธฐ ์„ค์ •์€ ๋‚จ๋Š”๋‹ค. '์ž๋™' ํ•ด์‹œ๋Š” Kenney 12๋ช…๋งŒ ์“ด๋‹ค. ์ •์ง€ ๊ทธ๋ฆผ ํ•œ ์žฅ์ด๋ผ ๋™์ž‘์€ ๋ชธ ์ „์ฒด์˜ ์˜คํ”„์…‹ยท๊ธฐ์šธ๊ธฐยท๋ˆŒ๋ฆผ์œผ๋กœ ๋ณด์ด๊ณ , ์•ˆ์ „๋ชจ ์œ„์น˜๋Š” ์Šคํ”„๋ผ์ดํŠธ์™€ ๊ฐ™์€ ์•ŒํŒŒ ์Šค์บ”(`scanHead`)์œผ๋กœ ์ฐพ๋Š”๋‹ค.
240
243
  - **ํŒŒํŠธ ์กฐํ•ฉ์€ ๊ตฝ๊ณ  ๊ฒน์นœ๋‹ค (FE-57)**: Kenney 12๋ช…์€ ๋ผˆ๋Œ€๊ฐ€ ๊ฐ™๊ณ  ๋ชจ๋ธ์ด ๋จธ๋ฆฌยท๋ชธํ†ต ๋‘ ๋ฉ์–ด๋ฆฌ๋ผ, ๊ตฝ๊ธฐ์—์„œ ๋จธ๋ฆฌ ๋ ˆ์ด์–ด(๋ชธํ†ต์€ ๊นŠ์ด๋งŒ ์จ์„œ ํŒ”์ด ์•ž์— ์˜ค๋ฉด ๋จธ๋ฆฌ๊ฐ€ ์ž˜๋ฆฐ๋‹ค)์™€ ๋ชธํ†ต ๋ ˆ์ด์–ด๋ฅผ ๋”ฐ๋กœ ๊ตฝ๋Š”๋‹ค. ๋ชจ๋‘ ๊ฐ™์€ ๋ฐฐ์œจ(๊ฐ€์žฅ ํฐ ์บ๋ฆญํ„ฐ ๊ธฐ์ค€)ยท๊ฐ™์€ ์•‰๊ธฐ ๋†’์ด๋ผ ์•„๋ฌด ๋จธ๋ฆฌ๋‚˜ ์•„๋ฌด ๋ชธ ์œ„์— ๊ฒน์น˜๋ฉด ๋งž๋Š”๋‹ค. ์ƒ‰ ๋ฐ”๊พธ๊ธฐ๋Š” ํŒ”๋ ˆํŠธ(8์—ดร—4๋ ) ์นธ์œผ๋กœ ๋ถ€์œ„๋ฅผ ๊ณ ๋ฅธ๋‹ค: ํ”ผ๋ถ€ = ๋จธ๋ฆฌ์™€ ํŒ”์— ํ•จ๊ป˜ ์“ฐ์ธ ์นธ, ๋จธ๋ฆฌ์นด๋ฝ = ๋จธ๋ฆฌ์˜ ๊ทธ๋‹ค์Œ ๋„“์€ ์นธ(์–ผ๊ตด์— ๋ถ™์€ ๋‚ฉ์ž‘ํ•œ ๋ˆˆยท์ž…์€ ๋บŒ), ์ƒ์˜ยทํ•˜์˜ = ๋ชธํ†ตยทํŒ” / ๋‹ค๋ฆฌ(์‹ ๋ฐœ ์œ„)์—์„œ ๊ฐ€์žฅ ๋„“์€ ์นธ. ๊ทธ ๋ถ€์œ„๋ฅผ ํฐ ์•Œ๋ฒ ๋„ ร— ์ƒ๋Œ€ ๋ฐ๊ธฐ๋กœ ๋‹ค์‹œ ๊ทธ๋ฆฐ ์Œ์˜์„ ๋งˆ์Šคํฌ ์‹œํŠธ์— ๋‘๊ณ , ๊ทธ๋ฆด ๋•Œ ์„ ํ˜• ์ƒ‰๊ณต๊ฐ„์—์„œ ์Œ์˜ ร— ์ƒˆ ์ƒ‰์œผ๋กœ ์น ํ•œ ๋ ˆ์ด์–ด๋ฅผ ์บ์‹œํ•œ๋‹ค(์ตœ๊ทผ 16๊ฐœ). ์†Œํ’ˆ์€ ์›๋ž˜ ์•ˆ๊ฒฝ์ด ์—†๋Š” male-f ์˜ ๋จธ๋ฆฌ ๋ผˆ์— ๋ถ™์—ฌ ๊ตฌ์šด ์‹œํŠธ ํ•˜๋‚˜๋ฅผ ๋ชจ๋“  ๋จธ๋ฆฌ์— ์–น๋Š”๋‹ค. ๊ฐ’์€ ๋ฌธ์ž์—ด `mix:<๋จธ๋ฆฌ>.<๋ชธ>.<๋จธ๋ฆฌ์นด๋ฝ>.<์ƒ์˜>.<ํ•˜์˜>.<์†Œํ’ˆ>` ์ด๋ผ ๊ธฐ์กด ์ €์žฅยท์ •๊ทœํ™”ยท๋Œ€์ƒ๋ณ„ ์„ค์ •์„ ๊ทธ๋Œ€๋กœ ํƒ„๋‹ค.
244
+ - **ํ…Œ๋งˆ ์ธต์€ ๊ตฌ์กฐ ์œ„์— ๋ฎ๋Š” ์Šคํ‚จ (FE-60)**: ํ• ๋กœ์œˆยทํฌ๋ฆฌ์Šค๋งˆ์Šคยท์—ฌ๋ฆ„ํœด๊ฐ€๋Š” ํ”„๋ฆฌ์…‹๊ณผ ๋‹ฌ๋ฆฌ ๋ ˆ์ด์•„์›ƒ์„ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. ๊ทธ๋ฆฌ๋Š” ํ‚ค๊ฐ€ `halloween/mint` ์ฒ˜๋Ÿผ ํ…Œ๋งˆ/๊ตฌ์กฐ๋ผ, ์„œ๋ฒ„๋Š” ์ง์ ‘ ๊ณ ๋ฅธ ๊ฐ’(`floor.style`)์ด ํ…Œ๋งˆ๋ฉด ํœด์‹ ๋ฐฉ ์ด๋ฆ„์„ ์ž๋™ ํ”„๋ฆฌ์…‹(`styleAuto`) ๊ตฌ์กฐ๋กœ ๋งํ•œ๋‹ค(`FS.baseOf`). ๋‚ ์งœ ์ž๋™(์„ค์ • 'ํ…Œ๋งˆ ์ธต')์€ ๋ธŒ๋ผ์šฐ์ € ์„ค์ •์ด๊ณ  ์ง์ ‘ ๊ณ ๋ฅด์ง€ ์•Š์€ ์ธต์—๋งŒ ๋ฎ์ด๋ฉฐ, ๊ตฌ์กฐ๊ฐ€ ๊ทธ๋Œ€๋กœ๋ผ ์„œ๋ฒ„์˜ ํœด์‹ ๋ฐฉ ๋ง๊ณผ ์–ด๊ธ‹๋‚˜์ง€ ์•Š๋Š”๋‹ค. ์†Œํ’ˆ ์ž๋ฆฌ๋Š” `decorPlan.js` ๊ฐ€ ๋ ˆ์ด์•„์›ƒ๋งŒ ๋ณด๊ณ  ์ •ํ•˜๊ณ (์ž๋ฆฌยท๋ช…์†Œยท๋ฌธ ์•ž ๋น„์šฐ๊ธฐ, ๊ตฌ์„ยท๋’ท๋ฒฝ ์šฐ์„ , ๋ฒฝ ์†Œํ’ˆ์€ ํ‚ค ํฐ ๋ฐ”๊นฅ๋ฒฝ์—๋งŒ), `map.js` ๊ฐ€ ํฐ ์†Œํ’ˆ์„ ๋ง‰์•„๋„ ์—˜๋ฆฌ๋ฒ ์ดํ„ฐ์—์„œ ๋ชจ๋“  ์ž๋ฆฌยท๋ช…์†Œ์— ๊ธธ์ด ๋‚จ์„ ๋•Œ๋งŒ ๋ง‰๋Š”๋‹ค. ๊ทธ๋ฆฌ๊ธฐ๋Š” `decor.js`(ํ‰๋ฉด ๋ฒกํ„ฐ, ๋ฐ˜์ง์ดยทํ”๋“ค๋ฆฌ๋Š” ๊ฒƒ๋งŒ ๋งค ํ”„๋ ˆ์ž„), ๋ฐ”๋‹ฅ ๋ฌด๋Šฌยท๋ฒฝ ์ƒ‰ยทํ‹ดํŠธ๋Š” ํŒ”๋ ˆํŠธ(`palette()`)๊ฐ€ ํ…Œ๋งˆ์—์„œ ์ค€๋‹ค. ์ •์  ์—”ํ‹ฐํ‹ฐ ์บ์‹œ๋Š” `๋ ˆ์ด์•„์›ƒ|ํ…Œ๋งˆ` ํ‚ค. ์ธต ์ „์ฒด ๋ถ„์œ„๊ธฐ(FE-63): ํ™”๋ฉด ๊ณต๊ฐ„ ๋ฐฐ๊ฒฝ ํ•˜๋Š˜(`drawThemeBackdrop`, ๋ณ„ยท๋‹ฌยท๋ฐ•์ฅ ๋–ผยท๋ˆˆ์†ก์ดยท๊ตฌ๋ฆ„), ๋ฌธ ๊ตฌ๋ฉ ์‚ฌ์ด ๋ฒฝ ํ† ๋ง‰๋งˆ๋‹ค ๊ฐ€๋žœ๋“œ(๊ทธ ๋ฒฝ ๋†’์ด์— ๋งž์ถฐ), ๋ฐฉ๋งˆ๋‹ค ๋Ÿฌ๊ทธยท์•ˆ๊ฐœ(`kind: 'flat'` ์€ ๋ฐ”๋‹ฅ ์บ์‹œ์— ๊ทธ๋ฆฐ๋‹ค), ๋ณต๋„ ๋นˆ ํƒ€์ผ์— ๋„์—„๋„์—„ ์†Œํ’ˆ.
245
+ - **ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ (FE-62)**: Graveyard Kit ์บ๋ฆญํ„ฐ๋Š” ๋ผˆ๋Œ€ ๋Œ€์‹  ๋ถ€์œ„๋ณ„ ๋ฉ”์‹œ(๋จธ๋ฆฌยท๋ชธํ†ตยทํŒ”ยท๋‹ค๋ฆฌ)๋กœ ์›€์ง์ด์ง€๋งŒ ๋™์ž‘ ์ด๋ฆ„์ด ๋ฏธ๋‹ˆ ์บ๋ฆญํ„ฐ์™€ ๊ฐ™์•„, ๊ตฝ๊ธฐ์—์„œ `head` ๋ฉ”์‹œ๋ฅผ ๋จธ๋ฆฌ ๋ ˆ์ด์–ด๋กœ ๋‚˜๋ˆ  ๊ฐ™์€ ์‹œํŠธ ๋ชจ์–‘(๋จธ๋ฆฌ ํ–‰ + ๋ชธํ†ต ํ–‰)์œผ๋กœ ๊ตฝ๋Š”๋‹ค(์ƒ‰ ๋งˆ์Šคํฌ ์—†์Œ). ๊ทธ๋ž˜์„œ ๋ฑ€ํŒŒ์ด์–ดยท์ข€๋น„ยทํ•ด๊ณจ์€ `MIX_PARTS` ๋กœ ๋จธ๋ฆฌยท๋ชธ ์„ž๊ธฐ์— ๋“ค์–ด๊ฐ„๋‹ค. Holiday Kit ์˜ ๋ˆˆ์‚ฌ๋žŒยท์ˆœ๋ก์€ ๋™์ž‘์ด ์—†์–ด ๋ฏธ๋‹ˆ ์บ๋ฆญํ„ฐ ํ‚ค๋กœ ์ค„์—ฌ ํ•œ ์žฅ์„ ๋ชจ๋“  ์นธ์— ๋ณต์‚ฌํ•˜๊ณ , ๊ทธ๋ฆด ๋•Œ ์˜ฌ๋ฆฐ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์€ ๋ชธ ์ „์ฒด ์›€์ง์ž„(`motionOf`)์„ ์ค€๋‹ค. ์‚ฐํƒ€ ๋ชจ์žยท๋ฃจ๋Œํ”„ ๋ฟ”ยทํ˜ธ๋ฐ• ํƒˆ์€ ๊ตฝ์ง€ ์•Š๋Š” ์ฝ”์ŠคํŠฌ(`ACCESSORIES` ์˜ `draw`)์œผ๋กœ ์•ˆ์ „๋ชจ์ฒ˜๋Ÿผ ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ฐพ์€ ๋จธ๋ฆฌ ์œ„์น˜์— ๊ทธ๋ฆฐ๋‹ค(๋จธ๋ฆฌ ๋ ˆ์ด์–ด๊ฐ€ ๋น„๋ฉด ๋ชธํ†ต ๋ ˆ์ด์–ด์—์„œ ์ฐพ๋Š”๋‹ค).
246
+ - **์•ผ์™ธ ๋‚ ์”จ๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ (FE-61)**: ๋น„ยท๋ˆˆ์€ ๋ทฐ์–ด ์„ค์ •(`aiOffice.weather`)์ด๋ผ ์„œ๋ฒ„๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๋Š”๋‹ค. ์‹ค์ œ ๋‚ ์”จ๋Š” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Open-Meteo ์— ์ง์ ‘ ๋ฌป๋Š”๋‹ค(ํ‚คยทCORS ๋ฌธ์ œ ์—†์Œ, ๋„์‹œ ์ด๋ฆ„๊ณผ ์ขŒํ‘œ๋งŒ): ์ง€์˜ค์ฝ”๋”ฉ์€ ์ด๋ฆ„์ด ๊ฐ™์€ ์ž‘์€ ๋งˆ์„์ด ๋จผ์ € ๋‚˜์˜ค๊ณ  ํ•œ๊ธ€ ์งง์€ ์ด๋ฆ„('์„œ์šธ')์ด ์•ˆ ์žกํ˜€, ํ–‰์ •๊ตฌ์—ญ ์ ‘๋ฏธ์‚ฌ๋ฅผ ๋ถ™์ธ ํ›„๋ณด๊นŒ์ง€ ๋ชจ์•„ ์ธ๊ตฌ๊ฐ€ ๊ฐ€์žฅ ๋งŽ์€ ๊ณณ์„ ๊ณ ๋ฅธ๋‹ค. ํ˜„์žฌ ๋‚ ์”จ ์ฝ”๋“œ(WMO)๋ฅผ 30๋ถ„๋งˆ๋‹ค ๋ฐ›์•„ ๋น„ยท๋ˆˆยท๋ง‘์Œ์œผ๋กœ ์ค„์ด๊ณ , ์ง€๊ธˆ ๋‚ด๋ฆด ๊ฒƒ์€ ์„ค์ •ยท์‹ค์ œ ๋‚ ์”จยท์ธต ํ…Œ๋งˆ(ํฌ๋ฆฌ์Šค๋งˆ์Šค๋Š” ๊ธฐ๋ณธ ๋ˆˆ)๋ฅผ ํ•ฉ์ณ ๋งค ํ”„๋ ˆ์ž„ ์ •ํ•œ๋‹ค(`effectiveWeather`). ๊ทธ๋ฆฌ๊ธฐ๋Š” ์•ผ์™ธ ๋ฐฉ ์œ„ ์ƒ์ž ์•ˆ์—์„œ ๋–จ์–ด์ง€๋Š” ์ž…์ž(์›”๋“œ ๊ณต๊ฐ„, ์—”ํ‹ฐํ‹ฐ ๋’ค์— ๊ทธ๋ ค ์•ž์„ ๋ฎ์Œ)์™€ ๋ฐ”๋‹ฅ ํ†ค, ๋น„ ์˜ค๋Š” ๋‚  ์žฅ๋ฉด ํ‹ดํŠธ.
241
247
  - **๋ฒ”์œ„**: Claude Code ํ›…๋งŒ ๋ฐ›๋Š”๋‹ค. claude.ai ์ฑ„ํŒ…ยทํด๋ผ์šฐ๋“œ ์„ธ์…˜์€ ํ›…์ด ์—†์–ด ๋‹ค๋ฃจ์ง€ ์•Š๋Š”๋‹ค.
242
248
 
243
249
  ## 9. ๋ณด์•ˆ (BE-21)
package/lib/floorStyle.js CHANGED
@@ -4,6 +4,13 @@
4
4
  // ์„œ๋ฒ„๋Š” ์ธต๋งˆ๋‹ค ํ”„๋ฆฌ์…‹์„ ์ •ํ•ด(์ง์ ‘ ์„ ํƒ > ์ธต id ํ•ด์‹œ) ํœด์‹ ๋ฐฉ cafe/terrace ๋ฅผ ๊ทธ ์ธต์˜ ํŽธ์˜์‹œ์„ค ์ด๋ฆ„ยท์•„์ด์ฝ˜ยท๋งํ’์„ ์œผ๋กœ ๋ฐ”๊ฟ” ๋งํ•œ๋‹ค.
5
5
 
6
6
  const PRESET_IDS = ['warm', 'cool', 'mint', 'lavender', 'sand', 'pastel'];
7
+ // ํ…Œ๋งˆ ์ธต (FE-60): ์ง์ ‘ ๊ณ ๋ฅผ ์ˆ˜๋งŒ ์žˆ๊ณ , ๊ตฌ์กฐ(ํœด์‹ ๋ฐฉ)๋Š” ๊ทธ ์ธต์˜ ์ž๋™ ํ”„๋ฆฌ์…‹์„ ๋”ฐ๋ฅธ๋‹ค
8
+ const THEME_IDS = ['halloween', 'christmas', 'summer'];
9
+ const isTheme = (id) => THEME_IDS.includes(id);
10
+ /** ์ง์ ‘ ๊ณ ๋ฅผ ์ˆ˜ ์žˆ๋Š” ๊ฐ’์ธ์ง€ (ํ”„๋ฆฌ์…‹ ๋˜๋Š” ํ…Œ๋งˆ) */
11
+ const isChoice = (id) => PRESET_IDS.includes(id) || isTheme(id);
12
+ /** ์ธต์˜ ๊ตฌ์กฐ๋ฅผ ์ •ํ•˜๋Š” ํ”„๋ฆฌ์…‹: ํ…Œ๋งˆ๋ฅผ ๊ณจ๋ž์œผ๋ฉด ์ž๋™ ํ”„๋ฆฌ์…‹ */
13
+ const baseOf = (floor) => (floor ? (isTheme(floor.style) ? floor.styleAuto : floor.style) : null);
7
14
  const DEMO_PRESET = 'pastel';
8
15
  const AUTO_PRESETS = PRESET_IDS.filter((id) => id !== DEMO_PRESET); // ์‹ค์ œ ์ธต์˜ ์ž๋™ ๋ฐฐ์ • ํ›„๋ณด (๋ฐ๋ชจ ์ธต์€ ์ „์ฒด)
9
16
  const PRESET_LAYOUT = { warm: 'classic', cool: 'studio', mint: 'fitness', lavender: 'play', sand: 'canteen', pastel: 'classic' };
@@ -68,4 +75,4 @@ function restInfo(presetId, room) {
68
75
  return ROOM_REST[id] || { label: id, icon: 'โ˜•', bubble: 'ํœด์‹ ์ค‘' };
69
76
  }
70
77
 
71
- module.exports = { PRESET_IDS, DEMO_PRESET, AUTO_PRESETS, PRESET_LAYOUT, LAYOUT_REST, ROOM_REST, hashStr, isDemoFloor, assignPresets, restRoomIn, restInfo };
78
+ module.exports = { PRESET_IDS, THEME_IDS, isTheme, isChoice, baseOf, DEMO_PRESET, AUTO_PRESETS, PRESET_LAYOUT, LAYOUT_REST, ROOM_REST, hashStr, isDemoFloor, assignPresets, restRoomIn, restInfo };
package/lib/state.js CHANGED
@@ -33,7 +33,7 @@ function restRoomFor(sessionId) {
33
33
  /** ํœด์‹ ์ „์ด: ๋ฐฉยท๋งํ’์„ ยท์•„์ด์ฝ˜์„ ์„ธ์…˜์˜ ํœด์‹ ์žฅ์†Œ + ์ธต์˜ ํŽธ์˜์‹œ์„ค(ํ”„๋ฆฌ์…‹)์— ๋งž์ถ˜๋‹ค (FE-36) */
34
34
  function rest(bubble, sessionId, floor) {
35
35
  const room = restRoomFor(sessionId);
36
- const info = FS.restInfo(floor && floor.style, room);
36
+ const info = FS.restInfo(FS.baseOf(floor), room);
37
37
  return { room, activity: 'rest', bubble: bubble || info.bubble, icon: info.icon };
38
38
  }
39
39
 
@@ -111,7 +111,7 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
111
111
  // ---------- ์ธต ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ (FE-36): ์ง์ ‘ ์„ ํƒ(styleFile ์— ์ €์žฅ) > ์ธต id ํ•ด์‹œ ์ž๋™ ๋ฐฐ์ • ----------
112
112
  const styleOverrides = new Map(); // floorId โ†’ presetId
113
113
  if (styleFile) {
114
- try { for (const [k, v] of Object.entries(JSON.parse(fs.readFileSync(styleFile, 'utf8')))) if (FS.PRESET_IDS.includes(v)) styleOverrides.set(k, v); } catch {}
114
+ try { for (const [k, v] of Object.entries(JSON.parse(fs.readFileSync(styleFile, 'utf8')))) if (FS.isChoice(v)) styleOverrides.set(k, v); } catch {}
115
115
  }
116
116
  function saveStyles() {
117
117
  if (!styleFile) return;
@@ -129,7 +129,7 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
129
129
  }
130
130
  /** ์ธต ํ”„๋ฆฌ์…‹ ์ง์ ‘ ์„ ํƒ (null ์ด๋ฉด ์ž๋™์œผ๋กœ). ์ €์žฅํ•˜๊ณ  ๋ฐ”๋€ ์ธต์„ 'floor' ๋กœ ์•Œ๋ฆฐ๋‹ค */
131
131
  function setFloorStyle(id, preset) {
132
- const p = FS.PRESET_IDS.includes(preset) ? preset : null;
132
+ const p = FS.isChoice(preset) ? preset : null;
133
133
  if (p) styleOverrides.set(id, p); else styleOverrides.delete(id);
134
134
  saveStyles();
135
135
  for (const f of refreshStyles()) emitter.emit('floor', publicFloor(f));
@@ -395,7 +395,7 @@ function createState({ me = 'me', statFile = defaultStat, titleOf = defaultTitle
395
395
  const a = mainAgent(s, now);
396
396
  set(a, rest(null, s.id, floor), now);
397
397
  s.status = 'idle';
398
- entry = pushLog(now, floor, s, a, 'โœ…', `${ev.lastMessage ? cut(ev.lastMessage, 60) : '์ž‘์—… ์™„๋ฃŒ'} โ†’ ${a.icon} ${FS.restInfo(floor.style, a.room).label}`);
398
+ entry = pushLog(now, floor, s, a, 'โœ…', `${ev.lastMessage ? cut(ev.lastMessage, 60) : '์ž‘์—… ์™„๋ฃŒ'} โ†’ ${a.icon} ${FS.restInfo(FS.baseOf(floor), a.room).label}`);
399
399
  break;
400
400
  }
401
401
  case 'PreCompact': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-ai-office",
3
- "version": "0.34.2",
3
+ "version": "0.37.1",
4
4
  "description": "Claude Code ์„ธ์…˜ ํ™œ๋™์„ ์•„์ด์†Œ๋ฉ”ํŠธ๋ฆญ ์‚ฌ๋ฌด์‹ค๋กœ ์‹ค์‹œ๊ฐ„ ์‹œ๊ฐํ™”",
5
5
  "keywords": [
6
6
  "claude",
package/public/index.html CHANGED
@@ -42,6 +42,9 @@
42
42
  <div class="settings-row"><span class="settings-label">๋ฐ๋ชจ <kbd>D</kbd></span><button id="demo-btn" class="tool" title="๊ฐ€์งœ ์„ธ์…˜ 4๊ฐœ๋กœ ์‚ฌ๋ฌด์‹ค์„ ๋ถ์ ์ด๊ฒŒ (์‹ค์ œ ์„ธ์…˜๊ณผ ๋ณ„๋„ ์ธต, '(๋ฐ๋ชจ)' ํ‘œ์‹œ)">๐ŸŽฌ ๊บผ์ง</button></div>
43
43
  <div class="settings-row"><span class="settings-label">์บ๋ฆญํ„ฐ ์™ธํ˜•</span><button id="customize-btn" class="tool" title="๋‚˜ / Claude / ์„œ๋ธŒ์—์ด์ „ํŠธ ์บ๋ฆญํ„ฐ ๊ณ ๋ฅด๊ธฐ โ€” 12๋ช… ์ค‘ ํ•˜๋‚˜ ๋˜๋Š” ๋จธ๋ฆฌยท๋ชธยท์ƒ‰ยท์†Œํ’ˆ ์กฐํ•ฉ, ์„ธ์…˜๋งˆ๋‹ค ์ž๋™ ๋˜๋Š” ์ง์ ‘ ์˜ฌ๋ฆฐ PNG, ์ง€๊ธˆ ๋ณด๋Š” ์„ธ์…˜์˜ ์บ๋ฆญํ„ฐ ์ด๋ฆ„">๐Ÿ‘ค ๊ณ ๋ฅด๊ธฐโ€ฆ</button></div>
44
44
  <div class="settings-row"><span class="settings-label">์‹œ๊ฐ„๋Œ€ ์กฐ๋ช…</span><select id="daylight-select" class="tool" title="์‹ค์ œ ์‹œ๊ฐ์— ๋”ฐ๋ผ ์ฐฝ๋ฌธยทํ…Œ๋ผ์Šคยท์‹ค๋‚ด ๋น›์ด ๋ฐ”๋€๋‹ˆ๋‹ค (์•„์นจ 06~10 ยท ๋‚ฎ 10~17 ยท ์ €๋… 17~20 ยท ๋ฐค 20~06). ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋กœ ์‹œ๊ฐ„๋Œ€๋ฅผ ๊ณ ์ •ํ•  ์ˆ˜๋„ ์žˆ์–ด์š”"><option value="auto">๐Ÿ•’ ์ž๋™ (์‹ค์ œ ์‹œ๊ฐ)</option><option value="off">๋”</option><option value="morning">๐ŸŒ… ์•„์นจ</option><option value="day">โ˜€๏ธ ๋‚ฎ</option><option value="evening">๐ŸŒ‡ ์ €๋…</option><option value="night">๐ŸŒ™ ๋ฐค</option></select></div>
45
+ <div class="settings-row"><span class="settings-label">์•ผ์™ธ ๋‚ ์”จ</span><select id="weather-select" class="tool" title="ํ…Œ๋ผ์Šคยท์˜ฅ์ƒ์ •์› ๊ฐ™์€ ์•ผ์™ธ ๋ฐฉ์— ๋น„ยท๋ˆˆ โ€” ํ…Œ๋งˆ์— ๋งž์ถฐ๋Š” ํฌ๋ฆฌ์Šค๋งˆ์Šค ์ธต์— ๋ˆˆ, ์‹ค์ œ ๋‚ ์”จ๋Š” ์•„๋ž˜ ๋„์‹œ์˜ ํ˜„์žฌ ๋‚ ์”จ(Open-Meteo, 30๋ถ„๋งˆ๋‹ค)"><option value="theme">๐ŸŽ„ ํ…Œ๋งˆ์— ๋งž์ถฐ</option><option value="real">๐ŸŒค๏ธ ์‹ค์ œ ๋‚ ์”จ</option><option value="clear">โ˜€๏ธ ๋ง‘์Œ</option><option value="rain">๐ŸŒง๏ธ ๋น„</option><option value="snow">โ„๏ธ ๋ˆˆ</option></select></div>
46
+ <div class="settings-row hidden" id="weather-place-row"><span class="settings-label">๋‚ ์”จ ๋„์‹œ</span><input id="weather-place" class="tool weather-place" placeholder="์˜ˆ: ์„œ์šธ, Tokyo" autocomplete="off" spellcheck="false" title="๋„์‹œ ์ด๋ฆ„์„ ์ ๊ณ  Enter โ€” ๋„์‹œ ์ด๋ฆ„๊ณผ ์ขŒํ‘œ๋งŒ Open-Meteo ๋กœ ๋ณด๋ƒ…๋‹ˆ๋‹ค"><span id="weather-status" class="muted weather-status"></span></div>
47
+ <div class="settings-row"><span class="settings-label">ํ…Œ๋งˆ ์ธต</span><select id="theme-mode-select" class="tool" title="ํ• ๋กœ์œˆยทํฌ๋ฆฌ์Šค๋งˆ์Šคยท์—ฌ๋ฆ„ํœด๊ฐ€ ํ…Œ๋งˆ ์ธต โ€” ์ง์ ‘ ๊ณ ๋ฅด๊ธฐ๋Š” ๐ŸŽจ ์—์„œ ์ธต๋งˆ๋‹ค, ๋‚ ์งœ์— ๋งž์ถฐ ์ž๋™์€ 10/15~11/1 ํ• ๋กœ์œˆ ยท 12/1~12/26 ํฌ๋ฆฌ์Šค๋งˆ์Šค ยท 7/15~8/31 ์—ฌ๋ฆ„ํœด๊ฐ€ (์ง์ ‘ ๊ณ ๋ฅธ ์ธต์€ ๊ทธ๋Œ€๋กœ)"><option value="manual">๐ŸŽจ ์ง์ ‘ ๊ณ ๋ฅด๊ธฐ</option><option value="season">๐Ÿ“… ๋‚ ์งœ์— ๋งž์ถฐ ์ž๋™</option></select></div>
45
48
  <div class="settings-row"><span class="settings-label">์ธต ์ธํ…Œ๋ฆฌ์–ด</span><button id="floor-style-btn" class="tool" title="์ง€๊ธˆ ๋ณด๋Š” ์ธต์˜ ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ (์ธต๋ณ„ ์•ˆ๋‚ด์˜ ๐ŸŽจ ์™€ ๊ฐ™์Œ) โ€” ์ƒ‰๊ณผ ์‚ฌ๋ฌด์‹ค ๊ตฌ์กฐ๊ฐ€ ๋ฐ”๋€๋‹ˆ๋‹ค">๐ŸŽจ ๊ณ ๋ฅด๊ธฐโ€ฆ</button></div>
46
49
  <div class="settings-row"><span class="settings-label">์–ธ์–ด</span><select id="lang-select" class="tool" title="ํ™”๋ฉด ์–ธ์–ด"><option value="ko">ํ•œ๊ตญ์–ด</option><option value="en">English</option></select></div>
47
50
  <div class="settings-row"><span class="settings-label">๋‹จ์ถ•ํ‚ค <kbd>?</kbd></span><button id="keys-btn" class="tool" title="ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค ํ‘œ">โŒจ๏ธ ๋ณด๊ธฐโ€ฆ</button></div>
@@ -1,12 +1,26 @@
1
1
  // ์บ๋ฆญํ„ฐ ๊ณ ๋ฅด๊ธฐ ๋ชจ๋‹ฌ: ๋Œ€์ƒ ์„ ํƒ โ†’ 12๋ช… ์ค‘ ํ•˜๋‚˜(Claude ๊ณ„์—ด์€ '์ž๋™: ์„ธ์…˜๋งˆ๋‹ค ๋‹ค๋ฅด๊ฒŒ'๋„) ๋˜๋Š” ์˜ฌ๋ฆฐ ๊ทธ๋ฆผ โ†’ ๊ฑท๋Š” ๋ฏธ๋ฆฌ๋ณด๊ธฐ โ†’ ์ €์žฅ(localStorage)
2
2
  // ์ปค์Šคํ…€ ์บ๋ฆญํ„ฐ(FE-56): '๏ผ‹ PNG ์˜ฌ๋ฆฌ๊ธฐ' ํƒ€์ผ์— ํŒŒ์ผ์„ ๊ณ ๋ฅด๊ฑฐ๋‚˜ ๋Œ์–ด๋‹ค ๋†“์œผ๋ฉด ํˆฌ๋ช… ์—ฌ๋ฐฑ์„ ์ž˜๋ผ ์ €์žฅํ•˜๊ณ  ๊ทธ ๊ทธ๋ฆผ์„ ๊ณ ๋ฅธ๋‹ค.
3
3
  // ํŒŒํŠธ ๋ฐ”๊พธ๊ธฐ(FE-57): ๋จธ๋ฆฌยท๋ชธํ†ต์„ ๋‹ค๋ฅธ ์บ๋ฆญํ„ฐ ๊ฒƒ์œผ๋กœ, ๋จธ๋ฆฌ์นด๋ฝยท์ƒ์˜ยทํ•˜์˜ ์ƒ‰, ์–ผ๊ตด ์†Œํ’ˆ. ๊ฐ’์€ 'mix:...' (characters.js)
4
- import { ACCESSORIES, CHARACTERS, DEFAULTS, drawCharacter, isCustomCharacter, lookValue, parseLook, preloadCharacters } from './scene/characters.js';
4
+ import { ACCESSORIES, CHARACTERS, DEFAULTS, MIX_PARTS, SPECIALS, drawCharacter, isCustomCharacter, lookValue, parseLook, preloadCharacters } from './scene/characters.js';
5
5
  import { MESSAGES, prepareImage } from './customCharacters.js';
6
6
 
7
7
  // ์ƒ‰ ๊ณ ๋ฅด๊ธฐ ์นธ (6์ž๋ฆฌ hex). ๊ทธ ๋ฐ–์˜ ์ƒ‰์€ ์ƒ‰ ๊ณ ๋ฅด๊ฐœ๋กœ
8
8
  export const HAIR_COLORS = ['2b2b30', '4a3426', '7b4a2c', 'b5793f', 'e3c16f', 'c4532f', '9aa0ad', 'f1efe8', 'e48bb4', '5b86d6'];
9
9
  export const CLOTH_COLORS = ['e8683a', 'f2b51d', '5ac487', '1f8a70', '4f7fd0', '2f3b66', '8a5cc7', 'd0507a', '2b2f3a', 'eceef3', '9aa0ad', '8b5a3c'];
10
+ // ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ ์ค„ (FE-62): ํ‚คํŠธ ์บ๋ฆญํ„ฐ + ์ฝ”์ŠคํŠฌ์„ ์”Œ์šด ์ถ”์ฒœ ์กฐํ•ฉ
11
+ export const THEME_LOOKS = [
12
+ ...SPECIALS.filter((c) => !c.still).map((c) => [c.id, c.label]),
13
+ ['mix:male-f.male-f.-.2b2f3a.4a3426.pumpkin', 'ํ˜ธ๋ฐ• ๋จธ๋ฆฌ'],
14
+ ['mix:male-b.male-b.f4f4f4.d42a32.b0262e.santa', '์‚ฐํƒ€'],
15
+ ...SPECIALS.filter((c) => c.still).map((c) => [c.id, c.label]),
16
+ ];
17
+ /** ์„ž์„ ์ˆ˜ ์—†๋Š” ๋จธ๋ฆฌยท๋ชธ(์œ ๋ นยท๋ˆˆ์‚ฌ๋žŒยท๋ฃจ๋Œํ”„)์ด ๋ผ๋ฉด ์„ž์„ ์ˆ˜ ์žˆ๊ฒŒ ๋งž์ถ˜๋‹ค */
18
+ function fixLook(l) {
19
+ if (l.head === l.body) return l;
20
+ if (!MIX_PARTS.includes(l.body)) l.body = MIX_PARTS.includes(l.head) ? l.head : DEFAULTS.me;
21
+ if (!MIX_PARTS.includes(l.head)) l.head = l.body;
22
+ return l;
23
+ }
10
24
  const COLOR_ROWS = [['hair', '๋จธ๋ฆฌ์นด๋ฝ', HAIR_COLORS], ['top', '์ƒ์˜', CLOTH_COLORS], ['bottom', 'ํ•˜์˜', CLOTH_COLORS]];
11
25
 
12
26
  const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
@@ -69,6 +83,8 @@ export function createCustomizer(store, avatars) {
69
83
  ${nameRow()}
70
84
  <div class="cz-group"><div class="cz-label">์บ๋ฆญํ„ฐ${allowAuto() ? ' <span class="muted">โ€” ์ž๋™์ด๋ฉด ์„ธ์…˜๋งˆ๋‹ค ๋‹ค๋ฅด๊ฒŒ</span>' : ''}</div>
71
85
  <div class="cz-presets">${auto}${CHARACTERS.map((c) => `<button class="cz-preset ${c === draft ? 'on' : ''}" data-char="${c}" title="${c}"><canvas class="cz-thumb" data-char="${c}"></canvas></button>`).join('')}</div></div>
86
+ <div class="cz-group"><div class="cz-label">ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ <span class="muted">โ€” ํ• ๋กœ์œˆยทํฌ๋ฆฌ์Šค๋งˆ์Šค (์ž๋™์—๋Š” ์„ž์ด์ง€ ์•Š์Œ)</span></div>
87
+ <div class="cz-presets">${THEME_LOOKS.map(([v, label]) => `<button class="cz-preset cz-theme ${v === draft ? 'on' : ''}" data-char="${esc(v)}" title="${esc(label)}"><canvas class="cz-thumb" data-char="${esc(v)}"></canvas><span>${esc(label)}</span></button>`).join('')}</div></div>
72
88
  ${partsGroup()}
73
89
  <div class="cz-group"><div class="cz-label">๋‚ด ์บ๋ฆญํ„ฐ <span class="muted">โ€” ๋ฐฐ๊ฒฝ ์—†๋Š” PNG ๋ฅผ ์˜ฌ๋ฆฌ๊ฑฐ๋‚˜ ๋Œ์–ด๋‹ค ๋†“๊ธฐ ยท ์ž๋™์—๋Š” ์„ž์ด์ง€ ์•Š์Œ</span></div>
74
90
  <div class="cz-presets">${customs}${upload}</div>
@@ -84,7 +100,7 @@ export function createCustomizer(store, avatars) {
84
100
  function setPart(part, value) {
85
101
  const l = baseLook();
86
102
  l[part] = value || null;
87
- draft = lookValue(l); armed = null;
103
+ draft = lookValue(fixLook(l)); armed = null;
88
104
  render();
89
105
  }
90
106
  /** ํŒŒํŠธ ๋ฐ”๊พธ๊ธฐ: ๋จธ๋ฆฌยท๋ชธํ†ต ์ค„(์ธ๋„ค์ผ), ์ƒ‰ ์ค„(์›๋ž˜ + ํŒ”๋ ˆํŠธ + ์ƒ‰ ๊ณ ๋ฅด๊ฐœ), ์†Œํ’ˆ ์ค„ */
@@ -93,13 +109,13 @@ export function createCustomizer(store, avatars) {
93
109
  const thumb = (look, zoom) => `<canvas class="cz-thumb" data-char="${esc(lookValue(look))}"${zoom ? ` data-zoom="${zoom}"` : ''}></canvas>`;
94
110
  const tile = (part, value, on, title, inner) => `<button class="cz-preset cz-mini ${on ? 'on' : ''}" data-part="${part}" data-value="${esc(value)}" title="${esc(title)}">${inner}</button>`;
95
111
  const row = (label, inner) => `<div class="cz-partrow"><span class="cz-partlabel">${label}</span><div class="cz-presets">${inner}</div></div>`;
96
- const heads = CHARACTERS.map((c) => tile('head', c, cur && cur.head === c, `${c} ์˜ ๋จธ๋ฆฌ`, thumb({ ...l, head: c, hair: null }, 'head'))).join('');
97
- const bodies = CHARACTERS.map((c) => tile('body', c, cur && cur.body === c, `${c} ์˜ ๋ชธ`, thumb({ ...l, body: c, top: null, bottom: null }))).join('');
112
+ const heads = MIX_PARTS.map((c) => tile('head', c, cur && cur.head === c, `${c} ์˜ ๋จธ๋ฆฌ`, thumb(fixLook({ ...l, head: c, hair: null }), 'head'))).join('');
113
+ const bodies = MIX_PARTS.map((c) => tile('body', c, cur && cur.body === c, `${c} ์˜ ๋ชธ`, thumb(fixLook({ ...l, body: c, top: null, bottom: null })))).join('');
98
114
  const colors = COLOR_ROWS.map(([part, label, list]) => row(label,
99
115
  `<button class="cz-swatch cz-orig ${cur && !cur[part] ? 'on' : ''}" data-part="${part}" data-value="" title="์›๋ž˜ ์ƒ‰">์›๋ž˜</button>`
100
116
  + list.map((c) => `<button class="cz-swatch ${cur && cur[part] === c ? 'on' : ''}" data-part="${part}" data-value="${c}" title="#${c}" style="--sw:#${c}"></button>`).join('')
101
117
  + `<label class="cz-swatch cz-pick ${cur && cur[part] && !list.includes(cur[part]) ? 'on' : ''}" title="๋‹ค๋ฅธ ์ƒ‰ ๊ณ ๋ฅด๊ธฐ" style="${cur && cur[part] && !list.includes(cur[part]) ? `--sw:#${cur[part]}` : ''}"><input type="color" class="cz-color" data-part="${part}" value="#${(cur && cur[part]) || list[0]}"></label>`)).join('');
102
- const accs = [{ id: '', label: '์—†์Œ' }, ...ACCESSORIES].map((a) => tile('acc', a.id, cur && (cur.acc || '') === a.id, a.label, `${thumb({ ...l, acc: a.id || null }, 'head')}<span>${esc(a.label)}</span>`)).join('');
118
+ const accs = [{ id: '', label: '์—†์Œ' }, ...ACCESSORIES].map((a) => tile('acc', a.id, cur && (cur.acc || '') === a.id, a.label, `${thumb({ ...fixLook({ ...l }), acc: a.id || null }, 'head')}<span>${esc(a.label)}</span>`)).join('');
103
119
  return `<div class="cz-group"><div class="cz-label">ํŒŒํŠธ ๋ฐ”๊พธ๊ธฐ <span class="muted">โ€” ${cur ? '๋จธ๋ฆฌยท๋ชธ์„ ์„ž๊ณ  ์ƒ‰ยท์†Œํ’ˆ ๊ณ ๋ฅด๊ธฐ' : '์ง€๊ธˆ ์บ๋ฆญํ„ฐ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค'}</span></div>
104
120
  ${row('๋จธ๋ฆฌ', heads)}${row('๋ชธ', bodies)}${colors}${row('์†Œํ’ˆ', accs)}</div>`;
105
121
  }
@@ -1,5 +1,5 @@
1
1
  // ์ธต ํ–‰์˜ ๐ŸŽจ ๋ฒ„ํŠผ โ†’ ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ ํŒ์˜ค๋ฒ„ (FE-34). ์„ ํƒ์€ localStorage(aiOffice.floorStyle.<floorId>) ์— ๋‚จ๋Š”๋‹ค.
2
- import { PRESETS, presetById } from './scene/floorStyle.js';
2
+ import { PRESETS, THEMES, presetById } from './scene/floorStyle.js';
3
3
  import { LAYOUTS } from './scene/layouts.js';
4
4
 
5
5
  export function createFloorStylePicker(store, book, { onChange } = {}) {
@@ -19,7 +19,9 @@ export function createFloorStylePicker(store, book, { onChange } = {}) {
19
19
  pop.innerHTML = `
20
20
  <div class="style-pop-head">๐ŸŽจ ${esc(floor.no)}F ์ธํ…Œ๋ฆฌ์–ด</div>
21
21
  <button class="style-opt ${chosen ? '' : 'on'}" data-preset=""><span class="style-chips">${chips(autoP)}</span><span class="style-name">์ž๋™ <span class="muted">(${esc(autoP.label)})</span></span></button>
22
- ${PRESETS.map((p) => `<button class="style-opt ${chosen === p.id ? 'on' : ''}" data-preset="${p.id}"><span class="style-chips">${chips(p)}</span><span class="style-name">${p.emoji} ${esc(p.label)}<span class="style-layout">${esc(LAYOUTS[p.layout] ? LAYOUTS[p.layout].label : '')}</span></span></button>`).join('')}`;
22
+ ${PRESETS.map((p) => `<button class="style-opt ${chosen === p.id ? 'on' : ''}" data-preset="${p.id}"><span class="style-chips">${chips(p)}</span><span class="style-name">${p.emoji} ${esc(p.label)}<span class="style-layout">${esc(LAYOUTS[p.layout] ? LAYOUTS[p.layout].label : '')}</span></span></button>`).join('')}
23
+ <div class="style-pop-sub">ํ…Œ๋งˆ <span class="muted">โ€” ๊ตฌ์กฐ๋Š” ๊ทธ๋Œ€๋กœ, ์ƒ‰๊ณผ ์†Œํ’ˆ๋งŒ</span></div>
24
+ ${THEMES.map((t) => `<button class="style-opt ${chosen === t.id ? 'on' : ''}" data-preset="${t.id}"><span class="style-chips">${chips(t)}</span><span class="style-name">${t.emoji} ${esc(t.label)}</span></button>`).join('')}`;
23
25
  }
24
26
  function open(id, anchor) {
25
27
  floorId = id;
package/public/js/i18n.js CHANGED
@@ -70,6 +70,18 @@ const DICT = {
70
70
  '๊ทธ๋ฆผ์ด ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค โ€” ํ•œ ๋ณ€ 8192px ์ดํ•˜๋กœ ์˜ฌ๋ ค ์ฃผ์„ธ์š”': 'Picture is too large โ€” each side must be 8192 px or less', '์ด๋ฏธ์ง€๋ฅผ ์ฝ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค โ€” ๋‹ค๋ฅธ ํŒŒ์ผ๋กœ ํ•ด ๋ณด์„ธ์š”': 'Could not read the image โ€” try another file', '๊ทธ๋ฆผ์ด ์ „๋ถ€ ํˆฌ๋ช…ํ•ฉ๋‹ˆ๋‹ค': 'The picture is fully transparent',
71
71
  '์ปค์Šคํ…€ ์บ๋ฆญํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์Šต๋‹ˆ๋‹ค โ€” ์•ˆ ์“ฐ๋Š” ๊ฒƒ์„ ์ง€์›Œ ์ฃผ์„ธ์š”': 'Too many custom characters โ€” delete ones you no longer use', '์ €์žฅ ๊ณต๊ฐ„์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค โ€” ์•ˆ ์“ฐ๋Š” ์ปค์Šคํ…€ ์บ๋ฆญํ„ฐ๋ฅผ ์ง€์›Œ ์ฃผ์„ธ์š”': 'Out of storage space โ€” delete custom characters you no longer use',
72
72
  '๋ฐฐ๊ฒฝ์ด ํˆฌ๋ช…ํ•˜์ง€ ์•Š์€ ๊ทธ๋ฆผ์ด๋ผ ์‚ฌ๊ฐํ˜• ๊ทธ๋Œ€๋กœ ๋ณด์ž…๋‹ˆ๋‹ค': 'The background is not transparent, so it shows as a rectangle', '์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค โ€” ์ €์žฅ์„ ๋ˆ„๋ฅด๋ฉด ์ด ๋Œ€์ƒ์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค': 'Added โ€” press Save to use it for this target',
73
+ // ์•ผ์™ธ ๋‚ ์”จ (FE-61)
74
+ '์•ผ์™ธ ๋‚ ์”จ': 'Outdoor weather', '๐ŸŽ„ ํ…Œ๋งˆ์— ๋งž์ถฐ': '๐ŸŽ„ Match theme', '๐ŸŒค๏ธ ์‹ค์ œ ๋‚ ์”จ': '๐ŸŒค๏ธ Real weather', 'โ˜€๏ธ ๋ง‘์Œ': 'โ˜€๏ธ Clear', '๐ŸŒง๏ธ ๋น„': '๐ŸŒง๏ธ Rain', 'โ„๏ธ ๋ˆˆ': 'โ„๏ธ Snow', '๋‚ ์”จ ๋„์‹œ': 'Weather city',
75
+ '์˜ˆ: ์„œ์šธ, Tokyo': 'e.g. Seoul, Tokyo', '๋„์‹œ๋ฅผ ์ ๊ณ  Enter': 'Type a city and press Enter', '๋ฐ›๋Š” ์ค‘โ€ฆ': 'Fetchingโ€ฆ', '์ฐพ๋Š” ์ค‘โ€ฆ': 'Searchingโ€ฆ', '๋‚ ์”จ๋ฅผ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค': 'Could not get the weather', '๋„์‹œ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค (์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ)': 'Could not look up the city (internet connection)',
76
+ 'ํ…Œ๋ผ์Šคยท์˜ฅ์ƒ์ •์› ๊ฐ™์€ ์•ผ์™ธ ๋ฐฉ์— ๋น„ยท๋ˆˆ โ€” ํ…Œ๋งˆ์— ๋งž์ถฐ๋Š” ํฌ๋ฆฌ์Šค๋งˆ์Šค ์ธต์— ๋ˆˆ, ์‹ค์ œ ๋‚ ์”จ๋Š” ์•„๋ž˜ ๋„์‹œ์˜ ํ˜„์žฌ ๋‚ ์”จ(Open-Meteo, 30๋ถ„๋งˆ๋‹ค)': 'Rain or snow over outdoor rooms such as the terrace or rooftop garden โ€” Match theme snows on Christmas floors, Real weather uses the current weather of the city below (Open-Meteo, every 30 minutes)',
77
+ '๋„์‹œ ์ด๋ฆ„์„ ์ ๊ณ  Enter โ€” ๋„์‹œ ์ด๋ฆ„๊ณผ ์ขŒํ‘œ๋งŒ Open-Meteo ๋กœ ๋ณด๋ƒ…๋‹ˆ๋‹ค': 'Type a city name and press Enter โ€” only the city name and coordinates are sent to Open-Meteo',
78
+ // ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ (FE-62)
79
+ 'ํ…Œ๋งˆ ์บ๋ฆญํ„ฐ': 'Theme characters', 'โ€” ํ• ๋กœ์œˆยทํฌ๋ฆฌ์Šค๋งˆ์Šค (์ž๋™์—๋Š” ์„ž์ด์ง€ ์•Š์Œ)': 'โ€” Halloween ยท Christmas (never picked by auto)', '๋ฑ€ํŒŒ์ด์–ด': 'Vampire', '์ข€๋น„': 'Zombie', 'ํ•ด๊ณจ': 'Skeleton', '์œ ๋ น': 'Ghost', 'ํ˜ธ๋ฐ• ๋จธ๋ฆฌ': 'Pumpkin head', '์‚ฐํƒ€': 'Santa', '๋ฃจ๋Œํ”„': 'Rudolph', '๋ˆˆ์‚ฌ๋žŒ': 'Snowman',
80
+ '์‚ฐํƒ€ ๋ชจ์ž': 'Santa hat', '๋ฃจ๋Œํ”„ ๋ฟ”': 'Reindeer antlers', 'ํ˜ธ๋ฐ• ํƒˆ': 'Pumpkin mask',
81
+ // ํ…Œ๋งˆ ์ธต (FE-60)
82
+ 'ํ…Œ๋งˆ ์ธต': 'Theme floors', '๐ŸŽจ ์ง์ ‘ ๊ณ ๋ฅด๊ธฐ': '๐ŸŽจ Pick manually', '๐Ÿ“… ๋‚ ์งœ์— ๋งž์ถฐ ์ž๋™': '๐Ÿ“… Auto by date', 'ํ…Œ๋งˆ': 'Themes', 'ํ• ๋กœ์œˆ': 'Halloween', 'ํฌ๋ฆฌ์Šค๋งˆ์Šค': 'Christmas', '์—ฌ๋ฆ„ํœด๊ฐ€': 'Summer vacation',
83
+ 'ํ• ๋กœ์œˆยทํฌ๋ฆฌ์Šค๋งˆ์Šคยท์—ฌ๋ฆ„ํœด๊ฐ€ ํ…Œ๋งˆ ์ธต โ€” ์ง์ ‘ ๊ณ ๋ฅด๊ธฐ๋Š” ๐ŸŽจ ์—์„œ ์ธต๋งˆ๋‹ค, ๋‚ ์งœ์— ๋งž์ถฐ ์ž๋™์€ 10/15~11/1 ํ• ๋กœ์œˆ ยท 12/1~12/26 ํฌ๋ฆฌ์Šค๋งˆ์Šค ยท 7/15~8/31 ์—ฌ๋ฆ„ํœด๊ฐ€ (์ง์ ‘ ๊ณ ๋ฅธ ์ธต์€ ๊ทธ๋Œ€๋กœ)': 'Halloween ยท Christmas ยท Summer vacation theme floors โ€” pick per floor from ๐ŸŽจ, or auto by date: Halloween 10/15โ€“11/1 ยท Christmas 12/1โ€“12/26 ยท Summer 7/15โ€“8/31 (floors you picked stay as they are)',
84
+ 'โ€” ๊ตฌ์กฐ๋Š” ๊ทธ๋Œ€๋กœ, ์ƒ‰๊ณผ ์†Œํ’ˆ๋งŒ': 'โ€” same layout, new colors and decorations',
73
85
  // ํŒŒํŠธ ๋ฐ”๊พธ๊ธฐ (FE-57)
74
86
  'ํŒŒํŠธ ๋ฐ”๊พธ๊ธฐ': 'Mix parts', 'โ€” ๋จธ๋ฆฌยท๋ชธ์„ ์„ž๊ณ  ์ƒ‰ยท์†Œํ’ˆ ๊ณ ๋ฅด๊ธฐ': 'โ€” mix heads and bodies, pick colors and accessories', 'โ€” ์ง€๊ธˆ ์บ๋ฆญํ„ฐ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค': 'โ€” starts from the current character',
75
87
  '๋จธ๋ฆฌ': 'Head', '๋ชธ': 'Body', '๋จธ๋ฆฌ์นด๋ฝ': 'Hair', '์ƒ์˜': 'Top', 'ํ•˜์˜': 'Bottom', '์†Œํ’ˆ': 'Accessory', '์›๋ž˜': 'Original', '์›๋ž˜ ์ƒ‰': 'Original color', '๋‹ค๋ฅธ ์ƒ‰ ๊ณ ๋ฅด๊ธฐ': 'Pick another color',
@@ -119,6 +131,8 @@ const RULES = [
119
131
  [/^(.+) ์„ธ์…˜์„ ๋ชฉ๋ก์—์„œ ์น˜์šฐ๊ธฐ$/, (m) => `Dismiss this ${tr(m[1])} session`],
120
132
  [/^(\d+)F (.+) ์˜ Claude$/, (m) => `Claude of ${m[1]}F ${m[2]}`],
121
133
  [/^(.+) \(๋‚˜\)$/, (m) => `${m[1]} (me)`],
134
+ [/^"(.+)" ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค$/, (m) => `Could not find "${m[1]}"`],
135
+ [/^(๐ŸŒง๏ธ ๋น„|โ„๏ธ ๋ˆˆ|โ˜€๏ธ ๋ง‘์Œ) ยท (\d\d:\d\d)$/, (m) => `${tr(m[1])} ยท ${m[2]}`],
122
136
  [/^(.+) ์˜ ๋จธ๋ฆฌ$/, (m) => `head of ${m[1]}`],
123
137
  [/^(.+) ์˜ ๋ชธ$/, (m) => `body of ${m[1]}`],
124
138
  [/^๐ŸŽจ (\d+)F ์ธํ…Œ๋ฆฌ์–ด$/, (m) => `๐ŸŽจ ${m[1]}F interior`],
package/public/js/main.js CHANGED
@@ -10,7 +10,8 @@ import { initTextTips } from './texttip.js';
10
10
  import { createAvatarStore } from './avatarStore.js';
11
11
  import { createCustomizer } from './customize.js';
12
12
  import { createOverview } from './overview.js';
13
- import { createFloorStyleBook } from './scene/floorStyle.js';
13
+ import { createFloorStyleBook, seasonTheme } from './scene/floorStyle.js';
14
+ import { geocode, currentWeather, REFRESH_MS } from './weather.js';
14
15
  import { createFloorStylePicker } from './floorStylePicker.js';
15
16
  import { layoutOfPreset } from './scene/map.js';
16
17
  import { buildLayout, roomLabelIn } from './scene/layouts.js';
@@ -23,7 +24,8 @@ const tooltip = createTooltip();
23
24
 
24
25
  // ---------- ๐ŸŽจ ์ธต ์ธํ…Œ๋ฆฌ์–ด (ํ”„๋ฆฌ์…‹ = ์ƒ‰ + ์‚ฌ๋ฌด์‹ค ๊ตฌ์กฐ) ----------
25
26
  const floorStyles = createFloorStyleBook(); // ์ธต๋ณ„ ์ธํ…Œ๋ฆฌ์–ด ํ”„๋ฆฌ์…‹ (FE-34/35): ์ง์ ‘ ๊ณ ๋ฅธ ๊ฒƒ > id ํ•ด์‹œ ์ž๋™ ๋ฐฐ์ •
26
- const styleOfFloor = (f) => floorStyles.resolve(f, store.floorsByNo());
27
+ // ์„ค์ • 'ํ…Œ๋งˆ ์ธต: ๋‚ ์งœ์— ๋งž์ถฐ ์ž๋™' ์ด๋ฉด ์ง์ ‘ ๊ณ ๋ฅด์ง€ ์•Š์€ ์ธต์— ๊ทธ ์ฒ ์˜ ํ…Œ๋งˆ(ํ• ๋กœ์œˆยทํฌ๋ฆฌ์Šค๋งˆ์Šคยท์—ฌ๋ฆ„ํœด๊ฐ€)๋ฅผ ๋ฎ๋Š”๋‹ค (FE-60)
28
+ const styleOfFloor = (f) => floorStyles.resolve(f, store.floorsByNo(), store.themeMode === 'season' ? seasonTheme() : null);
27
29
  function applyFloorStyle() { scene.setFloorStyle(styleOfFloor(store.selectedFloor())); }
28
30
  // ์บ๋ฆญํ„ฐ(createActors)๋ณด๋‹ค ๋จผ์ € ๊ตฌ๋…ํ•œ๋‹ค: ์ธต์ด ๋ฐ”๋€Œ๋ฉด ๋ ˆ์ด์•„์›ƒ๋ถ€ํ„ฐ ๋ฐ”๊พผ ๋’ค ์บ๋ฆญํ„ฐ๋ฅผ ๊ทธ ๋ ˆ์ด์•„์›ƒ์˜ ์ž๋ฆฌ์— ๋ฐฐ์น˜ํ•ด์•ผ ํ•œ๋‹ค
29
31
  store.subscribe((kind) => { if (kind === 'select' || kind === 'snapshot' || kind === 'update' || kind === 'remove') applyFloorStyle(); });
@@ -412,6 +414,56 @@ applyTheme();
412
414
 
413
415
  // ---------- ์‹œ๊ฐ„๋Œ€ ์กฐ๋ช… (FE-33): ์‹ค์ œ ์‹œ๊ฐ์œผ๋กœ ์•„์นจยท๋‚ฎยท์ €๋…ยท๋ฐค, ์„ค์ • ํ–‰์œผ๋กœ ๊ณ ์ • ๊ฐ€๋Šฅ ----------
414
416
  const daylightSel = document.getElementById('daylight-select');
417
+ // ---------- ์•ผ์™ธ ๋‚ ์”จ (FE-61) ----------
418
+ const weatherSel = document.getElementById('weather-select');
419
+ const weatherRow = document.getElementById('weather-place-row');
420
+ const weatherInput = document.getElementById('weather-place');
421
+ const weatherStatus = document.getElementById('weather-status');
422
+ const WEATHER_LABEL = { rain: '๐ŸŒง๏ธ ๋น„', snow: 'โ„๏ธ ๋ˆˆ', clear: 'โ˜€๏ธ ๋ง‘์Œ' };
423
+ let weatherBusy = false, weatherErr = '';
424
+ async function refreshWeather() {
425
+ if (store.weatherMode !== 'real' || !store.weatherPlace || weatherBusy) return;
426
+ weatherBusy = true;
427
+ try { store.setWeatherNow(await currentWeather(store.weatherPlace)); weatherErr = ''; } catch { weatherErr = '๋‚ ์”จ๋ฅผ ๋ฐ›์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค'; renderWeather(); }
428
+ weatherBusy = false;
429
+ }
430
+ function renderWeather() {
431
+ if (weatherSel.value !== store.weatherMode) weatherSel.value = store.weatherMode;
432
+ weatherRow.classList.toggle('hidden', store.weatherMode !== 'real');
433
+ const p = store.weatherPlace, w = store.weatherNow;
434
+ if (document.activeElement !== weatherInput) weatherInput.value = p ? p.name : '';
435
+ weatherStatus.textContent = weatherErr || (!p ? '๋„์‹œ๋ฅผ ์ ๊ณ  Enter' : w ? `${WEATHER_LABEL[w.kind]} ยท ${new Date(w.at).toTimeString().slice(0, 5)}` : '๋ฐ›๋Š” ์ค‘โ€ฆ');
436
+ scene.setWeather({ mode: store.weatherMode, real: w });
437
+ }
438
+ weatherSel.onchange = () => { store.setWeatherMode(weatherSel.value); refreshWeather(); };
439
+ weatherInput.addEventListener('keydown', (e) => {
440
+ e.stopPropagation(); // ๊ธ€์ž ๋‹จ์ถ•ํ‚ค๊ฐ€ ์ƒˆ์ง€ ์•Š๊ฒŒ
441
+ if (e.key !== 'Enter') return;
442
+ e.preventDefault();
443
+ const q = weatherInput.value.trim();
444
+ if (!q) { store.setWeatherPlace(null); return; }
445
+ weatherStatus.textContent = '์ฐพ๋Š” ์ค‘โ€ฆ';
446
+ geocode(q, getLang()).then((p) => {
447
+ if (!p) { weatherErr = `"${q}" ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค`; renderWeather(); weatherErr = ''; return; }
448
+ weatherErr = ''; store.setWeatherPlace(p); weatherInput.blur(); refreshWeather();
449
+ }).catch(() => { weatherErr = '๋„์‹œ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค (์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ)'; renderWeather(); weatherErr = ''; });
450
+ });
451
+ store.subscribe((kind) => { if (kind === 'option') renderWeather(); });
452
+ renderWeather();
453
+ refreshWeather();
454
+ setInterval(refreshWeather, REFRESH_MS);
455
+
456
+ const themeModeSel = document.getElementById('theme-mode-select');
457
+ let appliedThemeMode = store.themeMode;
458
+ function applyThemeMode() {
459
+ if (themeModeSel.value !== store.themeMode) themeModeSel.value = store.themeMode;
460
+ if (appliedThemeMode === store.themeMode) return;
461
+ appliedThemeMode = store.themeMode;
462
+ applyFloorStyle(); actors.sync(true); overview.invalidate(); dirty = true;
463
+ }
464
+ themeModeSel.onchange = () => store.setThemeMode(themeModeSel.value);
465
+ store.subscribe((kind) => { if (kind === 'option') applyThemeMode(); });
466
+ themeModeSel.value = store.themeMode;
415
467
  function applyDaylight() { scene.setDaylight(store.daylight); if (daylightSel.value !== store.daylight) daylightSel.value = store.daylight; }
416
468
  daylightSel.onchange = () => store.setDaylight(daylightSel.value);
417
469
  store.subscribe((kind) => { if (kind === 'option') applyDaylight(); });