Files
game-fe-agent/.claude/skills/dreaming/scripts/dreaming.js
2026-05-21 21:56:04 +09:00

370 lines
13 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* dreaming.js - "AI 코더"에서 "상태 저장형 컨벤션 가디언"으로 (Dreaming의 힘)
*
* 이 스크립트는 프로젝트 루트(CWD)의 코드베이스를 휴리스틱하게 스캔하여,
* 현재 프레임워크 상태, 액티브 Pinia 스토어, 커스텀 Composable, 컴포넌트 구조, 테일윈드 설정 등을 추출합니다.
* 분석된 내용은 .claude/project/dreaming-context.md 파일로 기록되어,
* Claude Code가 프로젝트의 최신 컨벤션과 아키텍처 상태를 항시 보존하고 인지하도록 돕습니다.
*/
const fs = require('fs');
const path = require('path');
const CWD = process.cwd();
const CLAUDE_DIR = path.join(CWD, '.claude');
const PROJECT_DIR = path.join(CLAUDE_DIR, 'project');
const OUTPUT_FILE = path.join(PROJECT_DIR, 'dreaming-context.md');
const CLAUDE_MD = path.join(CWD, 'CLAUDE.md');
// 헬퍼: 디렉토리 존재 여부 확인
function directoryExists(dirPath) {
try {
return fs.statSync(dirPath).isDirectory();
} catch (e) {
return false;
}
}
// 헬퍼: 파일 존재 여부 확인
function fileExists(filePath) {
try {
return fs.statSync(filePath).isFile();
} catch (e) {
return false;
}
}
// 헬퍼: 재귀적으로 파일 목록 가져오기 (옵션 포함)
function getFilesRecursive(dirPath, extFilter = [], ignoreDirs = ['node_modules', '.git', '.nuxt', 'dist']) {
let results = [];
if (!directoryExists(dirPath)) return results;
const list = fs.readdirSync(dirPath);
list.forEach((file) => {
const fullPath = path.join(dirPath, file);
const stat = fs.statSync(fullPath);
if (stat && stat.isDirectory()) {
if (!ignoreDirs.includes(file)) {
results = results.concat(getFilesRecursive(fullPath, extFilter, ignoreDirs));
}
} else {
const ext = path.extname(file);
if (extFilter.length === 0 || extFilter.includes(ext)) {
results.push(fullPath);
}
}
});
return results;
}
// 1. package.json 분석
function analyzePackageJson() {
const packagePath = path.join(CWD, 'package.json');
if (!fileExists(packagePath)) {
return { name: 'Unknown Project', framework: 'Unknown', techStack: [], scripts: [] };
}
try {
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
const techStack = [];
let framework = 'Vue/Nuxt';
if (deps['nuxt'] || deps['nuxt3'] || deps['nuxt-edge']) {
framework = `Nuxt (${deps['nuxt'] || deps['nuxt3'] || 'v3'})`;
techStack.push('Nuxt');
} else if (deps['vue']) {
framework = `Vue (${deps['vue']})`;
techStack.push('Vue');
} else if (deps['next']) {
framework = `Next.js (${deps['next']})`;
techStack.push('Next.js');
} else if (deps['react']) {
framework = `React (${deps['react']})`;
techStack.push('React');
}
if (deps['pinia'] || deps['@pinia/nuxt']) {
techStack.push('Pinia (상태 관리)');
}
if (deps['tailwindcss'] || deps['@nuxtjs/tailwindcss']) {
techStack.push('Tailwind CSS (스타일)');
}
if (deps['typescript']) {
techStack.push('TypeScript');
}
if (deps['vitest'] || deps['@vitest/ui']) {
techStack.push('Vitest (유닛 테스트)');
}
if (deps['eslint']) {
techStack.push('ESLint');
}
if (deps['prettier']) {
techStack.push('Prettier');
}
return {
name: pkg.name || 'Unnamed Project',
version: pkg.version || '1.0.0',
framework,
techStack,
scripts: pkg.scripts ? Object.keys(pkg.scripts) : []
};
} catch (e) {
return { name: 'Parsing Error', framework: 'Unknown', techStack: [], scripts: [], error: e.message };
}
}
// 2. 디렉토리 구조 스캔 및 요약
function scanDirectoryStructure() {
const dirsToScan = ['components', 'composables', 'stores', 'pages', 'server', 'layouts', 'middleware', 'plugins', 'types', 'assets'];
const summary = {};
dirsToScan.forEach((dirName) => {
const dirPath = path.join(CWD, dirName);
if (directoryExists(dirPath)) {
const files = getFilesRecursive(dirPath);
summary[dirName] = {
exists: true,
count: files.length,
examples: files.slice(0, 5).map(f => path.relative(CWD, f))
};
} else {
summary[dirName] = { exists: false, count: 0, examples: [] };
}
});
return summary;
}
// 3. Pinia 스토어 상세 분석
function analyzePiniaStores() {
const storesDir = path.join(CWD, 'stores');
const stores = [];
if (!directoryExists(storesDir)) {
// composables 내에 스토어가 정의되어 있을 수도 있으므로 추가 탐색 가능
return stores;
}
const files = getFilesRecursive(storesDir, ['.ts', '.js']);
files.forEach((file) => {
try {
const content = fs.readFileSync(file, 'utf8');
const filename = path.basename(file);
// defineStore 매칭
const defineStoreMatch = content.match(/defineStore\(\s*['"`]([^'"`]+)['"`]/);
const storeId = defineStoreMatch ? defineStoreMatch[1] : null;
// 상태(state) 필드 휴리스틱 추출
const stateFields = [];
const stateRegex = /const\s+([a-zA-Z0-9_$]+)\s*=\s*(ref|reactive|computed)/g;
let match;
while ((match = stateRegex.exec(content)) !== null) {
stateFields.push(`${match[1]} (${match[2]})`);
}
// 함수(actions) 추출
const actionFields = [];
const actionRegex = /function\s+([a-zA-Z0-9_$]+)/g;
while ((match = actionRegex.exec(content)) !== null) {
if (!match[1].startsWith('use')) {
actionFields.push(match[1]);
}
}
stores.push({
file: path.relative(CWD, file),
id: storeId || filename.replace(path.extname(filename), ''),
state: stateFields,
actions: actionFields
});
} catch (e) {
// ignore
}
});
return stores;
}
// 4. 커스텀 Composable 분석
function analyzeComposables() {
const composablesDir = path.join(CWD, 'composables');
const composables = [];
if (!directoryExists(composablesDir)) return composables;
const files = getFilesRecursive(composablesDir, ['.ts', '.js']);
files.forEach((file) => {
try {
const content = fs.readFileSync(file, 'utf8');
const filename = path.basename(file);
const relativePath = path.relative(CWD, file);
// export const useXxx 함수 매칭
const useFuncRegex = /export\s+const\s+(use[a-zA-Z0-9_$]+)/g;
const useFuncs = [];
let match;
while ((match = useFuncRegex.exec(content)) !== null) {
useFuncs.push(match[1]);
}
const defaultFuncRegex = /export\s+default\s+function\s+(use[a-zA-Z0-9_$]+)/;
const defaultMatch = content.match(defaultFuncRegex);
if (defaultMatch) {
useFuncs.push(defaultMatch[1]);
}
if (useFuncs.length > 0) {
composables.push({
file: relativePath,
functions: useFuncs
});
} else {
// 파일명이 useXxx 형태인 경우 추가
if (filename.startsWith('use')) {
composables.push({
file: relativePath,
functions: [filename.replace(path.extname(filename), '')]
});
}
}
} catch (e) {
// ignore
}
});
return composables;
}
// 5. 테스트 파일 통계
function analyzeTests() {
const testFiles = getFilesRecursive(CWD, ['.spec.ts', '.spec.js', '.test.ts', '.test.js']);
return {
count: testFiles.length,
files: testFiles.slice(0, 10).map(f => path.relative(CWD, f))
};
}
// 메인 실행기
function run() {
console.log('🤖 프로젝트 "Dreaming" 컨텍스트 분석 시작...');
const pkgInfo = analyzePackageJson();
const dirSummary = scanDirectoryStructure();
const stores = analyzePiniaStores();
const composables = analyzeComposables();
const tests = analyzeTests();
const timestamp = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' });
// 마크다운 문서 빌드
let md = `# 🧠 프로젝트 자율 인지 메모리 (Dreaming Context)
이 파일은 \`dreaming.js\` 스크립트에 의해 프로젝트 코드베이스를 분석하여 자동 생성되었습니다.
Claude Code가 프로젝트의 실시간 코드 구조, 사용 중인 스토어, 컴포넌트 레이아웃, 그리고 최신 개발 흐름을 완벽히 인지하도록 돕습니다.
* **최종 동기화 시간:** ${timestamp} (Asia/Seoul)
---
## 🏗 프로젝트 정보
* **프로젝트명:** \`${pkgInfo.name}\` (v${pkgInfo.version || '1.0.0'})
* **핵심 프레임워크:** \`${pkgInfo.framework}\`
* **기술 스택 라이브러리:**
${pkgInfo.techStack.map(tech => ` - ${tech}`).join('\n') || ' - (감지된 주요 라이브러리 없음)'}
---
## 📁 디렉토리 구조 및 컴포넌트 현황
현재 활성화되어 있는 프로젝트 레이아웃 정보입니다.
| 디렉토리 | 활성 여부 | 파일 개수 | 주요 샘플 파일 (최대 5개) |
|---|---|---|---|
${Object.entries(dirSummary).map(([name, info]) => {
return `| \`${name}/\` | ${info.exists ? '✅' : '❌'} | ${info.count}개 | ${info.examples.map(ex => `\`${path.basename(ex)}\``).join(', ') || '-'} |`;
}).join('\n')}
---
## 🍍 액티브 Pinia 스토어 목록
현재 코드베이스에 존재하는 글로벌 상태 저장소들의 템플릿 정보입니다. 새 기능을 개발할 때 아래 스토어를 재사용하거나 참고하세요.
${stores.length === 0 ? '*감지된 Pinia 스토어가 없습니다. (stores/ 디렉토리 없음 혹은 비어있음)*' : stores.map(store => {
return `### 📦 \`${store.id}\`
* **정의 파일:** \`${store.file}\`
* **감지된 상태 (state/computed):** ${store.state.length > 0 ? store.state.map(s => `\`${s}\``).join(', ') : '없음'}
* **감지된 액션 (actions/methods):** ${store.actions.length > 0 ? store.actions.map(a => `\`${a}\``).join(', ') : '없음'}
`;
}).join('\n')}
---
## 🎣 커스텀 Composable (useXxx) 목록
다양한 비즈니스 로직과 부수효과를 격리해 둔 커스텀 훅 목록입니다. 컴포넌트 내부에서 비즈니스 로직을 직접 짜기 전, 아래 훅들의 재사용 가능성을 먼저 타진하세요.
${composables.length === 0 ? '*감지된 커스텀 Composable이 없습니다. (composables/ 디렉토리 없음 혹은 비어있음)*' : composables.map(comp => {
return `- **파일:** \`${comp.file}\`
- **제공 함수:** ${comp.functions.map(f => `\`${f}()\``).join(', ')}
`;
}).join('\n')}
---
## 🧪 유닛 테스트 통계
현재까지 구축된 테스트 커버리지 현황입니다.
* **감지된 테스트 파일 수:** \`${tests.count}\`
${tests.count > 0 ? `* **최근 테스트 목록:**\n${tests.files.map(f => ` - \`${f}\``).join('\n')}` : ' *(새 기능을 추가할 때 반드시 Vitest 규격의 유닛 테스트를 함께 작성해야 함)*'}
---
## 🛠 실행 가능한 스크립트 (package.json)
프로젝트 구동 및 테스트 검증을 위해 사용 가능한 명령어 리스트입니다.
${pkgInfo.scripts.map(s => `- \`npm run ${s}\` (또는 pnpm/yarn/bun)`).join('\n') || '- 스크립트 없음'}
`;
// 디렉토리 및 파일 저장
if (!directoryExists(CLAUDE_DIR)) {
fs.mkdirSync(CLAUDE_DIR);
}
if (!directoryExists(PROJECT_DIR)) {
fs.mkdirSync(PROJECT_DIR);
}
fs.writeFileSync(OUTPUT_FILE, md, 'utf8');
console.log(`✅ Dreaming Context 업데이트 완료! -> ${path.relative(CWD, OUTPUT_FILE)}`);
// CLAUDE.md에 자동 임포트 추가 처리
if (fileExists(CLAUDE_MD)) {
let claudeMdContent = fs.readFileSync(CLAUDE_MD, 'utf8');
const importStr = '@.claude/project/dreaming-context.md';
if (!claudeMdContent.includes(importStr)) {
// '## 프로젝트 지침' 혹은 '## 공통 지침' 섹션 밑에 삽입 시도
const sectionMatch = claudeMdContent.match(/(## 프로젝트 지침\r?\n)/);
if (sectionMatch) {
claudeMdContent = claudeMdContent.replace(
sectionMatch[0],
`${sectionMatch[0]}${importStr}\n`
);
fs.writeFileSync(CLAUDE_MD, claudeMdContent, 'utf8');
console.log(`🔗 CLAUDE.md에 ${importStr} 동적 임포트 구문을 연결했습니다.`);
} else {
// 찾을 수 없다면 파일 상단 혹은 하단에 단순 추가
claudeMdContent = claudeMdContent + `\n\n## 자동 분석 컨텍스트\n${importStr}\n`;
fs.writeFileSync(CLAUDE_MD, claudeMdContent, 'utf8');
console.log(`🔗 CLAUDE.md 끝에 ${importStr} 동적 임포트 구문을 추가했습니다.`);
}
}
} else {
console.log(`⚠️ 프로젝트 루트에 CLAUDE.md 가 존재하지 않습니다. CLAUDE.md를 먼저 생성하고 @.claude/project/dreaming-context.md 임포트 선언을 수동으로 추가하는 것을 권장합니다.`);
}
}
// 스크립트 실행
run();