ltcai 0.1.0 → 0.1.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.md CHANGED
@@ -1,7 +1,12 @@
1
1
  # Lattice AI
2
2
 
3
- Local/cloud LLM workspace server with MLX, Ollama, vLLM, OpenAI-compatible providers,
4
- BYOK API keys, MCP recommendations, and editor extensions for VS Code, Cursor, and Antigravity.
3
+ Local/cloud LLM workspace server Apple Silicon MLX, OpenAI-compatible providers, MCP, VS Code/Cursor extension, Telegram bot.
4
+
5
+ ```bash
6
+ pip install ltcai # PyPI
7
+ npm install -g ltcai # npm
8
+ LTCAI # → http://localhost:4825
9
+ ```
5
10
 
6
11
  ---
7
12
 
@@ -9,26 +14,29 @@ BYOK API keys, MCP recommendations, and editor extensions for VS Code, Cursor, a
9
14
 
10
15
  ```
11
16
  Lattice AI/
12
- ├── server.py # FastAPI bridge server (port 4825)
13
- ├── llm_router.py # local/cloud model router
14
- ├── tools.py # local workspace tools
15
- ├── static/ # web UI
17
+ ├── server.py # FastAPI 브릿지 서버 (port 4825)
18
+ ├── llm_router.py # 로컬/클라우드 모델 라우터
19
+ ├── tools.py # 워크스페이스 도구 (파일, 터미널, 스크린샷 등)
20
+ ├── p_reinforce.py # P-Reinforce 지식 정원 엔진
21
+ ├── telegram_bot.py # 로컬 AI Telegram 미러 봇
22
+ ├── codex_telegram_bot.py # 클라우드 Codex Telegram 봇
23
+ ├── static/ # 웹 UI (indexd.html), 어드민 패널 (admin.html)
16
24
  ├── bin/ltcai.js # npm CLI entrypoint
17
- ├── pyproject.toml # PyPI metadata
18
- └── vscode-extension/ # VS Code/Cursor/Antigravity extension
25
+ ├── pyproject.toml # PyPI 메타데이터
26
+ └── vscode-extension/ # VS Code / Cursor / Antigravity 확장
19
27
  ```
20
28
 
21
29
  ---
22
30
 
23
31
  ## 빠른 시작
24
32
 
25
- ### 1. 서버 설치 & 실행
33
+ ### 설치 & 실행
26
34
 
27
35
  ```bash
28
- # PyPI
36
+ # PyPI (기본 — 클라우드 모델만)
29
37
  pip install ltcai
30
38
 
31
- # 로컬 MLX까지 함께 쓰려면
39
+ # PyPI (Apple Silicon MLX 포함)
32
40
  pip install "ltcai[local]"
33
41
 
34
42
  # npm
@@ -36,117 +44,214 @@ npm install -g ltcai
36
44
 
37
45
  # 서버 실행
38
46
  LTCAI
39
- # → http://localhost:4825 에서 실행됨
47
+ # → http://localhost:4825
40
48
  ```
41
49
 
42
- 개발 중에는 설치 없이도 실행할 수 있습니다.
50
+ #### 개발 모드
43
51
 
44
52
  ```bash
45
53
  python ltcai_cli.py
46
- python ltcai_cli.py --reload
47
- LTCAI doctor
54
+ python ltcai_cli.py --reload # 코드 변경 시 자동 재시작
55
+
56
+ LTCAI doctor # 의존성 및 환경 체크
48
57
  ```
49
58
 
50
- `npm install -g ltcai`로 설치한 경우 첫 실행 시 `~/.ltcai/npm-python`에 Python 가상환경을 만들고
51
- `requirements.txt`를 설치합니다. 자동 설치를 끄려면 `LTCAI_SKIP_NPM_BOOTSTRAP=1`을 설정하세요.
59
+ npm으로 설치한 경우 첫 실행 시 `~/.ltcai/npm-python`에 Python 가상환경을 자동으로 생성합니다.
60
+ 자동 설치를 끄려면 `LTCAI_SKIP_NPM_BOOTSTRAP=1`을 설정하세요.
61
+
62
+ 런타임 데이터는 기본적으로 `~/.ltcai/`에 저장됩니다. 경로 변경: `LATTICEAI_DATA_DIR=/path/to/data`
52
63
 
