memento-mcp-server 1.16.0-a → 1.16.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/infrastructure/scheduler/batch-scheduler.d.ts.map +1 -1
- package/dist/infrastructure/scheduler/batch-scheduler.js +6 -0
- package/dist/infrastructure/scheduler/batch-scheduler.js.map +1 -1
- package/dist/services/quality-assurance/quality-metrics-collector.d.ts +3 -1
- package/dist/services/quality-assurance/quality-metrics-collector.d.ts.map +1 -1
- package/dist/services/quality-assurance/quality-metrics-collector.js +286 -15
- package/dist/services/quality-assurance/quality-metrics-collector.js.map +1 -1
- package/dist/shared/utils/database.d.ts +7 -0
- package/dist/shared/utils/database.d.ts.map +1 -1
- package/dist/shared/utils/database.js +24 -0
- package/dist/shared/utils/database.js.map +1 -1
- package/dist/test/helpers/vector-search-quality-metrics.d.ts +1 -1
- package/dist/test/helpers/vector-search-quality-metrics.d.ts.map +1 -1
- package/dist/test/helpers/vector-search-quality-metrics.js +57 -18
- package/dist/test/helpers/vector-search-quality-metrics.js.map +1 -1
- package/package.json +2 -1
- package/scripts/generate-ground-truth.ts +353 -0
- package/scripts/quality-report.ts +33 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Ground Truth 데이터 생성 CLI 스크립트
|
|
4
|
+
*
|
|
5
|
+
* 사용법:
|
|
6
|
+
* npm run quality:ground-truth:generate
|
|
7
|
+
* npm run quality:ground-truth:generate -- --seed 12345
|
|
8
|
+
* npm run quality:ground-truth:generate -- --queries "React,TypeScript,database"
|
|
9
|
+
* npm run quality:ground-truth:generate -- --relevant-count 10
|
|
10
|
+
* npm run quality:ground-truth:generate -- --strategy random
|
|
11
|
+
* npm run quality:ground-truth:generate -- --output custom-path.json
|
|
12
|
+
*
|
|
13
|
+
* 예제:
|
|
14
|
+
* npm run quality:ground-truth:generate
|
|
15
|
+
* npm run quality:ground-truth:generate -- --seed 12345 --queries "React,TypeScript" --relevant-count 5
|
|
16
|
+
* npm run quality:ground-truth:generate -- --strategy pattern --output data/my-ground-truth.json
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync } from 'fs';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import Database from 'better-sqlite3';
|
|
22
|
+
import { initializeDatabase } from '../src/infrastructure/database/database/init.js';
|
|
23
|
+
import { DatabaseUtils } from '../src/shared/utils/database.js';
|
|
24
|
+
import {
|
|
25
|
+
generateGroundTruth,
|
|
26
|
+
saveGroundTruth,
|
|
27
|
+
loadGroundTruth,
|
|
28
|
+
type GroundTruthGenerationOptions,
|
|
29
|
+
type GroundTruth
|
|
30
|
+
} from '../src/test/helpers/vector-search-quality-metrics.js';
|
|
31
|
+
import { HybridSearchFactory } from '../src/domains/search/factories/hybrid-search.factory.js';
|
|
32
|
+
import type { HybridSearchQuery } from '../src/domains/search/algorithms/hybrid-search-engine.js';
|
|
33
|
+
import { getStopWords } from '../src/shared/utils/stopwords.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* CLI 옵션
|
|
37
|
+
*/
|
|
38
|
+
interface CliOptions {
|
|
39
|
+
seed?: number;
|
|
40
|
+
queries?: string;
|
|
41
|
+
relevantCount?: number;
|
|
42
|
+
strategy?: 'random' | 'first' | 'pattern' | 'search';
|
|
43
|
+
output?: string;
|
|
44
|
+
force?: boolean;
|
|
45
|
+
help?: boolean;
|
|
46
|
+
autoQueries?: boolean; // 메모리 내용에서 쿼리 자동 생성
|
|
47
|
+
queryCount?: number; // 자동 생성할 쿼리 수 (기본값: 5)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 명령줄 인자 파싱
|
|
52
|
+
*/
|
|
53
|
+
function parseArgs(): CliOptions {
|
|
54
|
+
const args = process.argv.slice(2);
|
|
55
|
+
const options: CliOptions = {};
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < args.length; i++) {
|
|
58
|
+
const arg = args[i];
|
|
59
|
+
if (arg === '--seed' && args[i + 1]) {
|
|
60
|
+
options.seed = parseInt(args[i + 1], 10);
|
|
61
|
+
i++;
|
|
62
|
+
} else if (arg === '--queries' && args[i + 1]) {
|
|
63
|
+
options.queries = args[i + 1];
|
|
64
|
+
i++;
|
|
65
|
+
} else if (arg === '--relevant-count' && args[i + 1]) {
|
|
66
|
+
options.relevantCount = parseInt(args[i + 1], 10);
|
|
67
|
+
i++;
|
|
68
|
+
} else if (arg === '--strategy' && args[i + 1]) {
|
|
69
|
+
options.strategy = args[i + 1] as 'random' | 'first' | 'pattern';
|
|
70
|
+
i++;
|
|
71
|
+
} else if (arg === '--output' && args[i + 1]) {
|
|
72
|
+
options.output = args[i + 1];
|
|
73
|
+
i++;
|
|
74
|
+
} else if (arg === '--force') {
|
|
75
|
+
options.force = true;
|
|
76
|
+
} else if (arg === '--auto-queries') {
|
|
77
|
+
options.autoQueries = true;
|
|
78
|
+
} else if (arg === '--query-count' && args[i + 1]) {
|
|
79
|
+
options.queryCount = parseInt(args[i + 1], 10);
|
|
80
|
+
i++;
|
|
81
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
82
|
+
options.help = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return options;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 도움말 출력
|
|
91
|
+
*/
|
|
92
|
+
function printHelp(): void {
|
|
93
|
+
console.log(`
|
|
94
|
+
Ground Truth 데이터 생성 CLI
|
|
95
|
+
|
|
96
|
+
사용법:
|
|
97
|
+
npm run quality:ground-truth:generate [options]
|
|
98
|
+
|
|
99
|
+
옵션:
|
|
100
|
+
--seed <number> 시드 값 (재현성을 위해 사용, 기본값: 12345)
|
|
101
|
+
--queries <string> 쿼리 목록 (쉼표로 구분, 기본값: "React,TypeScript,database,MCP,optimization")
|
|
102
|
+
--relevant-count <number> 각 쿼리당 관련 결과 수 (기본값: 5)
|
|
103
|
+
--strategy <strategy> 관련 결과 선택 전략 (random|first|pattern|search, 기본값: random)
|
|
104
|
+
--auto-queries 메모리 내용에서 쿼리 자동 생성 (권장)
|
|
105
|
+
--query-count <number> 자동 생성할 쿼리 수 (기본값: 5, --auto-queries 사용 시)
|
|
106
|
+
--output <file> 출력 파일 경로 (기본값: data/vector-search-quality-ground-truth.json)
|
|
107
|
+
--force 기존 파일이 있어도 덮어쓰기
|
|
108
|
+
--help, -h 도움말 출력
|
|
109
|
+
|
|
110
|
+
전략 설명:
|
|
111
|
+
random: 랜덤 선택 (시드 기반, 재현 가능)
|
|
112
|
+
first: 처음 N개 선택
|
|
113
|
+
pattern: 패턴 기반 선택 (쿼리별로 다른 패턴)
|
|
114
|
+
search: 실제 검색을 수행하여 관련 메모리 찾기 (--auto-queries와 함께 사용 권장)
|
|
115
|
+
|
|
116
|
+
예제:
|
|
117
|
+
npm run quality:ground-truth:generate
|
|
118
|
+
npm run quality:ground-truth:generate -- --auto-queries --strategy search
|
|
119
|
+
npm run quality:ground-truth:generate -- --seed 12345 --queries "React,TypeScript" --relevant-count 5
|
|
120
|
+
npm run quality:ground-truth:generate -- --auto-queries --query-count 10 --strategy search
|
|
121
|
+
npm run quality:ground-truth:generate -- --strategy pattern --output data/my-ground-truth.json
|
|
122
|
+
npm run quality:ground-truth:generate -- --force
|
|
123
|
+
`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 데이터베이스에서 메모리 ID 목록 조회
|
|
128
|
+
*/
|
|
129
|
+
async function getMemoryIds(db: Database.Database, limit: number = 1000): Promise<string[]> {
|
|
130
|
+
const memories = await DatabaseUtils.all(
|
|
131
|
+
db,
|
|
132
|
+
'SELECT id FROM memory_item ORDER BY created_at DESC LIMIT ?',
|
|
133
|
+
[limit]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return memories.map((memory: any) => memory.id);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 메모리 내용에서 키워드 추출
|
|
141
|
+
* 빈도 기반으로 주요 키워드를 추출합니다.
|
|
142
|
+
*/
|
|
143
|
+
function extractKeywordsFromMemories(
|
|
144
|
+
memories: Array<{ content: string }>,
|
|
145
|
+
maxKeywords: number = 10
|
|
146
|
+
): string[] {
|
|
147
|
+
const stopWords = getStopWords();
|
|
148
|
+
const wordFreq = new Map<string, number>();
|
|
149
|
+
|
|
150
|
+
// 각 메모리에서 단어 추출 및 빈도 계산
|
|
151
|
+
for (const memory of memories) {
|
|
152
|
+
const words = memory.content
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/[^\w\s가-힣]/g, ' ') // 특수문자 제거, 한글 유지
|
|
155
|
+
.split(/\s+/)
|
|
156
|
+
.filter(word => {
|
|
157
|
+
// 불용어 제거 및 최소 길이 체크
|
|
158
|
+
return word.length >= 2 &&
|
|
159
|
+
word.length <= 20 &&
|
|
160
|
+
!stopWords.has(word) &&
|
|
161
|
+
!/^\d+$/.test(word); // 숫자만 있는 단어 제외
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
for (const word of words) {
|
|
165
|
+
wordFreq.set(word, (wordFreq.get(word) || 0) + 1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 빈도순으로 정렬하고 상위 키워드 선택
|
|
170
|
+
const sortedKeywords = Array.from(wordFreq.entries())
|
|
171
|
+
.sort((a, b) => b[1] - a[1])
|
|
172
|
+
.slice(0, maxKeywords * 2) // 더 많이 선택하여 필터링
|
|
173
|
+
.map(([word]) => word)
|
|
174
|
+
.filter(word => word.length >= 2); // 최소 길이 재확인
|
|
175
|
+
|
|
176
|
+
// 최종 키워드 선택 (중복 제거 및 길이 제한)
|
|
177
|
+
const uniqueKeywords = Array.from(new Set(sortedKeywords))
|
|
178
|
+
.slice(0, maxKeywords);
|
|
179
|
+
|
|
180
|
+
return uniqueKeywords;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 실제 검색을 수행하여 관련 메모리 찾기
|
|
185
|
+
*/
|
|
186
|
+
async function generateGroundTruthFromSearch(
|
|
187
|
+
db: Database.Database,
|
|
188
|
+
queries: string[],
|
|
189
|
+
relevantCount: number = 5
|
|
190
|
+
): Promise<GroundTruth[]> {
|
|
191
|
+
const groundTruths: GroundTruth[] = [];
|
|
192
|
+
const searchEngine = HybridSearchFactory.createDefaultEngine(db);
|
|
193
|
+
|
|
194
|
+
for (const query of queries) {
|
|
195
|
+
try {
|
|
196
|
+
const searchQuery: HybridSearchQuery = {
|
|
197
|
+
query: query,
|
|
198
|
+
limit: relevantCount * 2 // 더 많은 결과를 가져와서 필터링
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const searchResult = await searchEngine.search(db, searchQuery);
|
|
202
|
+
|
|
203
|
+
// 검색 결과에서 상위 N개를 관련 메모리로 선택
|
|
204
|
+
const relevantIds = searchResult.items
|
|
205
|
+
.slice(0, relevantCount)
|
|
206
|
+
.map(item => item.id);
|
|
207
|
+
|
|
208
|
+
if (relevantIds.length > 0) {
|
|
209
|
+
groundTruths.push({
|
|
210
|
+
queryId: query,
|
|
211
|
+
relevantIds
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.warn(`⚠️ 쿼리 "${query}" 검색 실패:`, error instanceof Error ? error.message : String(error));
|
|
216
|
+
// 검색 실패 시 빈 Ground Truth 추가하지 않음
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return groundTruths;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 메인 함수
|
|
225
|
+
*/
|
|
226
|
+
async function main(): Promise<void> {
|
|
227
|
+
const options = parseArgs();
|
|
228
|
+
|
|
229
|
+
if (options.help) {
|
|
230
|
+
printHelp();
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
// 데이터베이스 초기화
|
|
236
|
+
console.log('🗄️ SQLite 데이터베이스 초기화 중...');
|
|
237
|
+
const db = await initializeDatabase();
|
|
238
|
+
|
|
239
|
+
// 메모리 ID 목록 조회
|
|
240
|
+
console.log('📋 메모리 ID 목록 조회 중...');
|
|
241
|
+
const memoryIds = await getMemoryIds(db);
|
|
242
|
+
|
|
243
|
+
if (memoryIds.length === 0) {
|
|
244
|
+
console.error('❌ 데이터베이스에 메모리가 없습니다. 먼저 메모리를 저장해주세요.');
|
|
245
|
+
db.close();
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(`✅ ${memoryIds.length}개의 메모리 ID를 찾았습니다.`);
|
|
250
|
+
|
|
251
|
+
// 출력 파일 경로 결정
|
|
252
|
+
const defaultPath = join(process.cwd(), 'data', 'vector-search-quality-ground-truth.json');
|
|
253
|
+
const outputPath = options.output || defaultPath;
|
|
254
|
+
|
|
255
|
+
// 기존 파일 확인
|
|
256
|
+
if (existsSync(outputPath) && !options.force) {
|
|
257
|
+
console.log(`⚠️ 기존 Ground Truth 파일이 존재합니다: ${outputPath}`);
|
|
258
|
+
console.log(' --force 옵션을 사용하여 덮어쓸 수 있습니다.');
|
|
259
|
+
const loaded = loadGroundTruth(outputPath);
|
|
260
|
+
if (loaded) {
|
|
261
|
+
console.log(` 현재 파일에는 ${loaded.length}개의 Ground Truth가 있습니다.`);
|
|
262
|
+
}
|
|
263
|
+
db.close();
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 쿼리 목록 결정
|
|
268
|
+
let queries: string[] | undefined;
|
|
269
|
+
|
|
270
|
+
if (options.autoQueries) {
|
|
271
|
+
// 메모리 내용에서 키워드 자동 추출
|
|
272
|
+
console.log('🔍 메모리 내용에서 키워드 추출 중...');
|
|
273
|
+
const memories = await DatabaseUtils.all(
|
|
274
|
+
db,
|
|
275
|
+
'SELECT content FROM memory_item ORDER BY created_at DESC LIMIT 100'
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const queryCount = options.queryCount || 5;
|
|
279
|
+
const extractedKeywords = extractKeywordsFromMemories(memories, queryCount);
|
|
280
|
+
queries = extractedKeywords;
|
|
281
|
+
|
|
282
|
+
console.log(`✅ ${queries.length}개의 키워드 추출 완료:`, queries.join(', '));
|
|
283
|
+
} else if (options.queries) {
|
|
284
|
+
// 수동으로 지정된 쿼리 사용
|
|
285
|
+
queries = options.queries.split(',').map(q => q.trim()).filter(q => q.length > 0);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Ground Truth 생성
|
|
289
|
+
let groundTruths: GroundTruth[];
|
|
290
|
+
|
|
291
|
+
if (options.strategy === 'search' && queries && queries.length > 0) {
|
|
292
|
+
// 실제 검색을 수행하여 관련 메모리 찾기
|
|
293
|
+
console.log('🔧 실제 검색을 수행하여 Ground Truth 생성 중...');
|
|
294
|
+
console.log(` 쿼리 수: ${queries.length}`);
|
|
295
|
+
console.log(` 쿼리당 관련 결과 수: ${options.relevantCount || 5}`);
|
|
296
|
+
|
|
297
|
+
groundTruths = await generateGroundTruthFromSearch(
|
|
298
|
+
db,
|
|
299
|
+
queries,
|
|
300
|
+
options.relevantCount || 5
|
|
301
|
+
);
|
|
302
|
+
} else {
|
|
303
|
+
// 기존 방식 (랜덤/패턴 선택)
|
|
304
|
+
const generationOptions: GroundTruthGenerationOptions = {
|
|
305
|
+
seed: options.seed,
|
|
306
|
+
queries: queries,
|
|
307
|
+
relevantCountPerQuery: options.relevantCount,
|
|
308
|
+
selectionStrategy: options.strategy
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
console.log('🔧 Ground Truth 생성 중...');
|
|
312
|
+
console.log(` 시드: ${generationOptions.seed || 12345}`);
|
|
313
|
+
console.log(` 쿼리 수: ${queries?.length || 5}`);
|
|
314
|
+
console.log(` 쿼리당 관련 결과 수: ${generationOptions.relevantCountPerQuery || 5}`);
|
|
315
|
+
console.log(` 선택 전략: ${generationOptions.selectionStrategy || 'random'}`);
|
|
316
|
+
|
|
317
|
+
groundTruths = generateGroundTruth(memoryIds, generationOptions);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Ground Truth 저장
|
|
321
|
+
console.log(`💾 Ground Truth 저장 중: ${outputPath}`);
|
|
322
|
+
saveGroundTruth(groundTruths, outputPath);
|
|
323
|
+
|
|
324
|
+
console.log(`✅ Ground Truth 생성 완료!`);
|
|
325
|
+
console.log(` 생성된 Ground Truth 수: ${groundTruths.length}`);
|
|
326
|
+
console.log(` 저장 위치: ${outputPath}`);
|
|
327
|
+
console.log(`\n📊 생성된 Ground Truth 요약:`);
|
|
328
|
+
groundTruths.forEach((gt, index) => {
|
|
329
|
+
console.log(` ${index + 1}. 쿼리: "${gt.queryId}", 관련 결과: ${gt.relevantIds.length}개`);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
console.log(`\n💡 다음 단계:`);
|
|
333
|
+
console.log(` 1. 품질 리포트 생성: npm run quality:report`);
|
|
334
|
+
console.log(` 2. Ground Truth 확인: cat ${outputPath}`);
|
|
335
|
+
|
|
336
|
+
db.close();
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error('❌ 오류 발생:', error instanceof Error ? error.message : String(error));
|
|
339
|
+
if (error instanceof Error && error.stack) {
|
|
340
|
+
console.error(error.stack);
|
|
341
|
+
}
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 스크립트 직접 실행 시
|
|
347
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
348
|
+
main().catch((error) => {
|
|
349
|
+
console.error('예상치 못한 오류:', error);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
@@ -35,6 +35,7 @@ interface CliOptions {
|
|
|
35
35
|
from?: string;
|
|
36
36
|
to?: string;
|
|
37
37
|
output?: string;
|
|
38
|
+
skipMeasure?: boolean; // 측정 건너뛰기 옵션
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -67,6 +68,8 @@ function parseArgs(): CliOptions {
|
|
|
67
68
|
} else if (arg === '--output' && args[i + 1]) {
|
|
68
69
|
options.output = args[i + 1];
|
|
69
70
|
i++;
|
|
71
|
+
} else if (arg === '--skip-measure') {
|
|
72
|
+
options.skipMeasure = true;
|
|
70
73
|
} else if (arg === '--help' || arg === '-h') {
|
|
71
74
|
printHelp();
|
|
72
75
|
process.exit(0);
|
|
@@ -93,6 +96,7 @@ function printHelp(): void {
|
|
|
93
96
|
--from <iso8601> 시작 시간 (ISO 8601 형식, 예: 2024-01-01T00:00:00Z)
|
|
94
97
|
--to <iso8601> 종료 시간 (ISO 8601 형식, 예: 2024-12-31T23:59:59Z)
|
|
95
98
|
--output <file> 출력 파일 경로 (지정하지 않으면 콘솔에 출력)
|
|
99
|
+
--skip-measure 품질 측정 건너뛰기 (기존 데이터로 리포트만 생성)
|
|
96
100
|
--help, -h 도움말 출력
|
|
97
101
|
|
|
98
102
|
예제:
|
|
@@ -124,6 +128,35 @@ async function main(): Promise<void> {
|
|
|
124
128
|
to: options.to
|
|
125
129
|
};
|
|
126
130
|
|
|
131
|
+
// 품질 측정 수행 (--skip-measure 옵션이 없는 경우)
|
|
132
|
+
if (!options.skipMeasure) {
|
|
133
|
+
console.log('🔍 품질 측정 수행 중...');
|
|
134
|
+
const context = options.context || 'default';
|
|
135
|
+
const namespaces = options.namespace ? [options.namespace] : undefined;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const measurementResult = await qualityService.measureQuality({
|
|
139
|
+
measurement_type: 'manual',
|
|
140
|
+
context,
|
|
141
|
+
namespaces,
|
|
142
|
+
record: true
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log(`✅ 품질 측정 완료`);
|
|
146
|
+
console.log(` 전체 상태: ${measurementResult.overall_status === 'pass' ? '✅ PASS' : measurementResult.overall_status === 'warning' ? '⚠️ WARNING' : '❌ FAIL'}`);
|
|
147
|
+
console.log(` 측정된 네임스페이스: ${measurementResult.namespaces.join(', ') || 'all'}`);
|
|
148
|
+
console.log(` 측정 시간: ${measurementResult.measured_at}`);
|
|
149
|
+
console.log('');
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.warn('⚠️ 품질 측정 중 오류 발생:', error instanceof Error ? error.message : String(error));
|
|
152
|
+
console.warn(' 기존 데이터로 리포트를 생성합니다.');
|
|
153
|
+
console.log('');
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
console.log('⏭️ 품질 측정 건너뛰기 (기존 데이터 사용)');
|
|
157
|
+
console.log('');
|
|
158
|
+
}
|
|
159
|
+
|
|
127
160
|
// 리포트 생성
|
|
128
161
|
console.log('📊 품질 리포트 생성 중...');
|
|
129
162
|
const report = await qualityService.generateReport(reportOptions);
|