docuking-mcp 2.5.7 → 2.8.0
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/handlers/docs.js +519 -0
- package/handlers/index.js +34 -0
- package/handlers/kingcast.js +618 -0
- package/handlers/sync.js +1507 -0
- package/handlers/validate.js +544 -0
- package/handlers/version.js +102 -0
- package/index.js +153 -2763
- package/lib/config.js +114 -0
- package/lib/files.js +212 -0
- package/lib/index.js +41 -0
- package/lib/init.js +272 -0
- package/lib/utils.js +74 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* AI 시대의 문서 협업 플랫폼 - AI가 문서를 Push/Pull 할 수 있게 해주는 MCP 서버
|
|
7
7
|
*
|
|
8
8
|
* 폴더 구조:
|
|
9
|
+
* - xx_Infra_Config/ : 시스템 설정 (.env 백업, 배포 정보 등) - Push 대상
|
|
10
|
+
* - xx_Policy/ : 정책 문서 (AI 행동 지침) - Push 대상, 킹캐스트 대상
|
|
9
11
|
* - yy_All_Docu/ : 문서 동기화 폴더 (Push/Pull 대상)
|
|
10
|
-
* - yy_All_Docu/_Infra_Config/: 민감 정보 백업 (.env 자동 복사, 팀 공유)
|
|
11
12
|
* - yy_All_Docu/_Private/ : 오너 비공개 폴더 (오너만 접근)
|
|
12
13
|
* - yy_Coworker_{폴더명}/ : 협업자 폴더 (yy_All_Docu와 별도, 동기화 대상)
|
|
13
14
|
* - zz_ai_1_Talk/ : AI 대화록 (킹톡, Push 대상)
|
|
@@ -15,6 +16,7 @@
|
|
|
15
16
|
* - zz_ai_3_Plan/ : AI 플랜 (킹플랜, Push 대상)
|
|
16
17
|
*
|
|
17
18
|
* 접두사 규칙:
|
|
19
|
+
* - xx_ : 시스템용 폴더 (인프라, 정책) - Push 대상
|
|
18
20
|
* - yy_ : 사람용 폴더 (문서, 협업자) - Push 대상
|
|
19
21
|
* - zz_ai_ : AI용 폴더 (Talk, Todo, Plan) - Push 대상
|
|
20
22
|
* - _ : 비공개 폴더 (본인만 접근)
|
|
@@ -33,318 +35,30 @@ import {
|
|
|
33
35
|
ListResourcesRequestSchema,
|
|
34
36
|
ReadResourceRequestSchema,
|
|
35
37
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
36
|
-
import fs from 'fs';
|
|
37
|
-
import path from 'path';
|
|
38
|
-
import crypto from 'crypto';
|
|
39
|
-
|
|
40
|
-
// 환경변수에서 API 엔드포인트 설정 (키는 로컬 config에서 읽음)
|
|
41
|
-
const API_ENDPOINT = process.env.DOCUKING_API_ENDPOINT || 'https://docuking.ai/api';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* API 키에서 협업자 폴더명 추출
|
|
45
|
-
* API 키 형식: sk_프로젝트8자_cw_폴더명_랜덤48자(hex)
|
|
46
|
-
* hex는 0-9, a-f만 포함하므로 언더스코어 없음
|
|
47
|
-
* 따라서 _cw_ 이후 마지막 _ 전까지가 폴더명
|
|
48
|
-
*/
|
|
49
|
-
function parseCoworkerFromApiKey(apiKey) {
|
|
50
|
-
if (!apiKey) return { isCoworker: false, coworkerFolder: null };
|
|
51
|
-
|
|
52
|
-
const cwIndex = apiKey.indexOf('_cw_');
|
|
53
|
-
if (cwIndex === -1) return { isCoworker: false, coworkerFolder: null };
|
|
54
|
-
|
|
55
|
-
const afterCw = apiKey.substring(cwIndex + 4);
|
|
56
|
-
const lastUnderscore = afterCw.lastIndexOf('_');
|
|
57
|
-
if (lastUnderscore === -1) return { isCoworker: false, coworkerFolder: null };
|
|
58
|
-
|
|
59
|
-
const folderName = afterCw.substring(0, lastUnderscore);
|
|
60
|
-
return {
|
|
61
|
-
isCoworker: !!folderName,
|
|
62
|
-
coworkerFolder: folderName || null,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 파일 크기 제한 (150MB) - Base64 인코딩 시 약 200MB, 서버 제한 200MB와 일치
|
|
67
|
-
const MAX_FILE_SIZE_MB = 150;
|
|
68
|
-
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 로컬 프로젝트의 .docuking/config.json에서 설정 읽기
|
|
72
|
-
*
|
|
73
|
-
* 키 저장 구조:
|
|
74
|
-
* {
|
|
75
|
-
* "projectId": "xxx",
|
|
76
|
-
* "projectName": "프로젝트이름",
|
|
77
|
-
* "apiKey": "sk_xxx"
|
|
78
|
-
* }
|
|
79
|
-
*/
|
|
80
|
-
function getLocalConfig(localPath) {
|
|
81
|
-
const configPath = path.join(localPath, '.docuking', 'config.json');
|
|
82
|
-
|
|
83
|
-
if (!fs.existsSync(configPath)) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
89
|
-
return JSON.parse(content);
|
|
90
|
-
} catch (e) {
|
|
91
|
-
console.error('[DocuKing] config.json 파싱 실패:', e.message);
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 로컬 config에서 API 키 가져오기
|
|
98
|
-
*/
|
|
99
|
-
function getApiKey(localPath) {
|
|
100
|
-
const config = getLocalConfig(localPath);
|
|
101
|
-
return config?.apiKey || '';
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* 로컬 config 저장하기 (.docuking/config.json)
|
|
106
|
-
*/
|
|
107
|
-
function saveLocalConfig(localPath, config) {
|
|
108
|
-
const docukingDir = path.join(localPath, '.docuking');
|
|
109
|
-
|
|
110
|
-
// .docuking 폴더 생성
|
|
111
|
-
if (!fs.existsSync(docukingDir)) {
|
|
112
|
-
fs.mkdirSync(docukingDir, { recursive: true });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const configPath = path.join(docukingDir, 'config.json');
|
|
116
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* 협업자 여부 및 zz_ai 폴더 기본 경로 반환
|
|
121
|
-
* - 오너: localPath (루트에 zz_ai_* 폴더)
|
|
122
|
-
* - 협업자: localPath/yy_Coworker_{폴더명} (협업자 폴더 안에 zz_ai_* 폴더)
|
|
123
|
-
*/
|
|
124
|
-
function getAiBasePath(localPath) {
|
|
125
|
-
const config = getLocalConfig(localPath);
|
|
126
|
-
if (!config || !config.apiKey) {
|
|
127
|
-
return { isCoworker: false, basePath: localPath, coworkerFolder: null };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const apiKey = config.apiKey;
|
|
131
|
-
const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
|
|
132
|
-
|
|
133
|
-
if (isCoworker && coworkerFolder) {
|
|
134
|
-
const coworkerFolderPath = path.join(localPath, `yy_Coworker_${coworkerFolder}`);
|
|
135
|
-
return { isCoworker: true, basePath: coworkerFolderPath, coworkerFolder };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return { isCoworker: false, basePath: localPath, coworkerFolder: null };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* CLAUDE.md에 DocuKing MCP 작업 기록 규칙 추가
|
|
143
|
-
* - 파일이 없으면 새로 생성
|
|
144
|
-
* - 파일이 있으면 DocuKing 섹션이 없을 때만 추가
|
|
145
|
-
* - Talk → Todo → Plan → Done 흐름 포함
|
|
146
|
-
*/
|
|
147
|
-
function updateClaudeMd(localPath) {
|
|
148
|
-
const claudeMdPath = path.join(localPath, 'CLAUDE.md');
|
|
149
|
-
const marker = '## DocuKing 연결됨';
|
|
150
|
-
const oldMarker = '## DocuKing AI 작업 기록 (필수)';
|
|
151
|
-
|
|
152
|
-
const docukingSection = `
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
${marker}
|
|
156
|
-
|
|
157
|
-
### MCP 도구
|
|
158
|
-
| 도구 | 용도 |
|
|
159
|
-
|------|------|
|
|
160
|
-
| docuking_push | 문서 업로드 ("올려줘") |
|
|
161
|
-
| docuking_pull | 문서 다운로드 ("가져와") |
|
|
162
|
-
| docuking_list | 서버 파일 목록 조회 |
|
|
163
|
-
| docuking_status | 동기화 상태 확인 |
|
|
164
|
-
| docuking_log | 커밋 히스토리 조회 |
|
|
165
|
-
| docuking_diff | 버전 간 차이 비교 |
|
|
166
|
-
| docuking_rollback | 특정 커밋으로 되돌림 |
|
|
167
|
-
| docuking_plan | 작업 시작 시 계획 기록 |
|
|
168
|
-
| docuking_done | 작업 완료 시 결과 기록 |
|
|
169
|
-
| docuking_todo | 킹투두 (프로젝트 할일) |
|
|
170
|
-
| docuking_talk | 대화록 저장 |
|
|
171
|
-
|
|
172
|
-
### 작업 흐름 (필수)
|
|
173
|
-
|
|
174
|
-
**1단계: Talk 기록 (작업 원인/배경)**
|
|
175
|
-
- 사용자 요청의 배경, 논의 내용을 먼저 기록
|
|
176
|
-
- Plan을 세우기 전에 반드시 Talk로 "왜 이 작업을 하는지" 남김
|
|
177
|
-
|
|
178
|
-
**2단계: Todo 등록 (할일 등록)**
|
|
179
|
-
- 의미 있는 작업이면 킹투두에 등록
|
|
180
|
-
- 나중에 추적/공유할 가치가 있는 작업
|
|
181
|
-
|
|
182
|
-
**3단계: Plan 생성 → 작업 → Done 완료**
|
|
183
|
-
- 기존대로 plan → 작업 → done
|
|
184
|
-
|
|
185
|
-
### Talk 자동 기록 (docuking_talk)
|
|
186
|
-
|
|
187
|
-
**다음 상황에서 AI가 자동으로 기록:**
|
|
188
|
-
- 설계 방향 결정 (A vs B 선택, "왜 이렇게?")
|
|
189
|
-
- 버그 원인 분석 및 해결책 논의
|
|
190
|
-
- 아키텍처/구조 변경 논의
|
|
191
|
-
- 사용자가 중요한 결정을 내릴 때
|
|
192
|
-
- Plan을 세우게 된 원인/배경 논의
|
|
193
|
-
|
|
194
|
-
### Todo 자동 등록 (docuking_todo)
|
|
195
|
-
|
|
196
|
-
**다음 상황에서 AI가 자동으로 등록:**
|
|
197
|
-
- 사용자가 "나중에 해야 할 것" 언급 시
|
|
198
|
-
- 작업 중 발견한 추가 개선사항
|
|
199
|
-
- "TODO", "FIXME" 성격의 발견 사항
|
|
200
|
-
- 의미 있는 작업 시작 시 (추적용)
|
|
201
|
-
|
|
202
|
-
### 규칙
|
|
203
|
-
1. 동기화 대상: yy_All_Docu/ + zz_ai_*/ 폴더 모두
|
|
204
|
-
2. Push는 사용자 요청 시에만
|
|
205
|
-
3. **Talk → Todo → Plan → Done** 순서 준수
|
|
206
|
-
4. Plan 전에 반드시 Talk로 배경 기록
|
|
207
|
-
`;
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
if (fs.existsSync(claudeMdPath)) {
|
|
211
|
-
// 파일이 있으면 DocuKing 섹션 존재 여부 확인
|
|
212
|
-
let content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
213
|
-
|
|
214
|
-
// 새 마커가 이미 있으면 스킵
|
|
215
|
-
if (content.includes(marker)) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// 구버전 마커가 있으면 교체
|
|
220
|
-
if (content.includes(oldMarker)) {
|
|
221
|
-
// 구버전 섹션 찾아서 교체 (--- 부터 끝까지 또는 다음 --- 전까지)
|
|
222
|
-
const oldSectionStart = content.lastIndexOf('---\n\n' + oldMarker);
|
|
223
|
-
if (oldSectionStart !== -1) {
|
|
224
|
-
content = content.substring(0, oldSectionStart) + docukingSection;
|
|
225
|
-
fs.writeFileSync(claudeMdPath, content, 'utf-8');
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 섹션이 없으면 끝에 추가
|
|
231
|
-
fs.appendFileSync(claudeMdPath, docukingSection, 'utf-8');
|
|
232
|
-
} else {
|
|
233
|
-
// 파일이 없으면 새로 생성
|
|
234
|
-
const newContent = `# Project Instructions
|
|
235
|
-
|
|
236
|
-
> AI가 이 프로젝트에서 작업할 때 참고할 지침
|
|
237
|
-
${docukingSection}`;
|
|
238
|
-
fs.writeFileSync(claudeMdPath, newContent, 'utf-8');
|
|
239
|
-
}
|
|
240
|
-
} catch (e) {
|
|
241
|
-
console.error('[DocuKing] CLAUDE.md 업데이트 실패:', e.message);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* IDE별 자동 승인 설정 추가
|
|
247
|
-
* - Claude Code: .claude/settings.local.json
|
|
248
|
-
* - Cursor: ~/.cursor/mcp.json (향후 지원)
|
|
249
|
-
* - Gravity: (향후 지원)
|
|
250
|
-
*/
|
|
251
|
-
function setupAutoApproval(localPath) {
|
|
252
|
-
// Claude Code 설정 (.claude/settings.local.json)
|
|
253
|
-
const claudeSettingsPath = path.join(localPath, '.claude', 'settings.local.json');
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
let settings = {};
|
|
257
|
-
|
|
258
|
-
// 기존 설정 읽기
|
|
259
|
-
if (fs.existsSync(claudeSettingsPath)) {
|
|
260
|
-
const content = fs.readFileSync(claudeSettingsPath, 'utf-8');
|
|
261
|
-
settings = JSON.parse(content);
|
|
262
|
-
} else {
|
|
263
|
-
// .claude 폴더 생성
|
|
264
|
-
const claudeDir = path.join(localPath, '.claude');
|
|
265
|
-
if (!fs.existsSync(claudeDir)) {
|
|
266
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// permissions 구조 초기화
|
|
271
|
-
if (!settings.permissions) settings.permissions = {};
|
|
272
|
-
if (!settings.permissions.allow) settings.permissions.allow = [];
|
|
273
|
-
|
|
274
|
-
let changed = false;
|
|
275
|
-
|
|
276
|
-
// 1. enableAllProjectMcpServers 플래그 추가 (MCP 서버 자동 승인)
|
|
277
|
-
if (!settings.enableAllProjectMcpServers) {
|
|
278
|
-
settings.enableAllProjectMcpServers = true;
|
|
279
|
-
changed = true;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// 2. 와일드카드로 모든 docuking 도구 자동 승인
|
|
283
|
-
const wildcardPermission = 'mcp__docuking__*';
|
|
284
|
-
if (!settings.permissions.allow.includes(wildcardPermission)) {
|
|
285
|
-
settings.permissions.allow.push(wildcardPermission);
|
|
286
|
-
changed = true;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (changed) {
|
|
290
|
-
fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
291
|
-
}
|
|
292
|
-
} catch (e) {
|
|
293
|
-
console.error('[DocuKing] Claude Code 설정 업데이트 실패:', e.message);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* .gitignore에 DocuKing 폴더 추가
|
|
299
|
-
* - yy_All_Docu/, zz_ai_1_Talk/, zz_ai_2_Todo/, zz_ai_3_Plan/, .docuking/
|
|
300
|
-
*/
|
|
301
|
-
function updateGitignore(localPath) {
|
|
302
|
-
const gitignorePath = path.join(localPath, '.gitignore');
|
|
303
|
-
const marker = '# DocuKing (문서는 DocuKing으로 관리)';
|
|
304
|
-
const docukingEntries = `
|
|
305
|
-
${marker}
|
|
306
|
-
yy_All_Docu/
|
|
307
|
-
zz_ai_1_Talk/
|
|
308
|
-
zz_ai_2_Todo/
|
|
309
|
-
zz_ai_3_Plan/
|
|
310
|
-
.docuking/
|
|
311
|
-
`;
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
if (fs.existsSync(gitignorePath)) {
|
|
315
|
-
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
316
|
-
if (content.includes(marker)) {
|
|
317
|
-
// 이미 DocuKing 항목이 있으면 스킵
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
// 끝에 추가
|
|
321
|
-
fs.appendFileSync(gitignorePath, docukingEntries, 'utf-8');
|
|
322
|
-
} else {
|
|
323
|
-
// 파일 없으면 새로 생성
|
|
324
|
-
fs.writeFileSync(gitignorePath, docukingEntries.trim() + '\n', 'utf-8');
|
|
325
|
-
}
|
|
326
|
-
} catch (e) {
|
|
327
|
-
console.error('[DocuKing] .gitignore 업데이트 실패:', e.message);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
38
|
|
|
39
|
+
// 핸들러 import
|
|
40
|
+
import {
|
|
41
|
+
handleInit,
|
|
42
|
+
handlePush,
|
|
43
|
+
handlePull,
|
|
44
|
+
handleList,
|
|
45
|
+
handleStatus,
|
|
46
|
+
} from './handlers/sync.js';
|
|
331
47
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
48
|
+
import {
|
|
49
|
+
handleLog,
|
|
50
|
+
handleDiff,
|
|
51
|
+
handleRollback,
|
|
52
|
+
} from './handlers/version.js';
|
|
335
53
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
54
|
+
import {
|
|
55
|
+
handleTodo,
|
|
56
|
+
handleTalk,
|
|
57
|
+
handlePlan,
|
|
58
|
+
handleDone,
|
|
59
|
+
} from './handlers/docs.js';
|
|
342
60
|
|
|
343
|
-
|
|
344
|
-
projectId: config.projectId,
|
|
345
|
-
projectName: config.projectName,
|
|
346
|
-
};
|
|
347
|
-
}
|
|
61
|
+
import { handleValidate } from './handlers/validate.js';
|
|
348
62
|
|
|
349
63
|
// MCP 서버 생성
|
|
350
64
|
const server = new Server(
|
|
@@ -366,7 +80,58 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
366
80
|
tools: [
|
|
367
81
|
{
|
|
368
82
|
name: 'docuking_init',
|
|
369
|
-
description:
|
|
83
|
+
description: `프로젝트를 DocuKing에 연결합니다. .docuking/config.json에 설정 저장, yy_All_Docu/ 폴더 생성.
|
|
84
|
+
|
|
85
|
+
**필수 파라미터:**
|
|
86
|
+
- projectId: 프로젝트 UUID
|
|
87
|
+
- projectName: 프로젝트 이름 (표시용)
|
|
88
|
+
- apiKey: API 키 (sk_xxx 형식)
|
|
89
|
+
- localPath: 로컬 프로젝트 경로
|
|
90
|
+
|
|
91
|
+
**오너 초기화 시:**
|
|
92
|
+
- xx_Infra_Config/, xx_Policy/ 폴더 생성 (시스템 설정)
|
|
93
|
+
- yy_All_Docu/, yy_All_Docu/_Private/ 폴더 생성
|
|
94
|
+
- zz_ai_1_Talk/, zz_ai_2_Todo/, zz_ai_3_Plan/ 폴더 생성
|
|
95
|
+
- CLAUDE.md에 DocuKing 연결 안내 추가
|
|
96
|
+
- 초기화 완료 후 정책 문서 작성 가이드 제공
|
|
97
|
+
|
|
98
|
+
**협업자 초기화 시:**
|
|
99
|
+
- yy_Coworker_{폴더명}/ 폴더 생성 (안에 _Private/, zz_ai_*/ 포함)
|
|
100
|
+
- CLAUDE.md에 DocuKing 연결 안내 추가
|
|
101
|
+
- "Pull을 실행하세요" 안내 (오너 정책이 킹캐스트로 로컬화됨)
|
|
102
|
+
|
|
103
|
+
**정책 문서 템플릿 (오너용, xx_Policy/ 폴더에 작성):**
|
|
104
|
+
|
|
105
|
+
[필수]
|
|
106
|
+
- 00_project_overview.md - 프로젝트 개요, 기술 스택, 팀 구성, 주요 URL
|
|
107
|
+
|
|
108
|
+
[구조/규칙]
|
|
109
|
+
- 01_folder_structure.md - 폴더 구조 규칙, 파일 명명 규칙
|
|
110
|
+
- 02_naming_convention.md - 변수/함수/클래스 명명 규칙
|
|
111
|
+
- 03_api_convention.md - API 엔드포인트 규칙, 요청/응답 형식, 에러 코드
|
|
112
|
+
|
|
113
|
+
[데이터베이스]
|
|
114
|
+
- 05_database_schema.md - 테이블/컬럼 명명, 타입 규칙, 필수 컬럼(id, createdAt 등)
|
|
115
|
+
- 06_database_migration.md - 마이그레이션 절차, 브랜치별 스키마 관리, 충돌 해결법
|
|
116
|
+
|
|
117
|
+
[개발 정책]
|
|
118
|
+
- 10_coding_style.md - 코딩 스타일, 린터 설정, 포매터 규칙
|
|
119
|
+
- 11_git_workflow.md - 브랜치 전략, 커밋 메시지 규칙, PR 규칙
|
|
120
|
+
- 12_testing_policy.md - 테스트 작성 규칙, 커버리지 기준
|
|
121
|
+
- 13_error_handling.md - 에러 처리 패턴, 로깅 규칙
|
|
122
|
+
|
|
123
|
+
[디자인/UI]
|
|
124
|
+
- 20_design_system.md - 디자인 시스템, 컬러, 타이포그래피
|
|
125
|
+
- 21_component_guide.md - 컴포넌트 작성 규칙, 스타일링 방식
|
|
126
|
+
|
|
127
|
+
[인프라/보안]
|
|
128
|
+
- 30_deployment.md - 배포 절차, 환경별 설정
|
|
129
|
+
- 31_security_policy.md - 보안 규칙, 인증/인가, 민감정보 처리
|
|
130
|
+
|
|
131
|
+
[AI 전용]
|
|
132
|
+
- 99_for_claude.md - Claude Code 행동 지침, 금지/필수 사항
|
|
133
|
+
|
|
134
|
+
init 완료 후 오너에게 "정책 문서를 작성하시겠습니까?" 제안할 것.`,
|
|
370
135
|
inputSchema: {
|
|
371
136
|
type: 'object',
|
|
372
137
|
properties: {
|
|
@@ -426,7 +191,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
426
191
|
},
|
|
427
192
|
{
|
|
428
193
|
name: 'docuking_pull',
|
|
429
|
-
description: '서버에서 문서를
|
|
194
|
+
description: '서버에서 문서를 다운로드합니다. "DocuKing에서 가져와" 요청 시 사용.\n\n**킹캐스트**: Pull 후 xx_Policy/, xx_Infra_Config/ 폴더 변경이 감지되면 자동으로 .claude/rules/local/에 로컬화됩니다.',
|
|
430
195
|
inputSchema: {
|
|
431
196
|
type: 'object',
|
|
432
197
|
properties: {
|
|
@@ -678,6 +443,57 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
678
443
|
required: ['localPath', 'action'],
|
|
679
444
|
},
|
|
680
445
|
},
|
|
446
|
+
{
|
|
447
|
+
name: 'docuking_validate',
|
|
448
|
+
description: `코드가 정책을 준수하는지 검증합니다 (킹밸리데이트).
|
|
449
|
+
|
|
450
|
+
**사용 시점:**
|
|
451
|
+
- "정책 검증해줘", "킹밸리데이트", "validate" 요청 시
|
|
452
|
+
- Pull 후 킹캐스트에서 정책 변경 감지 시 (자동 제안)
|
|
453
|
+
- PR/커밋 전 코드 검증 시
|
|
454
|
+
|
|
455
|
+
**검증 대상:**
|
|
456
|
+
- .claude/rules/local/ 의 정책 파일 기준 (킹캐스트로 로컬화된 정책)
|
|
457
|
+
- xx_Policy/ 정책 파일도 참조
|
|
458
|
+
- 폴더 구조, 명명 규칙, API 규칙, DB 스키마 등
|
|
459
|
+
|
|
460
|
+
**출력:**
|
|
461
|
+
- 정책별 위반 목록
|
|
462
|
+
- 파일:라인 위치
|
|
463
|
+
- 수정 제안
|
|
464
|
+
|
|
465
|
+
**예시:**
|
|
466
|
+
\`\`\`
|
|
467
|
+
[위반] 01_folder_structure.md
|
|
468
|
+
- src/utils/helper.js:1 - utils 폴더에 직접 파일 금지 (하위 폴더로 분류 필요)
|
|
469
|
+
- src/components/button.tsx:1 - 컴포넌트 파일명은 PascalCase 필수
|
|
470
|
+
|
|
471
|
+
[위반] 05_database_schema.md
|
|
472
|
+
- backend/src/entities/user.entity.ts:15 - createdAt 컬럼 누락
|
|
473
|
+
\`\`\``,
|
|
474
|
+
inputSchema: {
|
|
475
|
+
type: 'object',
|
|
476
|
+
properties: {
|
|
477
|
+
localPath: {
|
|
478
|
+
type: 'string',
|
|
479
|
+
description: '로컬 프로젝트 경로',
|
|
480
|
+
},
|
|
481
|
+
policy: {
|
|
482
|
+
type: 'string',
|
|
483
|
+
description: '특정 정책만 검증 (예: 05_database_schema.md). 생략 시 전체 검증.',
|
|
484
|
+
},
|
|
485
|
+
targetPath: {
|
|
486
|
+
type: 'string',
|
|
487
|
+
description: '특정 파일/폴더만 검증 (예: src/components). 생략 시 전체.',
|
|
488
|
+
},
|
|
489
|
+
autoFix: {
|
|
490
|
+
type: 'boolean',
|
|
491
|
+
description: '자동 수정 시도 여부 (기본값: false). 안전한 수정만 자동, 위험한 수정은 제안만.',
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
required: ['localPath'],
|
|
495
|
+
},
|
|
496
|
+
},
|
|
681
497
|
],
|
|
682
498
|
};
|
|
683
499
|
});
|
|
@@ -764,6 +580,7 @@ zz_ai_3_Plan/
|
|
|
764
580
|
|
|
765
581
|
### 3. docuking_pull
|
|
766
582
|
서버 문서를 로컬로 다운로드합니다.
|
|
583
|
+
**킹캐스트**: Pull 후 xx_Policy/, xx_Infra_Config/ 폴더 변경이 감지되면 자동으로 .claude/rules/local/에 로컬화됩니다.
|
|
767
584
|
|
|
768
585
|
### 4. docuking_list
|
|
769
586
|
서버 파일 목록을 조회합니다.
|
|
@@ -821,21 +638,6 @@ zz_ai_3_Plan/
|
|
|
821
638
|
| "DocuKing에서 가져와" | docuking_pull({ localPath }) |
|
|
822
639
|
| "DocuKing에 뭐 있어?" | docuking_list({ localPath }) |
|
|
823
640
|
|
|
824
|
-
## 사용 예시
|
|
825
|
-
|
|
826
|
-
\`\`\`
|
|
827
|
-
사용자: "프로젝트 3b8f95c1 연결해줘"
|
|
828
|
-
AI: docuking_init({ projectId: "3b8f95c1-f557-4a1d-8f1e-f34adb010256", localPath: "/current/path" })
|
|
829
|
-
|
|
830
|
-
사용자: "DocuKing에 올려줘"
|
|
831
|
-
AI: "어떤 변경인가요? 커밋 메시지를 알려주세요."
|
|
832
|
-
사용자: "문서 업데이트"
|
|
833
|
-
AI: docuking_push({ localPath: "/current/path", message: "문서 업데이트" })
|
|
834
|
-
|
|
835
|
-
사용자: "DocuKing에서 가져와"
|
|
836
|
-
AI: docuking_pull({ localPath: "/current/path" })
|
|
837
|
-
\`\`\`
|
|
838
|
-
|
|
839
641
|
## 협업 핵심 원칙: 남의 제사상을 건드리지 않는다
|
|
840
642
|
|
|
841
643
|
DocuKing 협업의 핵심 원칙입니다. **각자 자기 영역만 수정할 수 있고, 남의 영역은 읽기만 가능합니다.**
|
|
@@ -852,328 +654,39 @@ DocuKing 협업의 핵심 원칙입니다. **각자 자기 영역만 수정할
|
|
|
852
654
|
- Pull하면 이 전체가 로컬로 내려옴
|
|
853
655
|
- 경로만 알면 누구의 파일이든 읽고 활용 가능
|
|
854
656
|
|
|
855
|
-
##
|
|
856
|
-
|
|
857
|
-
DocuKing에는 **오너(Owner)**와 **참여자(Co-worker)** 두 가지 권한이 있습니다.
|
|
858
|
-
|
|
859
|
-
### 오너 (Owner) - 프로젝트 생성자
|
|
657
|
+
## 킹캐스트 (KingCast) - 정책 자동 배포
|
|
860
658
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
- \`yy_All_Docu/\` + \`zz_ai_*/\` 폴더에 Push 가능
|
|
864
|
-
- API Key: sk_로 시작
|
|
865
|
-
- 프로젝트 설정 변경 가능
|
|
866
|
-
- 참여자 초대 가능
|
|
659
|
+
오너가 \`xx_Policy/\` 폴더에 정책을 작성하고 Push하면,
|
|
660
|
+
협업자가 Pull할 때 자동으로 \`.claude/rules/local/\`에 서브파일 구조로 로컬화됩니다.
|
|
867
661
|
|
|
868
|
-
|
|
869
|
-
1. 프로젝트 생성 (웹에서)
|
|
870
|
-
2. MCP 설정 (한 번만)
|
|
871
|
-
3. 프로젝트 연결 (docuking_init)
|
|
872
|
-
4. 문서 작성 (\`yy_All_Docu/\` 안에)
|
|
873
|
-
5. Push (docuking_push)
|
|
874
|
-
|
|
875
|
-
**예시:**
|
|
876
|
-
\`\`\`
|
|
877
|
-
yy_All_Docu/
|
|
878
|
-
├── 정책/
|
|
879
|
-
│ └── README.md ← 오너가 작성
|
|
880
|
-
├── 기획/
|
|
881
|
-
│ └── 요구사항.md ← 오너가 작성
|
|
882
|
-
└── 개발/
|
|
883
|
-
└── API.md ← 오너가 작성
|
|
884
|
-
\`\`\`
|
|
885
|
-
|
|
886
|
-
### 참여자 (Co-worker) - 초대받은 사람
|
|
887
|
-
|
|
888
|
-
**특징:**
|
|
889
|
-
- 프로젝트에 초대받아 참여한 사람
|
|
890
|
-
- 초대 수락 시 서버에 \`yy_Coworker_{폴더명}/\` 빈폴더 자동 생성
|
|
891
|
-
- **읽기**: 전체 Pull 가능 (오너 + 다른 협업자 문서 모두)
|
|
892
|
-
- **쓰기**: 자신의 폴더(\`yy_Coworker_{폴더명}/\`)에만 Push 가능
|
|
893
|
-
- API Key: \`sk_xxx_cw_폴더명_\` 형식
|
|
894
|
-
- 프로젝트 설정 변경 불가능
|
|
895
|
-
|
|
896
|
-
**사용 시나리오:**
|
|
897
|
-
1. 초대 수락 (웹에서) → 프로젝트 바로 진입, 오너 문서 + 자기 빈폴더 보임
|
|
898
|
-
2. MCP 설정 (한 번만)
|
|
899
|
-
3. 프로젝트 연결 (\`docuking_init\`) → 로컬에 자기 폴더 생성
|
|
900
|
-
4. Pull (\`docuking_pull\`) → 합집합 전체가 내려옴
|
|
901
|
-
5. 내 폴더에 문서 작성 (\`yy_Coworker_{폴더명}/\`)
|
|
902
|
-
6. Push (\`docuking_push\`) → 자기 폴더만 올라감
|
|
903
|
-
|
|
904
|
-
**폴더 구조 (Pull 후 로컬):**
|
|
905
|
-
\`\`\`
|
|
906
|
-
project/
|
|
907
|
-
├── src/ ← 소스 코드 (git 관리)
|
|
908
|
-
├── yy_All_Docu/ ← 오너 문서 (읽기만)
|
|
909
|
-
│ ├── policy/
|
|
910
|
-
│ │ └── README.md ← 오너의 파일
|
|
911
|
-
│ └── plan/
|
|
912
|
-
│ └── requirements.md ← 오너의 파일
|
|
913
|
-
├── yy_Coworker_a_Kim/ ← 협업자 Kim의 폴더
|
|
914
|
-
│ ├── proposal.md
|
|
915
|
-
│ └── zz_ai_1_Talk/ ← Kim의 AI 대화록
|
|
916
|
-
├── yy_Coworker_b_Lee/ ← 협업자 Lee의 폴더
|
|
917
|
-
│ ├── design.md
|
|
918
|
-
│ └── zz_ai_1_Talk/ ← Lee의 AI 대화록
|
|
919
|
-
└── zz_ai_*/ ← 오너의 AI 폴더
|
|
920
|
-
\`\`\`
|
|
921
|
-
|
|
922
|
-
**중요 규칙:**
|
|
923
|
-
- Pull하면 합집합 전체가 내려옴 (오너 + 모든 협업자)
|
|
924
|
-
- Push는 자기 영역만 올라감 (남의 제사상 건드리지 않음)
|
|
925
|
-
- 협업자의 \`zz_ai_*/\`는 자기 폴더 안에 있음 (\`yy_Coworker_{폴더명}/zz_ai_*/\`)
|
|
926
|
-
- \`docuking_status\`로 현재 권한과 작업 폴더 확인 가능
|
|
927
|
-
|
|
928
|
-
**참여자가 남의 파일을 활용하고 싶을 때:**
|
|
929
|
-
1. Pull로 전체를 로컬에 가져옴
|
|
930
|
-
2. 경로로 찾아서 읽고 참고
|
|
931
|
-
3. 수정이 필요하면:
|
|
932
|
-
- 복사해서 자기 폴더에 가공 후 Push
|
|
933
|
-
- 또는 카톡/메일로 원작자에게 전달 → 업데이트 요청
|
|
934
|
-
4. 이건 스팟한 일, **대세는 킹폴더에 있는 것**
|
|
935
|
-
|
|
936
|
-
**오너 관점에서 협업자 상태 파악:**
|
|
937
|
-
| 상태 | 의미 |
|
|
938
|
-
|------|------|
|
|
939
|
-
| 폴더 없음 | 초대만 함, 아직 수락 안 함 |
|
|
940
|
-
| 빈폴더 | 수락함, 작업 대기 중 |
|
|
941
|
-
| 파일 있음 | 일하고 있음 |
|
|
942
|
-
|
|
943
|
-
**AI가 참여자에게 안내해야 할 내용:**
|
|
944
|
-
- 참여자의 작업 폴더는 \`yy_Coworker_{폴더명}/\`
|
|
945
|
-
- 남의 파일은 읽기만 가능, 수정 불가
|
|
946
|
-
- 수정 제안은 자기 폴더에 작성하거나 직접 연락
|
|
947
|
-
|
|
948
|
-
## AI 응답 가이드 (중요!)
|
|
949
|
-
|
|
950
|
-
### docuking_push 사용 시
|
|
951
|
-
|
|
952
|
-
**사용자 요청 예시:**
|
|
953
|
-
- "DocuKing에 올려줘"
|
|
954
|
-
- "문서 올려줘"
|
|
955
|
-
- "변경사항 업로드해줘"
|
|
956
|
-
|
|
957
|
-
**AI가 해야 할 일:**
|
|
958
|
-
1. 커밋 메시지가 없으면 반드시 물어보기: "어떤 변경인가요? 커밋 메시지를 알려주세요."
|
|
959
|
-
2. 메시지를 받으면 docuking_push 호출
|
|
960
|
-
3. **결과를 사용자에게 명확하게 전달:**
|
|
961
|
-
- 총 파일 개수
|
|
962
|
-
- 업로드된 파일 개수
|
|
963
|
-
- 스킵된 파일 개수 (변경 없음)
|
|
964
|
-
- 실패한 파일 개수 (있을 경우)
|
|
965
|
-
- 업로드된 파일 목록 표시
|
|
966
|
-
- 스킵된 파일 목록 표시 (변경 없어서 스킵됨)
|
|
967
|
-
|
|
968
|
-
**응답 예시:**
|
|
969
|
-
Push 완료! 총 10개 파일 중 3개 업로드, 6개 스킵(변경 없음), 1개 실패
|
|
970
|
-
|
|
971
|
-
### docuking_pull 사용 시
|
|
972
|
-
|
|
973
|
-
**사용자 요청 예시:**
|
|
974
|
-
- "DocuKing에서 가져와"
|
|
975
|
-
- "서버에서 문서 가져와"
|
|
976
|
-
- "최신 버전 가져와"
|
|
977
|
-
|
|
978
|
-
**AI가 해야 할 일:**
|
|
979
|
-
1. docuking_pull 호출
|
|
980
|
-
2. **결과를 사용자에게 명확하게 전달:**
|
|
981
|
-
- 가져온 파일 개수
|
|
982
|
-
- 가져온 파일 목록 표시
|
|
983
|
-
- 실패한 파일이 있으면 표시
|
|
984
|
-
|
|
985
|
-
### docuking_status 사용 시
|
|
986
|
-
|
|
987
|
-
**사용자 요청 예시:**
|
|
988
|
-
- "DocuKing 상태 확인"
|
|
989
|
-
- "동기화 상태 보여줘"
|
|
990
|
-
- "변경사항 확인"
|
|
991
|
-
|
|
992
|
-
**AI가 해야 할 일:**
|
|
993
|
-
1. docuking_status 호출
|
|
994
|
-
2. 결과를 그대로 사용자에게 전달 (권한, 파일 개수, 동기화 상태 등)
|
|
995
|
-
|
|
996
|
-
### 핵심 원칙 (AI 행동 지침)
|
|
997
|
-
|
|
998
|
-
1. **프로젝트 = 폴더**: 하나의 폴더가 하나의 프로젝트
|
|
999
|
-
2. **각 폴더마다 yy_All_Docu/ 생성**: 여러 폴더를 각각 연결 가능
|
|
1000
|
-
3. **현재 열려있는 폴더 기준**: 명령어는 현재 작업 중인 폴더에 적용
|
|
1001
|
-
4. **커밋 메시지 필수**: push 시 반드시 message 파라미터 포함
|
|
1002
|
-
5. **변경 없는 파일 자동 스킵**: 해시 비교로 변경 감지, 스킵된 파일은 결과에 명시
|
|
1003
|
-
6. **진행상태 표시**: 총 개수, 진행 중인 개수, 완료 개수를 명확히 표시
|
|
1004
|
-
7. **친절한 응답**: 도구 호출 후 상세한 결과를 사용자에게 전달
|
|
1005
|
-
8. **MCP 설정 vs 프로젝트 연결 구분**: MCP 설정은 전역(한 번만), 프로젝트 연결은 각 폴더마다
|
|
1006
|
-
9. **재시작 최소화**: 이미 MCP가 작동 중이면 재시작 없이 바로 프로젝트 연결 진행
|
|
1007
|
-
10. **기존 설정 보호**: MCP 설정 시 기존 서버 설정을 덮어쓰지 말고 추가만
|
|
1008
|
-
|
|
1009
|
-
### 대화록 자동 기록 (docuking_talk)
|
|
1010
|
-
|
|
1011
|
-
**언제 사용하는가:**
|
|
1012
|
-
- 중요한 설계 결정이 내려졌을 때
|
|
1013
|
-
- 아키텍처나 정책에 대한 논의가 있었을 때
|
|
1014
|
-
- 사용자가 "이거 기록해줘", "이 대화 저장해줘"라고 요청할 때
|
|
1015
|
-
- 여러 선택지 중 하나를 결정한 이유를 남겨야 할 때
|
|
1016
|
-
|
|
1017
|
-
**사용 예시:**
|
|
1018
|
-
\`\`\`
|
|
1019
|
-
사용자: "인증은 JWT로 하자. 세션은 관리가 복잡하니까"
|
|
1020
|
-
AI: (결정이 내려졌으므로 docuking_talk 호출)
|
|
1021
|
-
docuking_talk({
|
|
1022
|
-
localPath: "/current/path",
|
|
1023
|
-
title: "인증 방식 결정 - JWT 선택",
|
|
1024
|
-
content: "## 결정\\n인증 방식으로 JWT를 선택\\n\\n## 근거\\n- 세션 관리 복잡성 회피\\n- 무상태 아키텍처 선호",
|
|
1025
|
-
tags: ["설계", "인증", "결정"]
|
|
1026
|
-
})
|
|
1027
|
-
\`\`\`
|
|
1028
|
-
|
|
1029
|
-
**저장 위치:** \`zz_ai_1_Talk/YYYY-MM-DD_HHMM__제목.md\` (플랫 구조, Push 대상)
|
|
1030
|
-
|
|
1031
|
-
### 작업 계획 관리 (docuking_plan, docuking_done)
|
|
1032
|
-
|
|
1033
|
-
**언제 사용하는가:**
|
|
1034
|
-
- 복잡한 작업을 시작할 때 (여러 단계가 필요한 작업)
|
|
1035
|
-
- 작업 진행 상황을 기록해야 할 때
|
|
1036
|
-
- 작업이 완료되었을 때
|
|
1037
|
-
|
|
1038
|
-
**작업 흐름:**
|
|
1039
|
-
1. 작업 시작 → \`docuking_plan\` (새 계획 생성, planId 받음)
|
|
1040
|
-
2. 진행 중 → \`docuking_plan\` (planId로 업데이트)
|
|
1041
|
-
3. 작업 완료 → \`docuking_done\` (planId로 완료 처리)
|
|
1042
|
-
|
|
1043
|
-
**사용 예시:**
|
|
662
|
+
### 킹캐스트 동작 원리
|
|
1044
663
|
\`\`\`
|
|
1045
|
-
|
|
1046
|
-
AI: (복잡한 작업이므로 docuking_plan 호출)
|
|
1047
|
-
docuking_plan({
|
|
1048
|
-
localPath: "/current/path",
|
|
1049
|
-
title: "MCP talk 기능 구현",
|
|
1050
|
-
goal: "대화 내용을 자동으로 문서화하는 기능 추가",
|
|
1051
|
-
steps: [
|
|
1052
|
-
{ name: "도구 스키마 정의", status: "pending" },
|
|
1053
|
-
{ name: "핸들러 구현", status: "pending" },
|
|
1054
|
-
{ name: "테스트", status: "pending" }
|
|
1055
|
-
]
|
|
1056
|
-
})
|
|
1057
|
-
→ planId: "abc12345" 받음
|
|
1058
|
-
|
|
1059
|
-
(단계 완료 시)
|
|
1060
|
-
AI: docuking_plan({
|
|
1061
|
-
localPath: "/current/path",
|
|
1062
|
-
planId: "abc12345",
|
|
1063
|
-
title: "MCP talk 기능 구현",
|
|
1064
|
-
steps: [
|
|
1065
|
-
{ name: "도구 스키마 정의", status: "done", result: "index.js에 추가 완료" },
|
|
1066
|
-
{ name: "핸들러 구현", status: "in_progress" },
|
|
1067
|
-
{ name: "테스트", status: "pending" }
|
|
1068
|
-
]
|
|
1069
|
-
})
|
|
1070
|
-
|
|
1071
|
-
(작업 완료 시)
|
|
1072
|
-
AI: docuking_done({
|
|
1073
|
-
localPath: "/current/path",
|
|
1074
|
-
planId: "abc12345",
|
|
1075
|
-
summary: "docuking_talk, docuking_plan, docuking_done 3개 도구 구현 완료",
|
|
1076
|
-
artifacts: ["Docuking_mcp/index.js"]
|
|
1077
|
-
})
|
|
664
|
+
오너가 정책 작성 → Push → 협업자 Pull → 킹캐스트 자동 실행 → AI 행동 변화
|
|
1078
665
|
\`\`\`
|
|
1079
666
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
- AI 세션이 끊겨도 (컴팩션, 세션 종료) 다음 AI가 계획 문서를 보고 이어서 작업 가능
|
|
1084
|
-
- "어디까지 했더라?"가 아니라 "계획 문서 보고 이어서 진행"
|
|
1085
|
-
|
|
1086
|
-
### MCP 설정 관련 (AI가 처리해야 할 경우)
|
|
1087
|
-
|
|
1088
|
-
**사용자 요청 예시:**
|
|
1089
|
-
- "MCP 설정해줘"
|
|
1090
|
-
- "AI 연결하기"
|
|
1091
|
-
- "DocuKing MCP 설정"
|
|
1092
|
-
|
|
1093
|
-
**AI가 해야 할 일:**
|
|
1094
|
-
1. **먼저 확인**: 이미 MCP가 작동 중인지 확인 (도구 호출 가능 여부로 판단)
|
|
1095
|
-
2. **이미 작동 중이면**: "MCP는 이미 설정되어 있습니다. 바로 프로젝트 연결을 진행하겠습니다."라고 안내하고 프로젝트 연결로 진행
|
|
1096
|
-
3. **설정이 필요하면**:
|
|
1097
|
-
- 사용자의 AI 도구 종류 확인 (Cursor, Claude Code 등)
|
|
1098
|
-
- 해당 설정 파일 경로 안내
|
|
1099
|
-
- **기존 MCP 서버가 있으면 덮어쓰지 말고 추가만 하도록 명확히 안내**
|
|
1100
|
-
- 설정 완료 후 재시작 안내 (재시작 후 프로젝트 연결 단계로 진행)
|
|
1101
|
-
|
|
1102
|
-
**MCP 설정 파일 예시 (기존 서버가 있는 경우):**
|
|
1103
|
-
\`\`\`json
|
|
1104
|
-
{
|
|
1105
|
-
"mcpServers": {
|
|
1106
|
-
"filesystem": {
|
|
1107
|
-
"command": "npx",
|
|
1108
|
-
"args": ["-y", "@modelcontextprotocol/server-filesystem"]
|
|
1109
|
-
},
|
|
1110
|
-
"github": {
|
|
1111
|
-
"command": "npx",
|
|
1112
|
-
"args": ["-y", "@modelcontextprotocol/server-github"]
|
|
1113
|
-
},
|
|
1114
|
-
"docuking": {
|
|
1115
|
-
"command": "npx",
|
|
1116
|
-
"args": ["-y", "docuking-mcp"]
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
\`\`\`
|
|
1121
|
-
**⚠️ 중요**: 기존 설정을 덮어쓰지 말고 \`docuking\` 항목만 추가해야 함! (env에 키 없음 - 키는 각 프로젝트 .docuking/config.json에 저장)
|
|
1122
|
-
|
|
1123
|
-
### 프로젝트 연결
|
|
1124
|
-
|
|
1125
|
-
**사용자 요청 예시:**
|
|
1126
|
-
- "이 프로젝트를 DocuKing에 연결해줘. 프로젝트 ID는 [ID]"
|
|
1127
|
-
- "프로젝트 [ID] 연결해줘"
|
|
1128
|
-
- "이 폴더를 DocuKing에 연결해줘"
|
|
667
|
+
### 킹캐스트 대상 폴더
|
|
668
|
+
- \`xx_Infra_Config/\` - 환경 설정 (.env 백업, 배포 정보 등) → .claude/rules/local/infra/에 로컬화
|
|
669
|
+
- \`xx_Policy/\` - AI 행동 지침 → .claude/rules/local/에 로컬화
|
|
1129
670
|
|
|
1130
|
-
|
|
1131
|
-
1. 현재 폴더 경로 확인 (작업 중인 디렉토리)
|
|
1132
|
-
2. 프로젝트 ID, 이름, API 키 확인 (사용자가 제공하거나 요청)
|
|
1133
|
-
3. \`docuking_init(projectId, projectName, apiKey, localPath)\` 호출
|
|
1134
|
-
4. 연결 완료 메시지 전달:
|
|
1135
|
-
- "DocuKing 연결 완료! 📁 yy_All_Docu/ 폴더가 생성되었습니다."
|
|
1136
|
-
- "이제 'DocuKing에 올려줘' 명령을 사용할 수 있습니다."
|
|
1137
|
-
|
|
1138
|
-
**⚠️ 매우 중요:**
|
|
1139
|
-
- **MCP가 이미 작동 중이면 재시작 없이 바로 \`docuking_init\`을 호출하면 됩니다**
|
|
1140
|
-
- 재시작이 필요한 경우는 MCP 설정 파일을 처음 만들었을 때뿐입니다
|
|
1141
|
-
- 각 프로젝트의 키는 \`.docuking/config.json\`에 저장되므로 멀티 프로젝트 지원됩니다
|
|
1142
|
-
|
|
1143
|
-
### 여러 프로젝트 관리 (다중 프로젝트)
|
|
1144
|
-
|
|
1145
|
-
**핵심 개념:**
|
|
1146
|
-
- **프로젝트 = 하나의 폴더**
|
|
1147
|
-
- 한 컴퓨터에서 여러 폴더를 각각 다른 DocuKing 프로젝트와 연결 가능
|
|
1148
|
-
- 각 폴더마다 \`yy_All_Docu/\` 폴더가 독립적으로 생성됨
|
|
1149
|
-
|
|
1150
|
-
**예시:**
|
|
671
|
+
### 정책 파일 작성 템플릿 (오너용)
|
|
1151
672
|
\`\`\`
|
|
1152
|
-
|
|
1153
|
-
├──
|
|
1154
|
-
├──
|
|
1155
|
-
├──
|
|
1156
|
-
├──
|
|
1157
|
-
├──
|
|
1158
|
-
└── zz_ai_3_Plan/ ← AI 플랜 (Push 대상)
|
|
1159
|
-
|
|
1160
|
-
C:\\Projects\\MyWebsite\\
|
|
1161
|
-
├── pages/
|
|
1162
|
-
├── components/
|
|
1163
|
-
├── yy_All_Docu/ ← 프로젝트 B와 연결
|
|
1164
|
-
└── zz_ai_*/ ← AI 작업 폴더들 (Push 대상)
|
|
673
|
+
xx_Policy/
|
|
674
|
+
├── 00_project_overview.md # 프로젝트 개요
|
|
675
|
+
├── 01_folder_structure.md # 폴더 구조
|
|
676
|
+
├── 02_api_convention.md # API 규칙
|
|
677
|
+
├── 03_design_policy.md # 디자인 정책
|
|
678
|
+
├── 99_for_claude.md # AI 특화 지침
|
|
1165
679
|
\`\`\`
|
|
1166
680
|
|
|
1167
|
-
|
|
1168
|
-
-
|
|
1169
|
-
-
|
|
1170
|
-
-
|
|
681
|
+
### Pull 후 생성되는 파일
|
|
682
|
+
- \`.claude/rules/local/00_index.md\` - 정책 인덱스
|
|
683
|
+
- \`.claude/rules/local/00_project_overview.md\` - 정책 사본 (변수 치환됨)
|
|
684
|
+
- \`.claude/rules/local/_coworker_config.md\` - 협업자 설정 (협업자만)
|
|
1171
685
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
- 이 방법은 선택사항이며, 각 폴더에서 \`docuking_init\`을 실행해도 됨
|
|
686
|
+
### AI 필독: 킹캐스트 후 행동
|
|
687
|
+
1. Pull 후 "📢 킹캐스트" 메시지가 표시되면, 정책 변경이 감지된 것
|
|
688
|
+
2. \`.claude/rules/local/00_index.md\`를 읽고 정책 목록 확인
|
|
689
|
+
3. 각 정책 파일을 **읽어서** 내용을 숙지하고 규칙을 따라 작업
|
|
1177
690
|
|
|
1178
691
|
웹 탐색기: https://docuking.ai
|
|
1179
692
|
`;
|
|
@@ -1220,6 +733,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1220
733
|
return await handleDone(args);
|
|
1221
734
|
case 'docuking_todo':
|
|
1222
735
|
return await handleTodo(args);
|
|
736
|
+
case 'docuking_validate':
|
|
737
|
+
return await handleValidate(args);
|
|
1223
738
|
default:
|
|
1224
739
|
throw new Error(`Unknown tool: ${name}`);
|
|
1225
740
|
}
|
|
@@ -1235,2131 +750,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1235
750
|
}
|
|
1236
751
|
});
|
|
1237
752
|
|
|
1238
|
-
// docuking_init 구현
|
|
1239
|
-
async function handleInit(args) {
|
|
1240
|
-
const { projectId, projectName, apiKey, localPath } = args;
|
|
1241
|
-
|
|
1242
|
-
// API 키 필수 체크
|
|
1243
|
-
if (!apiKey) {
|
|
1244
|
-
return {
|
|
1245
|
-
content: [
|
|
1246
|
-
{
|
|
1247
|
-
type: 'text',
|
|
1248
|
-
text: `오류: apiKey가 필요합니다.
|
|
1249
|
-
|
|
1250
|
-
docuking_init 호출 시 apiKey 파라미터를 포함해주세요.`,
|
|
1251
|
-
},
|
|
1252
|
-
],
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
// Co-worker 권한은 API Key 형식에서 판단
|
|
1257
|
-
const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
|
|
1258
|
-
|
|
1259
|
-
// .docuking/config.json에 설정 저장
|
|
1260
|
-
saveLocalConfig(localPath, {
|
|
1261
|
-
projectId,
|
|
1262
|
-
projectName,
|
|
1263
|
-
apiKey,
|
|
1264
|
-
isCoworker,
|
|
1265
|
-
coworkerFolder,
|
|
1266
|
-
createdAt: new Date().toISOString(),
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
// 폴더 생성: 오너는 yy_All_Docu/, 협업자는 yy_Coworker_{폴더명}/ (별도)
|
|
1270
|
-
const mainFolderName = 'yy_All_Docu';
|
|
1271
|
-
const mainFolderPath = path.join(localPath, mainFolderName);
|
|
1272
|
-
|
|
1273
|
-
// yy_All_Docu 폴더는 항상 생성
|
|
1274
|
-
if (!fs.existsSync(mainFolderPath)) {
|
|
1275
|
-
fs.mkdirSync(mainFolderPath, { recursive: true });
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
let coworkerFolderName = null;
|
|
1279
|
-
let coworkerFolderPath = null;
|
|
1280
|
-
|
|
1281
|
-
// zz_ai_* 폴더 목록
|
|
1282
|
-
const aiFolders = ['zz_ai_1_Talk', 'zz_ai_2_Todo', 'zz_ai_3_Plan'];
|
|
1283
|
-
|
|
1284
|
-
if (isCoworker) {
|
|
1285
|
-
// 협업자: yy_Coworker_{폴더명}/ 폴더를 yy_All_Docu/ 밖에 별도 생성
|
|
1286
|
-
coworkerFolderName = `yy_Coworker_${coworkerFolder}`;
|
|
1287
|
-
coworkerFolderPath = path.join(localPath, coworkerFolderName);
|
|
1288
|
-
if (!fs.existsSync(coworkerFolderPath)) {
|
|
1289
|
-
fs.mkdirSync(coworkerFolderPath, { recursive: true });
|
|
1290
|
-
}
|
|
1291
|
-
// 협업자 폴더 안에 _Private/ 생성
|
|
1292
|
-
const coworkerPrivatePath = path.join(coworkerFolderPath, '_Private');
|
|
1293
|
-
if (!fs.existsSync(coworkerPrivatePath)) {
|
|
1294
|
-
fs.mkdirSync(coworkerPrivatePath, { recursive: true });
|
|
1295
|
-
}
|
|
1296
|
-
// 협업자 폴더 안에 zz_ai_* 폴더 생성
|
|
1297
|
-
for (const folder of aiFolders) {
|
|
1298
|
-
const folderPath = path.join(coworkerFolderPath, folder);
|
|
1299
|
-
if (!fs.existsSync(folderPath)) {
|
|
1300
|
-
fs.mkdirSync(folderPath, { recursive: true });
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
} else {
|
|
1304
|
-
// 오너: yy_All_Docu/ 안에 _Infra_Config/와 _Private/ 생성
|
|
1305
|
-
const infraConfigPath = path.join(mainFolderPath, '_Infra_Config');
|
|
1306
|
-
if (!fs.existsSync(infraConfigPath)) {
|
|
1307
|
-
fs.mkdirSync(infraConfigPath, { recursive: true });
|
|
1308
|
-
}
|
|
1309
|
-
const ownerPrivatePath = path.join(mainFolderPath, '_Private');
|
|
1310
|
-
if (!fs.existsSync(ownerPrivatePath)) {
|
|
1311
|
-
fs.mkdirSync(ownerPrivatePath, { recursive: true });
|
|
1312
|
-
}
|
|
1313
|
-
// 오너: 루트에 zz_ai_* 폴더 생성
|
|
1314
|
-
for (const folder of aiFolders) {
|
|
1315
|
-
const folderPath = path.join(localPath, folder);
|
|
1316
|
-
if (!fs.existsSync(folderPath)) {
|
|
1317
|
-
fs.mkdirSync(folderPath, { recursive: true });
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
// 연결 완료 안내 (오너/코워커에 따라 다른 메시지)
|
|
1323
|
-
if (isCoworker) {
|
|
1324
|
-
return {
|
|
1325
|
-
content: [
|
|
1326
|
-
{
|
|
1327
|
-
type: 'text',
|
|
1328
|
-
text: `DocuKing 연결 완료! (참여자)
|
|
1329
|
-
|
|
1330
|
-
📁 프로젝트: ${projectName}
|
|
1331
|
-
📂 yy_All_Docu/ 폴더가 생성되었습니다. (오너 문서 Pull 대상)
|
|
1332
|
-
📂 ${coworkerFolderName}/ 작업 폴더가 생성되었습니다. (내 Push 폴더)
|
|
1333
|
-
🔑 설정 저장: .docuking/config.json
|
|
1334
|
-
|
|
1335
|
-
참여자 사용법:
|
|
1336
|
-
- "DocuKing에서 가져와" → 오너 문서를 yy_All_Docu/에 Pull
|
|
1337
|
-
- ${coworkerFolderName}/ 폴더에 문서 작성
|
|
1338
|
-
- "DocuKing에 올려줘" → 내 문서를 서버에 Push
|
|
1339
|
-
|
|
1340
|
-
💡 참여자는 ${coworkerFolderName}/ 폴더에만 Push할 수 있습니다.
|
|
1341
|
-
오너 문서는 yy_All_Docu/에서 읽을 수 있지만, 수정은 자기 폴더에서 작성하세요.`,
|
|
1342
|
-
},
|
|
1343
|
-
],
|
|
1344
|
-
};
|
|
1345
|
-
} else {
|
|
1346
|
-
return {
|
|
1347
|
-
content: [
|
|
1348
|
-
{
|
|
1349
|
-
type: 'text',
|
|
1350
|
-
text: `DocuKing 연결 완료!
|
|
1351
|
-
|
|
1352
|
-
📁 프로젝트: ${projectName}
|
|
1353
|
-
📂 yy_All_Docu/ 폴더가 생성되었습니다.
|
|
1354
|
-
🔑 설정 저장: .docuking/config.json
|
|
1355
|
-
|
|
1356
|
-
이제부터 문서 관리는 DocuKing에서 시작합니다:
|
|
1357
|
-
- yy_All_Docu/ 하위에 문서를 넣으면 DocuKing 서버로 암호화되어 저장됩니다
|
|
1358
|
-
- 협업자들과 안전하게 문서를 공유할 수 있습니다
|
|
1359
|
-
|
|
1360
|
-
사용법:
|
|
1361
|
-
- "DocuKing에 올려줘" → 로컬 문서를 서버에 Push
|
|
1362
|
-
- "DocuKing에서 가져와" → 서버 문서를 로컬로 Pull
|
|
1363
|
-
|
|
1364
|
-
폴더 구조:
|
|
1365
|
-
- yy_All_Docu/ : 동기화 대상 (킹폴더)
|
|
1366
|
-
- zz_ai_1_Talk/ : AI 대화록 (Push 대상, 킹톡)
|
|
1367
|
-
- zz_ai_2_Todo/ : AI 투두 (Push 대상, 킹투두)
|
|
1368
|
-
- zz_ai_3_Plan/ : AI 플랜 (Push 대상, 킹플랜)`,
|
|
1369
|
-
},
|
|
1370
|
-
],
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
// docuking_push 구현
|
|
1376
|
-
async function handlePush(args) {
|
|
1377
|
-
// localPath가 없으면 현재 작업 디렉토리 사용
|
|
1378
|
-
const localPath = args.localPath || process.cwd();
|
|
1379
|
-
const { filePath, message, author } = args;
|
|
1380
|
-
|
|
1381
|
-
// 커밋 메시지 필수 체크
|
|
1382
|
-
if (!message || message.trim() === '') {
|
|
1383
|
-
return {
|
|
1384
|
-
content: [
|
|
1385
|
-
{
|
|
1386
|
-
type: 'text',
|
|
1387
|
-
text: `오류: 커밋 메시지가 필요합니다.
|
|
1388
|
-
|
|
1389
|
-
Git처럼 무엇을 변경했는지 명확히 작성해주세요.
|
|
1390
|
-
예: "README에 설치 가이드 추가"`,
|
|
1391
|
-
},
|
|
1392
|
-
],
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
// 로컬 config에서 API 키 읽기
|
|
1397
|
-
const apiKey = getApiKey(localPath);
|
|
1398
|
-
if (!apiKey) {
|
|
1399
|
-
return {
|
|
1400
|
-
content: [
|
|
1401
|
-
{
|
|
1402
|
-
type: 'text',
|
|
1403
|
-
text: `오류: API 키를 찾을 수 없습니다.
|
|
1404
|
-
먼저 docuking_init을 실행하세요.`,
|
|
1405
|
-
},
|
|
1406
|
-
],
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
// 프로젝트 정보 조회 (로컬 config에서)
|
|
1411
|
-
const projectInfo = getProjectInfo(localPath);
|
|
1412
|
-
if (projectInfo.error) {
|
|
1413
|
-
return {
|
|
1414
|
-
content: [
|
|
1415
|
-
{
|
|
1416
|
-
type: 'text',
|
|
1417
|
-
text: projectInfo.error,
|
|
1418
|
-
},
|
|
1419
|
-
],
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
const projectId = projectInfo.projectId;
|
|
1424
|
-
const projectName = projectInfo.projectName;
|
|
1425
|
-
|
|
1426
|
-
// ========================================
|
|
1427
|
-
// 1단계: Pull 먼저 실행 (git pull && git push 패턴)
|
|
1428
|
-
// ========================================
|
|
1429
|
-
let pullResultText = '';
|
|
1430
|
-
try {
|
|
1431
|
-
const pullResult = await handlePullInternal({ localPath, filePath });
|
|
1432
|
-
pullResultText = pullResult.text;
|
|
1433
|
-
} catch (e) {
|
|
1434
|
-
return {
|
|
1435
|
-
content: [
|
|
1436
|
-
{
|
|
1437
|
-
type: 'text',
|
|
1438
|
-
text: `❌ Pull 실패로 Push를 중단합니다.
|
|
1439
|
-
오류: ${e.message}
|
|
1440
|
-
|
|
1441
|
-
먼저 Pull 문제를 해결한 후 다시 시도하세요.`,
|
|
1442
|
-
},
|
|
1443
|
-
],
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
// ========================================
|
|
1448
|
-
// 2단계: Push 진행
|
|
1449
|
-
// ========================================
|
|
1450
|
-
|
|
1451
|
-
// Co-worker 권한은 API Key 형식에서 판단
|
|
1452
|
-
const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
|
|
1453
|
-
const coworkerFolderName = isCoworker ? `yy_Coworker_${coworkerFolder}` : null;
|
|
1454
|
-
|
|
1455
|
-
// 작업 폴더 결정: 프로젝트 루트 기준 절대경로 사용
|
|
1456
|
-
// 오너: yy_All_Docu/, zz_ai_*/ 폴더들 Push
|
|
1457
|
-
// 협업자: yy_Coworker_{폴더명}/ 폴더만 Push
|
|
1458
|
-
const mainFolderPath = path.join(localPath, 'yy_All_Docu');
|
|
1459
|
-
|
|
1460
|
-
// Push 대상 폴더 목록 (프로젝트 루트 기준)
|
|
1461
|
-
let pushTargetFolders = [];
|
|
1462
|
-
|
|
1463
|
-
// 디버그: Push 대상 판단 로그
|
|
1464
|
-
console.error(`[DocuKing] Push 권한: isCoworker=${isCoworker}, coworkerFolder=${coworkerFolder}, coworkerFolderName=${coworkerFolderName}`);
|
|
1465
|
-
|
|
1466
|
-
if (isCoworker) {
|
|
1467
|
-
// 협업자: yy_Coworker_{폴더명}/ 폴더만 Push
|
|
1468
|
-
const coworkerPath = path.join(localPath, coworkerFolderName);
|
|
1469
|
-
console.error(`[DocuKing] 협업자 Push 대상 폴더: ${coworkerPath}`);
|
|
1470
|
-
if (!fs.existsSync(coworkerPath)) {
|
|
1471
|
-
fs.mkdirSync(coworkerPath, { recursive: true });
|
|
1472
|
-
}
|
|
1473
|
-
pushTargetFolders.push({ localPath: coworkerPath, serverPrefix: coworkerFolderName });
|
|
1474
|
-
|
|
1475
|
-
// 협업자 폴더 내용 디버그 출력
|
|
1476
|
-
try {
|
|
1477
|
-
const entries = fs.readdirSync(coworkerPath, { withFileTypes: true });
|
|
1478
|
-
console.error(`[DocuKing] 협업자 폴더 내용: ${entries.map(e => e.name + (e.isDirectory() ? '/' : '')).join(', ')}`);
|
|
1479
|
-
} catch (e) {
|
|
1480
|
-
console.error(`[DocuKing] 협업자 폴더 읽기 실패: ${e.message}`);
|
|
1481
|
-
}
|
|
1482
|
-
} else {
|
|
1483
|
-
// 오너: yy_All_Docu/ 폴더 확인
|
|
1484
|
-
if (!fs.existsSync(mainFolderPath)) {
|
|
1485
|
-
return {
|
|
1486
|
-
content: [
|
|
1487
|
-
{
|
|
1488
|
-
type: 'text',
|
|
1489
|
-
text: `오류: yy_All_Docu 폴더가 없습니다.
|
|
1490
|
-
docuking_init을 먼저 실행하세요.`,
|
|
1491
|
-
},
|
|
1492
|
-
],
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
// 오너 Push 대상: yy_All_Docu/ + zz_ai_*/ 폴더들
|
|
1497
|
-
pushTargetFolders.push({ localPath: mainFolderPath, serverPrefix: 'yy_All_Docu' });
|
|
1498
|
-
|
|
1499
|
-
// zz_ai_* 폴더들 찾기
|
|
1500
|
-
const rootEntries = fs.readdirSync(localPath, { withFileTypes: true });
|
|
1501
|
-
for (const entry of rootEntries) {
|
|
1502
|
-
if (entry.isDirectory() && entry.name.startsWith('zz_ai_')) {
|
|
1503
|
-
const zzPath = path.join(localPath, entry.name);
|
|
1504
|
-
pushTargetFolders.push({ localPath: zzPath, serverPrefix: entry.name });
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
// .env 파일 자동 백업: _Infra_Config/ 폴더에 복사
|
|
1510
|
-
// 오너만 가능 (협업자는 _Infra_Config/ 에 접근 불가)
|
|
1511
|
-
if (!isCoworker) {
|
|
1512
|
-
const envFilePath = path.join(localPath, '.env');
|
|
1513
|
-
const infraConfigPath = path.join(mainFolderPath, '_Infra_Config');
|
|
1514
|
-
|
|
1515
|
-
if (fs.existsSync(envFilePath)) {
|
|
1516
|
-
// _Infra_Config 폴더 생성
|
|
1517
|
-
if (!fs.existsSync(infraConfigPath)) {
|
|
1518
|
-
fs.mkdirSync(infraConfigPath, { recursive: true });
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
// .env 파일 복사
|
|
1522
|
-
const envBackupPath = path.join(infraConfigPath, '.env');
|
|
1523
|
-
try {
|
|
1524
|
-
fs.copyFileSync(envFilePath, envBackupPath);
|
|
1525
|
-
console.error(`[DocuKing] .env → _Infra_Config/.env 자동 백업 완료`);
|
|
1526
|
-
} catch (err) {
|
|
1527
|
-
console.error(`[DocuKing] .env 백업 실패: ${err.message}`);
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
// backend/.env 파일도 백업
|
|
1532
|
-
const backendEnvPath = path.join(localPath, 'backend', '.env');
|
|
1533
|
-
if (fs.existsSync(backendEnvPath)) {
|
|
1534
|
-
if (!fs.existsSync(infraConfigPath)) {
|
|
1535
|
-
fs.mkdirSync(infraConfigPath, { recursive: true });
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
const backendEnvBackupPath = path.join(infraConfigPath, 'backend.env');
|
|
1539
|
-
try {
|
|
1540
|
-
fs.copyFileSync(backendEnvPath, backendEnvBackupPath);
|
|
1541
|
-
console.error(`[DocuKing] backend/.env → _Infra_Config/backend.env 자동 백업 완료`);
|
|
1542
|
-
} catch (err) {
|
|
1543
|
-
console.error(`[DocuKing] backend/.env 백업 실패: ${err.message}`);
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
// frontend/.env.local 파일도 백업
|
|
1548
|
-
const frontendEnvPath = path.join(localPath, 'frontend', '.env.local');
|
|
1549
|
-
if (fs.existsSync(frontendEnvPath)) {
|
|
1550
|
-
if (!fs.existsSync(infraConfigPath)) {
|
|
1551
|
-
fs.mkdirSync(infraConfigPath, { recursive: true });
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
const frontendEnvBackupPath = path.join(infraConfigPath, 'frontend.env.local');
|
|
1555
|
-
try {
|
|
1556
|
-
fs.copyFileSync(frontendEnvPath, frontendEnvBackupPath);
|
|
1557
|
-
console.error(`[DocuKing] frontend/.env.local → _Infra_Config/frontend.env.local 자동 백업 완료`);
|
|
1558
|
-
} catch (err) {
|
|
1559
|
-
console.error(`[DocuKing] frontend/.env.local 백업 실패: ${err.message}`);
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
// package.json 파일 백업 (루트)
|
|
1564
|
-
const packageJsonPath = path.join(localPath, 'package.json');
|
|
1565
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
1566
|
-
if (!fs.existsSync(infraConfigPath)) {
|
|
1567
|
-
fs.mkdirSync(infraConfigPath, { recursive: true });
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
const packageJsonBackupPath = path.join(infraConfigPath, 'package.json');
|
|
1571
|
-
try {
|
|
1572
|
-
fs.copyFileSync(packageJsonPath, packageJsonBackupPath);
|
|
1573
|
-
console.error(`[DocuKing] package.json → _Infra_Config/package.json 자동 백업 완료`);
|
|
1574
|
-
} catch (err) {
|
|
1575
|
-
console.error(`[DocuKing] package.json 백업 실패: ${err.message}`);
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
// backend/package.json 파일도 백업
|
|
1580
|
-
const backendPackageJsonPath = path.join(localPath, 'backend', 'package.json');
|
|
1581
|
-
if (fs.existsSync(backendPackageJsonPath)) {
|
|
1582
|
-
if (!fs.existsSync(infraConfigPath)) {
|
|
1583
|
-
fs.mkdirSync(infraConfigPath, { recursive: true });
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
const backendPackageJsonBackupPath = path.join(infraConfigPath, 'backend.package.json');
|
|
1587
|
-
try {
|
|
1588
|
-
fs.copyFileSync(backendPackageJsonPath, backendPackageJsonBackupPath);
|
|
1589
|
-
console.error(`[DocuKing] backend/package.json → _Infra_Config/backend.package.json 자동 백업 완료`);
|
|
1590
|
-
} catch (err) {
|
|
1591
|
-
console.error(`[DocuKing] backend/package.json 백업 실패: ${err.message}`);
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
// frontend/package.json 파일도 백업
|
|
1596
|
-
const frontendPackageJsonPath = path.join(localPath, 'frontend', 'package.json');
|
|
1597
|
-
if (fs.existsSync(frontendPackageJsonPath)) {
|
|
1598
|
-
if (!fs.existsSync(infraConfigPath)) {
|
|
1599
|
-
fs.mkdirSync(infraConfigPath, { recursive: true });
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
const frontendPackageJsonBackupPath = path.join(infraConfigPath, 'frontend.package.json');
|
|
1603
|
-
try {
|
|
1604
|
-
fs.copyFileSync(frontendPackageJsonPath, frontendPackageJsonBackupPath);
|
|
1605
|
-
console.error(`[DocuKing] frontend/package.json → _Infra_Config/frontend.package.json 자동 백업 완료`);
|
|
1606
|
-
} catch (err) {
|
|
1607
|
-
console.error(`[DocuKing] frontend/package.json 백업 실패: ${err.message}`);
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
// 파일 목록 수집
|
|
1613
|
-
const filesToPush = [];
|
|
1614
|
-
const excludedFiles = []; // 제외된 파일 목록
|
|
1615
|
-
const largeFiles = [];
|
|
1616
|
-
|
|
1617
|
-
if (filePath) {
|
|
1618
|
-
// 특정 파일만 - 첫 번째 대상 폴더에서 찾기
|
|
1619
|
-
const targetFolder = pushTargetFolders[0];
|
|
1620
|
-
const fullPath = path.join(targetFolder.localPath, filePath);
|
|
1621
|
-
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
|
1622
|
-
const fileType = getFileType(filePath);
|
|
1623
|
-
if (fileType === 'excluded') {
|
|
1624
|
-
return {
|
|
1625
|
-
content: [
|
|
1626
|
-
{
|
|
1627
|
-
type: 'text',
|
|
1628
|
-
text: `오류: ${filePath}는 지원하지 않는 파일 형식입니다.\n\n📦 압축/설치 파일(.zip, .jar, .exe 등)은 DocuKing에 업로드되지 않습니다.\n💡 이런 파일은 별도 공유 방법(Google Drive, NAS 등)을 사용하고,\n 문서에 다운로드 링크나 설치 가이드를 작성하세요.`,
|
|
1629
|
-
},
|
|
1630
|
-
],
|
|
1631
|
-
};
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
// 서버 경로: 폴더prefix/파일경로 (예: yy_All_Docu/회의록/test.md)
|
|
1635
|
-
const serverFilePath = `${targetFolder.serverPrefix}/${filePath}`;
|
|
1636
|
-
filesToPush.push({ path: filePath, serverPath: serverFilePath, fullPath, fileType });
|
|
1637
|
-
}
|
|
1638
|
-
} else {
|
|
1639
|
-
// 전체 파일 - 모든 대상 폴더에서 수집
|
|
1640
|
-
const emptyFolders = []; // 빈 폴더 목록
|
|
1641
|
-
|
|
1642
|
-
for (const targetFolder of pushTargetFolders) {
|
|
1643
|
-
if (!fs.existsSync(targetFolder.localPath)) continue;
|
|
1644
|
-
|
|
1645
|
-
const folderFiles = [];
|
|
1646
|
-
collectFilesSimple(targetFolder.localPath, '', folderFiles, excludedFiles, largeFiles);
|
|
1647
|
-
|
|
1648
|
-
// 디버그: 수집된 파일 목록 출력
|
|
1649
|
-
console.error(`[DocuKing] ${targetFolder.serverPrefix}/ 에서 수집된 파일 ${folderFiles.length}개:`);
|
|
1650
|
-
for (const f of folderFiles.slice(0, 10)) {
|
|
1651
|
-
console.error(` - ${f.path}`);
|
|
1652
|
-
}
|
|
1653
|
-
if (folderFiles.length > 10) {
|
|
1654
|
-
console.error(` ... 외 ${folderFiles.length - 10}개`);
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
// 서버 경로 추가: prefix/상대경로 (예: yy_All_Docu/회의록/test.md)
|
|
1658
|
-
for (const file of folderFiles) {
|
|
1659
|
-
file.serverPath = `${targetFolder.serverPrefix}/${file.path}`;
|
|
1660
|
-
filesToPush.push(file);
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
// 빈 폴더 수집
|
|
1664
|
-
const folderEmptyDirs = [];
|
|
1665
|
-
collectEmptyFolders(targetFolder.localPath, '', folderEmptyDirs);
|
|
1666
|
-
for (const emptyDir of folderEmptyDirs) {
|
|
1667
|
-
emptyFolders.push({
|
|
1668
|
-
localPath: emptyDir,
|
|
1669
|
-
serverPath: `${targetFolder.serverPrefix}/${emptyDir}`,
|
|
1670
|
-
});
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
// 대용량 파일 경고
|
|
1675
|
-
if (largeFiles.length > 0) {
|
|
1676
|
-
console.error(`\n[DocuKing] ⚠️ 대용량 파일 ${largeFiles.length}개 제외됨 (${MAX_FILE_SIZE_MB}MB 초과):`);
|
|
1677
|
-
for (const f of largeFiles) {
|
|
1678
|
-
console.error(` - ${f.path} (${f.sizeMB}MB)`);
|
|
1679
|
-
}
|
|
1680
|
-
console.error(`💡 대용량 파일은 Google Drive, NAS 등 별도 방법으로 공유하세요.\n`);
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
if (filesToPush.length === 0 && emptyFolders.length === 0) {
|
|
1685
|
-
return {
|
|
1686
|
-
content: [
|
|
1687
|
-
{
|
|
1688
|
-
type: 'text',
|
|
1689
|
-
text: 'Push할 파일이 없습니다.',
|
|
1690
|
-
},
|
|
1691
|
-
],
|
|
1692
|
-
};
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
// 총 용량 계산
|
|
1696
|
-
const totalSizeMB = filesToPush.reduce((sum, f) => sum + (f.sizeMB || 0), 0);
|
|
1697
|
-
|
|
1698
|
-
// 파일 업로드 (진행률 표시)
|
|
1699
|
-
const results = [];
|
|
1700
|
-
const total = filesToPush.length;
|
|
1701
|
-
let current = 0;
|
|
1702
|
-
let skipped = 0;
|
|
1703
|
-
|
|
1704
|
-
// 시작 안내 메시지 출력 (AI가 사용자에게 전달할 수 있도록)
|
|
1705
|
-
console.error(`[DocuKing] Push 시작: ${total}개 파일 (총 ${totalSizeMB.toFixed(1)}MB)`);
|
|
1706
|
-
console.error(`[DocuKing] 💡 실시간 진행상황은 DocuKing 웹(https://docuking.ai)에서 확인하세요`);
|
|
1707
|
-
|
|
1708
|
-
// Sync 시작 알림 (웹에서 프로그레스바 표시용)
|
|
1709
|
-
try {
|
|
1710
|
-
await fetch(`${API_ENDPOINT}/projects/${projectId}/sync/start`, {
|
|
1711
|
-
method: 'POST',
|
|
1712
|
-
headers: {
|
|
1713
|
-
'Content-Type': 'application/json',
|
|
1714
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1715
|
-
},
|
|
1716
|
-
body: JSON.stringify({ totalFiles: total }),
|
|
1717
|
-
});
|
|
1718
|
-
} catch (e) {
|
|
1719
|
-
console.error('[DocuKing] Sync 시작 알림 실패:', e.message);
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
// 서버에서 파일 해시 조회 (Git 스타일 동기화용)
|
|
1723
|
-
let serverPathToHash = {};
|
|
1724
|
-
let serverHashToPath = {};
|
|
1725
|
-
let serverAllPaths = [];
|
|
1726
|
-
try {
|
|
1727
|
-
const hashResponse = await fetch(
|
|
1728
|
-
`${API_ENDPOINT}/files/hashes-for-sync?projectId=${projectId}`,
|
|
1729
|
-
{
|
|
1730
|
-
headers: {
|
|
1731
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1732
|
-
},
|
|
1733
|
-
}
|
|
1734
|
-
);
|
|
1735
|
-
if (hashResponse.ok) {
|
|
1736
|
-
const hashData = await hashResponse.json();
|
|
1737
|
-
serverPathToHash = hashData.pathToHash || {};
|
|
1738
|
-
serverHashToPath = hashData.hashToPath || {};
|
|
1739
|
-
serverAllPaths = hashData.allPaths || [];
|
|
1740
|
-
}
|
|
1741
|
-
} catch (e) {
|
|
1742
|
-
// 해시 조회 실패는 무시 (처음 Push하는 경우 등)
|
|
1743
|
-
console.error('[DocuKing] 파일 해시 조회 실패:', e.message);
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
// 처리된 로컬 파일 경로 (서버 삭제용)
|
|
1747
|
-
const processedLocalPaths = new Set();
|
|
1748
|
-
let moved = 0;
|
|
1749
|
-
|
|
1750
|
-
for (const file of filesToPush) {
|
|
1751
|
-
current++;
|
|
1752
|
-
const progress = `${current}/${total}`;
|
|
1753
|
-
const sizeInfo = file.sizeMB >= 1 ? ` (${file.sizeMB.toFixed(1)}MB)` : '';
|
|
1754
|
-
processedLocalPaths.add(file.serverPath);
|
|
1755
|
-
|
|
1756
|
-
// 1MB 이상 파일은 업로드 시작 시 로그 출력 (사용자가 진행 상황 파악 가능)
|
|
1757
|
-
if (file.sizeMB >= 1) {
|
|
1758
|
-
console.error(`[DocuKing] ${progress} 업로드 중: ${file.path}${sizeInfo}`);
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
try {
|
|
1762
|
-
// 파일 해시 계산 (변경 감지)
|
|
1763
|
-
let fileHash;
|
|
1764
|
-
let content;
|
|
1765
|
-
let encoding = 'utf-8';
|
|
1766
|
-
|
|
1767
|
-
if (file.fileType === 'binary') {
|
|
1768
|
-
// 바이너리 파일은 Base64로 인코딩
|
|
1769
|
-
const buffer = fs.readFileSync(file.fullPath);
|
|
1770
|
-
fileHash = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
1771
|
-
content = buffer.toString('base64');
|
|
1772
|
-
encoding = 'base64';
|
|
1773
|
-
} else {
|
|
1774
|
-
// 텍스트 파일은 UTF-8
|
|
1775
|
-
content = fs.readFileSync(file.fullPath, 'utf-8');
|
|
1776
|
-
fileHash = crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
// Git 스타일 동기화:
|
|
1780
|
-
// 1. 같은 경로 + 같은 해시 → 스킵 (변경 없음)
|
|
1781
|
-
if (serverPathToHash[file.serverPath] === fileHash) {
|
|
1782
|
-
const resultText = `${progress} ⊘ ${file.serverPath} (변경 없음)`;
|
|
1783
|
-
results.push(resultText);
|
|
1784
|
-
skipped++;
|
|
1785
|
-
continue;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
// 2. 같은 해시가 다른 경로에 있음 → Move (경로 변경)
|
|
1789
|
-
const existingPath = serverHashToPath[fileHash];
|
|
1790
|
-
if (existingPath && existingPath !== file.serverPath) {
|
|
1791
|
-
try {
|
|
1792
|
-
const moveResponse = await fetch(`${API_ENDPOINT}/files/move`, {
|
|
1793
|
-
method: 'POST',
|
|
1794
|
-
headers: {
|
|
1795
|
-
'Content-Type': 'application/json',
|
|
1796
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1797
|
-
},
|
|
1798
|
-
body: JSON.stringify({
|
|
1799
|
-
projectId,
|
|
1800
|
-
oldPath: existingPath,
|
|
1801
|
-
newPath: file.serverPath,
|
|
1802
|
-
}),
|
|
1803
|
-
});
|
|
1804
|
-
|
|
1805
|
-
if (moveResponse.ok) {
|
|
1806
|
-
const resultText = `${progress} ↷ ${existingPath} → ${file.serverPath}`;
|
|
1807
|
-
results.push(resultText);
|
|
1808
|
-
moved++;
|
|
1809
|
-
// 이동된 파일의 해시 맵 업데이트
|
|
1810
|
-
delete serverHashToPath[fileHash];
|
|
1811
|
-
serverHashToPath[fileHash] = file.serverPath;
|
|
1812
|
-
continue;
|
|
1813
|
-
}
|
|
1814
|
-
} catch (e) {
|
|
1815
|
-
console.error(`[DocuKing] Move 실패: ${e.message}, Upload로 대체`);
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
// 3. 새 파일 또는 내용 변경 → Upload
|
|
1820
|
-
|
|
1821
|
-
// 재시도 로직 (최대 3회)
|
|
1822
|
-
let lastError = null;
|
|
1823
|
-
let success = false;
|
|
1824
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1825
|
-
try {
|
|
1826
|
-
// 대용량 파일 업로드를 위한 타임아웃 설정 (10분)
|
|
1827
|
-
const controller = new AbortController();
|
|
1828
|
-
const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000); // 10분
|
|
1829
|
-
|
|
1830
|
-
let response;
|
|
1831
|
-
try {
|
|
1832
|
-
response = await fetch(`${API_ENDPOINT}/files/push`, {
|
|
1833
|
-
method: 'POST',
|
|
1834
|
-
headers: {
|
|
1835
|
-
'Content-Type': 'application/json',
|
|
1836
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1837
|
-
},
|
|
1838
|
-
body: JSON.stringify({
|
|
1839
|
-
projectId,
|
|
1840
|
-
path: file.serverPath, // 서버 경로 (코워커는 yy_Coworker_{폴더명}/파일경로)
|
|
1841
|
-
content,
|
|
1842
|
-
encoding, // 'utf-8' 또는 'base64'
|
|
1843
|
-
message, // 커밋 메시지
|
|
1844
|
-
author, // 작성자 (optional)
|
|
1845
|
-
fileHash, // 파일 해시 (변경 감지용)
|
|
1846
|
-
}),
|
|
1847
|
-
signal: controller.signal,
|
|
1848
|
-
});
|
|
1849
|
-
clearTimeout(timeoutId);
|
|
1850
|
-
} catch (e) {
|
|
1851
|
-
clearTimeout(timeoutId);
|
|
1852
|
-
if (e.name === 'AbortError') {
|
|
1853
|
-
throw new Error(`파일 업로드 타임아웃 (10분 초과): ${file.path}`);
|
|
1854
|
-
}
|
|
1855
|
-
throw e;
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
if (response.ok) {
|
|
1859
|
-
const resultText = attempt > 1
|
|
1860
|
-
? `${progress} ✓ ${file.path} (재시도 ${attempt}회 성공)`
|
|
1861
|
-
: `${progress} ✓ ${file.path}`;
|
|
1862
|
-
results.push(resultText);
|
|
1863
|
-
success = true;
|
|
1864
|
-
break; // 성공하면 재시도 중단
|
|
1865
|
-
} else {
|
|
1866
|
-
const error = await response.text();
|
|
1867
|
-
lastError = error;
|
|
1868
|
-
// 4xx 에러는 재시도하지 않음 (클라이언트 오류)
|
|
1869
|
-
if (response.status >= 400 && response.status < 500) {
|
|
1870
|
-
throw new Error(error);
|
|
1871
|
-
}
|
|
1872
|
-
// 5xx 에러만 재시도
|
|
1873
|
-
if (attempt < 3) {
|
|
1874
|
-
const waitTime = attempt * 1000; // 1초, 2초, 3초
|
|
1875
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
} catch (e) {
|
|
1879
|
-
lastError = e.message;
|
|
1880
|
-
// 네트워크 오류 등은 재시도
|
|
1881
|
-
if (attempt < 3 && !e.message.includes('타임아웃')) {
|
|
1882
|
-
const waitTime = attempt * 1000;
|
|
1883
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
1884
|
-
} else {
|
|
1885
|
-
throw e;
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
if (!success) {
|
|
1891
|
-
const errorText = `${progress} ✗ ${file.path}: ${lastError}`;
|
|
1892
|
-
results.push(errorText);
|
|
1893
|
-
console.error(errorText);
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
// 진행 상황 업데이트 (매 파일마다 또는 5개마다)
|
|
1897
|
-
if (current % 5 === 0 || current === total || current === 1) {
|
|
1898
|
-
try {
|
|
1899
|
-
await fetch(`${API_ENDPOINT}/projects/${projectId}/sync/progress`, {
|
|
1900
|
-
method: 'POST',
|
|
1901
|
-
headers: {
|
|
1902
|
-
'Content-Type': 'application/json',
|
|
1903
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1904
|
-
},
|
|
1905
|
-
body: JSON.stringify({ progress: current }),
|
|
1906
|
-
});
|
|
1907
|
-
} catch (e) {
|
|
1908
|
-
// 진행 상황 업데이트 실패는 무시
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
} catch (e) {
|
|
1912
|
-
results.push(`${progress} ✗ ${file.path}: ${e.message}`);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
// 4. 서버에만 있고 로컬에 없는 파일 삭제 (Git rm과 동일)
|
|
1917
|
-
// 단, 협업자 폴더(yy_Coworker_*)는 삭제하지 않음 (오너가 협업자 파일을 삭제하면 안됨)
|
|
1918
|
-
let deleted = 0;
|
|
1919
|
-
const deletedFiles = [];
|
|
1920
|
-
if (serverAllPaths.length > 0 && !isCoworker) {
|
|
1921
|
-
// 코워커가 아닌 경우에만 삭제 수행 (오너 전용)
|
|
1922
|
-
// yy_Coworker_*로 시작하는 경로는 삭제 대상에서 제외
|
|
1923
|
-
const pathsToDelete = serverAllPaths.filter(p =>
|
|
1924
|
-
!processedLocalPaths.has(p) && !p.startsWith('yy_Coworker_')
|
|
1925
|
-
);
|
|
1926
|
-
|
|
1927
|
-
if (pathsToDelete.length > 0) {
|
|
1928
|
-
try {
|
|
1929
|
-
const deleteResponse = await fetch(`${API_ENDPOINT}/files/delete-batch`, {
|
|
1930
|
-
method: 'POST',
|
|
1931
|
-
headers: {
|
|
1932
|
-
'Content-Type': 'application/json',
|
|
1933
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1934
|
-
},
|
|
1935
|
-
body: JSON.stringify({
|
|
1936
|
-
projectId,
|
|
1937
|
-
paths: pathsToDelete,
|
|
1938
|
-
}),
|
|
1939
|
-
});
|
|
1940
|
-
|
|
1941
|
-
if (deleteResponse.ok) {
|
|
1942
|
-
const deleteResult = await deleteResponse.json();
|
|
1943
|
-
deleted = deleteResult.deleted || 0;
|
|
1944
|
-
deletedFiles.push(...pathsToDelete.slice(0, deleted));
|
|
1945
|
-
}
|
|
1946
|
-
} catch (e) {
|
|
1947
|
-
console.error('[DocuKing] 파일 삭제 실패:', e.message);
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
// 5. 빈 폴더 생성
|
|
1953
|
-
let createdEmptyFolders = 0;
|
|
1954
|
-
if (typeof emptyFolders !== 'undefined' && emptyFolders.length > 0) {
|
|
1955
|
-
for (const folder of emptyFolders) {
|
|
1956
|
-
try {
|
|
1957
|
-
const folderResponse = await fetch(`${API_ENDPOINT}/files`, {
|
|
1958
|
-
method: 'POST',
|
|
1959
|
-
headers: {
|
|
1960
|
-
'Content-Type': 'application/json',
|
|
1961
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1962
|
-
},
|
|
1963
|
-
body: JSON.stringify({
|
|
1964
|
-
projectId,
|
|
1965
|
-
path: folder.serverPath,
|
|
1966
|
-
type: 'folder',
|
|
1967
|
-
name: folder.localPath.split('/').pop(),
|
|
1968
|
-
}),
|
|
1969
|
-
});
|
|
1970
|
-
|
|
1971
|
-
if (folderResponse.ok) {
|
|
1972
|
-
createdEmptyFolders++;
|
|
1973
|
-
console.error(`[DocuKing] 빈 폴더 생성: ${folder.serverPath}`);
|
|
1974
|
-
}
|
|
1975
|
-
} catch (e) {
|
|
1976
|
-
console.error(`[DocuKing] 빈 폴더 생성 실패: ${folder.serverPath} - ${e.message}`);
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
// Sync 완료 알림
|
|
1982
|
-
try {
|
|
1983
|
-
await fetch(`${API_ENDPOINT}/projects/${projectId}/sync/complete`, {
|
|
1984
|
-
method: 'POST',
|
|
1985
|
-
headers: {
|
|
1986
|
-
'Content-Type': 'application/json',
|
|
1987
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1988
|
-
},
|
|
1989
|
-
});
|
|
1990
|
-
} catch (e) {
|
|
1991
|
-
console.error('[DocuKing] Sync 완료 알림 실패:', e.message);
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
const successCount = results.filter(r => r.includes('✓')).length;
|
|
1995
|
-
const failCount = results.filter(r => r.includes('✗')).length;
|
|
1996
|
-
const skippedCount = skipped; // 이미 계산된 스킵 개수 사용
|
|
1997
|
-
const excludedCount = excludedFiles.length;
|
|
1998
|
-
const movedCount = moved;
|
|
1999
|
-
|
|
2000
|
-
// 요약 정보
|
|
2001
|
-
let summary = `\n📦 커밋 메시지: "${message}"\n\n📊 처리 결과:\n - 총 파일: ${total}개\n - 업로드: ${successCount}개\n - 이동: ${movedCount}개\n - 삭제: ${deleted}개\n - 스킵 (변경 없음): ${skippedCount}개\n - 실패: ${failCount}개`;
|
|
2002
|
-
if (createdEmptyFolders > 0) {
|
|
2003
|
-
summary += `\n - 빈 폴더 생성: ${createdEmptyFolders}개`;
|
|
2004
|
-
}
|
|
2005
|
-
if (excludedCount > 0) {
|
|
2006
|
-
summary += `\n - 제외 (압축/설치파일): ${excludedCount}개`;
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
// 상세 결과를 표시 (Git처럼)
|
|
2010
|
-
let resultText = `✓ Push 완료!${summary}`;
|
|
2011
|
-
|
|
2012
|
-
// 업로드된 파일이 있으면 상세 목록 표시
|
|
2013
|
-
if (successCount > 0) {
|
|
2014
|
-
const uploadedFiles = results.filter(r => r.includes('✓') && !r.includes('재시도'));
|
|
2015
|
-
resultText += `\n\n📤 업로드된 파일 (${successCount}개):\n${uploadedFiles.map(r => ` ${r.replace(/^\d+\/\d+ /, '')}`).join('\n')}`;
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// 이동된 파일이 있으면 표시 (Git 스타일)
|
|
2019
|
-
if (movedCount > 0) {
|
|
2020
|
-
const movedFiles = results.filter(r => r.includes('↷'));
|
|
2021
|
-
resultText += `\n\n↷ 이동된 파일 (${movedCount}개):\n${movedFiles.map(r => ` ${r.replace(/^\d+\/\d+ /, '')}`).join('\n')}`;
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
// 삭제된 파일이 있으면 표시 (Git 스타일)
|
|
2025
|
-
if (deleted > 0) {
|
|
2026
|
-
resultText += `\n\n🗑️ 삭제된 파일 (${deleted}개, 로컬에 없음):\n${deletedFiles.slice(0, 20).map(f => ` ✗ ${f}`).join('\n')}`;
|
|
2027
|
-
if (deleted > 20) {
|
|
2028
|
-
resultText += `\n ... 외 ${deleted - 20}개`;
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
// 스킵된 파일이 있으면 표시
|
|
2033
|
-
if (skippedCount > 0) {
|
|
2034
|
-
const skippedFiles = results.filter(r => r.includes('⊘'));
|
|
2035
|
-
resultText += `\n\n⏭️ 스킵된 파일 (${skippedCount}개, 변경 없음):\n${skippedFiles.map(r => ` ${r.replace(/^\d+\/\d+ /, '')}`).join('\n')}`;
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
// 실패한 파일이 있으면 표시
|
|
2039
|
-
if (failCount > 0) {
|
|
2040
|
-
const failedFiles = results.filter(r => r.includes('✗'));
|
|
2041
|
-
resultText += `\n\n❌ 실패한 파일 (${failCount}개):\n${failedFiles.map(r => ` ${r.replace(/^\d+\/\d+ /, '')}`).join('\n')}`;
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
// 제외된 파일이 있으면 표시
|
|
2045
|
-
if (excludedCount > 0) {
|
|
2046
|
-
resultText += `\n\n📦 제외된 파일 (${excludedCount}개, 압축/설치파일):\n${excludedFiles.slice(0, 10).map(f => ` ⊖ ${f}`).join('\n')}`;
|
|
2047
|
-
if (excludedCount > 10) {
|
|
2048
|
-
resultText += `\n ... 외 ${excludedCount - 10}개`;
|
|
2049
|
-
}
|
|
2050
|
-
resultText += `\n\n💡 압축/설치 파일은 DocuKing에 저장되지 않습니다.\n 이런 파일은 별도 공유(Google Drive, NAS 등)를 사용하고,\n 문서에 다운로드 링크나 설치 가이드를 작성하세요.`;
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
resultText += `\n\n🌐 웹 탐색기에서 커밋 히스토리를 확인할 수 있습니다: https://docuking.ai`;
|
|
2054
|
-
|
|
2055
|
-
// Pull 결과가 있으면 앞에 추가
|
|
2056
|
-
let finalText = '';
|
|
2057
|
-
if (pullResultText) {
|
|
2058
|
-
// Pull에서 실제로 받은 파일이 있는 경우만 표시
|
|
2059
|
-
const pullHasChanges = pullResultText.includes('다운로드 (신규):') && !pullResultText.includes('다운로드 (신규): 0개');
|
|
2060
|
-
const pullHasUpdates = pullResultText.includes('업데이트 (변경됨):') && !pullResultText.includes('업데이트 (변경됨): 0개');
|
|
2061
|
-
|
|
2062
|
-
if (pullHasChanges || pullHasUpdates) {
|
|
2063
|
-
finalText = `📥 [1단계] Pull (서버 → 로컬)\n${pullResultText}\n\n${'─'.repeat(50)}\n\n📤 [2단계] Push (로컬 → 서버)\n${resultText}`;
|
|
2064
|
-
} else {
|
|
2065
|
-
// Pull에서 변경 없으면 간단히 표시
|
|
2066
|
-
finalText = `📥 Pull: 변경 없음 (최신 상태)\n\n📤 Push:\n${resultText}`;
|
|
2067
|
-
}
|
|
2068
|
-
} else {
|
|
2069
|
-
finalText = resultText;
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
return {
|
|
2073
|
-
content: [
|
|
2074
|
-
{
|
|
2075
|
-
type: 'text',
|
|
2076
|
-
text: finalText,
|
|
2077
|
-
},
|
|
2078
|
-
],
|
|
2079
|
-
};
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
// docuking_pull 내부 구현 (Push에서도 호출)
|
|
2083
|
-
// 반환값: { text: 결과문자열, downloaded, updated, skipped, failed }
|
|
2084
|
-
async function handlePullInternal(args) {
|
|
2085
|
-
const localPath = args.localPath || process.cwd();
|
|
2086
|
-
const { filePath } = args;
|
|
2087
|
-
|
|
2088
|
-
// 로컬 config에서 API 키 읽기
|
|
2089
|
-
const apiKey = getApiKey(localPath);
|
|
2090
|
-
if (!apiKey) {
|
|
2091
|
-
throw new Error('API 키를 찾을 수 없습니다. 먼저 docuking_init을 실행하세요.');
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
// 프로젝트 정보 조회 (로컬 config에서)
|
|
2095
|
-
const projectInfo = getProjectInfo(localPath);
|
|
2096
|
-
if (projectInfo.error) {
|
|
2097
|
-
throw new Error(projectInfo.error);
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
const projectId = projectInfo.projectId;
|
|
2101
|
-
|
|
2102
|
-
// yy_All_Docu 폴더 (없으면 생성)
|
|
2103
|
-
const mainFolderName = 'yy_All_Docu';
|
|
2104
|
-
const mainFolderPath = path.join(localPath, mainFolderName);
|
|
2105
|
-
if (!fs.existsSync(mainFolderPath)) {
|
|
2106
|
-
fs.mkdirSync(mainFolderPath, { recursive: true });
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
// 파일 목록 조회
|
|
2110
|
-
let files = [];
|
|
2111
|
-
|
|
2112
|
-
try {
|
|
2113
|
-
const response = await fetch(
|
|
2114
|
-
`${API_ENDPOINT}/files/tree?projectId=${projectId}`,
|
|
2115
|
-
{
|
|
2116
|
-
headers: {
|
|
2117
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
2118
|
-
},
|
|
2119
|
-
}
|
|
2120
|
-
);
|
|
2121
|
-
|
|
2122
|
-
if (!response.ok) {
|
|
2123
|
-
throw new Error(await response.text());
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
const data = await response.json();
|
|
2127
|
-
files = flattenTree(data.tree || []);
|
|
2128
|
-
} catch (e) {
|
|
2129
|
-
throw new Error(`파일 목록 조회 실패 - ${e.message}`);
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
if (filePath) {
|
|
2133
|
-
files = files.filter(f => f.path === filePath || f.path.startsWith(filePath + '/'));
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
if (files.length === 0) {
|
|
2137
|
-
// 빈 파일은 에러가 아님 - 정상 결과 반환
|
|
2138
|
-
return {
|
|
2139
|
-
text: 'Pull할 파일이 없습니다.',
|
|
2140
|
-
total: 0,
|
|
2141
|
-
downloaded: 0,
|
|
2142
|
-
updated: 0,
|
|
2143
|
-
skipped: 0,
|
|
2144
|
-
failed: 0,
|
|
2145
|
-
};
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
// 파일 다운로드
|
|
2149
|
-
const results = [];
|
|
2150
|
-
const total = files.length;
|
|
2151
|
-
let current = 0;
|
|
2152
|
-
let downloaded = 0; // 새로 다운로드
|
|
2153
|
-
let updated = 0; // 업데이트 (변경됨)
|
|
2154
|
-
let skipped = 0; // 스킵 (변경 없음)
|
|
2155
|
-
let failed = 0; // 실패
|
|
2156
|
-
|
|
2157
|
-
for (const file of files) {
|
|
2158
|
-
current++;
|
|
2159
|
-
const progress = `${current}/${total}`;
|
|
2160
|
-
|
|
2161
|
-
try {
|
|
2162
|
-
const response = await fetch(
|
|
2163
|
-
`${API_ENDPOINT}/files?projectId=${projectId}&path=${encodeURIComponent(file.path)}`,
|
|
2164
|
-
{
|
|
2165
|
-
headers: {
|
|
2166
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
2167
|
-
},
|
|
2168
|
-
}
|
|
2169
|
-
);
|
|
2170
|
-
|
|
2171
|
-
if (!response.ok) {
|
|
2172
|
-
results.push({ type: 'fail', path: file.path, error: await response.text() });
|
|
2173
|
-
failed++;
|
|
2174
|
-
continue;
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
const data = await response.json();
|
|
2178
|
-
|
|
2179
|
-
// 서버 경로에 따라 로컬 저장 위치 결정
|
|
2180
|
-
// 프로젝트 루트 기준 절대경로 그대로 사용
|
|
2181
|
-
// - yy_All_Docu/xxx → yy_All_Docu/xxx (오너 문서)
|
|
2182
|
-
// - yy_Coworker_xxx/yyy → yy_Coworker_xxx/yyy (협업자 폴더)
|
|
2183
|
-
// - zz_ai_xxx/yyy → zz_ai_xxx/yyy (AI 폴더)
|
|
2184
|
-
let fullPath;
|
|
2185
|
-
if (file.path.startsWith('yy_All_Docu/') ||
|
|
2186
|
-
file.path.startsWith('yy_Coworker_') ||
|
|
2187
|
-
file.path.startsWith('zz_ai_')) {
|
|
2188
|
-
// 서버 경로 그대로 로컬에 저장
|
|
2189
|
-
fullPath = path.join(localPath, file.path);
|
|
2190
|
-
} else {
|
|
2191
|
-
// 기타 (구버전 호환): yy_All_Docu/ 안에 저장
|
|
2192
|
-
fullPath = path.join(mainFolderPath, file.path);
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
// 인코딩에 따라 저장
|
|
2196
|
-
const content = data.file?.content || data.content || '';
|
|
2197
|
-
const encoding = data.file?.encoding || data.encoding || 'utf-8';
|
|
2198
|
-
|
|
2199
|
-
// 서버 파일 해시 계산
|
|
2200
|
-
let serverBuffer;
|
|
2201
|
-
if (encoding === 'base64') {
|
|
2202
|
-
serverBuffer = Buffer.from(content, 'base64');
|
|
2203
|
-
} else {
|
|
2204
|
-
serverBuffer = Buffer.from(content, 'utf-8');
|
|
2205
|
-
}
|
|
2206
|
-
const serverHash = crypto.createHash('sha256').update(serverBuffer).digest('hex');
|
|
2207
|
-
|
|
2208
|
-
// 로컬 파일 존재 여부 및 해시 비교
|
|
2209
|
-
let localExists = fs.existsSync(fullPath);
|
|
2210
|
-
let localHash = null;
|
|
2211
|
-
|
|
2212
|
-
if (localExists) {
|
|
2213
|
-
try {
|
|
2214
|
-
const localBuffer = fs.readFileSync(fullPath);
|
|
2215
|
-
localHash = crypto.createHash('sha256').update(localBuffer).digest('hex');
|
|
2216
|
-
} catch (e) {
|
|
2217
|
-
localExists = false;
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
// 변경 감지
|
|
2222
|
-
if (localExists && localHash === serverHash) {
|
|
2223
|
-
// 변경 없음 - 스킵
|
|
2224
|
-
results.push({ type: 'skip', path: file.path });
|
|
2225
|
-
skipped++;
|
|
2226
|
-
continue;
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
// 디렉토리 생성
|
|
2230
|
-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
2231
|
-
|
|
2232
|
-
// 파일 저장
|
|
2233
|
-
if (encoding === 'base64') {
|
|
2234
|
-
fs.writeFileSync(fullPath, serverBuffer);
|
|
2235
|
-
} else {
|
|
2236
|
-
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
if (localExists) {
|
|
2240
|
-
// 기존 파일 업데이트
|
|
2241
|
-
results.push({ type: 'update', path: file.path });
|
|
2242
|
-
updated++;
|
|
2243
|
-
} else {
|
|
2244
|
-
// 새로 다운로드
|
|
2245
|
-
results.push({ type: 'download', path: file.path });
|
|
2246
|
-
downloaded++;
|
|
2247
|
-
}
|
|
2248
|
-
} catch (e) {
|
|
2249
|
-
results.push({ type: 'fail', path: file.path, error: e.message });
|
|
2250
|
-
failed++;
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
// 상세 결과 구성 (Push 스타일)
|
|
2255
|
-
let resultText = `✓ Pull 완료!
|
|
2256
|
-
|
|
2257
|
-
📊 처리 결과:
|
|
2258
|
-
- 총 파일: ${total}개
|
|
2259
|
-
- 다운로드 (신규): ${downloaded}개
|
|
2260
|
-
- 업데이트 (변경됨): ${updated}개
|
|
2261
|
-
- 스킵 (변경 없음): ${skipped}개
|
|
2262
|
-
- 실패: ${failed}개`;
|
|
2263
|
-
|
|
2264
|
-
// 다운로드된 파일 목록
|
|
2265
|
-
const downloadedFiles = results.filter(r => r.type === 'download');
|
|
2266
|
-
if (downloadedFiles.length > 0) {
|
|
2267
|
-
resultText += `\n\n📥 다운로드된 파일 (${downloadedFiles.length}개):`;
|
|
2268
|
-
downloadedFiles.slice(0, 20).forEach(f => {
|
|
2269
|
-
resultText += `\n ✓ ${f.path}`;
|
|
2270
|
-
});
|
|
2271
|
-
if (downloadedFiles.length > 20) {
|
|
2272
|
-
resultText += `\n ... 외 ${downloadedFiles.length - 20}개`;
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
// 업데이트된 파일 목록
|
|
2277
|
-
const updatedFiles = results.filter(r => r.type === 'update');
|
|
2278
|
-
if (updatedFiles.length > 0) {
|
|
2279
|
-
resultText += `\n\n🔄 업데이트된 파일 (${updatedFiles.length}개):`;
|
|
2280
|
-
updatedFiles.slice(0, 20).forEach(f => {
|
|
2281
|
-
resultText += `\n ↻ ${f.path}`;
|
|
2282
|
-
});
|
|
2283
|
-
if (updatedFiles.length > 20) {
|
|
2284
|
-
resultText += `\n ... 외 ${updatedFiles.length - 20}개`;
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
// 스킵된 파일 목록
|
|
2289
|
-
const skippedFiles = results.filter(r => r.type === 'skip');
|
|
2290
|
-
if (skippedFiles.length > 0) {
|
|
2291
|
-
resultText += `\n\n⏭️ 스킵된 파일 (${skippedFiles.length}개, 변경 없음):`;
|
|
2292
|
-
skippedFiles.slice(0, 10).forEach(f => {
|
|
2293
|
-
resultText += `\n ⊘ ${f.path}`;
|
|
2294
|
-
});
|
|
2295
|
-
if (skippedFiles.length > 10) {
|
|
2296
|
-
resultText += `\n ... 외 ${skippedFiles.length - 10}개`;
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
// 실패한 파일 목록
|
|
2301
|
-
const failedFiles = results.filter(r => r.type === 'fail');
|
|
2302
|
-
if (failedFiles.length > 0) {
|
|
2303
|
-
resultText += `\n\n❌ 실패한 파일 (${failedFiles.length}개):`;
|
|
2304
|
-
failedFiles.forEach(f => {
|
|
2305
|
-
resultText += `\n ✗ ${f.path}: ${f.error}`;
|
|
2306
|
-
});
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
resultText += `\n\n🌐 웹 탐색기: https://docuking.ai`;
|
|
2310
|
-
|
|
2311
|
-
// 내부용 객체 반환 (Push에서 사용)
|
|
2312
|
-
return {
|
|
2313
|
-
text: resultText,
|
|
2314
|
-
total,
|
|
2315
|
-
downloaded,
|
|
2316
|
-
updated,
|
|
2317
|
-
skipped,
|
|
2318
|
-
failed,
|
|
2319
|
-
};
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
// docuking_pull 구현 (MCP 도구용 래퍼)
|
|
2323
|
-
async function handlePull(args) {
|
|
2324
|
-
try {
|
|
2325
|
-
const result = await handlePullInternal(args);
|
|
2326
|
-
return {
|
|
2327
|
-
content: [
|
|
2328
|
-
{
|
|
2329
|
-
type: 'text',
|
|
2330
|
-
text: result.text,
|
|
2331
|
-
},
|
|
2332
|
-
],
|
|
2333
|
-
};
|
|
2334
|
-
} catch (e) {
|
|
2335
|
-
return {
|
|
2336
|
-
content: [
|
|
2337
|
-
{
|
|
2338
|
-
type: 'text',
|
|
2339
|
-
text: `오류: ${e.message}`,
|
|
2340
|
-
},
|
|
2341
|
-
],
|
|
2342
|
-
};
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
// docuking_list 구현
|
|
2347
|
-
async function handleList(args) {
|
|
2348
|
-
const localPath = args.localPath || process.cwd();
|
|
2349
|
-
|
|
2350
|
-
// 로컬 config에서 API 키 읽기
|
|
2351
|
-
const apiKey = getApiKey(localPath);
|
|
2352
|
-
if (!apiKey) {
|
|
2353
|
-
return {
|
|
2354
|
-
content: [
|
|
2355
|
-
{
|
|
2356
|
-
type: 'text',
|
|
2357
|
-
text: `오류: API 키를 찾을 수 없습니다.
|
|
2358
|
-
먼저 docuking_init을 실행하세요.`,
|
|
2359
|
-
},
|
|
2360
|
-
],
|
|
2361
|
-
};
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
// 프로젝트 정보 조회 (로컬 config에서)
|
|
2365
|
-
const projectInfo = getProjectInfo(localPath);
|
|
2366
|
-
if (projectInfo.error) {
|
|
2367
|
-
return {
|
|
2368
|
-
content: [
|
|
2369
|
-
{
|
|
2370
|
-
type: 'text',
|
|
2371
|
-
text: projectInfo.error,
|
|
2372
|
-
},
|
|
2373
|
-
],
|
|
2374
|
-
};
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
const projectId = projectInfo.projectId;
|
|
2378
|
-
|
|
2379
|
-
try {
|
|
2380
|
-
const response = await fetch(
|
|
2381
|
-
`${API_ENDPOINT}/files/tree?projectId=${projectId}`,
|
|
2382
|
-
{
|
|
2383
|
-
headers: {
|
|
2384
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
2385
|
-
},
|
|
2386
|
-
}
|
|
2387
|
-
);
|
|
2388
|
-
|
|
2389
|
-
if (!response.ok) {
|
|
2390
|
-
throw new Error(await response.text());
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
const data = await response.json();
|
|
2394
|
-
const files = flattenTree(data.tree || []);
|
|
2395
|
-
|
|
2396
|
-
if (files.length === 0) {
|
|
2397
|
-
return {
|
|
2398
|
-
content: [
|
|
2399
|
-
{
|
|
2400
|
-
type: 'text',
|
|
2401
|
-
text: '서버에 저장된 파일이 없습니다.',
|
|
2402
|
-
},
|
|
2403
|
-
],
|
|
2404
|
-
};
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
const fileList = files.map(f => ` ${f.path}`).join('\n');
|
|
2408
|
-
return {
|
|
2409
|
-
content: [
|
|
2410
|
-
{
|
|
2411
|
-
type: 'text',
|
|
2412
|
-
text: `서버 파일 목록:\n\n${fileList}`,
|
|
2413
|
-
},
|
|
2414
|
-
],
|
|
2415
|
-
};
|
|
2416
|
-
} catch (e) {
|
|
2417
|
-
return {
|
|
2418
|
-
content: [
|
|
2419
|
-
{
|
|
2420
|
-
type: 'text',
|
|
2421
|
-
text: `오류: ${e.message}`,
|
|
2422
|
-
},
|
|
2423
|
-
],
|
|
2424
|
-
};
|
|
2425
|
-
}
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
// 파일 타입 정의
|
|
2429
|
-
const FILE_TYPES = {
|
|
2430
|
-
// 텍스트 파일 (UTF-8)
|
|
2431
|
-
TEXT: [
|
|
2432
|
-
'.md', '.txt', '.json', '.xml', '.yaml', '.yml',
|
|
2433
|
-
'.html', '.htm', '.css', '.js', '.ts', '.jsx', '.tsx',
|
|
2434
|
-
'.py', '.java', '.go', '.rs', '.rb', '.php', '.c', '.cpp', '.h',
|
|
2435
|
-
'.csv', '.svg', '.sql', '.sh', '.ps1', '.bat', '.cmd',
|
|
2436
|
-
'.env', '.gitignore', '.dockerignore', '.editorconfig',
|
|
2437
|
-
'.properties', '.ini', '.toml', '.conf', '.cfg',
|
|
2438
|
-
],
|
|
2439
|
-
|
|
2440
|
-
// 바이너리 파일 (Base64)
|
|
2441
|
-
BINARY: [
|
|
2442
|
-
// 이미지
|
|
2443
|
-
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.tiff',
|
|
2444
|
-
// 오피스
|
|
2445
|
-
'.xlsx', '.xls', '.docx', '.doc', '.pptx', '.ppt',
|
|
2446
|
-
// PDF
|
|
2447
|
-
'.pdf',
|
|
2448
|
-
// 한글
|
|
2449
|
-
'.hwpx', '.hwp',
|
|
2450
|
-
],
|
|
2451
|
-
|
|
2452
|
-
// 제외 파일 (업로드 거부) - 실행파일/시스템 이미지만
|
|
2453
|
-
// 영상/오디오는 150MB 이하면 허용 (유튜브 쇼츠 등)
|
|
2454
|
-
EXCLUDED: [
|
|
2455
|
-
// 압축/아카이브 (설치파일 포함 가능성 높음)
|
|
2456
|
-
'.zip', '.tar', '.gz', '.tgz', '.7z', '.rar', '.tar.Z',
|
|
2457
|
-
// Java 아카이브
|
|
2458
|
-
'.jar', '.war', '.ear', '.class',
|
|
2459
|
-
// 실행파일
|
|
2460
|
-
'.exe', '.msi', '.dll', '.so', '.dylib', '.com', '.app', '.pkg', '.deb', '.rpm',
|
|
2461
|
-
// macOS 스크립트
|
|
2462
|
-
'.scpt',
|
|
2463
|
-
// 디스크 이미지
|
|
2464
|
-
'.iso', '.dmg', '.img', '.vhd', '.vmdk',
|
|
2465
|
-
],
|
|
2466
|
-
};
|
|
2467
|
-
|
|
2468
|
-
function getFileType(fileName) {
|
|
2469
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
2470
|
-
|
|
2471
|
-
// .tar.gz, .tar.Z 등 복합 확장자 처리
|
|
2472
|
-
if (fileName.endsWith('.tar.gz') || fileName.endsWith('.tar.Z')) {
|
|
2473
|
-
return 'binary';
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
if (FILE_TYPES.EXCLUDED.includes(ext)) {
|
|
2477
|
-
return 'excluded';
|
|
2478
|
-
}
|
|
2479
|
-
if (FILE_TYPES.BINARY.includes(ext)) {
|
|
2480
|
-
return 'binary';
|
|
2481
|
-
}
|
|
2482
|
-
if (FILE_TYPES.TEXT.includes(ext)) {
|
|
2483
|
-
return 'text';
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
// 알 수 없는 확장자는 바이너리로 처리 (안전)
|
|
2487
|
-
return 'binary';
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
// 유틸: yy_All_Docu 폴더 존재 확인
|
|
2491
|
-
function hasAllDocuFolder(projectPath) {
|
|
2492
|
-
const allDocuPath = path.join(projectPath, 'yy_All_Docu');
|
|
2493
|
-
return fs.existsSync(allDocuPath);
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
// 유틸: 빈 폴더 수집 (재귀)
|
|
2497
|
-
function collectEmptyFolders(basePath, relativePath, results) {
|
|
2498
|
-
const fullPath = path.join(basePath, relativePath);
|
|
2499
|
-
if (!fs.existsSync(fullPath)) return;
|
|
2500
|
-
|
|
2501
|
-
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
2502
|
-
|
|
2503
|
-
// 현재 폴더가 비어있으면 (파일 없고, 하위 폴더도 없음)
|
|
2504
|
-
const hasFiles = entries.some(e => e.isFile());
|
|
2505
|
-
const subDirs = entries.filter(e => e.isDirectory());
|
|
2506
|
-
|
|
2507
|
-
if (!hasFiles && subDirs.length === 0) {
|
|
2508
|
-
// 완전히 빈 폴더
|
|
2509
|
-
results.push(relativePath);
|
|
2510
|
-
} else {
|
|
2511
|
-
// 하위 폴더 탐색
|
|
2512
|
-
for (const entry of subDirs) {
|
|
2513
|
-
const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
2514
|
-
collectEmptyFolders(basePath, entryRelPath, results);
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
// 유틸: 디렉토리 재귀 탐색 (zz_* 제외 없음, Push용)
|
|
2520
|
-
// excludedFiles: 제외된 파일 목록을 수집할 배열 (선택)
|
|
2521
|
-
// largeFiles: 대용량 파일 목록을 수집할 배열 (선택)
|
|
2522
|
-
function collectFilesSimple(basePath, relativePath, results, excludedFiles = null, largeFiles = null) {
|
|
2523
|
-
const fullPath = path.join(basePath, relativePath);
|
|
2524
|
-
if (!fs.existsSync(fullPath)) return;
|
|
2525
|
-
|
|
2526
|
-
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
2527
|
-
|
|
2528
|
-
for (const entry of entries) {
|
|
2529
|
-
const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
2530
|
-
|
|
2531
|
-
if (entry.isDirectory()) {
|
|
2532
|
-
collectFilesSimple(basePath, entryRelPath, results, excludedFiles, largeFiles);
|
|
2533
|
-
} else if (entry.isFile()) {
|
|
2534
|
-
const fileType = getFileType(entry.name);
|
|
2535
|
-
|
|
2536
|
-
// 제외 파일은 건너뜀
|
|
2537
|
-
if (fileType === 'excluded') {
|
|
2538
|
-
console.error(`[DocuKing] 제외됨: ${entryRelPath} (지원하지 않는 파일 형식)`);
|
|
2539
|
-
if (excludedFiles) {
|
|
2540
|
-
excludedFiles.push(entryRelPath);
|
|
2541
|
-
}
|
|
2542
|
-
continue;
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
// 파일 크기 확인
|
|
2546
|
-
const fileFullPath = path.join(fullPath, entry.name);
|
|
2547
|
-
const stats = fs.statSync(fileFullPath);
|
|
2548
|
-
const fileSizeMB = stats.size / (1024 * 1024);
|
|
2549
|
-
|
|
2550
|
-
// 대용량 파일은 별도 추적
|
|
2551
|
-
if (stats.size > MAX_FILE_SIZE_BYTES) {
|
|
2552
|
-
console.error(`[DocuKing] ⚠️ 대용량 파일 제외: ${entryRelPath} (${fileSizeMB.toFixed(1)}MB > ${MAX_FILE_SIZE_MB}MB)`);
|
|
2553
|
-
if (largeFiles) {
|
|
2554
|
-
largeFiles.push({ path: entryRelPath, sizeMB: fileSizeMB.toFixed(1) });
|
|
2555
|
-
}
|
|
2556
|
-
continue;
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
results.push({
|
|
2560
|
-
path: entryRelPath,
|
|
2561
|
-
fullPath: fileFullPath,
|
|
2562
|
-
fileType,
|
|
2563
|
-
sizeMB: fileSizeMB,
|
|
2564
|
-
});
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
// 유틸: 디렉토리 재귀 탐색 (기존 호환용, 현재 미사용)
|
|
2570
|
-
// excludedFiles: 제외된 파일 목록을 수집할 배열 (선택)
|
|
2571
|
-
// largeFiles: 대용량 파일 목록을 수집할 배열 (선택)
|
|
2572
|
-
function collectFiles(basePath, relativePath, results, excludedFiles = null, largeFiles = null) {
|
|
2573
|
-
const fullPath = path.join(basePath, relativePath);
|
|
2574
|
-
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
2575
|
-
|
|
2576
|
-
for (const entry of entries) {
|
|
2577
|
-
const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
2578
|
-
|
|
2579
|
-
if (entry.isDirectory()) {
|
|
2580
|
-
collectFiles(basePath, entryRelPath, results, excludedFiles, largeFiles);
|
|
2581
|
-
} else if (entry.isFile()) {
|
|
2582
|
-
const fileType = getFileType(entry.name);
|
|
2583
|
-
|
|
2584
|
-
// 제외 파일은 건너뜀 (excludedFiles 배열이 있으면 수집)
|
|
2585
|
-
if (fileType === 'excluded') {
|
|
2586
|
-
console.error(`[DocuKing] 제외됨: ${entryRelPath} (지원하지 않는 파일 형식)`);
|
|
2587
|
-
if (excludedFiles) {
|
|
2588
|
-
excludedFiles.push(entryRelPath);
|
|
2589
|
-
}
|
|
2590
|
-
continue;
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
|
-
// 파일 크기 확인
|
|
2594
|
-
const fileFullPath = path.join(fullPath, entry.name);
|
|
2595
|
-
const stats = fs.statSync(fileFullPath);
|
|
2596
|
-
const fileSizeMB = stats.size / (1024 * 1024);
|
|
2597
|
-
|
|
2598
|
-
// 대용량 파일은 별도 추적
|
|
2599
|
-
if (stats.size > MAX_FILE_SIZE_BYTES) {
|
|
2600
|
-
console.error(`[DocuKing] ⚠️ 대용량 파일 제외: ${entryRelPath} (${fileSizeMB.toFixed(1)}MB > ${MAX_FILE_SIZE_MB}MB)`);
|
|
2601
|
-
if (largeFiles) {
|
|
2602
|
-
largeFiles.push({ path: entryRelPath, sizeMB: fileSizeMB.toFixed(1) });
|
|
2603
|
-
}
|
|
2604
|
-
continue;
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
results.push({
|
|
2608
|
-
path: entryRelPath,
|
|
2609
|
-
fullPath: fileFullPath,
|
|
2610
|
-
fileType, // 'text' 또는 'binary'
|
|
2611
|
-
sizeMB: fileSizeMB, // 진행률 표시용
|
|
2612
|
-
});
|
|
2613
|
-
}
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
// 유틸: 트리 구조를 평탄화
|
|
2618
|
-
function flattenTree(tree, prefix = '') {
|
|
2619
|
-
const results = [];
|
|
2620
|
-
|
|
2621
|
-
for (const item of tree) {
|
|
2622
|
-
const itemPath = prefix ? `${prefix}/${item.name}` : item.name;
|
|
2623
|
-
|
|
2624
|
-
if (item.type === 'file') {
|
|
2625
|
-
results.push({ path: itemPath, name: item.name });
|
|
2626
|
-
} else if (item.children) {
|
|
2627
|
-
results.push(...flattenTree(item.children, itemPath));
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
return results;
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
// docuking_status 구현
|
|
2635
|
-
async function handleStatus(args) {
|
|
2636
|
-
const localPath = args.localPath || process.cwd();
|
|
2637
|
-
|
|
2638
|
-
// 로컬 config에서 API 키 읽기
|
|
2639
|
-
const apiKey = getApiKey(localPath);
|
|
2640
|
-
if (!apiKey) {
|
|
2641
|
-
return {
|
|
2642
|
-
content: [
|
|
2643
|
-
{
|
|
2644
|
-
type: 'text',
|
|
2645
|
-
text: `오류: API 키를 찾을 수 없습니다.
|
|
2646
|
-
먼저 docuking_init을 실행하세요.`,
|
|
2647
|
-
},
|
|
2648
|
-
],
|
|
2649
|
-
};
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
// 프로젝트 정보 조회 (로컬 config에서)
|
|
2653
|
-
const projectInfo = getProjectInfo(localPath);
|
|
2654
|
-
if (projectInfo.error) {
|
|
2655
|
-
return {
|
|
2656
|
-
content: [
|
|
2657
|
-
{
|
|
2658
|
-
type: 'text',
|
|
2659
|
-
text: projectInfo.error,
|
|
2660
|
-
},
|
|
2661
|
-
],
|
|
2662
|
-
};
|
|
2663
|
-
}
|
|
2664
|
-
|
|
2665
|
-
const projectId = projectInfo.projectId;
|
|
2666
|
-
const projectName = projectInfo.projectName;
|
|
2667
|
-
|
|
2668
|
-
// Co-worker 권한은 API Key 형식에서 판단
|
|
2669
|
-
const { isCoworker, coworkerFolder } = parseCoworkerFromApiKey(apiKey);
|
|
2670
|
-
const coworkerFolderName = isCoworker ? `yy_Coworker_${coworkerFolder}` : null;
|
|
2671
|
-
|
|
2672
|
-
// 권한 정보 구성
|
|
2673
|
-
let permissionInfo = '';
|
|
2674
|
-
if (isCoworker) {
|
|
2675
|
-
permissionInfo = `\n\n## 현재 권한: 참여자 (Co-worker)
|
|
2676
|
-
- 작업 폴더: ${coworkerFolderName}/
|
|
2677
|
-
- 읽기 권한: 전체 문서 (Pull로 yy_All_Docu/ 폴더의 문서 가져오기 가능)
|
|
2678
|
-
- 쓰기 권한: ${coworkerFolderName}/ 폴더만
|
|
2679
|
-
- 설명: 협업자 폴더에서 작업하면 자동으로 서버에 Push됩니다.`;
|
|
2680
|
-
} else {
|
|
2681
|
-
permissionInfo = `\n\n## 현재 권한: 오너 (Owner)
|
|
2682
|
-
- 읽기 권한: 전체 문서
|
|
2683
|
-
- 쓰기 권한: yy_All_Docu/ 폴더 전체 (제한 없음)
|
|
2684
|
-
- 설명: 프로젝트의 모든 폴더에 Push할 수 있습니다.`;
|
|
2685
|
-
}
|
|
2686
|
-
|
|
2687
|
-
// 서버 파일 목록 조회
|
|
2688
|
-
let serverFiles = [];
|
|
2689
|
-
try {
|
|
2690
|
-
const response = await fetch(
|
|
2691
|
-
`${API_ENDPOINT}/files/tree?projectId=${projectId}`,
|
|
2692
|
-
{
|
|
2693
|
-
headers: {
|
|
2694
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
2695
|
-
},
|
|
2696
|
-
}
|
|
2697
|
-
);
|
|
2698
|
-
if (response.ok) {
|
|
2699
|
-
const data = await response.json();
|
|
2700
|
-
serverFiles = flattenTree(data.tree || []);
|
|
2701
|
-
}
|
|
2702
|
-
} catch (e) {
|
|
2703
|
-
console.error('[DocuKing] 파일 목록 조회 실패:', e.message);
|
|
2704
|
-
}
|
|
2705
|
-
|
|
2706
|
-
// 로컬 파일 목록 조회
|
|
2707
|
-
let localFiles = [];
|
|
2708
|
-
let pushableFiles = [];
|
|
2709
|
-
const mainFolderPath = path.join(localPath, 'yy_All_Docu');
|
|
2710
|
-
|
|
2711
|
-
if (isCoworker) {
|
|
2712
|
-
// 협업자: yy_Coworker_{폴더명}/ 폴더에서 파일 수집
|
|
2713
|
-
const coworkerPath = path.join(localPath, coworkerFolderName);
|
|
2714
|
-
if (fs.existsSync(coworkerPath)) {
|
|
2715
|
-
collectFilesSimple(coworkerPath, '', localFiles);
|
|
2716
|
-
}
|
|
2717
|
-
pushableFiles = localFiles; // 협업자는 자기 폴더의 모든 파일 Push 가능
|
|
2718
|
-
} else {
|
|
2719
|
-
// 오너: yy_All_Docu/ + zz_ai_*/ 폴더에서 파일 수집
|
|
2720
|
-
if (fs.existsSync(mainFolderPath)) {
|
|
2721
|
-
collectFilesSimple(mainFolderPath, '', localFiles);
|
|
2722
|
-
}
|
|
2723
|
-
// zz_ai_* 폴더들도 수집
|
|
2724
|
-
const rootEntries = fs.readdirSync(localPath, { withFileTypes: true });
|
|
2725
|
-
for (const entry of rootEntries) {
|
|
2726
|
-
if (entry.isDirectory() && entry.name.startsWith('zz_ai_')) {
|
|
2727
|
-
const zzPath = path.join(localPath, entry.name);
|
|
2728
|
-
collectFilesSimple(zzPath, '', localFiles);
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
pushableFiles = localFiles; // 오너는 모든 파일 Push 가능
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
const projectNameInfo = projectName ? ` (${projectName})` : '';
|
|
2735
|
-
const workingFolder = isCoworker ? coworkerFolderName : 'yy_All_Docu';
|
|
2736
|
-
const statusText = `DocuKing 동기화 상태
|
|
2737
|
-
|
|
2738
|
-
**프로젝트**: ${projectId}${projectNameInfo}
|
|
2739
|
-
**작업 폴더**: ${workingFolder}/
|
|
2740
|
-
**로컬 파일**: ${localFiles.length}개
|
|
2741
|
-
**서버 파일**: ${serverFiles.length}개
|
|
2742
|
-
**Push 가능한 파일**: ${pushableFiles.length}개${permissionInfo}
|
|
2743
|
-
|
|
2744
|
-
## 사용 가능한 작업
|
|
2745
|
-
- **Push**: docuking_push({ localPath, message: "..." })
|
|
2746
|
-
- **Pull**: docuking_pull({ localPath })
|
|
2747
|
-
- **목록 조회**: docuking_list({ localPath })`;
|
|
2748
|
-
|
|
2749
|
-
return {
|
|
2750
|
-
content: [
|
|
2751
|
-
{
|
|
2752
|
-
type: 'text',
|
|
2753
|
-
text: statusText,
|
|
2754
|
-
},
|
|
2755
|
-
],
|
|
2756
|
-
};
|
|
2757
|
-
}
|
|
2758
|
-
|
|
2759
|
-
// docuking_log 구현
|
|
2760
|
-
async function handleLog(args) {
|
|
2761
|
-
const localPath = args.localPath || process.cwd();
|
|
2762
|
-
const { path: filePath, limit = 20 } = args;
|
|
2763
|
-
|
|
2764
|
-
const projectId = repoMapping[localPath];
|
|
2765
|
-
if (!projectId) {
|
|
2766
|
-
return {
|
|
2767
|
-
content: [
|
|
2768
|
-
{
|
|
2769
|
-
type: 'text',
|
|
2770
|
-
text: `오류: 이 프로젝트는 DocuKing에 연결되지 않았습니다.`,
|
|
2771
|
-
},
|
|
2772
|
-
],
|
|
2773
|
-
};
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
return {
|
|
2777
|
-
content: [
|
|
2778
|
-
{
|
|
2779
|
-
type: 'text',
|
|
2780
|
-
text: `log 도구는 아직 구현 중입니다.\n\n웹 탐색기(https://docuking.ai)에서 커밋 히스토리를 확인할 수 있습니다.`,
|
|
2781
|
-
},
|
|
2782
|
-
],
|
|
2783
|
-
};
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
// docuking_diff 구현
|
|
2787
|
-
async function handleDiff(args) {
|
|
2788
|
-
const localPath = args.localPath || process.cwd();
|
|
2789
|
-
const { path, version } = args;
|
|
2790
|
-
|
|
2791
|
-
const projectId = repoMapping[localPath];
|
|
2792
|
-
if (!projectId) {
|
|
2793
|
-
return {
|
|
2794
|
-
content: [
|
|
2795
|
-
{
|
|
2796
|
-
type: 'text',
|
|
2797
|
-
text: `오류: 이 프로젝트는 DocuKing에 연결되지 않았습니다.`,
|
|
2798
|
-
},
|
|
2799
|
-
],
|
|
2800
|
-
};
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
return {
|
|
2804
|
-
content: [
|
|
2805
|
-
{
|
|
2806
|
-
type: 'text',
|
|
2807
|
-
text: `diff 도구는 아직 구현 중입니다.\n\n웹 탐색기에서 파일 버전 비교를 사용할 수 있습니다.`,
|
|
2808
|
-
},
|
|
2809
|
-
],
|
|
2810
|
-
};
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
// docuking_rollback 구현
|
|
2814
|
-
async function handleRollback(args) {
|
|
2815
|
-
const localPath = args.localPath || process.cwd();
|
|
2816
|
-
const { commitId, path } = args;
|
|
2817
|
-
|
|
2818
|
-
const projectId = repoMapping[localPath];
|
|
2819
|
-
if (!projectId) {
|
|
2820
|
-
return {
|
|
2821
|
-
content: [
|
|
2822
|
-
{
|
|
2823
|
-
type: 'text',
|
|
2824
|
-
text: `오류: 이 프로젝트는 DocuKing에 연결되지 않았습니다.`,
|
|
2825
|
-
},
|
|
2826
|
-
],
|
|
2827
|
-
};
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
return {
|
|
2831
|
-
content: [
|
|
2832
|
-
{
|
|
2833
|
-
type: 'text',
|
|
2834
|
-
text: `rollback 도구는 아직 구현 중입니다.\n\n웹 탐색기에서 파일 롤백을 사용할 수 있습니다.`,
|
|
2835
|
-
},
|
|
2836
|
-
],
|
|
2837
|
-
};
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
// 날짜 기반 파일명 생성 유틸
|
|
2841
|
-
// prefix: 'T' for Talk, 'P' for Plan (optional)
|
|
2842
|
-
function generateDateFileName(title, prefix = '') {
|
|
2843
|
-
const now = new Date();
|
|
2844
|
-
const year = now.getFullYear();
|
|
2845
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
2846
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
2847
|
-
const hour = String(now.getHours()).padStart(2, '0');
|
|
2848
|
-
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
2849
|
-
|
|
2850
|
-
// 제목에서 파일명으로 사용 가능한 slug 생성
|
|
2851
|
-
const slug = title
|
|
2852
|
-
.replace(/[^\w\s가-힣-]/g, '') // 특수문자 제거
|
|
2853
|
-
.replace(/\s+/g, '_') // 공백을 언더스코어로
|
|
2854
|
-
.substring(0, 50); // 50자 제한
|
|
2855
|
-
|
|
2856
|
-
// 접두사가 있으면 "T_" 또는 "P_" 형태로 추가
|
|
2857
|
-
const prefixStr = prefix ? `${prefix}_` : '';
|
|
2858
|
-
|
|
2859
|
-
return {
|
|
2860
|
-
fileName: `${prefixStr}${year}-${month}-${day}_${hour}${minute}__${slug}.md`,
|
|
2861
|
-
yearMonth: `${year}/${month}`,
|
|
2862
|
-
timestamp: `${year}-${month}-${day} ${hour}:${minute}`,
|
|
2863
|
-
};
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
// 계획 ID 생성 (간단한 UUID-like)
|
|
2867
|
-
function generatePlanId() {
|
|
2868
|
-
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
2869
|
-
let id = '';
|
|
2870
|
-
for (let i = 0; i < 8; i++) {
|
|
2871
|
-
id += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
2872
|
-
}
|
|
2873
|
-
return id;
|
|
2874
|
-
}
|
|
2875
|
-
|
|
2876
|
-
// docuking_todo 구현 - 킹투두 (단일 파일 누적)
|
|
2877
|
-
async function handleTodo(args) {
|
|
2878
|
-
const localPath = args.localPath || process.cwd();
|
|
2879
|
-
const { action, todo, todoId } = args;
|
|
2880
|
-
|
|
2881
|
-
// 협업자 여부에 따라 zz_ai 경로 결정
|
|
2882
|
-
// - 오너: localPath/zz_ai_2_Todo/
|
|
2883
|
-
// - 협업자: localPath/yy_Coworker_{폴더명}/zz_ai_2_Todo/
|
|
2884
|
-
const { basePath } = getAiBasePath(localPath);
|
|
2885
|
-
const todoBasePath = path.join(basePath, 'zz_ai_2_Todo');
|
|
2886
|
-
const todoFilePath = path.join(todoBasePath, 'z_King_Todo.md');
|
|
2887
|
-
|
|
2888
|
-
// 폴더 생성
|
|
2889
|
-
if (!fs.existsSync(todoBasePath)) {
|
|
2890
|
-
fs.mkdirSync(todoBasePath, { recursive: true });
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
// 파일이 없으면 헤더와 함께 생성
|
|
2894
|
-
if (!fs.existsSync(todoFilePath)) {
|
|
2895
|
-
const header = `# TODO 목록
|
|
2896
|
-
|
|
2897
|
-
> 날짜 1개 = 등록일 (진행중) / 날짜 2개 = 등록일/완료일 (완료)
|
|
2898
|
-
|
|
2899
|
-
---
|
|
2900
|
-
|
|
2901
|
-
`;
|
|
2902
|
-
fs.writeFileSync(todoFilePath, header, 'utf-8');
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
// 파일 읽기
|
|
2906
|
-
let content = fs.readFileSync(todoFilePath, 'utf-8');
|
|
2907
|
-
|
|
2908
|
-
// 현재 TODO 번호 찾기 (가장 큰 번호)
|
|
2909
|
-
const todoPattern = /^(\d+)\. /gm;
|
|
2910
|
-
let maxId = 0;
|
|
2911
|
-
let match;
|
|
2912
|
-
while ((match = todoPattern.exec(content)) !== null) {
|
|
2913
|
-
const id = parseInt(match[1], 10);
|
|
2914
|
-
if (id > maxId) maxId = id;
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
const now = new Date();
|
|
2918
|
-
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
2919
|
-
const day = String(now.getDate()).padStart(2, '0');
|
|
2920
|
-
const dateStr = `${month}.${day}`;
|
|
2921
|
-
|
|
2922
|
-
if (action === 'add') {
|
|
2923
|
-
if (!todo) {
|
|
2924
|
-
return {
|
|
2925
|
-
content: [{ type: 'text', text: '오류: todo 파라미터가 필요합니다.' }],
|
|
2926
|
-
};
|
|
2927
|
-
}
|
|
2928
|
-
|
|
2929
|
-
const newId = maxId + 1;
|
|
2930
|
-
|
|
2931
|
-
// todo에서 태그와 키워드, 설명 분리
|
|
2932
|
-
// 형식: "[태그] 키워드 - 설명" 또는 "[태그] 키워드"
|
|
2933
|
-
let tag = '';
|
|
2934
|
-
let keyword = '';
|
|
2935
|
-
let description = '';
|
|
2936
|
-
|
|
2937
|
-
const tagMatch = todo.match(/^\[([^\]]+)\]\s*/);
|
|
2938
|
-
if (tagMatch) {
|
|
2939
|
-
tag = tagMatch[1];
|
|
2940
|
-
const rest = todo.slice(tagMatch[0].length);
|
|
2941
|
-
const descSplit = rest.split(/\s*[-–]\s*/);
|
|
2942
|
-
keyword = descSplit[0].trim();
|
|
2943
|
-
description = descSplit.slice(1).join(' - ').trim();
|
|
2944
|
-
} else {
|
|
2945
|
-
// 태그 없이 입력된 경우
|
|
2946
|
-
const descSplit = todo.split(/\s*[-–]\s*/);
|
|
2947
|
-
keyword = descSplit[0].trim();
|
|
2948
|
-
description = descSplit.slice(1).join(' - ').trim();
|
|
2949
|
-
}
|
|
2950
|
-
|
|
2951
|
-
// 킹투두 형식으로 작성
|
|
2952
|
-
let newTodo;
|
|
2953
|
-
if (tag) {
|
|
2954
|
-
newTodo = `${newId}. ⚙️ **[${tag}] ${keyword}** ${dateStr}\n`;
|
|
2955
|
-
} else {
|
|
2956
|
-
newTodo = `${newId}. ⚙️ **${keyword}** ${dateStr}\n`;
|
|
2957
|
-
}
|
|
2958
|
-
|
|
2959
|
-
if (description) {
|
|
2960
|
-
newTodo += ` ${description}\n`;
|
|
2961
|
-
}
|
|
2962
|
-
newTodo += '\n';
|
|
2963
|
-
|
|
2964
|
-
// 파일 끝에 추가
|
|
2965
|
-
fs.appendFileSync(todoFilePath, newTodo, 'utf-8');
|
|
2966
|
-
|
|
2967
|
-
return {
|
|
2968
|
-
content: [{
|
|
2969
|
-
type: 'text',
|
|
2970
|
-
text: `✓ 킹투두에 등록했습니다!
|
|
2971
|
-
|
|
2972
|
-
📝 #${newId}: ${tag ? `[${tag}] ` : ''}${keyword}${description ? ` - ${description}` : ''}
|
|
2973
|
-
📅 등록: ${dateStr}
|
|
2974
|
-
|
|
2975
|
-
💡 완료 시: docuking_todo(action: "done", todoId: ${newId})`,
|
|
2976
|
-
}],
|
|
2977
|
-
};
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
if (action === 'done') {
|
|
2981
|
-
if (!todoId) {
|
|
2982
|
-
return {
|
|
2983
|
-
content: [{ type: 'text', text: '오류: todoId 파라미터가 필요합니다.' }],
|
|
2984
|
-
};
|
|
2985
|
-
}
|
|
2986
|
-
|
|
2987
|
-
// 해당 번호의 TODO 찾아서 ⚙️ -> ✅ 변경 및 날짜 추가
|
|
2988
|
-
const todoLinePattern = new RegExp(`^(${todoId}\\. )⚙️( \\*\\*.*\\*\\* )(\\d+\\.\\d+)(.*)$`, 'm');
|
|
2989
|
-
const matched = content.match(todoLinePattern);
|
|
2990
|
-
|
|
2991
|
-
if (!matched) {
|
|
2992
|
-
// 이미 완료된 항목인지 확인
|
|
2993
|
-
const completedPattern = new RegExp(`^${todoId}\\. ✅`, 'm');
|
|
2994
|
-
if (completedPattern.test(content)) {
|
|
2995
|
-
return {
|
|
2996
|
-
content: [{ type: 'text', text: `킹투두 #${todoId}는 이미 완료 상태입니다.` }],
|
|
2997
|
-
};
|
|
2998
|
-
}
|
|
2999
|
-
return {
|
|
3000
|
-
content: [{ type: 'text', text: `오류: 킹투두 #${todoId}를 찾을 수 없습니다.` }],
|
|
3001
|
-
};
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
// ⚙️ -> ✅ 변경 + 완료 날짜 추가 (등록일/완료일)
|
|
3005
|
-
const updatedContent = content.replace(
|
|
3006
|
-
todoLinePattern,
|
|
3007
|
-
`$1✅$2$3/${dateStr}$4`
|
|
3008
|
-
);
|
|
3009
|
-
|
|
3010
|
-
fs.writeFileSync(todoFilePath, updatedContent, 'utf-8');
|
|
3011
|
-
|
|
3012
|
-
// 완료된 TODO 내용 추출
|
|
3013
|
-
const keywordMatch = matched[2].match(/\*\*(.+)\*\*/);
|
|
3014
|
-
const todoKeyword = keywordMatch ? keywordMatch[1] : '';
|
|
3015
|
-
|
|
3016
|
-
return {
|
|
3017
|
-
content: [{
|
|
3018
|
-
type: 'text',
|
|
3019
|
-
text: `✓ 킹투두 #${todoId} 완료!
|
|
3020
|
-
|
|
3021
|
-
✅ ${todoKeyword}
|
|
3022
|
-
📅 완료: ${dateStr}`,
|
|
3023
|
-
}],
|
|
3024
|
-
};
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
if (action === 'list') {
|
|
3028
|
-
// 미완료(⚙️) TODO 추출
|
|
3029
|
-
const pendingPattern = /^(\d+)\. ⚙️ \*\*(.+)\*\* (\d+\.\d+)/gm;
|
|
3030
|
-
const pendingTodos = [];
|
|
3031
|
-
let listMatch;
|
|
3032
|
-
while ((listMatch = pendingPattern.exec(content)) !== null) {
|
|
3033
|
-
pendingTodos.push({ id: listMatch[1], keyword: listMatch[2], date: listMatch[3] });
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
// 완료된(✅) TODO 수 세기
|
|
3037
|
-
const completedCount = (content.match(/^(\d+)\. ✅/gm) || []).length;
|
|
3038
|
-
|
|
3039
|
-
if (pendingTodos.length === 0) {
|
|
3040
|
-
return {
|
|
3041
|
-
content: [{
|
|
3042
|
-
type: 'text',
|
|
3043
|
-
text: `📋 킹투두 미결: 없음
|
|
3044
|
-
|
|
3045
|
-
✅ 완료: ${completedCount}개
|
|
3046
|
-
📁 전체 기록: zz_ai_2_Todo/z_King_Todo.md`,
|
|
3047
|
-
}],
|
|
3048
|
-
};
|
|
3049
|
-
}
|
|
3050
|
-
|
|
3051
|
-
const listText = pendingTodos.map(t => ` #${t.id}: ${t.keyword} (${t.date})`).join('\n');
|
|
3052
|
-
|
|
3053
|
-
return {
|
|
3054
|
-
content: [{
|
|
3055
|
-
type: 'text',
|
|
3056
|
-
text: `📋 킹투두 미결: ${pendingTodos.length}개
|
|
3057
|
-
|
|
3058
|
-
${listText}
|
|
3059
|
-
|
|
3060
|
-
✅ 완료: ${completedCount}개
|
|
3061
|
-
📁 전체 기록: zz_ai_2_Todo/z_King_Todo.md`,
|
|
3062
|
-
}],
|
|
3063
|
-
};
|
|
3064
|
-
}
|
|
3065
|
-
|
|
3066
|
-
return {
|
|
3067
|
-
content: [{ type: 'text', text: '오류: action은 add, done, list 중 하나여야 합니다.' }],
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
|
-
// docuking_talk 구현 - 대화록 자동 저장
|
|
3072
|
-
async function handleTalk(args) {
|
|
3073
|
-
const localPath = args.localPath || process.cwd();
|
|
3074
|
-
const { title, content, tags = [] } = args;
|
|
3075
|
-
|
|
3076
|
-
// 협업자 여부에 따라 zz_ai 경로 결정
|
|
3077
|
-
// - 오너: localPath/zz_ai_1_Talk/
|
|
3078
|
-
// - 협업자: localPath/yy_Coworker_{폴더명}/zz_ai_1_Talk/
|
|
3079
|
-
const { basePath } = getAiBasePath(localPath);
|
|
3080
|
-
const talkBasePath = path.join(basePath, 'zz_ai_1_Talk');
|
|
3081
|
-
|
|
3082
|
-
// 날짜 기반 파일명 생성 (T_ 접두사)
|
|
3083
|
-
const { fileName, timestamp } = generateDateFileName(title, 'T');
|
|
3084
|
-
const talkFilePath = path.join(talkBasePath, fileName);
|
|
3085
|
-
|
|
3086
|
-
// 폴더 생성
|
|
3087
|
-
if (!fs.existsSync(talkBasePath)) {
|
|
3088
|
-
fs.mkdirSync(talkBasePath, { recursive: true });
|
|
3089
|
-
}
|
|
3090
|
-
|
|
3091
|
-
// 태그 문자열
|
|
3092
|
-
const tagString = tags.length > 0 ? `\n태그: ${tags.map(t => `#${t}`).join(' ')}` : '';
|
|
3093
|
-
|
|
3094
|
-
// 마크다운 문서 생성
|
|
3095
|
-
const document = `# ${title}
|
|
3096
|
-
|
|
3097
|
-
> 기록 시간: ${timestamp}${tagString}
|
|
3098
|
-
|
|
3099
|
-
---
|
|
3100
|
-
|
|
3101
|
-
${content}
|
|
3102
|
-
|
|
3103
|
-
---
|
|
3104
|
-
*이 문서는 AI와의 대화에서 자동 생성되었습니다.*
|
|
3105
|
-
`;
|
|
3106
|
-
|
|
3107
|
-
// 파일 저장
|
|
3108
|
-
fs.writeFileSync(talkFilePath, document, 'utf-8');
|
|
3109
|
-
|
|
3110
|
-
const relativePath = path.relative(localPath, talkFilePath).replace(/\\/g, '/');
|
|
3111
|
-
|
|
3112
|
-
return {
|
|
3113
|
-
content: [
|
|
3114
|
-
{
|
|
3115
|
-
type: 'text',
|
|
3116
|
-
text: `✓ 대화록 저장 완료!
|
|
3117
|
-
|
|
3118
|
-
📝 제목: ${title}
|
|
3119
|
-
📁 경로: ${relativePath}
|
|
3120
|
-
🕐 시간: ${timestamp}${tags.length > 0 ? `\n🏷️ 태그: ${tags.join(', ')}` : ''}
|
|
3121
|
-
|
|
3122
|
-
💡 DocuKing에 Push하면 웹에서도 확인할 수 있습니다.`,
|
|
3123
|
-
},
|
|
3124
|
-
],
|
|
3125
|
-
};
|
|
3126
|
-
}
|
|
3127
|
-
|
|
3128
|
-
// docuking_plan 구현 - 작업 계획 생성/업데이트
|
|
3129
|
-
async function handlePlan(args) {
|
|
3130
|
-
const localPath = args.localPath || process.cwd();
|
|
3131
|
-
const { planId, title, goal, steps = [], notes } = args;
|
|
3132
|
-
|
|
3133
|
-
// 협업자 여부에 따라 zz_ai 경로 결정
|
|
3134
|
-
// - 오너: localPath/zz_ai_3_Plan/
|
|
3135
|
-
// - 협업자: localPath/yy_Coworker_{폴더명}/zz_ai_3_Plan/
|
|
3136
|
-
const { basePath } = getAiBasePath(localPath);
|
|
3137
|
-
const planBasePath = path.join(basePath, 'zz_ai_3_Plan');
|
|
3138
|
-
|
|
3139
|
-
// 기존 계획 업데이트 또는 새 계획 생성
|
|
3140
|
-
let targetPlanId = planId;
|
|
3141
|
-
let isNew = !planId;
|
|
3142
|
-
let planFilePath;
|
|
3143
|
-
let existingContent = null;
|
|
3144
|
-
|
|
3145
|
-
if (planId) {
|
|
3146
|
-
// 기존 계획 찾기
|
|
3147
|
-
const planFiles = findPlanFiles(planBasePath, planId);
|
|
3148
|
-
if (planFiles.length === 0) {
|
|
3149
|
-
return {
|
|
3150
|
-
content: [
|
|
3151
|
-
{
|
|
3152
|
-
type: 'text',
|
|
3153
|
-
text: `오류: planId '${planId}'에 해당하는 계획을 찾을 수 없습니다.`,
|
|
3154
|
-
},
|
|
3155
|
-
],
|
|
3156
|
-
};
|
|
3157
|
-
}
|
|
3158
|
-
planFilePath = planFiles[0];
|
|
3159
|
-
existingContent = fs.readFileSync(planFilePath, 'utf-8');
|
|
3160
|
-
} else {
|
|
3161
|
-
// 새 계획 생성
|
|
3162
|
-
targetPlanId = generatePlanId();
|
|
3163
|
-
const { fileName, timestamp } = generateDateFileName(title, 'P');
|
|
3164
|
-
|
|
3165
|
-
if (!fs.existsSync(planBasePath)) {
|
|
3166
|
-
fs.mkdirSync(planBasePath, { recursive: true });
|
|
3167
|
-
}
|
|
3168
|
-
|
|
3169
|
-
// 파일명에 planId 포함
|
|
3170
|
-
const fileNameWithId = fileName.replace('.md', `__${targetPlanId}.md`);
|
|
3171
|
-
planFilePath = path.join(planBasePath, fileNameWithId);
|
|
3172
|
-
}
|
|
3173
|
-
|
|
3174
|
-
// 현재 시간
|
|
3175
|
-
const now = new Date();
|
|
3176
|
-
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
3177
|
-
|
|
3178
|
-
// 단계 상태 문자열 생성
|
|
3179
|
-
const stepsMarkdown = steps.length > 0
|
|
3180
|
-
? steps.map((step, i) => {
|
|
3181
|
-
const statusIcon = step.status === 'done' ? '✅' : step.status === 'in_progress' ? '🔄' : '⬜';
|
|
3182
|
-
const resultText = step.result ? ` → ${step.result}` : '';
|
|
3183
|
-
return `${i + 1}. ${statusIcon} ${step.name}${resultText}`;
|
|
3184
|
-
}).join('\n')
|
|
3185
|
-
: '(단계 미정의)';
|
|
3186
|
-
|
|
3187
|
-
// 진행률 계산
|
|
3188
|
-
const doneCount = steps.filter(s => s.status === 'done').length;
|
|
3189
|
-
const totalCount = steps.length;
|
|
3190
|
-
const progress = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0;
|
|
3191
|
-
|
|
3192
|
-
// 마크다운 문서 생성
|
|
3193
|
-
let document;
|
|
3194
|
-
if (isNew) {
|
|
3195
|
-
document = `# ${title}
|
|
3196
|
-
|
|
3197
|
-
> Plan ID: \`${targetPlanId}\`
|
|
3198
|
-
> 생성: ${timestamp}
|
|
3199
|
-
> 상태: 진행 중 (${progress}%)
|
|
3200
|
-
|
|
3201
|
-
---
|
|
3202
|
-
|
|
3203
|
-
## 목표
|
|
3204
|
-
${goal || '(목표 미정의)'}
|
|
3205
|
-
|
|
3206
|
-
## 진행 단계
|
|
3207
|
-
${stepsMarkdown}
|
|
3208
|
-
|
|
3209
|
-
## 노트
|
|
3210
|
-
${notes || '(없음)'}
|
|
3211
|
-
|
|
3212
|
-
---
|
|
3213
|
-
|
|
3214
|
-
## 진행 기록
|
|
3215
|
-
- ${timestamp}: 계획 생성
|
|
3216
|
-
|
|
3217
|
-
---
|
|
3218
|
-
*이 문서는 AI 작업 계획에서 자동 생성되었습니다.*
|
|
3219
|
-
`;
|
|
3220
|
-
} else {
|
|
3221
|
-
// 기존 문서 업데이트 (진행 기록에 추가)
|
|
3222
|
-
const updateEntry = `- ${timestamp}: 계획 업데이트 (진행률 ${progress}%)`;
|
|
3223
|
-
|
|
3224
|
-
// 기존 내용에서 섹션 업데이트
|
|
3225
|
-
document = existingContent
|
|
3226
|
-
.replace(/> 상태: .+/, `> 상태: 진행 중 (${progress}%)`)
|
|
3227
|
-
.replace(/## 진행 단계\n[\s\S]*?(?=\n## )/, `## 진행 단계\n${stepsMarkdown}\n\n`)
|
|
3228
|
-
.replace(/## 진행 기록\n/, `## 진행 기록\n${updateEntry}\n`);
|
|
3229
|
-
|
|
3230
|
-
if (notes) {
|
|
3231
|
-
document = document.replace(/## 노트\n[\s\S]*?(?=\n---\n\n## 진행 기록)/, `## 노트\n${notes}\n\n`);
|
|
3232
|
-
}
|
|
3233
|
-
}
|
|
3234
|
-
|
|
3235
|
-
// 파일 저장
|
|
3236
|
-
fs.writeFileSync(planFilePath, document, 'utf-8');
|
|
3237
|
-
|
|
3238
|
-
const relativePath = path.relative(localPath, planFilePath).replace(/\\/g, '/');
|
|
3239
|
-
|
|
3240
|
-
return {
|
|
3241
|
-
content: [
|
|
3242
|
-
{
|
|
3243
|
-
type: 'text',
|
|
3244
|
-
text: `✓ 작업 계획 ${isNew ? '생성' : '업데이트'} 완료!
|
|
3245
|
-
|
|
3246
|
-
📋 제목: ${title}
|
|
3247
|
-
🆔 Plan ID: ${targetPlanId}
|
|
3248
|
-
📁 경로: ${relativePath}
|
|
3249
|
-
📊 진행률: ${progress}% (${doneCount}/${totalCount})
|
|
3250
|
-
|
|
3251
|
-
💡 이 planId를 기억해두세요. 나중에 업데이트하거나 완료 처리할 때 필요합니다.
|
|
3252
|
-
💡 DocuKing에 Push하면 웹에서도 확인할 수 있습니다.`,
|
|
3253
|
-
},
|
|
3254
|
-
],
|
|
3255
|
-
};
|
|
3256
|
-
}
|
|
3257
|
-
|
|
3258
|
-
// 계획 파일 찾기 유틸
|
|
3259
|
-
function findPlanFiles(basePath, planId) {
|
|
3260
|
-
const results = [];
|
|
3261
|
-
|
|
3262
|
-
if (!fs.existsSync(basePath)) {
|
|
3263
|
-
return results;
|
|
3264
|
-
}
|
|
3265
|
-
|
|
3266
|
-
function searchDir(dirPath) {
|
|
3267
|
-
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
3268
|
-
for (const entry of entries) {
|
|
3269
|
-
const fullPath = path.join(dirPath, entry.name);
|
|
3270
|
-
if (entry.isDirectory()) {
|
|
3271
|
-
searchDir(fullPath);
|
|
3272
|
-
} else if (entry.isFile() && entry.name.includes(`__${planId}.md`)) {
|
|
3273
|
-
results.push(fullPath);
|
|
3274
|
-
}
|
|
3275
|
-
}
|
|
3276
|
-
}
|
|
3277
|
-
|
|
3278
|
-
searchDir(basePath);
|
|
3279
|
-
return results;
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
|
-
// docuking_done 구현 - 작업 완료 처리
|
|
3283
|
-
async function handleDone(args) {
|
|
3284
|
-
const localPath = args.localPath || process.cwd();
|
|
3285
|
-
const { planId, summary, artifacts = [] } = args;
|
|
3286
|
-
|
|
3287
|
-
// 협업자 여부에 따라 zz_ai 경로 결정
|
|
3288
|
-
// - 오너: localPath/zz_ai_3_Plan/
|
|
3289
|
-
// - 협업자: localPath/yy_Coworker_{폴더명}/zz_ai_3_Plan/
|
|
3290
|
-
const { basePath } = getAiBasePath(localPath);
|
|
3291
|
-
const planBasePath = path.join(basePath, 'zz_ai_3_Plan');
|
|
3292
|
-
|
|
3293
|
-
// 계획 파일 찾기
|
|
3294
|
-
const planFiles = findPlanFiles(planBasePath, planId);
|
|
3295
|
-
if (planFiles.length === 0) {
|
|
3296
|
-
return {
|
|
3297
|
-
content: [
|
|
3298
|
-
{
|
|
3299
|
-
type: 'text',
|
|
3300
|
-
text: `오류: planId '${planId}'에 해당하는 계획을 찾을 수 없습니다.`,
|
|
3301
|
-
},
|
|
3302
|
-
],
|
|
3303
|
-
};
|
|
3304
|
-
}
|
|
3305
|
-
|
|
3306
|
-
const planFilePath = planFiles[0];
|
|
3307
|
-
let content = fs.readFileSync(planFilePath, 'utf-8');
|
|
3308
|
-
|
|
3309
|
-
// 현재 시간
|
|
3310
|
-
const now = new Date();
|
|
3311
|
-
const timestamp = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
3312
|
-
|
|
3313
|
-
// 산출물 목록
|
|
3314
|
-
const artifactsMarkdown = artifacts.length > 0
|
|
3315
|
-
? artifacts.map(a => `- ${a}`).join('\n')
|
|
3316
|
-
: '(없음)';
|
|
3317
|
-
|
|
3318
|
-
// 완료 섹션 추가
|
|
3319
|
-
const completionSection = `
|
|
3320
|
-
|
|
3321
|
-
## ✅ 완료
|
|
3322
|
-
> 완료 시간: ${timestamp}
|
|
3323
|
-
|
|
3324
|
-
### 요약
|
|
3325
|
-
${summary}
|
|
3326
|
-
|
|
3327
|
-
### 산출물
|
|
3328
|
-
${artifactsMarkdown}
|
|
3329
|
-
`;
|
|
3330
|
-
|
|
3331
|
-
// 상태 업데이트 및 완료 섹션 추가
|
|
3332
|
-
content = content
|
|
3333
|
-
.replace(/> 상태: .+/, `> 상태: ✅ 완료`)
|
|
3334
|
-
.replace(/---\n\*이 문서는 AI 작업 계획에서 자동 생성되었습니다.\*/, `${completionSection}\n---\n*이 문서는 AI 작업 계획에서 자동 생성되었습니다.*`);
|
|
3335
|
-
|
|
3336
|
-
// 진행 기록에 완료 추가
|
|
3337
|
-
content = content.replace(/## 진행 기록\n/, `## 진행 기록\n- ${timestamp}: ✅ 작업 완료\n`);
|
|
3338
|
-
|
|
3339
|
-
// 파일 저장
|
|
3340
|
-
fs.writeFileSync(planFilePath, content, 'utf-8');
|
|
3341
|
-
|
|
3342
|
-
const relativePath = path.relative(localPath, planFilePath).replace(/\\/g, '/');
|
|
3343
|
-
|
|
3344
|
-
return {
|
|
3345
|
-
content: [
|
|
3346
|
-
{
|
|
3347
|
-
type: 'text',
|
|
3348
|
-
text: `✅ 작업 완료 처리됨!
|
|
3349
|
-
|
|
3350
|
-
🆔 Plan ID: ${planId}
|
|
3351
|
-
📁 경로: ${relativePath}
|
|
3352
|
-
🕐 완료 시간: ${timestamp}
|
|
3353
|
-
|
|
3354
|
-
📝 요약: ${summary}
|
|
3355
|
-
${artifacts.length > 0 ? `📦 산출물: ${artifacts.length}개` : ''}
|
|
3356
|
-
|
|
3357
|
-
💡 DocuKing에 Push하면 웹에서 완료된 작업을 확인할 수 있습니다.`,
|
|
3358
|
-
},
|
|
3359
|
-
],
|
|
3360
|
-
};
|
|
3361
|
-
}
|
|
3362
|
-
|
|
3363
753
|
// 서버 시작
|
|
3364
754
|
async function main() {
|
|
3365
755
|
const transport = new StdioServerTransport();
|