53
- Lattice AI stores runtime data in `~/.ltcai/` by default. Override it with
54
- `LATTICEAI_DATA_DIR=/path/to/data` when running `LTCAI`.
64
+ ---
55
65
 
56
- ### 2. 모델 로드 (터미널 or 확장 프로그램에서)
66
+ ## 로컬 모드 (Apple Silicon)
57
67
 
58
68
  ```bash
59
- # 터미널에서 직접
60
- curl -X POST http://localhost:4825/models/load \
61
- -H "Content-Type: application/json" \
62
- -d '{"model_id": "mlx-community/Qwen2.5-Coder-7B-Instruct-4bit"}'
69
+ LATTICEAI_MODE=local \
70
+ LATTICEAI_LOCAL_MODEL=mlx-community/gemma-4-26b-a4b-it-4bit \
71
+ LTCAI
63
72
  ```
64
73
 
65
- 또는 확장 프로그램에서 `Cmd+Shift+M` 모델 선택
74
+ - MLX 로컬 모델 자동 로드
75
+ - Telegram 미러 봇 활성화 가능
76
+ - 파일/터미널/스크린샷 도구 사용 가능
77
+
78
+ ---
79
+
80
+ ## 퍼블릭 모드 (클라우드 서버)
66
81
 
67
- ### 3. 확장 프로그램 설치
82
+ Render, Fly.io, Railway, VPS 등에서 운영할 때 사용합니다. MLX를 사용하지 않고 클라우드 모델로 동작합니다.
68
83
 
69
84
  ```bash
70
- cd vscode-extension
71
- npm install
72
- npm run build
73
- npm run package:vsix
85
+ LATTICEAI_MODE=public \
86
+ LATTICEAI_ALLOW_LOCAL_MODELS=false \
87
+ LATTICEAI_ENABLE_TELEGRAM=false \
88
+ LATTICEAI_PUBLIC_MODEL=openai:gpt-4o-mini \
89
+ OPENAI_API_KEY=sk-... \
90
+ LATTICEAI_INVITE_CODE=my-secret-code \
91
+ LTCAI
92
+ ```
93
+
94
+ 지원 클라우드 모델 프리픽스:
74
95
 
75
- # VS Code / Cursor / Antigravity에서:
76
- # 1. Extensions 패널 → "..." → "Install from VSIX" 또는
77
- # 2. 로컬 CLI가 있으면:
78
- npm run install:all
96
+ ```
97
+ openai:gpt-4o-mini
98
+ openrouter:openai/gpt-4o-mini
99
+ groq:llama-3.1-8b-instant
100
+ together:meta-llama/Llama-3.3-70B-Instruct-Turbo
79
101
  ```
80
102
 
81
- ---
103
+ ### Docker
82
104
 
83
- ## 모델/비용 구조
105
+ ```bash
106
+ docker build -t lattice-ai .
107
+ docker run --rm -p 4825:4825 \
108
+ -e OPENAI_API_KEY="$OPENAI_API_KEY" \
109
+ -e LATTICEAI_INVITE_CODE="my-secret-code" \
110
+ -v "$PWD/.data:/data" \
111
+ lattice-ai
112
+ ```
84
113
 
85
- - Local LLM: MLX, Ollama, vLLM, LM Studio, llama.cpp
86
- - Cloud LLM: OpenAI, OpenRouter, Groq, Together, xAI 등 OpenAI-compatible provider
87
- - API 비용: 사용자가 본인 API key를 입력하는 BYOK 구조입니다. 사용자별 키로 호출되므로 키 소유자가 사용량을 부담합니다.
88
- - 초대 링크 게이트는 기본 비활성화되어 있습니다. 다시 켜려면 `LATTICEAI_INVITE_GATE_ENABLED=true`를 설정하세요.
114
+ ### 퍼블릭 서버 체크리스트
89
115
 
