ARCHITECTURE.md•30.6 kB
# WhaTap MXQL CLI + MCP - 아키텍처 설계
## 🎯 설계 철학
**CLI First, MCP as Extension**
- CLI: 독립적인 도구로 먼저 개발 및 테스트
- MCP: CLI의 core 로직을 재사용하여 Claude Code와 통합
## 📦 프로젝트 구조
```
whatap-mxql-cli/
├── package.json
├── tsconfig.json
├── README.md
├── .gitignore
│
├── bin/
│ ├── whatap.js # CLI 실행 파일 (#!/usr/bin/env node)
│ └── whatap-mcp.js # MCP 서버 실행 파일
│
├── src/
│ ├── core/ # 핵심 비즈니스 로직 (CLI + MCP 공유)
│ │ ├── auth/
│ │ │ ├── AuthManager.ts
│ │ │ └── SessionStore.ts
│ │ ├── api/
│ │ │ ├── WhatapClient.ts
│ │ │ └── MxqlExecutor.ts
│ │ ├── validator/
│ │ │ └── MxqlValidator.ts
│ │ └── types/
│ │ └── index.ts
│ │
│ ├── cli/ # CLI 인터페이스
│ │ ├── index.ts # CLI 메인 진입점
│ │ ├── commands/
│ │ │ ├── login.ts
│ │ │ ├── logout.ts
│ │ │ ├── query.ts
│ │ │ ├── projects.ts
│ │ │ ├── validate.ts
│ │ │ ├── status.ts
│ │ │ └── config.ts
│ │ └── ui/
│ │ ├── formatters.ts # 테이블, JSON, CSV 포맷팅
│ │ ├── prompts.ts # 대화형 입력
│ │ └── spinner.ts # 로딩 표시
│ │
│ └── mcp/
│ ├── server.ts # MCP 서버 메인
│ └── tools/
│ ├── login.ts # MCP Tool: login
│ ├── query.ts # MCP Tool: query
│ ├── projects.ts # MCP Tool: projects
│ └── validate.ts # MCP Tool: validate
│
├── dist/ # 빌드 출력 (TypeScript → JavaScript)
│
└── test/
├── core/
├── cli/
└── mcp/
```
## 🔧 Core 모듈 설계
### 1. AuthManager
**책임**: WhaTap 인증 처리 및 세션 관리
```typescript
// src/core/auth/AuthManager.ts
export interface LoginCredentials {
email: string;
password: string;
serviceUrl?: string;
}
export interface Session {
email: string;
accountId: number;
cookies: {
wa: string;
jsessionid: string;
};
apiToken?: string;
serviceUrl: string;
createdAt: Date;
expiresAt: Date;
}
export class AuthManager {
private session: Session | null = null;
private sessionStore: SessionStore;
constructor(sessionStore: SessionStore) {
this.sessionStore = sessionStore;
}
/**
* WhaTap 로그인 (3단계)
* 1. CSRF 토큰 획득
* 2. 웹 로그인 (쿠키)
* 3. API 토큰 획득
*/
async login(credentials: LoginCredentials): Promise<Session> {
const serviceUrl = credentials.serviceUrl || 'https://service.whatap.io';
// Step 1: CSRF 토큰 획득
const csrf = await this.getCsrfToken(serviceUrl);
// Step 2: 웹 로그인
const { wa, jsessionid } = await this.webLogin(
serviceUrl,
credentials.email,
credentials.password,
csrf
);
// Step 3: API 토큰 획득
const { accountId, apiToken } = await this.getMobileToken(
serviceUrl,
credentials.email,
credentials.password,
{ wa, jsessionid }
);
// 세션 생성 및 저장
this.session = {
email: credentials.email,
accountId,
cookies: { wa, jsessionid },
apiToken,
serviceUrl,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24시간
};
await this.sessionStore.save(this.session);
return this.session;
}
/**
* 저장된 세션 복원
*/
async loadSession(): Promise<Session | null> {
this.session = await this.sessionStore.load();
return this.session;
}
/**
* 로그아웃
*/
async logout(): Promise<void> {
this.session = null;
await this.sessionStore.clear();
}
/**
* 세션 유효성 확인
*/
isAuthenticated(): boolean {
if (!this.session) return false;
return new Date(this.session.expiresAt) > new Date();
}
/**
* 쿠키 헤더 문자열 생성
*/
getCookieHeader(): string {
if (!this.session) throw new Error('Not authenticated');
return `wa=${this.session.cookies.wa}; JSESSIONID=${this.session.cookies.jsessionid}`;
}
getSession(): Session {
if (!this.session) throw new Error('Not authenticated');
return this.session;
}
// Private methods
private async getCsrfToken(serviceUrl: string): Promise<string> {
// GET /account/login?lang=en
// HTML 파싱하여 CSRF 토큰 추출
}
private async webLogin(
serviceUrl: string,
email: string,
password: string,
csrf: string
): Promise<{ wa: string; jsessionid: string }> {
// POST /account/login
// Form data: email, password, _csrf, rememberMe=on
// Response cookies에서 wa, JSESSIONID 추출
}
private async getMobileToken(
serviceUrl: string,
email: string,
password: string,
cookies: { wa: string; jsessionid: string }
): Promise<{ accountId: number; apiToken: string }> {
// POST /mobile/api/login
// With cookies from webLogin
}
}
```
### 2. SessionStore
**책임**: 세션 암호화 저장 및 로드
```typescript
// src/core/auth/SessionStore.ts
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import * as crypto from 'crypto';
export class SessionStore {
private configDir: string;
private sessionFile: string;
private encryptionKey: Buffer;
constructor() {
this.configDir = path.join(os.homedir(), '.whatap-mxql');
this.sessionFile = path.join(this.configDir, 'session.enc');
// 암호화 키 생성/로드
this.encryptionKey = this.getOrCreateEncryptionKey();
}
async save(session: Session): Promise<void> {
await fs.mkdir(this.configDir, { recursive: true });
const encrypted = this.encrypt(JSON.stringify(session));
await fs.writeFile(this.sessionFile, encrypted, 'utf-8');
}
async load(): Promise<Session | null> {
try {
const encrypted = await fs.readFile(this.sessionFile, 'utf-8');
const decrypted = this.decrypt(encrypted);
const session = JSON.parse(decrypted) as Session;
// 만료 확인
if (new Date(session.expiresAt) < new Date()) {
await this.clear();
return null;
}
return session;
} catch (error) {
if ((error as any).code === 'ENOENT') {
return null;
}
throw error;
}
}
async clear(): Promise<void> {
try {
await fs.unlink(this.sessionFile);
} catch (error) {
if ((error as any).code !== 'ENOENT') {
throw error;
}
}
}
private encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return JSON.stringify({
iv: iv.toString('hex'),
encrypted,
authTag: authTag.toString('hex'),
});
}
private decrypt(encrypted: string): string {
const { iv, encrypted: encryptedText, authTag } = JSON.parse(encrypted);
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
this.encryptionKey,
Buffer.from(iv, 'hex')
);
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
private getOrCreateEncryptionKey(): Buffer {
const keyFile = path.join(this.configDir, '.key');
try {
return fs.readFileSync(keyFile);
} catch {
const key = crypto.randomBytes(32);
fs.mkdirSync(this.configDir, { recursive: true });
fs.writeFileSync(keyFile, key);
fs.chmodSync(keyFile, 0o600); // 소유자만 읽기/쓰기
return key;
}
}
}
```
### 3. WhatapClient
**책임**: WhaTap API 호출
```typescript
// src/core/api/WhatapClient.ts
import axios, { AxiosInstance } from 'axios';
import type { AuthManager } from '../auth/AuthManager';
export interface MxqlRequest {
type: 'mxql';
pcode: number;
params: {
mql: string;
stime: number;
etime: number;
limit: number;
};
path: 'text' | 'path';
}
export interface Project {
pcode: number;
name: string;
type: string;
status: string;
}
export class WhatapClient {
private axios: AxiosInstance;
private authManager: AuthManager;
constructor(authManager: AuthManager) {
this.authManager = authManager;
const session = authManager.getSession();
this.axios = axios.create({
baseURL: session.serviceUrl,
timeout: 45000,
headers: {
'User-Agent': 'WhatapMxqlCLI/1.0.0',
},
});
// 쿠키 인터셉터
this.axios.interceptors.request.use((config) => {
config.headers['Cookie'] = this.authManager.getCookieHeader();
return config;
});
}
/**
* MXQL 쿼리 실행
*/
async executeMxql(request: MxqlRequest): Promise<any> {
const response = await this.axios.post('/yard/api/flush', request);
return response.data;
}
/**
* 프로젝트 목록 조회
*/
async getProjects(): Promise<Project[]> {
// TODO: 실제 API 엔드포인트 확인 필요
const response = await this.axios.get('/v2/project/list');
return response.data;
}
/**
* 프로젝트 상세 조회
*/
async getProject(pcode: number): Promise<Project> {
const response = await this.axios.get(`/v2/project/${pcode}`);
return response.data;
}
}
```
### 4. MxqlExecutor
**책임**: MXQL 쿼리 실행 편의 기능
```typescript
// src/core/api/MxqlExecutor.ts
import type { WhatapClient, MxqlRequest } from './WhatapClient';
export interface QueryOptions {
pcode: number;
mql: string;
stime?: number;
etime?: number;
limit?: number;
}
export interface QueryResult {
data: any[];
rowCount: number;
executionTimeMs: number;
query: QueryOptions;
}
export class MxqlExecutor {
constructor(private client: WhatapClient) {}
async execute(options: QueryOptions): Promise<QueryResult> {
const stime = options.stime || Date.now() - 5 * 60 * 1000; // 기본: 5분 전
const etime = options.etime || Date.now();
const limit = options.limit || 10000;
const request: MxqlRequest = {
type: 'mxql',
pcode: options.pcode,
params: {
mql: options.mql,
stime,
etime,
limit,
},
path: 'text',
};
const startTime = Date.now();
const data = await this.client.executeMxql(request);
const executionTimeMs = Date.now() - startTime;
return {
data: Array.isArray(data) ? data : [data],
rowCount: Array.isArray(data) ? data.length : 1,
executionTimeMs,
query: options,
};
}
async executeFromFile(pcode: number, filePath: string): Promise<QueryResult> {
const fs = await import('fs/promises');
const mql = await fs.readFile(filePath, 'utf-8');
return this.execute({ pcode, mql });
}
}
```
---
## 🖥️ CLI 모듈 설계
### CLI 프레임워크: Commander.js
**선택 이유**:
- 간단하고 직관적
- TypeScript 지원 우수
- 널리 사용되는 표준
### CLI 진입점
```typescript
// src/cli/index.ts
import { Command } from 'commander';
import { loginCommand } from './commands/login';
import { logoutCommand } from './commands/logout';
import { queryCommand } from './commands/query';
import { projectsCommand } from './commands/projects';
import { validateCommand } from './commands/validate';
import { statusCommand } from './commands/status';
const program = new Command();
program
.name('whatap')
.description('WhaTap MXQL CLI - Query and manage WhaTap monitoring data')
.version('1.0.0');
// 명령어 등록
program.addCommand(loginCommand);
program.addCommand(logoutCommand);
program.addCommand(queryCommand);
program.addCommand(projectsCommand);
program.addCommand(validateCommand);
program.addCommand(statusCommand);
program.parse();
```
### 명령어: whatap login
```typescript
// src/cli/commands/login.ts
import { Command } from 'commander';
import { input, password } from '@inquirer/prompts';
import ora from 'ora';
import chalk from 'chalk';
import { AuthManager } from '../../core/auth/AuthManager';
import { SessionStore } from '../../core/auth/SessionStore';
export const loginCommand = new Command('login')
.description('Login to WhaTap service')
.option('-e, --email <email>', 'WhaTap account email')
.option('-p, --password <password>', 'WhaTap account password')
.option('-u, --url <url>', 'Service URL', 'https://service.whatap.io')
.action(async (options) => {
try {
// 대화형 입력 (옵션으로 제공되지 않은 경우)
const email = options.email || await input({
message: 'WhaTap Email:',
validate: (value) => value.includes('@') || 'Invalid email format'
});
const pwd = options.password || await password({
message: 'Password:',
mask: '*'
});
// 로그인 진행
const spinner = ora('Logging in to WhaTap...').start();
const sessionStore = new SessionStore();
const authManager = new AuthManager(sessionStore);
const session = await authManager.login({
email,
password: pwd,
serviceUrl: options.url
});
spinner.succeed(chalk.green('Login successful!'));
console.log(chalk.blue('\nSession Information:'));
console.log(` Account ID: ${session.accountId}`);
console.log(` Email: ${session.email}`);
console.log(` Service: ${session.serviceUrl}`);
console.log(` Expires: ${new Date(session.expiresAt).toLocaleString()}`);
} catch (error) {
console.error(chalk.red('Login failed:'), error.message);
process.exit(1);
}
});
```
### 명령어: whatap query
```typescript
// src/cli/commands/query.ts
import { Command } from 'commander';
import { input, editor } from '@inquirer/prompts';
import ora from 'ora';
import chalk from 'chalk';
import Table from 'cli-table3';
import { AuthManager } from '../../core/auth/AuthManager';
import { SessionStore } from '../../core/auth/SessionStore';
import { WhatapClient } from '../../core/api/WhatapClient';
import { MxqlExecutor } from '../../core/api/MxqlExecutor';
import { formatTable, formatJson, formatCsv } from '../ui/formatters';
export const queryCommand = new Command('query')
.description('Execute MXQL query')
.requiredOption('-p, --pcode <number>', 'Project code', parseInt)
.option('-m, --mql <query>', 'MXQL query string')
.option('-f, --file <path>', 'Read MXQL query from file')
.option('-i, --interactive', 'Interactive query mode')
.option('--stime <timestamp>', 'Start time (ms)', parseInt)
.option('--etime <timestamp>', 'End time (ms)', parseInt)
.option('--limit <number>', 'Result limit', parseInt, 10000)
.option('--format <type>', 'Output format (table|json|csv)', 'table')
.action(async (options) => {
try {
// 인증 확인
const sessionStore = new SessionStore();
const authManager = new AuthManager(sessionStore);
await authManager.loadSession();
if (!authManager.isAuthenticated()) {
console.error(chalk.red('Not authenticated. Please run: whatap login'));
process.exit(1);
}
// MXQL 쿼리 가져오기
let mql: string;
if (options.interactive) {
mql = await editor({
message: 'Enter MXQL query:',
default: 'CATEGORY app_counter\nTAGLOAD\nSELECT'
});
} else if (options.file) {
const fs = await import('fs/promises');
mql = await fs.readFile(options.file, 'utf-8');
} else if (options.mql) {
mql = options.mql;
} else {
console.error(chalk.red('Please provide query with --mql, --file, or --interactive'));
process.exit(1);
}
// 쿼리 실행
const spinner = ora('Executing MXQL query...').start();
const client = new WhatapClient(authManager);
const executor = new MxqlExecutor(client);
const result = await executor.execute({
pcode: options.pcode,
mql,
stime: options.stime,
etime: options.etime,
limit: options.limit,
});
spinner.succeed(chalk.green(`Query executed successfully in ${result.executionTimeMs}ms`));
// 결과 출력
console.log(chalk.blue(`\nRows returned: ${result.rowCount}`));
console.log('');
if (options.format === 'json') {
console.log(formatJson(result.data));
} else if (options.format === 'csv') {
console.log(formatCsv(result.data));
} else {
console.log(formatTable(result.data));
}
} catch (error) {
console.error(chalk.red('Query failed:'), error.message);
process.exit(1);
}
});
```
### 명령어: whatap projects
```typescript
// src/cli/commands/projects.ts
import { Command } from 'commander';
import ora from 'ora';
import chalk from 'chalk';
import Table from 'cli-table3';
import { AuthManager } from '../../core/auth/AuthManager';
import { SessionStore } from '../../core/auth/SessionStore';
import { WhatapClient } from '../../core/api/WhatapClient';
export const projectsCommand = new Command('projects')
.description('Manage WhaTap projects')
.addCommand(
new Command('list')
.description('List all accessible projects')
.option('--json', 'Output as JSON')
.action(async (options) => {
try {
// 인증 확인
const sessionStore = new SessionStore();
const authManager = new AuthManager(sessionStore);
await authManager.loadSession();
if (!authManager.isAuthenticated()) {
console.error(chalk.red('Not authenticated. Please run: whatap login'));
process.exit(1);
}
const spinner = ora('Fetching projects...').start();
const client = new WhatapClient(authManager);
const projects = await client.getProjects();
spinner.succeed(chalk.green(`Found ${projects.length} projects`));
if (options.json) {
console.log(JSON.stringify(projects, null, 2));
} else {
const table = new Table({
head: ['PCode', 'Name', 'Type', 'Status'],
colWidths: [12, 40, 15, 12]
});
projects.forEach(p => {
table.push([p.pcode, p.name, p.type, p.status]);
});
console.log(table.toString());
}
} catch (error) {
console.error(chalk.red('Failed to fetch projects:'), error.message);
process.exit(1);
}
})
)
.addCommand(
new Command('show')
.description('Show project details')
.argument('<pcode>', 'Project code')
.action(async (pcode) => {
// 프로젝트 상세 정보 조회
})
);
```
### UI 포맷터
```typescript
// src/cli/ui/formatters.ts
import Table from 'cli-table3';
export function formatTable(data: any[]): string {
if (data.length === 0) {
return 'No data';
}
const keys = Object.keys(data[0]);
const table = new Table({
head: keys,
colWidths: keys.map(() => 20)
});
data.forEach(row => {
table.push(keys.map(key => String(row[key])));
});
return table.toString();
}
export function formatJson(data: any[]): string {
return JSON.stringify(data, null, 2);
}
export function formatCsv(data: any[]): string {
if (data.length === 0) {
return '';
}
const keys = Object.keys(data[0]);
const header = keys.join(',');
const rows = data.map(row =>
keys.map(key => {
const value = String(row[key]);
return value.includes(',') ? `"${value}"` : value;
}).join(',')
);
return [header, ...rows].join('\n');
}
```
---
## 🔌 MCP 모듈 설계
### MCP 서버 메인
```typescript
// src/mcp/server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
// Core 모듈 import (CLI와 공유)
import { AuthManager } from '../core/auth/AuthManager';
import { SessionStore } from '../core/auth/SessionStore';
import { WhatapClient } from '../core/api/WhatapClient';
import { MxqlExecutor } from '../core/api/MxqlExecutor';
// MCP Tools
import { loginTool, handleLogin } from './tools/login';
import { queryTool, handleQuery } from './tools/query';
import { projectsTool, handleProjects } from './tools/projects';
import { validateTool, handleValidate } from './tools/validate';
// MCP 서버 생성
const server = new Server(
{
name: 'whatap-mxql-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// 공유 인스턴스
const sessionStore = new SessionStore();
const authManager = new AuthManager(sessionStore);
let whatapClient: WhatapClient | null = null;
// Tool 목록 제공
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [loginTool, queryTool, projectsTool, validateTool],
};
});
// Tool 실행
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'whatap_login':
const session = await handleLogin(authManager, args);
whatapClient = new WhatapClient(authManager);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
accountId: session.accountId,
email: session.email,
}),
},
],
};
case 'whatap_query':
if (!whatapClient) {
throw new Error('Not authenticated. Please call whatap_login first.');
}
const executor = new MxqlExecutor(whatapClient);
const result = await handleQuery(executor, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result),
},
],
};
case 'whatap_projects':
if (!whatapClient) {
throw new Error('Not authenticated. Please call whatap_login first.');
}
const projects = await handleProjects(whatapClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(projects),
},
],
};
case 'whatap_validate':
const validation = await handleValidate(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(validation),
},
],
};
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
}),
},
],
isError: true,
};
}
});
// 서버 시작
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
// 세션 복원 시도
await authManager.loadSession();
if (authManager.isAuthenticated()) {
whatapClient = new WhatapClient(authManager);
}
}
main().catch(console.error);
```
### MCP Tool 정의
```typescript
// src/mcp/tools/query.ts
import type { MxqlExecutor } from '../../core/api/MxqlExecutor';
export const queryTool = {
name: 'whatap_query',
description: 'Execute MXQL query against WhaTap monitoring data',
inputSchema: {
type: 'object',
properties: {
pcode: {
type: 'number',
description: 'Project code (pcode)',
},
mql: {
type: 'string',
description: 'MXQL query string',
},
stime: {
type: 'number',
description: 'Start time in milliseconds (optional)',
},
etime: {
type: 'number',
description: 'End time in milliseconds (optional)',
},
limit: {
type: 'number',
description: 'Result limit (default: 10000)',
},
},
required: ['pcode', 'mql'],
},
};
export async function handleQuery(executor: MxqlExecutor, args: any) {
const result = await executor.execute({
pcode: args.pcode,
mql: args.mql,
stime: args.stime,
etime: args.etime,
limit: args.limit,
});
return {
success: true,
data: result.data,
rowCount: result.rowCount,
executionTimeMs: result.executionTimeMs,
};
}
```
---
## 📦 package.json
```json
{
"name": "whatap-mxql-cli",
"version": "1.0.0",
"description": "WhaTap MXQL CLI and MCP Server",
"main": "dist/index.js",
"bin": {
"whatap": "./bin/whatap.js",
"whatap-mcp": "./bin/whatap-mcp.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/cli/index.ts",
"mcp": "tsx src/mcp/server.ts",
"test": "jest",
"lint": "eslint src/**/*.ts",
"format": "prettier --write src/**/*.ts"
},
"keywords": ["whatap", "mxql", "cli", "mcp", "monitoring"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"commander": "^12.0.0",
"@inquirer/prompts": "^4.0.0",
"axios": "^1.6.0",
"ora": "^8.0.0",
"chalk": "^5.3.0",
"cli-table3": "^0.6.3",
"cheerio": "^1.0.0-rc.12"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.3.0",
"tsx": "^4.7.0",
"jest": "^29.7.0",
"@types/jest": "^29.5.0",
"eslint": "^8.56.0",
"prettier": "^3.2.0"
}
}
```
---
## 🚀 사용 시나리오
### 시나리오 1: CLI 단독 사용
```bash
# 설치
npm install -g whatap-mxql-cli
# 로그인
whatap login
# Email: user@example.com
# Password: ********
# ✓ Login successful!
# 프로젝트 목록 확인
whatap projects list
# ┌────────────┬──────────────────┬─────────┬────────┐
# │ PCode │ Name │ Type │ Status │
# ├────────────┼──────────────────┼─────────┼────────┤
# │ 12345 │ Production APM │ APM │ active │
# │ 67890 │ DB Monitoring │ DBX │ active │
# └────────────┴──────────────────┴─────────┴────────┘
# MXQL 쿼리 실행
whatap query --pcode 12345 --mql "CATEGORY app_counter\nTAGLOAD\nSELECT"
# 파일에서 쿼리 실행
whatap query --pcode 12345 --file my_query.mql --format json > result.json
# 대화형 쿼리
whatap query --pcode 12345 --interactive
```
### 시나리오 2: Claude Code와 MCP 통합
**MCP 설정** (`~/.config/claude-code/mcp.json`):
```json
{
"mcpServers": {
"whatap": {
"command": "whatap-mcp"
}
}
}
```
**Claude Code에서 사용**:
```
User: WhaTap에 로그인해줘 (user@example.com / password123)
Claude: [whatap_login tool 호출]
✓ WhaTap에 성공적으로 로그인했습니다.
Account ID: 12345
User: pcode 12345의 최근 5분간 CPU 사용률을 조회해줘
Claude: [whatap_query tool 호출]
MXQL 쿼리를 실행했습니다. 결과:
- 총 42개 레코드
- 실행 시간: 234ms
- 평균 CPU: 45.2%
...
User: 내가 접근 가능한 프로젝트 목록 보여줘
Claude: [whatap_projects tool 호출]
접근 가능한 프로젝트 목록:
1. Production APM (12345) - APM - active
2. DB Monitoring (67890) - DBX - active
```
---
## 🔒 보안 고려사항
1. **세션 저장소**
- AES-256-GCM 암호화
- 키 파일 권한: 0600 (소유자만 읽기/쓰기)
- 24시간 자동 만료
2. **비밀번호 처리**
- 메모리에만 존재 (저장 안 함)
- CLI: `@inquirer/prompts`의 password 타입 (마스킹)
3. **HTTPS 강제**
- serviceUrl이 https가 아니면 거부
4. **에러 처리**
- 민감한 정보 로그 출력 금지
- 스택 트레이스 제한
---
## 🧪 테스트 전략
### 단위 테스트
```typescript
// test/core/auth/AuthManager.test.ts
describe('AuthManager', () => {
it('should login successfully with valid credentials', async () => {
// ...
});
it('should throw error for invalid credentials', async () => {
// ...
});
it('should restore session from storage', async () => {
// ...
});
});
```
### 통합 테스트
```typescript
// test/cli/commands/query.test.ts
describe('whatap query command', () => {
it('should execute query and display results', async () => {
// CLI 명령어 실행 및 출력 검증
});
});
```
### E2E 테스트
```bash
# test/e2e/test.sh
whatap login --email test@example.com --password test123
whatap projects list
whatap query --pcode 12345 --mql "CATEGORY app_counter"
whatap logout
```
---
## 📋 개발 로드맵
### Phase 1: Core 구현 (1주)
- [ ] AuthManager 구현
- [ ] SessionStore 구현
- [ ] WhatapClient 구현
- [ ] MxqlExecutor 구현
- [ ] 단위 테스트
### Phase 2: CLI 구현 (1주)
- [ ] CLI 프레임워크 설정
- [ ] login, logout 명령어
- [ ] query 명령어 (기본)
- [ ] projects 명령어
- [ ] UI 포맷터 (table, json, csv)
### Phase 3: CLI 고도화 (3-4일)
- [ ] query 명령어 고도화 (interactive, file)
- [ ] validate 명령어
- [ ] status, config 명령어
- [ ] 에러 핸들링 강화
### Phase 4: MCP 구현 (2-3일)
- [ ] MCP 서버 기본 구조
- [ ] MCP Tools 구현
- [ ] MCP <-> Core 통합
### Phase 5: 테스트 및 문서화 (3-4일)
- [ ] 통합 테스트
- [ ] E2E 테스트
- [ ] README 작성
- [ ] 사용 가이드 문서
### Phase 6: 배포 (1-2일)
- [ ] npm 패키지 배포
- [ ] GitHub Actions CI/CD
- [ ] 버전 관리 전략
**총 예상 기간: 2.5-3주**