fe be 연결점 분석기

#!/usr/bin/env node import fs from 'fs'; import path from 'path'; const XML_ITEM_TAGS = ['select', 'insert', 'update', 'delete']; const JAVA_EXTENSIONS = new Set(['.java']); const VUE_EXTENSIONS = new Set(['.vue', '.js', '.ts', '.jsx', '.tsx']); function printUsage() { console.log([ 'Usage:', ' node vogen/fe2be.connectinfo.js [output-path]', '', 'Output:', ' 화결연결분석out.txt' ].join('\n')); } function readText(filePath) { return fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''); } function assertDirectory(dirPath, label) { if (!dirPath) { throw new Error(`${label} is required.`); } if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) { throw new Error(`${label} is not a directory: ${dirPath}`); } } function walkFiles(rootDir, extensions) { const files = []; const stack = [rootDir]; while (stack.length > 0) { const current = stack.pop(); const entries = fs.readdirSync(current, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { if (!['node_modules', '.git', 'dist', 'build', 'target'].includes(entry.name)) { stack.push(fullPath); } } else if (entry.isFile() && extensions.has(path.extname(entry.name))) { files.push(fullPath); } } } return files.sort((a, b) => a.localeCompare(b)); } function lineNumberAt(text, index) { return text.slice(0, index).split(/\r?\n/).length; } function getAttribute(tagText, name) { const pattern = new RegExp(`\\b${escapeRegExp(name)}\\s*=\\s*("([^"]*)"|'([^']*)')`, 'i'); const match = tagText.match(pattern); return match ? (match[2] || match[3] || '') : ''; } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function stripComments(text) { return text.replace(//g, '').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, ''); } function extractXmlStatements(xmlFiles) { const statements = []; for (const filePath of xmlFiles) { const raw = readText(filePath); const text = stripComments(raw); const mapperTag = text.match(/]*>/i); const namespace = mapperTag ? getAttribute(mapperTag[0], 'namespace') : ''; for (const tagName of XML_ITEM_TAGS) { const pattern = new RegExp(`<${tagName}\\b[^>]*>[\\s\\S]*?<\\/${tagName}>`, 'gi'); let match; while ((match = pattern.exec(text)) !== null) { const openTag = match[0].match(new RegExp(`^<${tagName}\\b[^>]*>`, 'i'))?.[0] || ''; const id = getAttribute(openTag, 'id'); if (!id) { continue; } statements.push({ namespace, id, tagName, filePath, line: lineNumberAt(text, match.index), key: namespace ? `${namespace}.${id}` : id }); } } } return statements; } function findBackendJavaRoot(mapperDir) { const ancestors = []; let current = path.resolve(mapperDir); while (true) { ancestors.push(current); const parent = path.dirname(current); if (parent === current) { break; } current = parent; } for (const ancestor of ancestors) { const javaRoot = path.join(ancestor, 'src', 'main', 'java'); if (fs.existsSync(javaRoot) && fs.statSync(javaRoot).isDirectory()) { return javaRoot; } } const markerIndex = path.resolve(mapperDir).toLowerCase().lastIndexOf(`${path.sep}src${path.sep}main${path.sep}resources`); if (markerIndex >= 0) { const projectRoot = path.resolve(mapperDir).slice(0, markerIndex); const javaRoot = path.join(projectRoot, 'src', 'main', 'java'); if (fs.existsSync(javaRoot)) { return javaRoot; } } return ancestors.find((ancestor) => walkFiles(ancestor, JAVA_EXTENSIONS).length > 0) || path.resolve(mapperDir); } function parseJavaFile(filePath) { const text = readText(filePath); const packageName = text.match(/\bpackage\s+([\w.]+)\s*;/)?.[1] || ''; const classMatch = text.match(/\b(?:class|interface)\s+([A-Za-z_$][\w$]*)\b/); const className = classMatch?.[1] || path.basename(filePath, '.java'); const classPrefix = classMatch ? text.slice(Math.max(0, classMatch.index - 800), classMatch.index) : ''; return { filePath, text, packageName, className, fqn: packageName ? `${packageName}.${className}` : className, classMapping: extractMappingPath(classPrefix), methods: extractJavaMethods(text) }; } function extractJavaMethods(text) { const methods = []; const pattern = /((?:@\w+(?:\s*\([^)]*\))?\s*)*)(?:public|protected|private)?\s*(?:static\s+)?(?:final\s+)?[\w<>\[\], ?]+\s+([A-Za-z_$][\w$]*)\s*\([^;{}]*\)\s*(?:throws[^{]+)?\{/g; let match; while ((match = pattern.exec(text)) !== null) { const openBraceIndex = pattern.lastIndex - 1; const closeBraceIndex = findMatchingBrace(text, openBraceIndex); if (closeBraceIndex < 0) { continue; } methods.push({ annotations: match[1] || '', name: match[2], body: text.slice(openBraceIndex + 1, closeBraceIndex), startLine: lineNumberAt(text, match.index), mapping: extractMappingPath(match[1] || '') }); pattern.lastIndex = closeBraceIndex + 1; } return methods; } function findMatchingBrace(text, openBraceIndex) { let depth = 0; let quote = ''; for (let index = openBraceIndex; index < text.length; index += 1) { const char = text[index]; const prev = text[index - 1]; if (quote) { if (char === quote && prev !== '\\') { quote = ''; } continue; } if (char === '"' || char === "'") { quote = char; continue; } if (char === '{') { depth += 1; } else if (char === '}') { depth -= 1; if (depth === 0) { return index; } } } return -1; } function extractMappingPath(annotationText) { const mappingPattern = /@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*(?:\(([\s\S]*?)\))?/g; const paths = []; let match; while ((match = mappingPattern.exec(annotationText)) !== null) { const args = match[2] || ''; const stringValues = [...args.matchAll(/"([^"]*)"/g)].map((item) => item[1]); paths.push(...stringValues.filter((value) => value.startsWith('/'))); if (stringValues.length === 0 && match[1] !== 'RequestMapping') { paths.push(''); } } return paths[0] || ''; } function combineUrl(basePath, methodPath) { const joined = `/${[basePath, methodPath] .filter((part) => part !== undefined && part !== null && part !== '') .map((part) => String(part).replace(/^\/+|\/+$/g, '')) .filter(Boolean) .join('/')}`; return joined === '/' ? '' : joined.replace(/\/+/g, '/'); } function bodyCallsMethod(body, methodName) { return new RegExp(`\\.${escapeRegExp(methodName)}\\s*\\(`).test(body); } function bodyMentionsStatement(body, statement) { return ( new RegExp(`\\.${escapeRegExp(statement.id)}\\s*\\(`).test(body) || body.includes(statement.key) || body.includes(`${statement.namespace}.${statement.id}`) ); } function findDaoFile(javaFiles, statement) { if (!statement.namespace) { return null; } const simpleName = statement.namespace.split('.').pop(); return javaFiles.find((file) => file.fqn === statement.namespace || file.className === simpleName) || null; } function findServiceHits(javaFiles, statement) { return javaFiles .flatMap((file) => file.methods .filter((method) => bodyMentionsStatement(method.body, statement)) .map((method) => ({ file, method })) ) .filter((hit) => hit.file.className !== statement.namespace.split('.').pop()); } function findControllerHits(javaFiles, serviceHits, statement) { const serviceMethodNames = new Set(serviceHits.map((hit) => hit.method.name)); return javaFiles.flatMap((file) => { const looksLikeController = /Controller\b/.test(file.className) || /@(RestController|Controller)\b/.test(file.text) || file.methods.some((method) => method.mapping); if (!looksLikeController) { return []; } return file.methods .filter((method) => { const callsService = [...serviceMethodNames].some((name) => bodyCallsMethod(method.body, name)); return callsService || bodyMentionsStatement(method.body, statement); }) .map((method) => ({ file, method, url: combineUrl(file.classMapping, method.mapping) })); }); } function findVueHits(vueFiles, url) { if (!url) { return []; } const normalizedUrl = normalizeUrlForMatch(url); if (!normalizedUrl) { return []; } const hits = []; for (const filePath of vueFiles) { const text = readText(filePath); const lines = text.split(/\r?\n/); lines.forEach((line, index) => { if (lineMatchesUrl(line, normalizedUrl)) { hits.push({ filePath, line: index + 1, text: line.trim(), handler: findNearbyClickHandler(lines, index) || findNearestJsFunction(lines, index) }); } }); } return hits; } function lineMatchesUrl(line, normalizedUrl) { const stringValues = [...String(line).matchAll(/["'`]([^"'`]*\/[^"'`]*)["'`]/g)].map((match) => normalizeUrlForMatch(match[1])); return stringValues.some((value) => { return value.includes(normalizedUrl) || normalizedUrl.includes(value) || urlPatternMatches(normalizedUrl, value); }); } function normalizeUrlForMatch(value) { return String(value) .replace(/\$\{[^}]+\}/g, '{}') .replace(/\{[^}]+\}/g, '{}') .replace(/:[A-Za-z_$][\w$]*/g, '{}') .replace(/\/+/g, '/') .trim(); } function urlPatternMatches(patternUrl, actualUrl) { const patternParts = patternUrl.split('/').filter(Boolean); const actualParts = actualUrl.split('/').filter(Boolean); if (patternParts.length > actualParts.length) { return false; } return patternParts.every((part, index) => part === '{}' || part === actualParts[index]); } function findNearbyClickHandler(lines, lineIndex) { const start = Math.max(0, lineIndex - 8); const end = Math.min(lines.length - 1, lineIndex + 8); for (let index = start; index <= end; index += 1) { const match = lines[index].match(/@click(?:\.[\w.]+)?\s*=\s*"([^"]+)"/) || lines[index].match(/\bv-on:click\s*=\s*"([^"]+)"/); if (match) { return `@click="${match[1]}" line ${index + 1}`; } } return ''; } function findNearestJsFunction(lines, lineIndex) { for (let index = lineIndex; index >= Math.max(0, lineIndex - 80); index -= 1) { const match = lines[index].match(/\b(?:async\s+)?([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/) || lines[index].match(/\b([A-Za-z_$][\w$]*)\s*:\s*(?:async\s*)?\([^)]*\)\s*=>/); if (match) { return `${match[1]}() line ${index + 1}`; } } return ''; } function analyze(mapperDir, frontendDir) { const xmlFiles = walkFiles(mapperDir, new Set(['.xml'])); const statements = extractXmlStatements(xmlFiles); const javaRoot = findBackendJavaRoot(mapperDir); const javaFiles = walkFiles(javaRoot, JAVA_EXTENSIONS).map(parseJavaFile); const vueFiles = walkFiles(frontendDir, VUE_EXTENSIONS); const chains = statements.map((statement) => { const daoFile = findDaoFile(javaFiles, statement); const serviceHits = findServiceHits(javaFiles, statement); const controllerHits = findControllerHits(javaFiles, serviceHits, statement); const vueHitsByUrl = controllerHits.map((controllerHit) => ({ controllerHit, vueHits: findVueHits(vueFiles, controllerHit.url) })); return { statement, daoFile, serviceHits, controllerHits, vueHitsByUrl }; }); return { mapperDir, frontendDir, javaRoot, xmlFileCount: xmlFiles.length, javaFileCount: javaFiles.length, vueFileCount: vueFiles.length, chains }; } function formatAnalysis(analysis) { const lines = [ '# 화면-백엔드 연결 분석', '', `XML mapper path: ${analysis.mapperDir}`, `Backend java path: ${analysis.javaRoot}`, `Frontend vue path: ${analysis.frontendDir}`, `XML files: ${analysis.xmlFileCount}`, `Java files: ${analysis.javaFileCount}`, `Vue/JS files: ${analysis.vueFileCount}`, `Mapper statements: ${analysis.chains.length}`, '' ]; analysis.chains.forEach((chain, index) => { const statement = chain.statement; lines.push(`## ${index + 1}. ${statement.key}`); lines.push(`XML: ${statement.filePath}:${statement.line} (${statement.tagName})`); lines.push(`DAO: ${chain.daoFile ? `${chain.daoFile.fqn} (${chain.daoFile.filePath})` : 'NOT FOUND'}`); lines.push(''); lines.push('Service:'); if (chain.serviceHits.length === 0) { lines.push('- NOT FOUND'); } else { for (const hit of chain.serviceHits) { lines.push(`- ${hit.file.className}.${hit.method.name} (${hit.file.filePath}:${hit.method.startLine})`); } } lines.push(''); lines.push('Controller / URL / Vue:'); if (chain.controllerHits.length === 0) { lines.push('- NOT FOUND'); } else { for (const { controllerHit, vueHits } of chain.vueHitsByUrl) { const controller = controllerHit; lines.push(`- ${controller.file.className}.${controller.method.name} (${controller.file.filePath}:${controller.method.startLine})`); lines.push(` URL: ${controller.url || 'NO_MAPPING_PATH_FOUND'}`); if (vueHits.length === 0) { lines.push(' Vue: NOT FOUND'); } else { for (const vueHit of vueHits) { lines.push(` Vue: ${vueHit.filePath}:${vueHit.line}`); if (vueHit.handler) { lines.push(` 화면/버튼: ${vueHit.handler}`); } lines.push(` 호출문: ${vueHit.text}`); } } } } lines.push(''); }); return `${lines.join('\n')}\n`; } function main() { const args = process.argv.slice(2); if (args.includes('-h') || args.includes('--help')) { printUsage(); return; } const mapperDir = path.resolve(process.cwd(), args[0] || ''); const frontendDir = path.resolve(process.cwd(), args[1] || ''); const outputPath = path.resolve(process.cwd(), args[2] || '화결연결분석out.txt'); assertDirectory(mapperDir, 'backend mybatis xml dir'); assertDirectory(frontendDir, 'frontend vue dir'); const analysis = analyze(mapperDir, frontendDir); fs.writeFileSync(outputPath, formatAnalysis(analysis), 'utf8'); console.log(`Generated ${outputPath}`); console.log(`Mapper statements: ${analysis.chains.length}`); console.log(`Backend java path: ${analysis.javaRoot}`); } try { main(); } catch (error) { console.error(error.message); process.exit(1); }

댓글

이 블로그의 인기 게시물

food eff privacy

판다 스픽 , 개인정보 처리방침 , Panda Speak Privacy Term

판다 수학 개인정보 처리방침 , Privacy , Panda Math