90
- ## 보안 기본값
116
+ - `LATTICEAI_MODE=public` 설정
117
+ - 클라우드 API 키 설정 (`OPENAI_API_KEY` 등)
118
+ - `LATTICEAI_INVITE_CODE`를 비공개 값으로 설정
119
+ - `/data`에 영구 볼륨 마운트
120
+ - HTTPS 리버스 프록시 앞에 두기 (nginx, Caddy 등)
91
121
 
92
- - 기본 서버 바인딩은 `127.0.0.1:4825`입니다. 같은 네트워크에서 접속하게 하려면 명시적으로 `LATTICEAI_HOST=0.0.0.0`을 설정하세요.
93
- - CORS는 기본적으로 localhost만 허용합니다. 네트워크 공개가 필요하면 `LATTICEAI_CORS_ALLOW_NETWORK=true`를 명시적으로 설정하세요.
94
- - 사용자 API key는 OS keyring/Keychain에 저장합니다. keyring을 사용할 수 없는 환경에서 평문 저장을 허용하려면 `LATTICEAI_ALLOW_PLAINTEXT_API_KEYS=true`를 직접 설정해야 합니다.
95
- - 히스토리 저장 전 API key/token/password 패턴은 마스킹됩니다.
122
+ ---
96
123
 
97
- ## 지원 모델 예시 (M5 32GB 기준)
124
+ ## 모델
125
+
126
+ ### 지원 모델 예시 (M-series Mac 기준)
98
127
 
99
128
  | 모델 | 용도 | 크기 | 추천도 |
100
129
  |------|------|------|--------|
101
- | `mlx-community/Qwen2.5-Coder-7B-Instruct-4bit` | 코딩 | ~4GB | ⭐⭐⭐ |
102
- | `mlx-community/Qwen2.5-Coder-14B-Instruct-4bit` | 코딩 | ~8GB | ⭐⭐⭐⭐ |
130
+ | `mlx-community/gemma-4-26b-a4b-it-4bit` | 범용/코딩 | ~14GB | ⭐⭐⭐⭐⭐ |
103
131
  | `mlx-community/Qwen2.5-Coder-32B-Instruct-4bit` | 코딩 | ~18GB | ⭐⭐⭐⭐⭐ |
104
- | `mlx-community/Llama-3.1-8B-Instruct-4bit` | 범용 | ~4.5GB| ⭐⭐⭐ |
105
- | `mlx-community/DeepSeek-R1-0528-4bit` | 추론 | ~38GB | ⭐⭐⭐⭐ |
106
- | `mlx-community/Phi-4-4bit` | 코딩 | ~8GB | ⭐⭐⭐⭐ |
107
- | `mlx-community/gemma-3-27b-it-4bit` | 범용 | ~15GB | ⭐⭐⭐ |
132
+ | `mlx-community/Qwen2.5-Coder-14B-Instruct-4bit` | 코딩 | ~8GB | ⭐⭐⭐⭐ |
133
+ | `mlx-community/Qwen2.5-Coder-7B-Instruct-4bit` | 코딩 | ~4GB | ⭐⭐⭐ |
134
+ | `mlx-community/DeepSeek-R1-0528-4bit` | 추론 | ~38GB | ⭐⭐⭐⭐ |
135
+ | `mlx-community/Phi-4-4bit` | 코딩 | ~8GB | ⭐⭐⭐⭐ |
136
+ | `mlx-community/Llama-3.1-8B-Instruct-4bit` | 범용 | ~4.5GB | ⭐⭐⭐ |
137
+
138
+ > **32GB Mac 추천**: gemma-4-26b-a4b-it-4bit — 빠르고 뛰어난 범용 성능
139
+
140
+ ### 멀티모델 핫스왑
141
+
142
+ ```bash
143
+ # 모델 로드
144
+ curl -X POST localhost:4825/models/load \
145
+ -H "Content-Type: application/json" \
146
+ -d '{"model_id": "mlx-community/Qwen2.5-Coder-14B-Instruct-4bit"}'
147
+
148
+ # 즉시 전환 (재로드 없음)
149
+ curl -X POST localhost:4825/models/switch/mlx-community%2FQwen2.5-Coder-14B-Instruct-4bit
108
150
 
109
- > **M5 32GB 추천**: Qwen2.5-Coder-32B-Instruct-4bit (18GB) — 32GB에서 여유롭게 동작
151
+ # 언로드
152
+ curl -X DELETE localhost:4825/models/unload/mlx-community%2FQwen2.5-Coder-14B-Instruct-4bit
153
+ ```
110
154
 
