companionbot 0.4.2 → 0.5.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/dist/agents/index.js +1 -1
- package/dist/agents/manager.js +30 -0
- package/dist/ai/claude.js +4 -3
- package/dist/cron/parser.js +153 -20
- package/dist/cron/scheduler.js +7 -3
- package/dist/heartbeat/index.js +5 -1
- package/dist/telegram/bot.js +3 -3
- package/dist/telegram/handlers/commands.js +39 -6
- package/dist/telegram/handlers/messages.js +115 -5
- package/dist/telegram/utils/index.js +2 -0
- package/dist/telegram/utils/prompt.js +10 -7
- package/dist/telegram/utils/secrets.js +64 -0
- package/dist/tools/index.js +382 -22
- package/dist/workspace/index.js +3 -1
- package/dist/workspace/load.js +251 -7
- package/package.json +1 -1
- package/templates/AGENTS.md +77 -10
- package/templates/MEMORY.md +58 -4
package/dist/workspace/load.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { getWorkspacePath, getWorkspaceFilePath, getDailyMemoryPath } from "./paths.js";
|
|
4
|
+
// 카테고리별 태그 (텍스트 기반, 이모지 제거)
|
|
5
|
+
const CATEGORY_TAG = {
|
|
6
|
+
preference: "preference",
|
|
7
|
+
project: "project",
|
|
8
|
+
event: "event",
|
|
9
|
+
decision: "decision",
|
|
10
|
+
user_info: "user_info",
|
|
11
|
+
other: "note",
|
|
12
|
+
};
|
|
13
|
+
// 카테고리별 한글 라벨
|
|
14
|
+
const CATEGORY_LABEL = {
|
|
15
|
+
preference: "선호",
|
|
16
|
+
project: "프로젝트",
|
|
17
|
+
event: "이벤트",
|
|
18
|
+
decision: "결정",
|
|
19
|
+
user_info: "사용자 정보",
|
|
20
|
+
other: "기타",
|
|
21
|
+
};
|
|
4
22
|
async function readFileOrNull(filePath) {
|
|
5
23
|
try {
|
|
6
24
|
return await fs.readFile(filePath, "utf-8");
|
|
@@ -28,20 +46,127 @@ export async function saveWorkspaceFile(filename, content) {
|
|
|
28
46
|
const filePath = getWorkspaceFilePath(filename);
|
|
29
47
|
await fs.writeFile(filePath, content, "utf-8");
|
|
30
48
|
}
|
|
31
|
-
|
|
49
|
+
/**
|
|
50
|
+
* 텍스트 정규화 (중복 체크용)
|
|
51
|
+
* - 소문자로 변환, 공백/구두점 정리
|
|
52
|
+
*/
|
|
53
|
+
function normalizeForComparison(text) {
|
|
54
|
+
return text
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.replace(/[^\w\s가-힣]/g, "") // 구두점 제거
|
|
57
|
+
.replace(/\s+/g, " ") // 공백 정리
|
|
58
|
+
.trim();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 유사도 체크 (간단한 Jaccard 유사도)
|
|
62
|
+
* @returns 0~1 사이 값 (1이면 동일)
|
|
63
|
+
*/
|
|
64
|
+
function calculateSimilarity(text1, text2) {
|
|
65
|
+
const words1 = new Set(normalizeForComparison(text1).split(" "));
|
|
66
|
+
const words2 = new Set(normalizeForComparison(text2).split(" "));
|
|
67
|
+
const intersection = new Set([...words1].filter(x => words2.has(x)));
|
|
68
|
+
const union = new Set([...words1, ...words2]);
|
|
69
|
+
if (union.size === 0)
|
|
70
|
+
return 0;
|
|
71
|
+
return intersection.size / union.size;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 오늘 메모리에서 중복 체크
|
|
75
|
+
* @param content 새로 저장할 내용
|
|
76
|
+
* @param threshold 유사도 임계값 (기본 0.8 = 80% 이상 유사하면 중복)
|
|
77
|
+
* @returns 중복 여부와 유사한 기존 항목
|
|
78
|
+
*/
|
|
79
|
+
async function checkDuplicate(content, threshold = 0.8) {
|
|
32
80
|
const memoryPath = getDailyMemoryPath();
|
|
33
|
-
|
|
34
|
-
|
|
81
|
+
try {
|
|
82
|
+
const existingContent = await fs.readFile(memoryPath, "utf-8");
|
|
83
|
+
// 기존 엔트리들 파싱 (## 시간 형식)
|
|
84
|
+
const entries = existingContent.split(/^## /gm).filter(e => e.trim());
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
// 첫 줄(타임스탬프) 제외한 내용 추출
|
|
87
|
+
const lines = entry.split("\n");
|
|
88
|
+
const entryContent = lines.slice(1).join("\n").trim();
|
|
89
|
+
const similarity = calculateSimilarity(content, entryContent);
|
|
90
|
+
if (similarity >= threshold) {
|
|
91
|
+
return { isDuplicate: true, existingEntry: entryContent.slice(0, 100) };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// 파일이 없으면 중복 없음
|
|
97
|
+
}
|
|
98
|
+
return { isDuplicate: false };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 개선된 메모리 저장 함수
|
|
102
|
+
* - 중복 체크
|
|
103
|
+
* - 카테고리별 구조화
|
|
104
|
+
* - 개선된 타임스탬프 포맷
|
|
105
|
+
*/
|
|
106
|
+
export async function appendToMemory(content, options) {
|
|
107
|
+
const category = options?.category || "other";
|
|
108
|
+
const skipDuplicateCheck = options?.skipDuplicateCheck || false;
|
|
109
|
+
// 중복 체크
|
|
110
|
+
if (!skipDuplicateCheck) {
|
|
111
|
+
const { isDuplicate, existingEntry } = await checkDuplicate(content);
|
|
112
|
+
if (isDuplicate) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
message: "Similar memory already exists today",
|
|
116
|
+
isDuplicate: true,
|
|
117
|
+
existingEntry,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const memoryPath = getDailyMemoryPath();
|
|
122
|
+
const now = new Date();
|
|
123
|
+
// 개선된 타임스탬프: HH:MM 형식
|
|
124
|
+
const timestamp = now.toLocaleTimeString("ko-KR", {
|
|
125
|
+
hour: "2-digit",
|
|
126
|
+
minute: "2-digit",
|
|
127
|
+
hour12: false
|
|
128
|
+
});
|
|
129
|
+
// 카테고리 태그와 함께 저장 (이모지 제거, 텍스트만)
|
|
130
|
+
const tag = CATEGORY_TAG[category];
|
|
131
|
+
const entry = `\n## ${timestamp} [${tag}]\n${content}\n`;
|
|
35
132
|
try {
|
|
36
133
|
await fs.appendFile(memoryPath, entry, "utf-8");
|
|
134
|
+
return { success: true, message: "Memory saved" };
|
|
37
135
|
}
|
|
38
136
|
catch {
|
|
39
137
|
// 파일이 없으면 헤더와 함께 생성
|
|
40
|
-
const
|
|
41
|
-
|
|
138
|
+
const dateStr = now.toLocaleDateString("ko-KR", {
|
|
139
|
+
year: "numeric",
|
|
140
|
+
month: "2-digit",
|
|
141
|
+
day: "2-digit",
|
|
142
|
+
weekday: "short",
|
|
143
|
+
});
|
|
144
|
+
// YYYY-MM-DD (요일) 형식
|
|
145
|
+
const isoDate = now.toISOString().split("T")[0];
|
|
146
|
+
const weekday = now.toLocaleDateString("ko-KR", { weekday: "short" });
|
|
147
|
+
const header = `# ${isoDate} (${weekday})\n`;
|
|
42
148
|
await fs.writeFile(memoryPath, header + entry, "utf-8");
|
|
149
|
+
return { success: true, message: "Memory saved (new daily file created)" };
|
|
43
150
|
}
|
|
44
151
|
}
|
|
152
|
+
// 하위 호환성을 위한 별칭 (기존 코드에서 string만 넘기는 경우)
|
|
153
|
+
export async function appendToMemoryLegacy(content) {
|
|
154
|
+
await appendToMemory(content);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 날짜 라벨 생성 (오늘, 어제, n일 전)
|
|
158
|
+
*/
|
|
159
|
+
function getDateLabel(daysAgo) {
|
|
160
|
+
if (daysAgo === 0)
|
|
161
|
+
return "오늘";
|
|
162
|
+
if (daysAgo === 1)
|
|
163
|
+
return "어제";
|
|
164
|
+
return `${daysAgo}일 전`;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 최근 일일 메모리를 로드합니다.
|
|
168
|
+
* 날짜별로 라벨이 포함된 형태로 반환합니다.
|
|
169
|
+
*/
|
|
45
170
|
export async function loadRecentMemories(days = 7) {
|
|
46
171
|
const memories = [];
|
|
47
172
|
const today = new Date();
|
|
@@ -51,8 +176,127 @@ export async function loadRecentMemories(days = 7) {
|
|
|
51
176
|
const memoryPath = getDailyMemoryPath(date);
|
|
52
177
|
const content = await readFileOrNull(memoryPath);
|
|
53
178
|
if (content) {
|
|
54
|
-
|
|
179
|
+
const dateStr = date.toISOString().split("T")[0];
|
|
180
|
+
const label = getDateLabel(i);
|
|
181
|
+
memories.push({ date: dateStr, label, content });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (memories.length === 0) {
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
// 날짜별로 명확하게 구분된 형식으로 반환
|
|
188
|
+
return memories
|
|
189
|
+
.map((m) => `### ${m.date} (${m.label})\n\n${m.content}`)
|
|
190
|
+
.join("\n\n---\n\n");
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 가장 최근 일일 메모리만 로드합니다 (오늘, 어제).
|
|
194
|
+
* 컨텍스트 크기가 제한된 경우 사용합니다.
|
|
195
|
+
*/
|
|
196
|
+
export async function loadTodayAndYesterdayMemories() {
|
|
197
|
+
return loadRecentMemories(2);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 일일 메모리 파일 목록 조회
|
|
201
|
+
*/
|
|
202
|
+
export async function listDailyMemoryFiles() {
|
|
203
|
+
const memoryDir = path.join(getWorkspacePath(), "memory");
|
|
204
|
+
const files = [];
|
|
205
|
+
const today = new Date();
|
|
206
|
+
today.setHours(0, 0, 0, 0);
|
|
207
|
+
try {
|
|
208
|
+
const entries = await fs.readdir(memoryDir, { withFileTypes: true });
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
if (entry.isFile() && entry.name.match(/^\d{4}-\d{2}-\d{2}\.md$/)) {
|
|
211
|
+
const filePath = path.join(memoryDir, entry.name);
|
|
212
|
+
const stat = await fs.stat(filePath);
|
|
213
|
+
const dateStr = entry.name.replace(".md", "");
|
|
214
|
+
const fileDate = new Date(dateStr);
|
|
215
|
+
const ageInDays = Math.floor((today.getTime() - fileDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
216
|
+
files.push({
|
|
217
|
+
date: dateStr,
|
|
218
|
+
path: filePath,
|
|
219
|
+
sizeBytes: stat.size,
|
|
220
|
+
ageInDays,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// 날짜순 정렬 (최신 먼저)
|
|
225
|
+
files.sort((a, b) => b.date.localeCompare(a.date));
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// 디렉토리가 없으면 빈 배열 반환
|
|
229
|
+
}
|
|
230
|
+
return files;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 일일 메모리 파일 읽기 (날짜 문자열로)
|
|
234
|
+
*/
|
|
235
|
+
export async function getDailyMemoryContent(dateStr) {
|
|
236
|
+
const memoryDir = path.join(getWorkspacePath(), "memory");
|
|
237
|
+
const filePath = path.join(memoryDir, `${dateStr}.md`);
|
|
238
|
+
return readFileOrNull(filePath);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* MEMORY.md에 내용 추가 (장기 기억)
|
|
242
|
+
*/
|
|
243
|
+
export async function appendToLongTermMemory(content, section) {
|
|
244
|
+
const memoryPath = getWorkspaceFilePath("MEMORY.md");
|
|
245
|
+
const timestamp = new Date().toLocaleDateString("ko-KR");
|
|
246
|
+
const sectionHeader = section ? `### ${section}\n` : "";
|
|
247
|
+
const entry = `\n## ${timestamp}에 정리됨\n${sectionHeader}${content}\n`;
|
|
248
|
+
try {
|
|
249
|
+
const existing = await readFileOrNull(memoryPath);
|
|
250
|
+
if (existing) {
|
|
251
|
+
await fs.appendFile(memoryPath, entry, "utf-8");
|
|
55
252
|
}
|
|
253
|
+
else {
|
|
254
|
+
// 파일이 없으면 헤더와 함께 생성
|
|
255
|
+
const header = `# 장기 기억\n\n중요한 기억들이 여기에 보관됩니다.\n`;
|
|
256
|
+
await fs.writeFile(memoryPath, header + entry, "utf-8");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
throw new Error(`Failed to write to MEMORY.md: ${error}`);
|
|
56
261
|
}
|
|
57
|
-
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 일일 메모리 파일 삭제
|
|
265
|
+
*/
|
|
266
|
+
export async function deleteDailyMemory(dateStr) {
|
|
267
|
+
const memoryDir = path.join(getWorkspacePath(), "memory");
|
|
268
|
+
const filePath = path.join(memoryDir, `${dateStr}.md`);
|
|
269
|
+
try {
|
|
270
|
+
await fs.unlink(filePath);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 여러 일일 메모리 파일 삭제
|
|
279
|
+
*/
|
|
280
|
+
export async function deleteOldDailyMemories(olderThanDays) {
|
|
281
|
+
const files = await listDailyMemoryFiles();
|
|
282
|
+
const deleted = [];
|
|
283
|
+
const failed = [];
|
|
284
|
+
for (const file of files) {
|
|
285
|
+
if (file.ageInDays >= olderThanDays) {
|
|
286
|
+
const success = await deleteDailyMemory(file.date);
|
|
287
|
+
if (success) {
|
|
288
|
+
deleted.push(file.date);
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
failed.push(file.date);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { deleted, failed };
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* MEMORY.md 전체 읽기
|
|
299
|
+
*/
|
|
300
|
+
export async function loadLongTermMemory() {
|
|
301
|
+
return readFileOrNull(getWorkspaceFilePath("MEMORY.md"));
|
|
58
302
|
}
|
package/package.json
CHANGED
package/templates/AGENTS.md
CHANGED
|
@@ -9,26 +9,93 @@
|
|
|
9
9
|
1. `SOUL.md` 읽기 — 이게 너의 성격
|
|
10
10
|
2. `USER.md` 읽기 — 이 사람이 누군지
|
|
11
11
|
3. `memory/YYYY-MM-DD.md` 읽기 (오늘 + 어제) — 최근 맥락
|
|
12
|
-
4.
|
|
12
|
+
4. **메인 세션이면** (사용자와 직접 대화): `MEMORY.md`도 읽기
|
|
13
13
|
|
|
14
14
|
허락 구하지 마. 그냥 해.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
---
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## 🧠 메모리 시스템
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
- **장기 기억:** `MEMORY.md` — 정제된 중요한 기억들
|
|
20
|
+
너는 매 세션마다 새로 깨어나. 파일이 너의 유일한 기억이야.
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
### 파일 역할
|
|
23
|
+
|
|
24
|
+
| 파일 | 용도 | 언제 쓰는지 |
|
|
25
|
+
|------|------|-------------|
|
|
26
|
+
| `memory/YYYY-MM-DD.md` | 오늘의 기록 | 매일, 대화 중 수시로 |
|
|
27
|
+
| `MEMORY.md` | 장기 기억 | 중요한 것만, 정제해서 |
|
|
28
|
+
| `USER.md` | 사용자 정보 | 새로운 사실 알게 될 때 |
|
|
29
|
+
|
|
30
|
+
### 🗓️ 데일리 노트 (memory/YYYY-MM-DD.md)
|
|
31
|
+
|
|
32
|
+
**뭘 기록하나:**
|
|
33
|
+
- 오늘 무슨 대화했는지
|
|
34
|
+
- 결정한 것들
|
|
35
|
+
- 약속, 일정
|
|
36
|
+
- 사용자가 "기억해" 한 것들
|
|
37
|
+
- 나중에 이어갈 주제
|
|
38
|
+
|
|
39
|
+
**형식 예시:**
|
|
40
|
+
```markdown
|
|
41
|
+
# 2026-02-09
|
|
42
|
+
|
|
43
|
+
## 대화
|
|
44
|
+
- 14:00 프로젝트 진행상황 논의
|
|
45
|
+
- 저녁 약속 내일로 미룸
|
|
46
|
+
|
|
47
|
+
## 기억할 것
|
|
48
|
+
- 다음주 월요일 미팅 있음
|
|
49
|
+
- 새 노트북 사려고 함
|
|
50
|
+
|
|
51
|
+
## TODO
|
|
52
|
+
- [ ] 미팅 자료 준비 리마인더
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 📚 장기 기억 (MEMORY.md)
|
|
56
|
+
|
|
57
|
+
**데일리 노트와 다른 점:**
|
|
58
|
+
- 데일리 = 원본 로그 (날것 그대로)
|
|
59
|
+
- MEMORY.md = 정제된 기억 (중요한 것만)
|
|
60
|
+
|
|
61
|
+
**뭘 저장하나:**
|
|
62
|
+
- 사용자 취향, 선호도
|
|
63
|
+
- 반복되는 패턴
|
|
64
|
+
- 중요한 결정과 이유
|
|
65
|
+
- 배운 교훈
|
|
66
|
+
- 진행 중인 장기 프로젝트
|
|
67
|
+
|
|
68
|
+
**뭘 저장하지 말아야 하나:**
|
|
69
|
+
- 비밀번호, API 키
|
|
70
|
+
- 민감한 개인정보
|
|
71
|
+
- 일회성 정보
|
|
72
|
+
|
|
73
|
+
**🔒 보안 규칙:**
|
|
74
|
+
- 메인 세션에서만 MEMORY.md 로드
|
|
75
|
+
- 그룹 채팅이나 공유 환경에선 로드하지 마
|
|
76
|
+
- 개인 맥락이 외부에 노출되면 안 됨
|
|
24
77
|
|
|
25
78
|
### 📝 적어둬 - "기억해둘게"는 안 돼!
|
|
26
79
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
80
|
+
이건 진짜 중요해:
|
|
81
|
+
|
|
82
|
+
- **"기억해둘게"는 거짓말** — 세션 끝나면 사라져
|
|
83
|
+
- **파일만이 진짜 기억** — 적지 않으면 까먹어
|
|
84
|
+
- 누가 "이거 기억해" 하면 → 즉시 파일에 써
|
|
85
|
+
- 중요한 정보 나오면 → 대화 중에 바로 기록
|
|
30
86
|
- 실수하면 → 기록해서 다음에 안 반복하게
|
|
31
|
-
|
|
87
|
+
|
|
88
|
+
**원칙: 파일 > 머릿속** 📝
|
|
89
|
+
|
|
90
|
+
### 🔄 메모리 정리 (하트비트 중)
|
|
91
|
+
|
|
92
|
+
며칠에 한 번씩:
|
|
93
|
+
1. 최근 `memory/YYYY-MM-DD.md` 파일들 훑어보기
|
|
94
|
+
2. 장기적으로 중요한 것 → `MEMORY.md`에 추가
|
|
95
|
+
3. 오래되거나 더 이상 relevant 안 한 것 → 정리
|
|
96
|
+
4. `USER.md`에 새로 알게 된 정보 업데이트
|
|
97
|
+
|
|
98
|
+
일기장(데일리) 검토하고 중요한 건 장기 기억으로 옮기는 거야.
|
|
32
99
|
|
|
33
100
|
## 안전
|
|
34
101
|
|
package/templates/MEMORY.md
CHANGED
|
@@ -1,10 +1,64 @@
|
|
|
1
1
|
# MEMORY.md - 장기 기억
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
<!--
|
|
4
|
+
## 📖 사용 가이드
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
이 파일은 너의 "장기 기억"이야. 매일의 일기(memory/YYYY-MM-DD.md)에서
|
|
7
|
+
중요한 것만 골라서 여기에 정리해.
|
|
8
|
+
|
|
9
|
+
### 언제 업데이트하나?
|
|
10
|
+
- 새로운 중요한 정보를 알게 됐을 때
|
|
11
|
+
- 의미 있는 결정이나 사건이 있었을 때
|
|
12
|
+
- 일일 노트가 쌓여서 정리가 필요할 때
|
|
13
|
+
|
|
14
|
+
### 일일 노트 → 장기 기억 옮기기
|
|
15
|
+
1. 최근 일일 노트들을 훑어봐
|
|
16
|
+
2. "이거 나중에도 기억해야 해" 싶은 것만 골라
|
|
17
|
+
3. 적절한 섹션에 정리해서 추가
|
|
18
|
+
4. 구체적인 날짜는 필요할 때만 기록
|
|
19
|
+
|
|
20
|
+
### 원칙
|
|
21
|
+
- 간결하게: 핵심만, 장황하게 쓰지 마
|
|
22
|
+
- 최신으로: 오래된 정보는 업데이트하거나 삭제
|
|
23
|
+
- 실용적으로: 나중에 쓸모 있는 것만
|
|
24
|
+
|
|
25
|
+
⚠️ **보안**: 비밀번호, API 키, 민감한 정보는 절대 적지 마!
|
|
26
|
+
-->
|
|
7
27
|
|
|
8
28
|
---
|
|
9
29
|
|
|
10
|
-
|
|
30
|
+
## 👤 사용자
|
|
31
|
+
|
|
32
|
+
<!-- 이름, 특징, 선호하는 것, 기억할 점 -->
|
|
33
|
+
|
|
34
|
+
- 이름:
|
|
35
|
+
- 특징:
|
|
36
|
+
- 선호:
|
|
37
|
+
|
|
38
|
+
## 🎯 중요한 결정들
|
|
39
|
+
|
|
40
|
+
<!-- 함께 내린 결정, 왜 그렇게 했는지 -->
|
|
41
|
+
|
|
42
|
+
### YYYY-MM-DD: 제목
|
|
43
|
+
- 결정:
|
|
44
|
+
- 이유:
|
|
45
|
+
|
|
46
|
+
## 🔧 진행 중
|
|
47
|
+
|
|
48
|
+
<!-- 현재 작업 중인 프로젝트나 관심사 -->
|
|
49
|
+
|
|
50
|
+
### 프로젝트명
|
|
51
|
+
- 상태:
|
|
52
|
+
- 메모:
|
|
53
|
+
|
|
54
|
+
## 💡 교훈
|
|
55
|
+
|
|
56
|
+
<!-- 실수에서 배운 것, 앞으로 기억할 점 -->
|
|
57
|
+
|
|
58
|
+
-
|
|
59
|
+
|
|
60
|
+
## 📝 기타
|
|
61
|
+
|
|
62
|
+
<!-- 분류 안 되는 중요한 것들 -->
|
|
63
|
+
|
|
64
|
+
-
|