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 +212 -83
- package/package.json +1 -1
- package/server.py +21 -0
- package/static/indexd.html +137 -0
package/README.md
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
# Lattice AI
|
|
2
2
|
|
|
3
|
-
Local/cloud LLM workspace server
|
|
4
|
-
|
|
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
|
|
13
|
-
├── llm_router.py #
|
|
14
|
-
├── tools.py #
|
|
15
|
-
├──
|
|
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
|
|
18
|
-
└── vscode-extension/ # VS Code/Cursor/Antigravity
|
|
25
|
+
├── pyproject.toml # PyPI 메타데이터
|
|
26
|
+
└── vscode-extension/ # VS Code / Cursor / Antigravity 확장
|
|
19
27
|
```
|
|
20
28
|
|
|
21
29
|
---
|
|
22
30
|
|
|
23
31
|
## 빠른 시작
|
|
24
32
|
|
|
25
|
-
###
|
|
33
|
+
### 설치 & 실행
|
|
26
34
|
|
|
27
35
|
```bash
|
|
28
|
-
# PyPI
|
|
36
|
+
# PyPI (기본 — 클라우드 모델만)
|
|
29
37
|
pip install ltcai
|
|
30
38
|
|
|
31
|
-
#
|
|
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
|
-
|
|
54
|
+
python ltcai_cli.py --reload # 코드 변경 시 자동 재시작
|
|
55
|
+
|
|
56
|
+
LTCAI doctor # 의존성 및 환경 체크
|
|
48
57
|
```
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
npm으로 설치한 경우 첫 실행 시 `~/.ltcai/npm-python`에 Python 가상환경을 자동으로 생성합니다.
|
|
60
|
+
자동 설치를 끄려면 `LTCAI_SKIP_NPM_BOOTSTRAP=1`을 설정하세요.
|
|
61
|
+
|
|
62
|
+
런타임 데이터는 기본적으로 `~/.ltcai/`에 저장됩니다. 경로 변경: `LATTICEAI_DATA_DIR=/path/to/data`
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
`LATTICEAI_DATA_DIR=/path/to/data` when running `LTCAI`.
|
|
64
|
+
---
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
## 로컬 모드 (Apple Silicon)
|
|
57
67
|
|
|
58
68
|
```bash
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
74
|
+
- MLX 로컬 모델 자동 로드
|
|
75
|
+
- Telegram 미러 봇 활성화 가능
|
|
76
|
+
- 파일/터미널/스크린샷 도구 사용 가능
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 퍼블릭 모드 (클라우드 서버)
|
|
66
81
|
|
|
67
|
-
|
|
82
|
+
Render, Fly.io, Railway, VPS 등에서 운영할 때 사용합니다. MLX를 사용하지 않고 클라우드 모델로 동작합니다.
|
|
68
83
|
|
|
69
84
|
```bash
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
124
|
+
## 모델
|
|
125
|
+
|
|
126
|
+
### 지원 모델 예시 (M-series Mac 기준)
|
|
98
127
|
|
|
99
128
|
| 모델 | 용도 | 크기 | 추천도 |
|
|
100
129
|
|------|------|------|--------|
|
|
101
|
-
| `mlx-community/
|
|
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/
|
|
105
|
-
| `mlx-community/
|
|
106
|
-
| `mlx-community/
|
|
107
|
-
| `mlx-community/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
166
|
+
```bash
|
|
167
|
+
cd vscode-extension
|
|
168
|
+
npm install
|
|
169
|
+
npm run build
|
|
170
|
+
npm run package:vsix
|
|
123
171
|
|
|
124
|
-
#
|
|
125
|
-
|
|
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
|
-
##
|
|
189
|
+
## Telegram 봇
|
|
145
190
|
|
|
146
|
-
|
|
191
|
+
### 1. 로컬 AI 봇 (local 모드)
|
|
147
192
|
|
|
193
|
+
로컬 Lattice AI 서버와 대화하고 웹 채팅을 Telegram으로 미러링합니다.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
LATTICEAI_TELEGRAM_BOT_TOKEN=your-token LTCAI
|
|
148
197
|
```
|
|
149
|
-
|
|
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
|
-
|
|
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 | `/
|
|
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
|
-
|
|
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
|
|
302
|
+
<key>Label</key><string>com.ltcai</string>
|
|
187
303
|
<key>ProgramArguments</key>
|
|
188
304
|
<array>
|
|
189
|
-
<string>/usr/bin/
|
|
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.
|
|
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
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)
|
package/static/indexd.html
CHANGED
|
@@ -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',
|