111
155
  ---
112
156
 
113
- ## 멀티모델 핫스왑
157
+ ## 에디터 확장
114
158
 
115
- 여러 모델을 동시에 메모리에 올려두고 즉시 전환 가능:
159
+ | 마켓플레이스 | 링크 |
160
+ |---|---|
161
+ | VS Code / Cursor | [marketplace.visualstudio.com](https://marketplace.visualstudio.com/items?itemName=parktaesoo.ltcai) |
162
+ | Antigravity / VSCodium | [open-vsx.org](https://open-vsx.org/extension/parktaesoo/ltcai) |
116
163
 
117
- ```bash
118
- # 모델 A 로드
119
- curl -X POST localhost:4825/models/load -d '{"model_id":"mlx-community/Qwen2.5-Coder-7B-Instruct-4bit"}'
164
+ ### 수동 설치 (VSIX)
120
165
 
121
- # 모델 B도 함께 로드
122
- curl -X POST localhost:4825/models/load -d '{"model_id":"mlx-community/Llama-3.1-8B-Instruct-4bit"}'
166
+ ```bash
167
+ cd vscode-extension
168
+ npm install
169
+ npm run build
170
+ npm run package:vsix
123
171
 
124
- # B A 즉시 전환 (재로드 없음)
125
- curl -X POST localhost:4825/models/switch/mlx-community%2FQwen2.5-Coder-7B-Instruct-4bit
172
+ # 설치 (모든 에디터 번에)
173
+ npm run install:all
126
174
 
127
- # 메모리 해제
128
- curl -X DELETE localhost:4825/models/unload/mlx-community%2FLlama-3.1-8B-Instruct-4bit
175
+ # 또는 에디터에서: Extensions → "..." → "Install from VSIX"
129
176
  ```
130
177
 
131
- ---
132
-
133
- ## 키보드 단축키
178
+ ### 키보드 단축키
134
179
 
135
180
  | 단축키 | 기능 |
136
181
  |--------|------|
137
182
  | `Cmd+Shift+A` | 채팅 패널 열기 |
138
- | `Cmd+Shift+E` | 선택 코드 편집 (선택 필요) |
183
+ | `Cmd+Shift+E` | 선택 코드 편집 |
139
184
  | `Cmd+Shift+M` | 모델 로드 / 전환 |
140
- | 우클릭 메뉴 | Explain / Edit / Garden에 저장 |
185
+ | 우클릭 메뉴 | Explain / Edit / Knowledge Garden에 저장 |
141
186
 
142
187
  ---
143
188
 
144
- ## P-Reinforce 지식 정원사
189
+ ## Telegram
145
190
 
146
- 지식은 `~/.ltcai-ai-brain/`에 자동 분류 저장:
191
+ ### 1. 로컬 AI 봇 (local 모드)
147
192
 
193
+ 로컬 Lattice AI 서버와 대화하고 웹 채팅을 Telegram으로 미러링합니다.
194
+
195
+ ```bash
196
+ LATTICEAI_TELEGRAM_BOT_TOKEN=your-token LTCAI
148
197
  ```
149
- ~/.ltcai-ai-brain/
198
+
199
+ ### 2. Codex 클라우드 봇
200
+
201
+ Telegram에서 GPT 기반 개발 어시스턴트와 대화하고, 선택적으로 GitHub 이슈를 생성합니다.
202
+
203
+ ```bash
204
+ CODEX_TELEGRAM_BOT_TOKEN=your-token \
205
+ OPENAI_API_KEY=sk-... \
206
+ CODEX_OPENAI_MODEL=gpt-4o \
207
+ python codex_telegram_bot.py
208
+ ```
209
+
210
+ Telegram 명령어: `/start` `/reset` `/issue 제목`
211
+
212
+ 선택적으로 GitHub 이슈 연동:
213
+
214
+ ```bash
215
+ GITHUB_TOKEN=ghp-... GITHUB_REPO=owner/repo
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 보안
221
+
222
+ - **인증**: 모든 `/tools/*`, `/agent`, `/mcp/*`, `/local/*` 등 민감 엔드포인트는 로그인 세션 필요 (`REQUIRE_AUTH=true` 시)
223
+ - **세션**: 7일 TTL, 서버 메모리 저장 (재시작 시 로그아웃)
224
+ - **바인딩**: 기본 `127.0.0.1:4825` — 외부 접근 허용 시 명시적으로 `LATTICEAI_HOST=0.0.0.0` 설정
225
+ - **CORS**: 기본 localhost만 허용 — 외부 허용 시 `LATTICEAI_CORS_ALLOW_NETWORK=true`
226
+ - **API 키**: OS keyring/Keychain 저장 — 평문 저장 허용 시 `LATTICEAI_ALLOW_PLAINTEXT_API_KEYS=true`
227
+ - **히스토리**: 저장 전 API key/token/password 패턴 자동 마스킹
228
+ - **쿠키**: `HttpOnly + SameSite=Lax` (CSRF 방어)
229
+
230
+ ---
231
+
232
+ ## 어드민 패널
233
+
234
+ `http://localhost:4825/admin` — 관리자 계정으로 로그인 후 접근 가능
235
+
236
+ - 사용자 목록 및 역할 관리 (admin / user)
237
+ - 사용자 비활성화 / 삭제
238
+ - 대시보드 (메모리, 모델, 시스템 상태)
239
+
240
+ > **첫 번째로 가입한 계정이 자동으로 admin이 됩니다.** 서버를 처음 실행한 후 `/register` 또는 웹 UI에서 회원가입하면 됩니다. 이후 추가 admin은 어드민 패널에서 지정할 수 있습니다.
241
+ >
242
+ > 환경변수로 admin을 고정할 수도 있습니다:
243
+ > ```bash
244
+ > LATTICEAI_ADMIN_EMAILS=you@example.com LTCAI
245
+ > ```
246
+
247
+ ---
248
+
249
+ ## P-Reinforce 지식 정원
250
+
251
+ 코드/텍스트를 `~/.ltcai-brain/`에 자동 분류 저장합니다.
252
+
253
+ ```
254
+ ~/.ltcai-brain/
150
255
  ├── INDEX.md
151
256
  ├── 00_Raw/ # 원시 데이터, 아이디어
152
257
  ├── 10_Wiki/ # 검증된 개념, 레퍼런스
@@ -155,7 +260,15 @@ curl -X DELETE localhost:4825/models/unload/mlx-community%2FLlama-3.1-8B-Instruc
155
260
  └── 40_Log/ # 날짜별 작업 로그
156
261
  ```
157
262
 
158
- 사용법: 에디터에서 텍스트 선택 → 우클릭 → **"Save to Knowledge Garden"**
263
+ 에디터에서 텍스트 선택 → 우클릭 → **"Save to Knowledge Garden"**
264
+
265
+ 또는 API:
266
+
267
+ ```bash
268
+ curl -X POST localhost:4825/garden \
269
+ -H "Content-Type: application/json" \
270
+ -d '{"content": "학습한 내용", "category": "10_Wiki"}'
271
+ ```
159
272
 
160
273
  ---
161
274
 
@@ -165,35 +278,51 @@ curl -X DELETE localhost:4825/models/unload/mlx-community%2FLlama-3.1-8B-Instruc
165
278
  |--------|------|------|
166
279
  | GET | `/health` | 서버 상태, 현재 모델 |
167
280
  | GET | `/models` | 추천 모델 목록 + 로드 상태 |
168
- | POST | `/models/load` | 모델 로드 (캐시 지원) |
281
+ | POST | `/models/load` | 모델 로드 |
169
282
  | POST | `/models/switch/{id}` | 활성 모델 전환 |
170
283
  | DELETE | `/models/unload/{id}` | 모델 언로드 |
171
- | POST | `/chat` | 생성 (stream=true/false) |
172
- | POST | `/garden` | P-Reinforce 저장 |
284
+ | POST | `/chat` | 채팅 생성 (`stream=true/false`) |
285
+ | POST | `/agent` | 파일 생성/수정 에이전트 |
286
+ | POST | `/garden` | 지식 정원 저장 |
173
287
  | GET | `/garden/tree` | 지식 트리 조회 |
288
+ | GET | `/tools/list_dir` | 디렉토리 목록 |
289
+ | POST | `/tools/run_command` | 터미널 명령 실행 |
290
+ | GET | `/mcp/installed` | 설치된 MCP 목록 |
174
291
 
175
292
  ---
176
293
 
177
- ## 자동 시작 설정 (선택)
294
+ ## 자동 시작 (Mac)
178
295
 
179
296
  ```bash
180
- # launchd plist Mac 부팅시 자동 시작
181
- cat > ~/Library/LaunchAgents/com.ltcai.mlx.plist << 'EOF'
297
+ cat > ~/Library/LaunchAgents/com.ltcai.plist << 'EOF'
182
298
  <?xml version="1.0" encoding="UTF-8"?>
183
299
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
184
300
  <plist version="1.0">
185
301
  <dict>
186
- <key>Label</key><string>com.ltcai.mlx</string>
302
+ <key>Label</key><string>com.ltcai</string>
187
303
  <key>ProgramArguments</key>
188
304
  <array>
189
- <string>/usr/bin/python3</string>
190
- <string>/path/to/LTCAI-ai-mlx/server/server.py</string>
305
+ <string>/usr/local/bin/LTCAI</string>
191
306
  </array>
192
307
  <key>RunAtLoad</key><true/>
193
308
  <key>KeepAlive</key><true/>
309
+ <key>StandardOutPath</key><string>/tmp/ltcai.log</string>
310
+ <key>StandardErrorPath</key><string>/tmp/ltcai.err</string>
194
311
  </dict>
195
312
  </plist>
196
313
  EOF
197
314
 
198
- launchctl load ~/Library/LaunchAgents/com.ltcai.mlx.plist
315
+ launchctl load ~/Library/LaunchAgents/com.ltcai.plist
316
+ ```
317
+
318
+ 또는 동봉된 스크립트 사용:
319
+
320
+ ```bash
321
+ ./start_ai.sh # 자동 재시작 + caffeinate (슬립 방지)
199
322
  ```
323
+
324
+ ---
325
+
326
+ ## 라이선스
327
+
328
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Lattice AI local MLX/cloud LLM workspace server",
5
5
  "bin": {
6
6
  "ltcai": "bin/ltcai.js",
package/server.py CHANGED
@@ -1241,6 +1241,27 @@ async def logout(request: Request):
1241
1241
  response.delete_cookie("session_token")
1242
1242
  return response
1243
1243
 
1244
+ class ChangePasswordRequest(BaseModel):
1245
+ current_password: str
1246
+ new_password: str
1247
+
1248
+ @app.post("/account/change-password")
1249
+ async def change_password(req: ChangePasswordRequest, request: Request):
1250
+ email = require_user(request)
1251
+ if not email:
1252
+ raise HTTPException(status_code=401, detail="인증이 필요합니다.")
1253
+ if len(req.new_password) < 4:
1254
+ raise HTTPException(status_code=400, detail="새 비밀번호는 4자 이상이어야 합니다.")
1255
+ users = load_users()
1256
+ user = users.get(email)
1257
+ if not user:
1258
+ raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
1259
+ if not verify_and_migrate_password(email, req.current_password, user.get("password", ""), users):
1260
+ raise HTTPException(status_code=401, detail="현재 비밀번호가 틀렸습니다.")
1261
+ users[email]["password"] = hash_password(req.new_password)
1262
+ save_users(users)
1263
+ return {"status": "ok", "message": "비밀번호가 변경되었습니다."}
1264
+
1244
1265
  @app.get("/admin/summary")
1245
1266
  async def admin_summary(request: Request):
1246
1267
  _, users = require_admin(request)
@@ -407,6 +407,56 @@
407
407
  background: rgba(255,255,255,0.04);
408
408
  }
409
409
 
410
+ .pw-modal-overlay {
411
+ display: none;
412
+ position: fixed;
413
+ inset: 0;
414
+ background: rgba(0,0,0,0.6);
415
+ backdrop-filter: blur(4px);
416
+ z-index: 1000;
417
+ align-items: center;
418
+ justify-content: center;
419
+ }
420
+ .pw-modal-overlay.open { display: flex; }
421
+ .pw-modal {
422
+ background: var(--surface, #1e293b);
423
+ border: 1px solid rgba(255,255,255,0.08);
424
+ border-radius: 16px;
425
+ padding: 28px;
426
+ width: 100%;
427
+ max-width: 360px;
428
+ display: flex;
429
+ flex-direction: column;
430
+ gap: 16px;
431
+ box-shadow: 0 20px 60px rgba(0,0,0,0.5);
432
+ }
433
+ .pw-modal h3 { font-size: 15px; font-weight: 600; margin: 0; }
434
+ .pw-field { display: flex; flex-direction: column; gap: 5px; }
435
+ .pw-field label { font-size: 11px; color: var(--muted); }
436
+ .pw-field input {
437
+ background: rgba(0,0,0,0.3);
438
+ border: 1px solid rgba(255,255,255,0.08);
439
+ border-radius: 8px;
440
+ color: var(--text, #f8fafc);
441
+ padding: 8px 12px;
442
+ font-size: 13px;
443
+ outline: none;
444
+ transition: border-color .15s;
445
+ }
446
+ .pw-field input:focus { border-color: var(--accent, #6366f1); }
447
+ .pw-actions { display: flex; gap: 8px; }
448
+ .pw-actions button {
449
+ flex: 1; padding: 8px; border-radius: 8px; border: none;
450
+ cursor: pointer; font-size: 13px; font-weight: 500; transition: all .15s;
451
+ }
452
+ .pw-cancel { background: rgba(255,255,255,0.06); color: var(--muted); }
453
+ .pw-cancel:hover { background: rgba(255,255,255,0.1); }
454
+ .pw-submit { background: var(--accent, #6366f1); color: #fff; }
455
+ .pw-submit:hover { opacity: 0.85; }
456
+ .pw-msg { font-size: 12px; min-height: 16px; }
457
+ .pw-msg.error { color: #f87171; }
458
+ .pw-msg.success { color: #4ade80; }
459
+
410
460
  .messages-viewport {
411
461
  flex: 1;
412
462
  overflow-y: auto;
@@ -2782,10 +2832,34 @@
2782
2832
  </div>
2783
2833
  <div class="header-pills">
2784
2834
  <div class="status-pill"><i class="ti ti-device-desktop"></i> Local</div>
2835
+ <button onclick="openPwModal()" class="logout-btn" title="비밀번호 변경"><i class="ti ti-user"></i></button>
2785
2836
  <button onclick="logout()" class="logout-btn">로그아웃</button>
2786
2837
  </div>
2787
2838
  </header>
2788
2839
 
2840
+ <div class="pw-modal-overlay" id="pw-modal-overlay">
2841
+ <div class="pw-modal">
2842
+ <h3>🔐 비밀번호 변경</h3>
2843
+ <div class="pw-field">
2844
+ <label>현재 비밀번호</label>
2845
+ <input type="password" id="pw-cur" placeholder="현재 비밀번호">
2846
+ </div>
2847
+ <div class="pw-field">
2848
+ <label>새 비밀번호</label>
2849
+ <input type="password" id="pw-new" placeholder="새 비밀번호 (4자 이상)">
2850
+ </div>
2851
+ <div class="pw-field">
2852
+ <label>새 비밀번호 확인</label>
2853
+ <input type="password" id="pw-new2" placeholder="새 비밀번호 재입력">
2854
+ </div>
2855
+ <div class="pw-msg" id="pw-msg"></div>
2856
+ <div class="pw-actions">
2857
+ <button class="pw-cancel" onclick="closePwModal()">취소</button>
2858
+ <button class="pw-submit" id="pw-submit-btn" onclick="submitPwChange()">변경</button>
2859
+ </div>
2860
+ </div>
2861
+ </div>
2862
+
2789
2863
  <section class="ops-strip" aria-label="workspace status">
2790
2864
  <div class="ops-card primary interactive" onclick="openModelPanel()">
2791
2865
  <div>
@@ -3262,6 +3336,69 @@
3262
3336
  location.reload();
3263
3337
  }
3264
3338
 
3339
+ function openPwModal() {
3340
+ document.getElementById('pw-cur').value = '';
3341
+ document.getElementById('pw-new').value = '';
3342
+ document.getElementById('pw-new2').value = '';
3343
+ const msg = document.getElementById('pw-msg');
3344
+ msg.textContent = '';
3345
+ msg.className = 'pw-msg';
3346
+ document.getElementById('pw-modal-overlay').classList.add('open');
3347
+ }
3348
+ function closePwModal() {
3349
+ document.getElementById('pw-modal-overlay').classList.remove('open');
3350
+ }
3351
+ document.addEventListener('click', (e) => {
3352
+ const overlay = document.getElementById('pw-modal-overlay');
3353
+ if (e.target === overlay) closePwModal();
3354
+ });
3355
+ async function submitPwChange() {
3356
+ const cur = document.getElementById('pw-cur').value;
3357
+ const nw = document.getElementById('pw-new').value;
3358
+ const nw2 = document.getElementById('pw-new2').value;
3359
+ const msg = document.getElementById('pw-msg');
3360
+ const btn = document.getElementById('pw-submit-btn');
3361
+ if (!cur || !nw || !nw2) {
3362
+ msg.textContent = '모든 항목을 입력해주세요.';
3363
+ msg.className = 'pw-msg error';
3364
+ return;
3365
+ }
3366
+ if (nw !== nw2) {
3367
+ msg.textContent = '새 비밀번호가 일치하지 않습니다.';
3368
+ msg.className = 'pw-msg error';
3369
+ return;
3370
+ }
3371
+ if (nw.length < 4) {
3372
+ msg.textContent = '새 비밀번호는 4자 이상이어야 합니다.';
3373
+ msg.className = 'pw-msg error';
3374
+ return;
3375
+ }
3376
+ btn.disabled = true;
3377
+ btn.textContent = '변경 중...';
3378
+ try {
3379
+ const res = await fetch('/account/change-password', {
3380
+ method: 'POST',
3381
+ headers: { 'Content-Type': 'application/json' },
3382
+ body: JSON.stringify({ current_password: cur, new_password: nw })
3383
+ });
3384
+ const data = await res.json();
3385
+ if (res.ok) {
3386
+ msg.textContent = '✅ 비밀번호가 변경되었습니다.';
3387
+ msg.className = 'pw-msg success';
3388
+ setTimeout(closePwModal, 1500);
3389
+ } else {
3390
+ msg.textContent = data.detail || '변경 실패';
3391
+ msg.className = 'pw-msg error';
3392
+ }
3393
+ } catch {
3394
+ msg.textContent = '서버 연결 실패';
3395
+ msg.className = 'pw-msg error';
3396
+ } finally {
3397
+ btn.disabled = false;
3398
+ btn.textContent = '변경';
3399
+ }
3400
+ }
3401
+
3265
3402
  function adminHeaders() {
3266
3403
  return {
3267
3404
  'Content-Type': 'application/json',