From b3f8206d4708b6f6d5a4502a2cdac5709a10e32d Mon Sep 17 00:00:00 2001 From: hahmee Date: Tue, 10 Mar 2026 13:29:00 +0900 Subject: [PATCH] docs(ko-KR): add Korean translation for skills - 15 skill categories (17 files): coding-standards, tdd-workflow, frontend-patterns, backend-patterns, security-review (2 files), postgres-patterns, verification-loop, continuous-learning, continuous-learning-v2, eval-harness, iterative-retrieval, strategic-compact, golang-patterns, golang-testing, clickhouse-io, project-guidelines-example --- docs/ko-KR/skills/backend-patterns/SKILL.md | 598 +++++++++++++++ docs/ko-KR/skills/clickhouse-io/SKILL.md | 439 +++++++++++ docs/ko-KR/skills/coding-standards/SKILL.md | 530 +++++++++++++ .../skills/continuous-learning-v2/SKILL.md | 363 +++++++++ .../ko-KR/skills/continuous-learning/SKILL.md | 119 +++ docs/ko-KR/skills/eval-harness/SKILL.md | 270 +++++++ docs/ko-KR/skills/frontend-patterns/SKILL.md | 642 ++++++++++++++++ docs/ko-KR/skills/golang-patterns/SKILL.md | 674 ++++++++++++++++ docs/ko-KR/skills/golang-testing/SKILL.md | 720 ++++++++++++++++++ .../ko-KR/skills/iterative-retrieval/SKILL.md | 211 +++++ docs/ko-KR/skills/postgres-patterns/SKILL.md | 147 ++++ .../project-guidelines-example/SKILL.md | 349 +++++++++ docs/ko-KR/skills/security-review/SKILL.md | 495 ++++++++++++ .../cloud-infrastructure-security.md | 361 +++++++++ docs/ko-KR/skills/strategic-compact/SKILL.md | 103 +++ docs/ko-KR/skills/tdd-workflow/SKILL.md | 410 ++++++++++ docs/ko-KR/skills/verification-loop/SKILL.md | 126 +++ 17 files changed, 6557 insertions(+) create mode 100644 docs/ko-KR/skills/backend-patterns/SKILL.md create mode 100644 docs/ko-KR/skills/clickhouse-io/SKILL.md create mode 100644 docs/ko-KR/skills/coding-standards/SKILL.md create mode 100644 docs/ko-KR/skills/continuous-learning-v2/SKILL.md create mode 100644 docs/ko-KR/skills/continuous-learning/SKILL.md create mode 100644 docs/ko-KR/skills/eval-harness/SKILL.md create mode 100644 docs/ko-KR/skills/frontend-patterns/SKILL.md create mode 100644 docs/ko-KR/skills/golang-patterns/SKILL.md create mode 100644 docs/ko-KR/skills/golang-testing/SKILL.md create mode 100644 docs/ko-KR/skills/iterative-retrieval/SKILL.md create mode 100644 docs/ko-KR/skills/postgres-patterns/SKILL.md create mode 100644 docs/ko-KR/skills/project-guidelines-example/SKILL.md create mode 100644 docs/ko-KR/skills/security-review/SKILL.md create mode 100644 docs/ko-KR/skills/security-review/cloud-infrastructure-security.md create mode 100644 docs/ko-KR/skills/strategic-compact/SKILL.md create mode 100644 docs/ko-KR/skills/tdd-workflow/SKILL.md create mode 100644 docs/ko-KR/skills/verification-loop/SKILL.md diff --git a/docs/ko-KR/skills/backend-patterns/SKILL.md b/docs/ko-KR/skills/backend-patterns/SKILL.md new file mode 100644 index 00000000..5118b328 --- /dev/null +++ b/docs/ko-KR/skills/backend-patterns/SKILL.md @@ -0,0 +1,598 @@ +--- +name: backend-patterns +description: Node.js, Express, Next.js API 라우트를 위한 백엔드 아키텍처 패턴, API 설계, 데이터베이스 최적화 및 서버 사이드 모범 사례. +origin: ECC +--- + +# 백엔드 개발 패턴 + +확장 가능한 서버 사이드 애플리케이션을 위한 백엔드 아키텍처 패턴과 모범 사례. + +## 활성화 시점 + +- REST 또는 GraphQL API 엔드포인트를 설계할 때 +- Repository, Service 또는 Controller 레이어를 구현할 때 +- 데이터베이스 쿼리를 최적화할 때 (N+1, 인덱싱, 커넥션 풀링) +- 캐싱을 추가할 때 (Redis, 인메모리, HTTP 캐시 헤더) +- 백그라운드 작업이나 비동기 처리를 설정할 때 +- API를 위한 에러 처리 및 유효성 검사를 구조화할 때 +- 미들웨어를 구축할 때 (인증, 로깅, 요청 제한) + +## API 설계 패턴 + +### RESTful API 구조 + +```typescript +// ✅ Resource-based URLs +GET /api/markets # List resources +GET /api/markets/:id # Get single resource +POST /api/markets # Create resource +PUT /api/markets/:id # Replace resource +PATCH /api/markets/:id # Update resource +DELETE /api/markets/:id # Delete resource + +// ✅ Query parameters for filtering, sorting, pagination +GET /api/markets?status=active&sort=volume&limit=20&offset=0 +``` + +### Repository 패턴 + +```typescript +// Abstract data access logic +interface MarketRepository { + findAll(filters?: MarketFilters): Promise + findById(id: string): Promise + create(data: CreateMarketDto): Promise + update(id: string, data: UpdateMarketDto): Promise + delete(id: string): Promise +} + +class SupabaseMarketRepository implements MarketRepository { + async findAll(filters?: MarketFilters): Promise { + let query = supabase.from('markets').select('*') + + if (filters?.status) { + query = query.eq('status', filters.status) + } + + if (filters?.limit) { + query = query.limit(filters.limit) + } + + const { data, error } = await query + + if (error) throw new Error(error.message) + return data + } + + // Other methods... +} +``` + +### Service 레이어 패턴 + +```typescript +// Business logic separated from data access +class MarketService { + constructor(private marketRepo: MarketRepository) {} + + async searchMarkets(query: string, limit: number = 10): Promise { + // Business logic + const embedding = await generateEmbedding(query) + const results = await this.vectorSearch(embedding, limit) + + // Fetch full data + const markets = await this.marketRepo.findByIds(results.map(r => r.id)) + + // Sort by similarity + return markets.sort((a, b) => { + const scoreA = results.find(r => r.id === a.id)?.score || 0 + const scoreB = results.find(r => r.id === b.id)?.score || 0 + return scoreA - scoreB + }) + } + + private async vectorSearch(embedding: number[], limit: number) { + // Vector search implementation + } +} +``` + +### 미들웨어 패턴 + +```typescript +// Request/response processing pipeline +export function withAuth(handler: NextApiHandler): NextApiHandler { + return async (req, res) => { + const token = req.headers.authorization?.replace('Bearer ', '') + + if (!token) { + return res.status(401).json({ error: 'Unauthorized' }) + } + + try { + const user = await verifyToken(token) + req.user = user + return handler(req, res) + } catch (error) { + return res.status(401).json({ error: 'Invalid token' }) + } + } +} + +// Usage +export default withAuth(async (req, res) => { + // Handler has access to req.user +}) +``` + +## 데이터베이스 패턴 + +### 쿼리 최적화 + +```typescript +// ✅ GOOD: Select only needed columns +const { data } = await supabase + .from('markets') + .select('id, name, status, volume') + .eq('status', 'active') + .order('volume', { ascending: false }) + .limit(10) + +// ❌ BAD: Select everything +const { data } = await supabase + .from('markets') + .select('*') +``` + +### N+1 쿼리 방지 + +```typescript +// ❌ BAD: N+1 query problem +const markets = await getMarkets() +for (const market of markets) { + market.creator = await getUser(market.creator_id) // N queries +} + +// ✅ GOOD: Batch fetch +const markets = await getMarkets() +const creatorIds = markets.map(m => m.creator_id) +const creators = await getUsers(creatorIds) // 1 query +const creatorMap = new Map(creators.map(c => [c.id, c])) + +markets.forEach(market => { + market.creator = creatorMap.get(market.creator_id) +}) +``` + +### 트랜잭션 패턴 + +```typescript +async function createMarketWithPosition( + marketData: CreateMarketDto, + positionData: CreatePositionDto +) { + // Use Supabase transaction + const { data, error } = await supabase.rpc('create_market_with_position', { + market_data: marketData, + position_data: positionData + }) + + if (error) throw new Error('Transaction failed') + return data +} + +// SQL function in Supabase +CREATE OR REPLACE FUNCTION create_market_with_position( + market_data jsonb, + position_data jsonb +) +RETURNS jsonb +LANGUAGE plpgsql +AS $$ +BEGIN + -- Start transaction automatically + INSERT INTO markets VALUES (market_data); + INSERT INTO positions VALUES (position_data); + RETURN jsonb_build_object('success', true); +EXCEPTION + WHEN OTHERS THEN + -- Rollback happens automatically + RETURN jsonb_build_object('success', false, 'error', SQLERRM); +END; +$$; +``` + +## 캐싱 전략 + +### Redis 캐싱 레이어 + +```typescript +class CachedMarketRepository implements MarketRepository { + constructor( + private baseRepo: MarketRepository, + private redis: RedisClient + ) {} + + async findById(id: string): Promise { + // Check cache first + const cached = await this.redis.get(`market:${id}`) + + if (cached) { + return JSON.parse(cached) + } + + // Cache miss - fetch from database + const market = await this.baseRepo.findById(id) + + if (market) { + // Cache for 5 minutes + await this.redis.setex(`market:${id}`, 300, JSON.stringify(market)) + } + + return market + } + + async invalidateCache(id: string): Promise { + await this.redis.del(`market:${id}`) + } +} +``` + +### Cache-Aside 패턴 + +```typescript +async function getMarketWithCache(id: string): Promise { + const cacheKey = `market:${id}` + + // Try cache + const cached = await redis.get(cacheKey) + if (cached) return JSON.parse(cached) + + // Cache miss - fetch from DB + const market = await db.markets.findUnique({ where: { id } }) + + if (!market) throw new Error('Market not found') + + // Update cache + await redis.setex(cacheKey, 300, JSON.stringify(market)) + + return market +} +``` + +## 에러 처리 패턴 + +### 중앙화된 에러 핸들러 + +```typescript +class ApiError extends Error { + constructor( + public statusCode: number, + public message: string, + public isOperational = true + ) { + super(message) + Object.setPrototypeOf(this, ApiError.prototype) + } +} + +export function errorHandler(error: unknown, req: Request): Response { + if (error instanceof ApiError) { + return NextResponse.json({ + success: false, + error: error.message + }, { status: error.statusCode }) + } + + if (error instanceof z.ZodError) { + return NextResponse.json({ + success: false, + error: 'Validation failed', + details: error.errors + }, { status: 400 }) + } + + // Log unexpected errors + console.error('Unexpected error:', error) + + return NextResponse.json({ + success: false, + error: 'Internal server error' + }, { status: 500 }) +} + +// Usage +export async function GET(request: Request) { + try { + const data = await fetchData() + return NextResponse.json({ success: true, data }) + } catch (error) { + return errorHandler(error, request) + } +} +``` + +### 지수 백오프를 이용한 재시도 + +```typescript +async function fetchWithRetry( + fn: () => Promise, + maxRetries = 3 +): Promise { + let lastError: Error + + for (let i = 0; i < maxRetries; i++) { + try { + return await fn() + } catch (error) { + lastError = error as Error + + if (i < maxRetries - 1) { + // Exponential backoff: 1s, 2s, 4s + const delay = Math.pow(2, i) * 1000 + await new Promise(resolve => setTimeout(resolve, delay)) + } + } + } + + throw lastError! +} + +// Usage +const data = await fetchWithRetry(() => fetchFromAPI()) +``` + +## 인증 및 인가 + +### JWT 토큰 검증 + +```typescript +import jwt from 'jsonwebtoken' + +interface JWTPayload { + userId: string + email: string + role: 'admin' | 'user' +} + +export function verifyToken(token: string): JWTPayload { + try { + const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload + return payload + } catch (error) { + throw new ApiError(401, 'Invalid token') + } +} + +export async function requireAuth(request: Request) { + const token = request.headers.get('authorization')?.replace('Bearer ', '') + + if (!token) { + throw new ApiError(401, 'Missing authorization token') + } + + return verifyToken(token) +} + +// Usage in API route +export async function GET(request: Request) { + const user = await requireAuth(request) + + const data = await getDataForUser(user.userId) + + return NextResponse.json({ success: true, data }) +} +``` + +### 역할 기반 접근 제어 + +```typescript +type Permission = 'read' | 'write' | 'delete' | 'admin' + +interface User { + id: string + role: 'admin' | 'moderator' | 'user' +} + +const rolePermissions: Record = { + admin: ['read', 'write', 'delete', 'admin'], + moderator: ['read', 'write', 'delete'], + user: ['read', 'write'] +} + +export function hasPermission(user: User, permission: Permission): boolean { + return rolePermissions[user.role].includes(permission) +} + +export function requirePermission(permission: Permission) { + return (handler: (request: Request, user: User) => Promise) => { + return async (request: Request) => { + const user = await requireAuth(request) + + if (!hasPermission(user, permission)) { + throw new ApiError(403, 'Insufficient permissions') + } + + return handler(request, user) + } + } +} + +// Usage - HOF wraps the handler +export const DELETE = requirePermission('delete')( + async (request: Request, user: User) => { + // Handler receives authenticated user with verified permission + return new Response('Deleted', { status: 200 }) + } +) +``` + +## 요청 제한 + +### 간단한 인메모리 요청 제한기 + +```typescript +class RateLimiter { + private requests = new Map() + + async checkLimit( + identifier: string, + maxRequests: number, + windowMs: number + ): Promise { + const now = Date.now() + const requests = this.requests.get(identifier) || [] + + // Remove old requests outside window + const recentRequests = requests.filter(time => now - time < windowMs) + + if (recentRequests.length >= maxRequests) { + return false // Rate limit exceeded + } + + // Add current request + recentRequests.push(now) + this.requests.set(identifier, recentRequests) + + return true + } +} + +const limiter = new RateLimiter() + +export async function GET(request: Request) { + const ip = request.headers.get('x-forwarded-for') || 'unknown' + + const allowed = await limiter.checkLimit(ip, 100, 60000) // 100 req/min + + if (!allowed) { + return NextResponse.json({ + error: 'Rate limit exceeded' + }, { status: 429 }) + } + + // Continue with request +} +``` + +## 백그라운드 작업 및 큐 + +### 간단한 큐 패턴 + +```typescript +class JobQueue { + private queue: T[] = [] + private processing = false + + async add(job: T): Promise { + this.queue.push(job) + + if (!this.processing) { + this.process() + } + } + + private async process(): Promise { + this.processing = true + + while (this.queue.length > 0) { + const job = this.queue.shift()! + + try { + await this.execute(job) + } catch (error) { + console.error('Job failed:', error) + } + } + + this.processing = false + } + + private async execute(job: T): Promise { + // Job execution logic + } +} + +// Usage for indexing markets +interface IndexJob { + marketId: string +} + +const indexQueue = new JobQueue() + +export async function POST(request: Request) { + const { marketId } = await request.json() + + // Add to queue instead of blocking + await indexQueue.add({ marketId }) + + return NextResponse.json({ success: true, message: 'Job queued' }) +} +``` + +## 로깅 및 모니터링 + +### 구조화된 로깅 + +```typescript +interface LogContext { + userId?: string + requestId?: string + method?: string + path?: string + [key: string]: unknown +} + +class Logger { + log(level: 'info' | 'warn' | 'error', message: string, context?: LogContext) { + const entry = { + timestamp: new Date().toISOString(), + level, + message, + ...context + } + + console.log(JSON.stringify(entry)) + } + + info(message: string, context?: LogContext) { + this.log('info', message, context) + } + + warn(message: string, context?: LogContext) { + this.log('warn', message, context) + } + + error(message: string, error: Error, context?: LogContext) { + this.log('error', message, { + ...context, + error: error.message, + stack: error.stack + }) + } +} + +const logger = new Logger() + +// Usage +export async function GET(request: Request) { + const requestId = crypto.randomUUID() + + logger.info('Fetching markets', { + requestId, + method: 'GET', + path: '/api/markets' + }) + + try { + const markets = await fetchMarkets() + return NextResponse.json({ success: true, data: markets }) + } catch (error) { + logger.error('Failed to fetch markets', error as Error, { requestId }) + return NextResponse.json({ error: 'Internal error' }, { status: 500 }) + } +} +``` + +**기억하세요**: 백엔드 패턴은 확장 가능하고 유지보수 가능한 서버 사이드 애플리케이션을 가능하게 합니다. 복잡도 수준에 맞는 패턴을 선택하세요. diff --git a/docs/ko-KR/skills/clickhouse-io/SKILL.md b/docs/ko-KR/skills/clickhouse-io/SKILL.md new file mode 100644 index 00000000..736667b8 --- /dev/null +++ b/docs/ko-KR/skills/clickhouse-io/SKILL.md @@ -0,0 +1,439 @@ +--- +name: clickhouse-io +description: 고성능 분석 워크로드를 위한 ClickHouse 데이터베이스 패턴, 쿼리 최적화, 분석 및 데이터 엔지니어링 모범 사례. +origin: ECC +--- + +# ClickHouse 분석 패턴 + +고성능 분석 및 데이터 엔지니어링을 위한 ClickHouse 전용 패턴. + +## 활성화 시점 + +- ClickHouse 테이블 스키마 설계 시 (MergeTree 엔진 선택) +- 분석 쿼리 작성 시 (집계, 윈도우 함수, 조인) +- 쿼리 성능 최적화 시 (파티션 프루닝, 프로젝션, 구체화된 뷰) +- 대량 데이터 수집 시 (배치 삽입, Kafka 통합) +- PostgreSQL/MySQL에서 ClickHouse로 분석 마이그레이션 시 +- 실시간 대시보드 또는 시계열 분석 구현 시 + +## 개요 + +ClickHouse는 온라인 분석 처리(OLAP)를 위한 컬럼 지향 데이터베이스 관리 시스템(DBMS)입니다. 대규모 데이터셋에 대한 빠른 분석 쿼리에 최적화되어 있습니다. + +**주요 특징:** +- 컬럼 지향 저장소 +- 데이터 압축 +- 병렬 쿼리 실행 +- 분산 쿼리 +- 실시간 분석 + +## 테이블 설계 패턴 + +### MergeTree 엔진 (가장 일반적) + +```sql +CREATE TABLE markets_analytics ( + date Date, + market_id String, + market_name String, + volume UInt64, + trades UInt32, + unique_traders UInt32, + avg_trade_size Float64, + created_at DateTime +) ENGINE = MergeTree() +PARTITION BY toYYYYMM(date) +ORDER BY (date, market_id) +SETTINGS index_granularity = 8192; +``` + +### ReplacingMergeTree (중복 제거) + +```sql +-- For data that may have duplicates (e.g., from multiple sources) +CREATE TABLE user_events ( + event_id String, + user_id String, + event_type String, + timestamp DateTime, + properties String +) ENGINE = ReplacingMergeTree() +PARTITION BY toYYYYMM(timestamp) +ORDER BY (user_id, event_id, timestamp) +PRIMARY KEY (user_id, event_id); +``` + +### AggregatingMergeTree (사전 집계) + +```sql +-- For maintaining aggregated metrics +CREATE TABLE market_stats_hourly ( + hour DateTime, + market_id String, + total_volume AggregateFunction(sum, UInt64), + total_trades AggregateFunction(count, UInt32), + unique_users AggregateFunction(uniq, String) +) ENGINE = AggregatingMergeTree() +PARTITION BY toYYYYMM(hour) +ORDER BY (hour, market_id); + +-- Query aggregated data +SELECT + hour, + market_id, + sumMerge(total_volume) AS volume, + countMerge(total_trades) AS trades, + uniqMerge(unique_users) AS users +FROM market_stats_hourly +WHERE hour >= toStartOfHour(now() - INTERVAL 24 HOUR) +GROUP BY hour, market_id +ORDER BY hour DESC; +``` + +## 쿼리 최적화 패턴 + +### 효율적인 필터링 + +```sql +-- ✅ GOOD: Use indexed columns first +SELECT * +FROM markets_analytics +WHERE date >= '2025-01-01' + AND market_id = 'market-123' + AND volume > 1000 +ORDER BY date DESC +LIMIT 100; + +-- ❌ BAD: Filter on non-indexed columns first +SELECT * +FROM markets_analytics +WHERE volume > 1000 + AND market_name LIKE '%election%' + AND date >= '2025-01-01'; +``` + +### 집계 + +```sql +-- ✅ GOOD: Use ClickHouse-specific aggregation functions +SELECT + toStartOfDay(created_at) AS day, + market_id, + sum(volume) AS total_volume, + count() AS total_trades, + uniq(trader_id) AS unique_traders, + avg(trade_size) AS avg_size +FROM trades +WHERE created_at >= today() - INTERVAL 7 DAY +GROUP BY day, market_id +ORDER BY day DESC, total_volume DESC; + +-- ✅ Use quantile for percentiles (more efficient than percentile) +SELECT + quantile(0.50)(trade_size) AS median, + quantile(0.95)(trade_size) AS p95, + quantile(0.99)(trade_size) AS p99 +FROM trades +WHERE created_at >= now() - INTERVAL 1 HOUR; +``` + +### 윈도우 함수 + +```sql +-- Calculate running totals +SELECT + date, + market_id, + volume, + sum(volume) OVER ( + PARTITION BY market_id + ORDER BY date + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) AS cumulative_volume +FROM markets_analytics +WHERE date >= today() - INTERVAL 30 DAY +ORDER BY market_id, date; +``` + +## 데이터 삽입 패턴 + +### 배치 삽입 (권장) + +```typescript +import { ClickHouse } from 'clickhouse' + +const clickhouse = new ClickHouse({ + url: process.env.CLICKHOUSE_URL, + port: 8123, + basicAuth: { + username: process.env.CLICKHOUSE_USER, + password: process.env.CLICKHOUSE_PASSWORD + } +}) + +// ✅ Batch insert (efficient) +async function bulkInsertTrades(trades: Trade[]) { + const values = trades.map(trade => `( + '${trade.id}', + '${trade.market_id}', + '${trade.user_id}', + ${trade.amount}, + '${trade.timestamp.toISOString()}' + )`).join(',') + + await clickhouse.query(` + INSERT INTO trades (id, market_id, user_id, amount, timestamp) + VALUES ${values} + `).toPromise() +} + +// ❌ Individual inserts (slow) +async function insertTrade(trade: Trade) { + // Don't do this in a loop! + await clickhouse.query(` + INSERT INTO trades VALUES ('${trade.id}', ...) + `).toPromise() +} +``` + +### 스트리밍 삽입 + +```typescript +// For continuous data ingestion +import { createWriteStream } from 'fs' +import { pipeline } from 'stream/promises' + +async function streamInserts() { + const stream = clickhouse.insert('trades').stream() + + for await (const batch of dataSource) { + stream.write(batch) + } + + await stream.end() +} +``` + +## 구체화된 뷰 + +### 실시간 집계 + +```sql +-- Create materialized view for hourly stats +CREATE MATERIALIZED VIEW market_stats_hourly_mv +TO market_stats_hourly +AS SELECT + toStartOfHour(timestamp) AS hour, + market_id, + sumState(amount) AS total_volume, + countState() AS total_trades, + uniqState(user_id) AS unique_users +FROM trades +GROUP BY hour, market_id; + +-- Query the materialized view +SELECT + hour, + market_id, + sumMerge(total_volume) AS volume, + countMerge(total_trades) AS trades, + uniqMerge(unique_users) AS users +FROM market_stats_hourly +WHERE hour >= now() - INTERVAL 24 HOUR +GROUP BY hour, market_id; +``` + +## 성능 모니터링 + +### 쿼리 성능 + +```sql +-- Check slow queries +SELECT + query_id, + user, + query, + query_duration_ms, + read_rows, + read_bytes, + memory_usage +FROM system.query_log +WHERE type = 'QueryFinish' + AND query_duration_ms > 1000 + AND event_time >= now() - INTERVAL 1 HOUR +ORDER BY query_duration_ms DESC +LIMIT 10; +``` + +### 테이블 통계 + +```sql +-- Check table sizes +SELECT + database, + table, + formatReadableSize(sum(bytes)) AS size, + sum(rows) AS rows, + max(modification_time) AS latest_modification +FROM system.parts +WHERE active +GROUP BY database, table +ORDER BY sum(bytes) DESC; +``` + +## 일반적인 분석 쿼리 + +### 시계열 분석 + +```sql +-- Daily active users +SELECT + toDate(timestamp) AS date, + uniq(user_id) AS daily_active_users +FROM events +WHERE timestamp >= today() - INTERVAL 30 DAY +GROUP BY date +ORDER BY date; + +-- Retention analysis +SELECT + signup_date, + countIf(days_since_signup = 0) AS day_0, + countIf(days_since_signup = 1) AS day_1, + countIf(days_since_signup = 7) AS day_7, + countIf(days_since_signup = 30) AS day_30 +FROM ( + SELECT + user_id, + min(toDate(timestamp)) AS signup_date, + toDate(timestamp) AS activity_date, + dateDiff('day', signup_date, activity_date) AS days_since_signup + FROM events + GROUP BY user_id, activity_date +) +GROUP BY signup_date +ORDER BY signup_date DESC; +``` + +### 퍼널 분석 + +```sql +-- Conversion funnel +SELECT + countIf(step = 'viewed_market') AS viewed, + countIf(step = 'clicked_trade') AS clicked, + countIf(step = 'completed_trade') AS completed, + round(clicked / viewed * 100, 2) AS view_to_click_rate, + round(completed / clicked * 100, 2) AS click_to_completion_rate +FROM ( + SELECT + user_id, + session_id, + event_type AS step + FROM events + WHERE event_date = today() +) +GROUP BY session_id; +``` + +### 코호트 분석 + +```sql +-- User cohorts by signup month +SELECT + toStartOfMonth(signup_date) AS cohort, + toStartOfMonth(activity_date) AS month, + dateDiff('month', cohort, month) AS months_since_signup, + count(DISTINCT user_id) AS active_users +FROM ( + SELECT + user_id, + min(toDate(timestamp)) OVER (PARTITION BY user_id) AS signup_date, + toDate(timestamp) AS activity_date + FROM events +) +GROUP BY cohort, month, months_since_signup +ORDER BY cohort, months_since_signup; +``` + +## 데이터 파이프라인 패턴 + +### ETL 패턴 + +```typescript +// Extract, Transform, Load +async function etlPipeline() { + // 1. Extract from source + const rawData = await extractFromPostgres() + + // 2. Transform + const transformed = rawData.map(row => ({ + date: new Date(row.created_at).toISOString().split('T')[0], + market_id: row.market_slug, + volume: parseFloat(row.total_volume), + trades: parseInt(row.trade_count) + })) + + // 3. Load to ClickHouse + await bulkInsertToClickHouse(transformed) +} + +// Run periodically +setInterval(etlPipeline, 60 * 60 * 1000) // Every hour +``` + +### 변경 데이터 캡처 (CDC) + +```typescript +// Listen to PostgreSQL changes and sync to ClickHouse +import { Client } from 'pg' + +const pgClient = new Client({ connectionString: process.env.DATABASE_URL }) + +pgClient.query('LISTEN market_updates') + +pgClient.on('notification', async (msg) => { + const update = JSON.parse(msg.payload) + + await clickhouse.insert('market_updates', [ + { + market_id: update.id, + event_type: update.operation, // INSERT, UPDATE, DELETE + timestamp: new Date(), + data: JSON.stringify(update.new_data) + } + ]) +}) +``` + +## 모범 사례 + +### 1. 파티셔닝 전략 +- 시간별 파티셔닝 (보통 월 또는 일) +- 파티션이 너무 많은 것 방지 (성능 영향) +- 파티션 키에 DATE 타입 사용 + +### 2. 정렬 키 +- 가장 자주 필터링되는 컬럼을 먼저 배치 +- 카디널리티 고려 (높은 카디널리티 먼저) +- 정렬이 압축에 영향을 미침 + +### 3. 데이터 타입 +- 가장 작은 적절한 타입 사용 (UInt32 vs UInt64) +- 반복되는 문자열에 LowCardinality 사용 +- 범주형 데이터에 Enum 사용 + +### 4. 피해야 할 것 +- SELECT * (컬럼을 명시) +- FINAL (쿼리 전에 데이터를 병합) +- 너무 많은 JOIN (분석을 위해 비정규화) +- 작은 빈번한 삽입 (배치 처리) + +### 5. 모니터링 +- 쿼리 성능 추적 +- 디스크 사용량 모니터링 +- 병합 작업 확인 +- 슬로우 쿼리 로그 검토 + +**기억하세요**: ClickHouse는 분석 워크로드에 탁월합니다. 쿼리 패턴에 맞게 테이블을 설계하고, 배치 삽입을 사용하며, 실시간 집계를 위해 구체화된 뷰를 활용하세요. diff --git a/docs/ko-KR/skills/coding-standards/SKILL.md b/docs/ko-KR/skills/coding-standards/SKILL.md new file mode 100644 index 00000000..e3e708f8 --- /dev/null +++ b/docs/ko-KR/skills/coding-standards/SKILL.md @@ -0,0 +1,530 @@ +--- +name: coding-standards +description: TypeScript, JavaScript, React, Node.js 개발을 위한 범용 코딩 표준, 모범 사례 및 패턴. +origin: ECC +--- + +# 코딩 표준 및 모범 사례 + +모든 프로젝트에 적용 가능한 범용 코딩 표준. + +## 활성화 시점 + +- 새 프로젝트 또는 모듈을 시작할 때 +- 코드 품질 및 유지보수성을 검토할 때 +- 기존 코드를 컨벤션에 맞게 리팩터링할 때 +- 네이밍, 포맷팅 또는 구조적 일관성을 적용할 때 +- 린팅, 포맷팅 또는 타입 검사 규칙을 설정할 때 +- 새 기여자에게 코딩 컨벤션을 안내할 때 + +## 코드 품질 원칙 + +### 1. 가독성 우선 +- 코드는 작성보다 읽히는 횟수가 더 많다 +- 명확한 변수 및 함수 이름 사용 +- 주석보다 자기 문서화 코드를 선호 +- 일관된 포맷팅 유지 + +### 2. KISS (Keep It Simple, Stupid) +- 동작하는 가장 단순한 해결책 +- 과도한 엔지니어링 지양 +- 조기 최적화 금지 +- 이해하기 쉬운 코드 > 영리한 코드 + +### 3. DRY (Don't Repeat Yourself) +- 공통 로직을 함수로 추출 +- 재사용 가능한 컴포넌트 생성 +- 모듈 간 유틸리티 공유 +- 복사-붙여넣기 프로그래밍 지양 + +### 4. YAGNI (You Aren't Gonna Need It) +- 필요하기 전에 기능을 만들지 않기 +- 추측에 의한 일반화 지양 +- 필요할 때만 복잡성 추가 +- 단순하게 시작하고 필요할 때 리팩터링 + +## TypeScript/JavaScript 표준 + +### 변수 네이밍 + +```typescript +// ✅ GOOD: Descriptive names +const marketSearchQuery = 'election' +const isUserAuthenticated = true +const totalRevenue = 1000 + +// ❌ BAD: Unclear names +const q = 'election' +const flag = true +const x = 1000 +``` + +### 함수 네이밍 + +```typescript +// ✅ GOOD: Verb-noun pattern +async function fetchMarketData(marketId: string) { } +function calculateSimilarity(a: number[], b: number[]) { } +function isValidEmail(email: string): boolean { } + +// ❌ BAD: Unclear or noun-only +async function market(id: string) { } +function similarity(a, b) { } +function email(e) { } +``` + +### 불변성 패턴 (필수) + +```typescript +// ✅ ALWAYS use spread operator +const updatedUser = { + ...user, + name: 'New Name' +} + +const updatedArray = [...items, newItem] + +// ❌ NEVER mutate directly +user.name = 'New Name' // BAD +items.push(newItem) // BAD +``` + +### 에러 처리 + +```typescript +// ✅ GOOD: Comprehensive error handling +async function fetchData(url: string) { + try { + const response = await fetch(url) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + return await response.json() + } catch (error) { + console.error('Fetch failed:', error) + throw new Error('Failed to fetch data') + } +} + +// ❌ BAD: No error handling +async function fetchData(url) { + const response = await fetch(url) + return response.json() +} +``` + +### Async/Await 모범 사례 + +```typescript +// ✅ GOOD: Parallel execution when possible +const [users, markets, stats] = await Promise.all([ + fetchUsers(), + fetchMarkets(), + fetchStats() +]) + +// ❌ BAD: Sequential when unnecessary +const users = await fetchUsers() +const markets = await fetchMarkets() +const stats = await fetchStats() +``` + +### 타입 안전성 + +```typescript +// ✅ GOOD: Proper types +interface Market { + id: string + name: string + status: 'active' | 'resolved' | 'closed' + created_at: Date +} + +function getMarket(id: string): Promise { + // Implementation +} + +// ❌ BAD: Using 'any' +function getMarket(id: any): Promise { + // Implementation +} +``` + +## React 모범 사례 + +### 컴포넌트 구조 + +```typescript +// ✅ GOOD: Functional component with types +interface ButtonProps { + children: React.ReactNode + onClick: () => void + disabled?: boolean + variant?: 'primary' | 'secondary' +} + +export function Button({ + children, + onClick, + disabled = false, + variant = 'primary' +}: ButtonProps) { + return ( + + ) +} + +// ❌ BAD: No types, unclear structure +export function Button(props) { + return +} +``` + +### 커스텀 Hook + +```typescript +// ✅ GOOD: Reusable custom hook +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => clearTimeout(handler) + }, [value, delay]) + + return debouncedValue +} + +// Usage +const debouncedQuery = useDebounce(searchQuery, 500) +``` + +### 상태 관리 + +```typescript +// ✅ GOOD: Proper state updates +const [count, setCount] = useState(0) + +// Functional update for state based on previous state +setCount(prev => prev + 1) + +// ❌ BAD: Direct state reference +setCount(count + 1) // Can be stale in async scenarios +``` + +### 조건부 렌더링 + +```typescript +// ✅ GOOD: Clear conditional rendering +{isLoading && } +{error && } +{data && } + +// ❌ BAD: Ternary hell +{isLoading ? : error ? : data ? : null} +``` + +## API 설계 표준 + +### REST API 컨벤션 + +``` +GET /api/markets # List all markets +GET /api/markets/:id # Get specific market +POST /api/markets # Create new market +PUT /api/markets/:id # Update market (full) +PATCH /api/markets/:id # Update market (partial) +DELETE /api/markets/:id # Delete market + +# Query parameters for filtering +GET /api/markets?status=active&limit=10&offset=0 +``` + +### 응답 형식 + +```typescript +// ✅ GOOD: Consistent response structure +interface ApiResponse { + success: boolean + data?: T + error?: string + meta?: { + total: number + page: number + limit: number + } +} + +// Success response +return NextResponse.json({ + success: true, + data: markets, + meta: { total: 100, page: 1, limit: 10 } +}) + +// Error response +return NextResponse.json({ + success: false, + error: 'Invalid request' +}, { status: 400 }) +``` + +### 입력 유효성 검사 + +```typescript +import { z } from 'zod' + +// ✅ GOOD: Schema validation +const CreateMarketSchema = z.object({ + name: z.string().min(1).max(200), + description: z.string().min(1).max(2000), + endDate: z.string().datetime(), + categories: z.array(z.string()).min(1) +}) + +export async function POST(request: Request) { + const body = await request.json() + + try { + const validated = CreateMarketSchema.parse(body) + // Proceed with validated data + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ + success: false, + error: 'Validation failed', + details: error.errors + }, { status: 400 }) + } + } +} +``` + +## 파일 구성 + +### 프로젝트 구조 + +``` +src/ +├── app/ # Next.js App Router +│ ├── api/ # API routes +│ ├── markets/ # Market pages +│ └── (auth)/ # Auth pages (route groups) +├── components/ # React components +│ ├── ui/ # Generic UI components +│ ├── forms/ # Form components +│ └── layouts/ # Layout components +├── hooks/ # Custom React hooks +├── lib/ # Utilities and configs +│ ├── api/ # API clients +│ ├── utils/ # Helper functions +│ └── constants/ # Constants +├── types/ # TypeScript types +└── styles/ # Global styles +``` + +### 파일 네이밍 + +``` +components/Button.tsx # PascalCase for components +hooks/useAuth.ts # camelCase with 'use' prefix +lib/formatDate.ts # camelCase for utilities +types/market.types.ts # camelCase with .types suffix +``` + +## 주석 및 문서화 + +### 주석을 작성해야 하는 경우 + +```typescript +// ✅ GOOD: Explain WHY, not WHAT +// Use exponential backoff to avoid overwhelming the API during outages +const delay = Math.min(1000 * Math.pow(2, retryCount), 30000) + +// Deliberately using mutation here for performance with large arrays +items.push(newItem) + +// ❌ BAD: Stating the obvious +// Increment counter by 1 +count++ + +// Set name to user's name +name = user.name +``` + +### 공개 API를 위한 JSDoc + +```typescript +/** + * Searches markets using semantic similarity. + * + * @param query - Natural language search query + * @param limit - Maximum number of results (default: 10) + * @returns Array of markets sorted by similarity score + * @throws {Error} If OpenAI API fails or Redis unavailable + * + * @example + * ```typescript + * const results = await searchMarkets('election', 5) + * console.log(results[0].name) // "Trump vs Biden" + * ``` + */ +export async function searchMarkets( + query: string, + limit: number = 10 +): Promise { + // Implementation +} +``` + +## 성능 모범 사례 + +### 메모이제이션 + +```typescript +import { useMemo, useCallback } from 'react' + +// ✅ GOOD: Memoize expensive computations +const sortedMarkets = useMemo(() => { + return markets.sort((a, b) => b.volume - a.volume) +}, [markets]) + +// ✅ GOOD: Memoize callbacks +const handleSearch = useCallback((query: string) => { + setSearchQuery(query) +}, []) +``` + +### 지연 로딩 + +```typescript +import { lazy, Suspense } from 'react' + +// ✅ GOOD: Lazy load heavy components +const HeavyChart = lazy(() => import('./HeavyChart')) + +export function Dashboard() { + return ( + }> + + + ) +} +``` + +### 데이터베이스 쿼리 + +```typescript +// ✅ GOOD: Select only needed columns +const { data } = await supabase + .from('markets') + .select('id, name, status') + .limit(10) + +// ❌ BAD: Select everything +const { data } = await supabase + .from('markets') + .select('*') +``` + +## 테스트 표준 + +### 테스트 구조 (AAA 패턴) + +```typescript +test('calculates similarity correctly', () => { + // Arrange + const vector1 = [1, 0, 0] + const vector2 = [0, 1, 0] + + // Act + const similarity = calculateCosineSimilarity(vector1, vector2) + + // Assert + expect(similarity).toBe(0) +}) +``` + +### 테스트 네이밍 + +```typescript +// ✅ GOOD: Descriptive test names +test('returns empty array when no markets match query', () => { }) +test('throws error when OpenAI API key is missing', () => { }) +test('falls back to substring search when Redis unavailable', () => { }) + +// ❌ BAD: Vague test names +test('works', () => { }) +test('test search', () => { }) +``` + +## 코드 스멜 감지 + +다음 안티패턴을 주의하세요: + +### 1. 긴 함수 +```typescript +// ❌ BAD: Function > 50 lines +function processMarketData() { + // 100 lines of code +} + +// ✅ GOOD: Split into smaller functions +function processMarketData() { + const validated = validateData() + const transformed = transformData(validated) + return saveData(transformed) +} +``` + +### 2. 깊은 중첩 +```typescript +// ❌ BAD: 5+ levels of nesting +if (user) { + if (user.isAdmin) { + if (market) { + if (market.isActive) { + if (hasPermission) { + // Do something + } + } + } + } +} + +// ✅ GOOD: Early returns +if (!user) return +if (!user.isAdmin) return +if (!market) return +if (!market.isActive) return +if (!hasPermission) return + +// Do something +``` + +### 3. 매직 넘버 +```typescript +// ❌ BAD: Unexplained numbers +if (retryCount > 3) { } +setTimeout(callback, 500) + +// ✅ GOOD: Named constants +const MAX_RETRIES = 3 +const DEBOUNCE_DELAY_MS = 500 + +if (retryCount > MAX_RETRIES) { } +setTimeout(callback, DEBOUNCE_DELAY_MS) +``` + +**기억하세요**: 코드 품질은 타협할 수 없습니다. 명확하고 유지보수 가능한 코드가 빠른 개발과 자신감 있는 리팩터링을 가능하게 합니다. diff --git a/docs/ko-KR/skills/continuous-learning-v2/SKILL.md b/docs/ko-KR/skills/continuous-learning-v2/SKILL.md new file mode 100644 index 00000000..31cdbd13 --- /dev/null +++ b/docs/ko-KR/skills/continuous-learning-v2/SKILL.md @@ -0,0 +1,363 @@ +--- +name: continuous-learning-v2 +description: 훅을 통해 세션을 관찰하고, 신뢰도 점수가 있는 원자적 본능을 생성하며, 이를 스킬/명령어/에이전트로 진화시키는 본능 기반 학습 시스템. v2.1에서는 프로젝트 간 오염을 방지하기 위한 프로젝트 범위 본능이 추가되었습니다. +origin: ECC +version: 2.1.0 +--- + +# 지속적 학습 v2.1 - 본능 기반 아키텍처 + +Claude Code 세션을 원자적 "본능(instinct)" -- 신뢰도 점수가 있는 작은 학습된 행동 -- 을 통해 재사용 가능한 지식으로 변환하는 고급 학습 시스템입니다. + +**v2.1**에서는 **프로젝트 범위 본능**이 추가되었습니다 -- React 패턴은 React 프로젝트에, Python 규칙은 Python 프로젝트에 유지되며, 범용 패턴(예: "항상 입력 유효성 검사")은 전역으로 공유됩니다. + +## 활성화 시점 + +- Claude Code 세션에서 자동 학습 설정 시 +- 훅을 통한 본능 기반 행동 추출 구성 시 +- 학습된 행동의 신뢰도 임계값 조정 시 +- 본능 라이브러리 검토, 내보내기, 가져오기 시 +- 본능을 완전한 스킬, 명령어 또는 에이전트로 진화 시 +- 프로젝트 범위 vs 전역 본능 관리 시 +- 프로젝트에서 전역 범위로 본능 승격 시 + +## v2.1의 새로운 기능 + +| 기능 | v2.0 | v2.1 | +|---------|------|------| +| 저장소 | 전역 (~/.claude/homunculus/) | 프로젝트 범위 (projects//) | +| 범위 | 모든 본능이 어디서나 적용 | 프로젝트 범위 + 전역 | +| 감지 | 없음 | git remote URL / 저장소 경로 | +| 승격 | 해당 없음 | 2개 이상 프로젝트에서 확인 시 프로젝트 -> 전역 | +| 명령어 | 4개 (status/evolve/export/import) | 6개 (+promote/projects) | +| 프로젝트 간 | 오염 위험 | 기본적으로 격리 | + +## v2의 새로운 기능 (v1 대비) + +| 기능 | v1 | v2 | +|---------|----|----| +| 관찰 | Stop 훅 (세션 종료) | PreToolUse/PostToolUse (100% 신뢰성) | +| 분석 | 메인 컨텍스트 | 백그라운드 에이전트 (Haiku) | +| 세분성 | 전체 스킬 | 원자적 "본능" | +| 신뢰도 | 없음 | 0.3-0.9 가중치 | +| 진화 | 직접 스킬로 | 본능 -> 클러스터 -> 스킬/명령어/에이전트 | +| 공유 | 없음 | 본능 내보내기/가져오기 | + +## 본능 모델 + +본능은 작은 학습된 행동입니다: + +```yaml +--- +id: prefer-functional-style +trigger: "when writing new functions" +confidence: 0.7 +domain: "code-style" +source: "session-observation" +scope: project +project_id: "a1b2c3d4e5f6" +project_name: "my-react-app" +--- + +# Prefer Functional Style + +## Action +Use functional patterns over classes when appropriate. + +## Evidence +- Observed 5 instances of functional pattern preference +- User corrected class-based approach to functional on 2025-01-15 +``` + +**속성:** +- **원자적** -- 하나의 트리거, 하나의 액션 +- **신뢰도 가중치** -- 0.3 = 잠정적, 0.9 = 거의 확실 +- **도메인 태그** -- code-style, testing, git, debugging, workflow 등 +- **증거 기반** -- 어떤 관찰이 이를 생성했는지 추적 +- **범위 인식** -- `project` (기본값) 또는 `global` + +## 작동 방식 + +``` +세션 활동 (git 저장소 내) + | + | 훅이 프롬프트 + 도구 사용을 캡처 (100% 신뢰성) + | + 프로젝트 컨텍스트 감지 (git remote / 저장소 경로) + v ++---------------------------------------------+ +| projects//observations.jsonl | +| (프롬프트, 도구 호출, 결과, 프로젝트) | ++---------------------------------------------+ + | + | 관찰자 에이전트가 읽기 (백그라운드, Haiku) + v ++---------------------------------------------+ +| 패턴 감지 | +| * 사용자 수정 -> 본능 | +| * 에러 해결 -> 본능 | +| * 반복 워크플로우 -> 본능 | +| * 범위 결정: 프로젝트 또는 전역? | ++---------------------------------------------+ + | + | 생성/업데이트 + v ++---------------------------------------------+ +| projects//instincts/personal/ | +| * prefer-functional.yaml (0.7) [project] | +| * use-react-hooks.yaml (0.9) [project] | ++---------------------------------------------+ +| instincts/personal/ (전역) | +| * always-validate-input.yaml (0.85) [global]| +| * grep-before-edit.yaml (0.6) [global] | ++---------------------------------------------+ + | + | /evolve 클러스터링 + /promote + v ++---------------------------------------------+ +| projects//evolved/ (프로젝트 범위) | +| evolved/ (전역) | +| * commands/new-feature.md | +| * skills/testing-workflow.md | +| * agents/refactor-specialist.md | ++---------------------------------------------+ +``` + +## 프로젝트 감지 + +시스템이 현재 프로젝트를 자동으로 감지합니다: + +1. **`CLAUDE_PROJECT_DIR` 환경 변수** (최우선 순위) +2. **`git remote get-url origin`** -- 이식 가능한 프로젝트 ID를 생성하기 위해 해시됨 (서로 다른 머신에서 같은 저장소는 같은 ID를 가짐) +3. **`git rev-parse --show-toplevel`** -- 저장소 경로를 사용한 폴백 (머신별) +4. **전역 폴백** -- 프로젝트가 감지되지 않으면 본능은 전역 범위로 이동 + +각 프로젝트는 12자 해시 ID를 받습니다 (예: `a1b2c3d4e5f6`). `~/.claude/homunculus/projects.json`의 레지스트리 파일이 ID를 사람이 읽을 수 있는 이름에 매핑합니다. + +## 빠른 시작 + +### 1. 관찰 훅 활성화 + +`~/.claude/settings.json`에 추가하세요. + +**플러그인으로 설치한 경우** (권장): + +```json +{ + "hooks": { + "PreToolUse": [{ + "matcher": "*", + "hooks": [{ + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh" + }] + }], + "PostToolUse": [{ + "matcher": "*", + "hooks": [{ + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/hooks/observe.sh" + }] + }] + } +} +``` + +**수동으로 `~/.claude/skills`에 설치한 경우**: + +```json +{ + "hooks": { + "PreToolUse": [{ + "matcher": "*", + "hooks": [{ + "type": "command", + "command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh" + }] + }], + "PostToolUse": [{ + "matcher": "*", + "hooks": [{ + "type": "command", + "command": "~/.claude/skills/continuous-learning-v2/hooks/observe.sh" + }] + }] + } +} +``` + +### 2. 디렉터리 구조 초기화 + +시스템은 첫 사용 시 자동으로 디렉터리를 생성하지만, 수동으로도 생성할 수 있습니다: + +```bash +# Global directories +mkdir -p ~/.claude/homunculus/{instincts/{personal,inherited},evolved/{agents,skills,commands},projects} + +# Project directories are auto-created when the hook first runs in a git repo +``` + +### 3. 본능 명령어 사용 + +```bash +/instinct-status # 학습된 본능 표시 (프로젝트 + 전역) +/evolve # 관련 본능을 스킬/명령어로 클러스터링 +/instinct-export # 본능을 파일로 내보내기 +/instinct-import # 다른 사람의 본능 가져오기 +/promote # 프로젝트 본능을 전역 범위로 승격 +/projects # 모든 알려진 프로젝트와 본능 개수 목록 +``` + +## 명령어 + +| 명령어 | 설명 | +|---------|-------------| +| `/instinct-status` | 모든 본능 (프로젝트 범위 + 전역) 을 신뢰도와 함께 표시 | +| `/evolve` | 관련 본능을 스킬/명령어로 클러스터링, 승격 제안 | +| `/instinct-export` | 본능 내보내기 (범위/도메인으로 필터링 가능) | +| `/instinct-import ` | 범위 제어와 함께 본능 가져오기 | +| `/promote [id]` | 프로젝트 본능을 전역 범위로 승격 | +| `/projects` | 모든 알려진 프로젝트와 본능 개수 목록 | + +## 구성 + +백그라운드 관찰자를 제어하려면 `config.json`을 편집하세요: + +```json +{ + "version": "2.1", + "observer": { + "enabled": false, + "run_interval_minutes": 5, + "min_observations_to_analyze": 20 + } +} +``` + +| 키 | 기본값 | 설명 | +|-----|---------|-------------| +| `observer.enabled` | `false` | 백그라운드 관찰자 에이전트 활성화 | +| `observer.run_interval_minutes` | `5` | 관찰자가 관찰 결과를 분석하는 빈도 | +| `observer.min_observations_to_analyze` | `20` | 분석 실행 전 최소 관찰 횟수 | + +기타 동작 (관찰 캡처, 본능 임계값, 프로젝트 범위, 승격 기준)은 `instinct-cli.py`와 `observe.sh`의 코드 기본값으로 구성됩니다. + +## 파일 구조 + +``` +~/.claude/homunculus/ ++-- identity.json # 프로필, 기술 수준 ++-- projects.json # 레지스트리: 프로젝트 해시 -> 이름/경로/리모트 ++-- observations.jsonl # 전역 관찰 결과 (폴백) ++-- instincts/ +| +-- personal/ # 전역 자동 학습된 본능 +| +-- inherited/ # 전역 가져온 본능 ++-- evolved/ +| +-- agents/ # 전역 생성된 에이전트 +| +-- skills/ # 전역 생성된 스킬 +| +-- commands/ # 전역 생성된 명령어 ++-- projects/ + +-- a1b2c3d4e5f6/ # 프로젝트 해시 (git remote URL에서) + | +-- observations.jsonl + | +-- observations.archive/ + | +-- instincts/ + | | +-- personal/ # 프로젝트별 자동 학습 + | | +-- inherited/ # 프로젝트별 가져온 것 + | +-- evolved/ + | +-- skills/ + | +-- commands/ + | +-- agents/ + +-- f6e5d4c3b2a1/ # 다른 프로젝트 + +-- ... +``` + +## 범위 결정 가이드 + +| 패턴 유형 | 범위 | 예시 | +|-------------|-------|---------| +| 언어/프레임워크 규칙 | **project** | "React hooks 사용", "Django REST 패턴 따르기" | +| 파일 구조 선호도 | **project** | "`__tests__`/에 테스트", "src/components/에 컴포넌트" | +| 코드 스타일 | **project** | "함수형 스타일 사용", "dataclasses 선호" | +| 에러 처리 전략 | **project** | "에러에 Result 타입 사용" | +| 보안 관행 | **global** | "사용자 입력 유효성 검사", "SQL 새니타이징" | +| 일반 모범 사례 | **global** | "테스트 먼저 작성", "항상 에러 처리" | +| 도구 워크플로우 선호도 | **global** | "편집 전 Grep", "쓰기 전 Read" | +| Git 관행 | **global** | "Conventional commits", "작고 집중된 커밋" | + +## 본능 승격 (프로젝트 -> 전역) + +같은 본능이 높은 신뢰도로 여러 프로젝트에 나타나면, 전역 범위로 승격할 후보가 됩니다. + +**자동 승격 기준:** +- 2개 이상 프로젝트에서 같은 본능 ID +- 평균 신뢰도 >= 0.8 + +**승격 방법:** + +```bash +# Promote a specific instinct +python3 instinct-cli.py promote prefer-explicit-errors + +# Auto-promote all qualifying instincts +python3 instinct-cli.py promote + +# Preview without changes +python3 instinct-cli.py promote --dry-run +``` + +`/evolve` 명령어도 승격 후보를 제안합니다. + +## 신뢰도 점수 + +신뢰도는 시간이 지남에 따라 진화합니다: + +| 점수 | 의미 | 동작 | +|-------|---------|----------| +| 0.3 | 잠정적 | 제안되지만 강제되지 않음 | +| 0.5 | 보통 | 관련 시 적용 | +| 0.7 | 강함 | 적용이 자동 승인됨 | +| 0.9 | 거의 확실 | 핵심 행동 | + +**신뢰도가 증가하는 경우:** +- 패턴이 반복적으로 관찰됨 +- 사용자가 제안된 행동을 수정하지 않음 +- 다른 소스의 유사한 본능이 동의함 + +**신뢰도가 감소하는 경우:** +- 사용자가 행동을 명시적으로 수정함 +- 패턴이 오랜 기간 관찰되지 않음 +- 모순되는 증거가 나타남 + +## 왜 관찰에 스킬이 아닌 훅을 사용하나요? + +> "v1은 관찰에 스킬을 의존했습니다. 스킬은 확률적입니다 -- Claude의 판단에 따라 약 50-80%의 확률로 실행됩니다." + +훅은 **100% 확률로** 결정적으로 실행됩니다. 이는 다음을 의미합니다: +- 모든 도구 호출이 관찰됨 +- 패턴이 누락되지 않음 +- 학습이 포괄적임 + +## 하위 호환성 + +v2.1은 v2.0 및 v1과 완전히 호환됩니다: +- `~/.claude/homunculus/instincts/`의 기존 전역 본능이 전역 본능으로 계속 작동 +- v1의 기존 `~/.claude/skills/learned/` 스킬이 계속 작동 +- Stop 훅이 여전히 실행됨 (하지만 이제 v2에도 데이터를 공급) +- 점진적 마이그레이션: 둘 다 병렬로 실행 가능 + +## 개인정보 보호 + +- 관찰 결과는 사용자의 머신에 **로컬**로 유지 +- 프로젝트 범위 본능은 프로젝트별로 격리됨 +- **본능**(패턴)만 내보낼 수 있음 -- 원시 관찰 결과는 아님 +- 실제 코드나 대화 내용은 공유되지 않음 +- 내보내기와 승격 대상을 사용자가 제어 + +## 관련 자료 + +- [Skill Creator](https://skill-creator.app) - 저장소 히스토리에서 본능 생성 +- Homunculus - v2 본능 기반 아키텍처에 영감을 준 커뮤니티 프로젝트 (원자적 관찰, 신뢰도 점수, 본능 진화 파이프라인) +- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 지속적 학습 섹션 + +--- + +*본능 기반 학습: Claude에게 당신의 패턴을 가르치기, 한 번에 하나의 프로젝트씩.* diff --git a/docs/ko-KR/skills/continuous-learning/SKILL.md b/docs/ko-KR/skills/continuous-learning/SKILL.md new file mode 100644 index 00000000..d279ec32 --- /dev/null +++ b/docs/ko-KR/skills/continuous-learning/SKILL.md @@ -0,0 +1,119 @@ +--- +name: continuous-learning +description: Claude Code 세션에서 재사용 가능한 패턴을 자동으로 추출하여 향후 사용을 위한 학습된 스킬로 저장합니다. +origin: ECC +--- + +# 지속적 학습 스킬 + +Claude Code 세션 종료 시 자동으로 평가하여 학습된 스킬로 저장할 수 있는 재사용 가능한 패턴을 추출합니다. + +## 활성화 시점 + +- Claude Code 세션에서 자동 패턴 추출을 설정할 때 +- 세션 평가를 위한 Stop Hook을 구성할 때 +- `~/.claude/skills/learned/`에서 학습된 스킬을 검토하거나 큐레이션할 때 +- 추출 임계값이나 패턴 카테고리를 조정할 때 +- v1 (이 방식)과 v2 (본능 기반) 접근법을 비교할 때 + +## 작동 방식 + +이 스킬은 각 세션 종료 시 **Stop Hook**으로 실행됩니다: + +1. **세션 평가**: 세션에 충분한 메시지가 있는지 확인 (기본값: 10개 이상) +2. **패턴 감지**: 세션에서 추출 가능한 패턴을 식별 +3. **스킬 추출**: 유용한 패턴을 `~/.claude/skills/learned/`에 저장 + +## 구성 + +`config.json`을 편집하여 사용자 지정합니다: + +```json +{ + "min_session_length": 10, + "extraction_threshold": "medium", + "auto_approve": false, + "learned_skills_path": "~/.claude/skills/learned/", + "patterns_to_detect": [ + "error_resolution", + "user_corrections", + "workarounds", + "debugging_techniques", + "project_specific" + ], + "ignore_patterns": [ + "simple_typos", + "one_time_fixes", + "external_api_issues" + ] +} +``` + +## 패턴 유형 + +| 패턴 | 설명 | +|---------|-------------| +| `error_resolution` | 특정 에러가 어떻게 해결되었는지 | +| `user_corrections` | 사용자 수정으로부터의 패턴 | +| `workarounds` | 프레임워크/라이브러리 특이점에 대한 해결책 | +| `debugging_techniques` | 효과적인 디버깅 접근법 | +| `project_specific` | 프로젝트 고유 컨벤션 | + +## Hook 설정 + +`~/.claude/settings.json`에 추가합니다: + +```json +{ + "hooks": { + "Stop": [{ + "matcher": "*", + "hooks": [{ + "type": "command", + "command": "~/.claude/skills/continuous-learning/evaluate-session.sh" + }] + }] + } +} +``` + +## Stop Hook을 사용하는 이유 + +- **경량**: 세션 종료 시 한 번만 실행 +- **비차단**: 모든 메시지에 지연을 추가하지 않음 +- **완전한 컨텍스트**: 전체 세션 트랜스크립트에 접근 가능 + +## 관련 항목 + +- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 지속적 학습 섹션 +- `/learn` 명령어 - 세션 중 수동 패턴 추출 + +--- + +## 비교 노트 (연구: 2025년 1월) + +### vs Homunculus + +Homunculus v2는 더 정교한 접근법을 취합니다: + +| 기능 | 우리의 접근법 | Homunculus v2 | +|---------|--------------|---------------| +| 관찰 | Stop Hook (세션 종료 시) | PreToolUse/PostToolUse Hook (100% 신뢰) | +| 분석 | 메인 컨텍스트 | 백그라운드 에이전트 (Haiku) | +| 세분성 | 완전한 스킬 | 원자적 "본능" | +| 신뢰도 | 없음 | 0.3-0.9 가중치 | +| 진화 | 스킬로 직접 | 본능 -> 클러스터 -> 스킬/명령어/에이전트 | +| 공유 | 없음 | 본능 내보내기/가져오기 | + +**Homunculus의 핵심 통찰:** +> "v1은 관찰을 스킬에 의존했습니다. 스킬은 확률적이어서 약 50-80%의 확률로 실행됩니다. v2는 관찰에 Hook(100% 신뢰)을 사용하고 본능을 학습된 행동의 원자 단위로 사용합니다." + +### 잠재적 v2 개선 사항 + +1. **본능 기반 학습** - 신뢰도 점수가 있는 더 작고 원자적인 행동 +2. **백그라운드 관찰자** - 병렬로 분석하는 Haiku 에이전트 +3. **신뢰도 감쇠** - 반박 시 본능의 신뢰도 감소 +4. **도메인 태깅** - code-style, testing, git, debugging 등 +5. **진화 경로** - 관련 본능을 스킬/명령어로 클러스터링 + +자세한 사양은 `docs/continuous-learning-v2-spec.md`를 참조하세요. diff --git a/docs/ko-KR/skills/eval-harness/SKILL.md b/docs/ko-KR/skills/eval-harness/SKILL.md new file mode 100644 index 00000000..6422e6de --- /dev/null +++ b/docs/ko-KR/skills/eval-harness/SKILL.md @@ -0,0 +1,270 @@ +--- +name: eval-harness +description: 평가 주도 개발(EDD) 원칙을 구현하는 Claude Code 세션용 공식 평가 프레임워크 +origin: ECC +tools: Read, Write, Edit, Bash, Grep, Glob +--- + +# 평가 하네스 스킬 + +Claude Code 세션을 위한 공식 평가 프레임워크로, 평가 주도 개발(EDD) 원칙을 구현합니다. + +## 활성화 시점 + +- AI 지원 워크플로우에 평가 주도 개발(EDD) 설정 시 +- Claude Code 작업 완료에 대한 합격/불합격 기준 정의 시 +- pass@k 메트릭으로 에이전트 신뢰성 측정 시 +- 프롬프트 또는 에이전트 변경에 대한 회귀 테스트 스위트 생성 시 +- 모델 버전 간 에이전트 성능 벤치마킹 시 + +## 철학 + +평가 주도 개발은 평가를 "AI 개발의 단위 테스트"로 취급합니다: +- 구현 전에 예상 동작 정의 +- 개발 중 지속적으로 평가 실행 +- 각 변경 시 회귀 추적 +- 신뢰성 측정을 위해 pass@k 메트릭 사용 + +## 평가 유형 + +### 기능 평가 +Claude가 이전에 할 수 없었던 것을 할 수 있는지 테스트: +```markdown +[CAPABILITY EVAL: feature-name] +Task: Description of what Claude should accomplish +Success Criteria: + - [ ] Criterion 1 + - [ ] Criterion 2 + - [ ] Criterion 3 +Expected Output: Description of expected result +``` + +### 회귀 평가 +변경 사항이 기존 기능을 손상시키지 않는지 확인: +```markdown +[REGRESSION EVAL: feature-name] +Baseline: SHA or checkpoint name +Tests: + - existing-test-1: PASS/FAIL + - existing-test-2: PASS/FAIL + - existing-test-3: PASS/FAIL +Result: X/Y passed (previously Y/Y) +``` + +## 채점자 유형 + +### 1. 코드 기반 채점자 +코드를 사용한 결정론적 검사: +```bash +# Check if file contains expected pattern +grep -q "export function handleAuth" src/auth.ts && echo "PASS" || echo "FAIL" + +# Check if tests pass +npm test -- --testPathPattern="auth" && echo "PASS" || echo "FAIL" + +# Check if build succeeds +npm run build && echo "PASS" || echo "FAIL" +``` + +### 2. 모델 기반 채점자 +Claude를 사용하여 개방형 출력 평가: +```markdown +[MODEL GRADER PROMPT] +Evaluate the following code change: +1. Does it solve the stated problem? +2. Is it well-structured? +3. Are edge cases handled? +4. Is error handling appropriate? + +Score: 1-5 (1=poor, 5=excellent) +Reasoning: [explanation] +``` + +### 3. 사람 채점자 +수동 검토 플래그: +```markdown +[HUMAN REVIEW REQUIRED] +Change: Description of what changed +Reason: Why human review is needed +Risk Level: LOW/MEDIUM/HIGH +``` + +## 메트릭 + +### pass@k +"k번 시도 중 최소 한 번 성공" +- pass@1: 첫 번째 시도 성공률 +- pass@3: 3번 시도 내 성공 +- 일반적인 목표: pass@3 > 90% + +### pass^k +"k번 시행 모두 성공" +- 신뢰성에 대한 더 높은 기준 +- pass^3: 3회 연속 성공 +- 핵심 경로에 사용 + +## 평가 워크플로우 + +### 1. 정의 (코딩 전) +```markdown +## EVAL DEFINITION: feature-xyz + +### Capability Evals +1. Can create new user account +2. Can validate email format +3. Can hash password securely + +### Regression Evals +1. Existing login still works +2. Session management unchanged +3. Logout flow intact + +### Success Metrics +- pass@3 > 90% for capability evals +- pass^3 = 100% for regression evals +``` + +### 2. 구현 +정의된 평가를 통과하기 위한 코드 작성. + +### 3. 평가 +```bash +# Run capability evals +[Run each capability eval, record PASS/FAIL] + +# Run regression evals +npm test -- --testPathPattern="existing" + +# Generate report +``` + +### 4. 보고서 +```markdown +EVAL REPORT: feature-xyz +======================== + +Capability Evals: + create-user: PASS (pass@1) + validate-email: PASS (pass@2) + hash-password: PASS (pass@1) + Overall: 3/3 passed + +Regression Evals: + login-flow: PASS + session-mgmt: PASS + logout-flow: PASS + Overall: 3/3 passed + +Metrics: + pass@1: 67% (2/3) + pass@3: 100% (3/3) + +Status: READY FOR REVIEW +``` + +## 통합 패턴 + +### 구현 전 +``` +/eval define feature-name +``` +`.claude/evals/feature-name.md`에 평가 정의 파일 생성 + +### 구현 중 +``` +/eval check feature-name +``` +현재 평가를 실행하고 상태 보고 + +### 구현 후 +``` +/eval report feature-name +``` +전체 평가 보고서 생성 + +## 평가 저장소 + +프로젝트에 평가 저장: +``` +.claude/ + evals/ + feature-xyz.md # 평가 정의 + feature-xyz.log # 평가 실행 이력 + baseline.json # 회귀 베이스라인 +``` + +## 모범 사례 + +1. **코딩 전에 평가 정의** - 성공 기준에 대한 명확한 사고를 강제 +2. **자주 평가 실행** - 회귀를 조기에 포착 +3. **시간에 따른 pass@k 추적** - 신뢰성 추세 모니터링 +4. **가능하면 코드 채점자 사용** - 결정론적 > 확률적 +5. **보안에는 사람 검토** - 보안 검사를 완전히 자동화하지 말 것 +6. **평가를 빠르게 유지** - 느린 평가는 실행되지 않음 +7. **코드와 함께 평가 버전 관리** - 평가는 일급 산출물 + +## 예시: 인증 추가 + +```markdown +## EVAL: add-authentication + +### Phase 1: 정의 (10분) +Capability Evals: +- [ ] User can register with email/password +- [ ] User can login with valid credentials +- [ ] Invalid credentials rejected with proper error +- [ ] Sessions persist across page reloads +- [ ] Logout clears session + +Regression Evals: +- [ ] Public routes still accessible +- [ ] API responses unchanged +- [ ] Database schema compatible + +### Phase 2: 구현 (가변) +[Write code] + +### Phase 3: 평가 +Run: /eval check add-authentication + +### Phase 4: 보고서 +EVAL REPORT: add-authentication +============================== +Capability: 5/5 passed (pass@3: 100%) +Regression: 3/3 passed (pass^3: 100%) +Status: SHIP IT +``` + +## 제품 평가 (v1.8) + +행동 품질을 단위 테스트만으로 포착할 수 없을 때 제품 평가를 사용하세요. + +### 채점자 유형 + +1. 코드 채점자 (결정론적 어서션) +2. 규칙 채점자 (정규식/스키마 제약 조건) +3. 모델 채점자 (LLM 심사위원 루브릭) +4. 사람 채점자 (모호한 출력에 대한 수동 판정) + +### pass@k 가이드 + +- `pass@1`: 직접 신뢰성 +- `pass@3`: 제어된 재시도 하에서의 실용적 신뢰성 +- `pass^3`: 안정성 테스트 (3회 모두 통과해야 함) + +권장 임계값: +- 기능 평가: pass@3 >= 0.90 +- 회귀 평가: 릴리스 핵심 경로에 pass^3 = 1.00 + +### 평가 안티패턴 + +- 알려진 평가 예시에 프롬프트 과적합 +- 정상 경로 출력만 측정 +- 합격률을 쫓으면서 비용과 지연 시간 변동 무시 +- 릴리스 게이트에 불안정한 채점자 허용 + +### 최소 평가 산출물 레이아웃 + +- `.claude/evals/.md` 정의 +- `.claude/evals/.log` 실행 이력 +- `docs/releases//eval-summary.md` 릴리스 스냅샷 diff --git a/docs/ko-KR/skills/frontend-patterns/SKILL.md b/docs/ko-KR/skills/frontend-patterns/SKILL.md new file mode 100644 index 00000000..cf3cdd45 --- /dev/null +++ b/docs/ko-KR/skills/frontend-patterns/SKILL.md @@ -0,0 +1,642 @@ +--- +name: frontend-patterns +description: React, Next.js, 상태 관리, 성능 최적화 및 UI 모범 사례를 위한 프론트엔드 개발 패턴. +origin: ECC +--- + +# 프론트엔드 개발 패턴 + +React, Next.js 및 고성능 사용자 인터페이스를 위한 모던 프론트엔드 패턴. + +## 활성화 시점 + +- React 컴포넌트를 구축할 때 (합성, props, 렌더링) +- 상태를 관리할 때 (useState, useReducer, Zustand, Context) +- 데이터 페칭을 구현할 때 (SWR, React Query, server components) +- 성능을 최적화할 때 (메모이제이션, 가상화, 코드 분할) +- 폼을 다룰 때 (유효성 검사, 제어 입력, Zod 스키마) +- 클라이언트 사이드 라우팅과 네비게이션을 처리할 때 +- 접근성 있고 반응형인 UI 패턴을 구축할 때 + +## 컴포넌트 패턴 + +### 상속보다 합성 + +```typescript +// ✅ GOOD: Component composition +interface CardProps { + children: React.ReactNode + variant?: 'default' | 'outlined' +} + +export function Card({ children, variant = 'default' }: CardProps) { + return
{children}
+} + +export function CardHeader({ children }: { children: React.ReactNode }) { + return
{children}
+} + +export function CardBody({ children }: { children: React.ReactNode }) { + return
{children}
+} + +// Usage + + Title + Content + +``` + +### Compound Components + +```typescript +interface TabsContextValue { + activeTab: string + setActiveTab: (tab: string) => void +} + +const TabsContext = createContext(undefined) + +export function Tabs({ children, defaultTab }: { + children: React.ReactNode + defaultTab: string +}) { + const [activeTab, setActiveTab] = useState(defaultTab) + + return ( + + {children} + + ) +} + +export function TabList({ children }: { children: React.ReactNode }) { + return
{children}
+} + +export function Tab({ id, children }: { id: string, children: React.ReactNode }) { + const context = useContext(TabsContext) + if (!context) throw new Error('Tab must be used within Tabs') + + return ( + + ) +} + +// Usage + + + Overview + Details + + +``` + +### Render Props 패턴 + +```typescript +interface DataLoaderProps { + url: string + children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode +} + +export function DataLoader({ url, children }: DataLoaderProps) { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + fetch(url) + .then(res => res.json()) + .then(setData) + .catch(setError) + .finally(() => setLoading(false)) + }, [url]) + + return <>{children(data, loading, error)} +} + +// Usage + url="/api/markets"> + {(markets, loading, error) => { + if (loading) return + if (error) return + return + }} + +``` + +## 커스텀 Hook 패턴 + +### 상태 관리 Hook + +```typescript +export function useToggle(initialValue = false): [boolean, () => void] { + const [value, setValue] = useState(initialValue) + + const toggle = useCallback(() => { + setValue(v => !v) + }, []) + + return [value, toggle] +} + +// Usage +const [isOpen, toggleOpen] = useToggle() +``` + +### 비동기 데이터 페칭 Hook + +```typescript +interface UseQueryOptions { + onSuccess?: (data: T) => void + onError?: (error: Error) => void + enabled?: boolean +} + +export function useQuery( + key: string, + fetcher: () => Promise, + options?: UseQueryOptions +) { + const [data, setData] = useState(null) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + const refetch = useCallback(async () => { + setLoading(true) + setError(null) + + try { + const result = await fetcher() + setData(result) + options?.onSuccess?.(result) + } catch (err) { + const error = err as Error + setError(error) + options?.onError?.(error) + } finally { + setLoading(false) + } + }, [fetcher, options]) + + useEffect(() => { + if (options?.enabled !== false) { + refetch() + } + }, [key, refetch, options?.enabled]) + + return { data, error, loading, refetch } +} + +// Usage +const { data: markets, loading, error, refetch } = useQuery( + 'markets', + () => fetch('/api/markets').then(r => r.json()), + { + onSuccess: data => console.log('Fetched', data.length, 'markets'), + onError: err => console.error('Failed:', err) + } +) +``` + +### Debounce Hook + +```typescript +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + return () => clearTimeout(handler) + }, [value, delay]) + + return debouncedValue +} + +// Usage +const [searchQuery, setSearchQuery] = useState('') +const debouncedQuery = useDebounce(searchQuery, 500) + +useEffect(() => { + if (debouncedQuery) { + performSearch(debouncedQuery) + } +}, [debouncedQuery]) +``` + +## 상태 관리 패턴 + +### Context + Reducer 패턴 + +```typescript +interface State { + markets: Market[] + selectedMarket: Market | null + loading: boolean +} + +type Action = + | { type: 'SET_MARKETS'; payload: Market[] } + | { type: 'SELECT_MARKET'; payload: Market } + | { type: 'SET_LOADING'; payload: boolean } + +function reducer(state: State, action: Action): State { + switch (action.type) { + case 'SET_MARKETS': + return { ...state, markets: action.payload } + case 'SELECT_MARKET': + return { ...state, selectedMarket: action.payload } + case 'SET_LOADING': + return { ...state, loading: action.payload } + default: + return state + } +} + +const MarketContext = createContext<{ + state: State + dispatch: Dispatch +} | undefined>(undefined) + +export function MarketProvider({ children }: { children: React.ReactNode }) { + const [state, dispatch] = useReducer(reducer, { + markets: [], + selectedMarket: null, + loading: false + }) + + return ( + + {children} + + ) +} + +export function useMarkets() { + const context = useContext(MarketContext) + if (!context) throw new Error('useMarkets must be used within MarketProvider') + return context +} +``` + +## 성능 최적화 + +### 메모이제이션 + +```typescript +// ✅ useMemo for expensive computations +const sortedMarkets = useMemo(() => { + return markets.sort((a, b) => b.volume - a.volume) +}, [markets]) + +// ✅ useCallback for functions passed to children +const handleSearch = useCallback((query: string) => { + setSearchQuery(query) +}, []) + +// ✅ React.memo for pure components +export const MarketCard = React.memo(({ market }) => { + return ( +
+

{market.name}

+

{market.description}

+
+ ) +}) +``` + +### 코드 분할 및 지연 로딩 + +```typescript +import { lazy, Suspense } from 'react' + +// ✅ Lazy load heavy components +const HeavyChart = lazy(() => import('./HeavyChart')) +const ThreeJsBackground = lazy(() => import('./ThreeJsBackground')) + +export function Dashboard() { + return ( +
+ }> + + + + + + +
+ ) +} +``` + +### 긴 리스트를 위한 가상화 + +```typescript +import { useVirtualizer } from '@tanstack/react-virtual' + +export function VirtualMarketList({ markets }: { markets: Market[] }) { + const parentRef = useRef(null) + + const virtualizer = useVirtualizer({ + count: markets.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 100, // Estimated row height + overscan: 5 // Extra items to render + }) + + return ( +
+
+ {virtualizer.getVirtualItems().map(virtualRow => ( +
+ +
+ ))} +
+
+ ) +} +``` + +## 폼 처리 패턴 + +### 유효성 검사가 포함된 제어 폼 + +```typescript +interface FormData { + name: string + description: string + endDate: string +} + +interface FormErrors { + name?: string + description?: string + endDate?: string +} + +export function CreateMarketForm() { + const [formData, setFormData] = useState({ + name: '', + description: '', + endDate: '' + }) + + const [errors, setErrors] = useState({}) + + const validate = (): boolean => { + const newErrors: FormErrors = {} + + if (!formData.name.trim()) { + newErrors.name = 'Name is required' + } else if (formData.name.length > 200) { + newErrors.name = 'Name must be under 200 characters' + } + + if (!formData.description.trim()) { + newErrors.description = 'Description is required' + } + + if (!formData.endDate) { + newErrors.endDate = 'End date is required' + } + + setErrors(newErrors) + return Object.keys(newErrors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!validate()) return + + try { + await createMarket(formData) + // Success handling + } catch (error) { + // Error handling + } + } + + return ( +
+ setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="Market name" + /> + {errors.name && {errors.name}} + + {/* Other fields */} + + +
+ ) +} +``` + +## Error Boundary 패턴 + +```typescript +interface ErrorBoundaryState { + hasError: boolean + error: Error | null +} + +export class ErrorBoundary extends React.Component< + { children: React.ReactNode }, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { + hasError: false, + error: null + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Error boundary caught:', error, errorInfo) + } + + render() { + if (this.state.hasError) { + return ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ) + } + + return this.props.children + } +} + +// Usage + + + +``` + +## 애니메이션 패턴 + +### Framer Motion 애니메이션 + +```typescript +import { motion, AnimatePresence } from 'framer-motion' + +// ✅ List animations +export function AnimatedMarketList({ markets }: { markets: Market[] }) { + return ( + + {markets.map(market => ( + + + + ))} + + ) +} + +// ✅ Modal animations +export function Modal({ isOpen, onClose, children }: ModalProps) { + return ( + + {isOpen && ( + <> + + + {children} + + + )} + + ) +} +``` + +## 접근성 패턴 + +### 키보드 네비게이션 + +```typescript +export function Dropdown({ options, onSelect }: DropdownProps) { + const [isOpen, setIsOpen] = useState(false) + const [activeIndex, setActiveIndex] = useState(0) + + const handleKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setActiveIndex(i => Math.min(i + 1, options.length - 1)) + break + case 'ArrowUp': + e.preventDefault() + setActiveIndex(i => Math.max(i - 1, 0)) + break + case 'Enter': + e.preventDefault() + onSelect(options[activeIndex]) + setIsOpen(false) + break + case 'Escape': + setIsOpen(false) + break + } + } + + return ( +
+ {/* Dropdown implementation */} +
+ ) +} +``` + +### 포커스 관리 + +```typescript +export function Modal({ isOpen, onClose, children }: ModalProps) { + const modalRef = useRef(null) + const previousFocusRef = useRef(null) + + useEffect(() => { + if (isOpen) { + // Save currently focused element + previousFocusRef.current = document.activeElement as HTMLElement + + // Focus modal + modalRef.current?.focus() + } else { + // Restore focus when closing + previousFocusRef.current?.focus() + } + }, [isOpen]) + + return isOpen ? ( +
e.key === 'Escape' && onClose()} + > + {children} +
+ ) : null +} +``` + +**기억하세요**: 모던 프론트엔드 패턴은 유지보수 가능하고 고성능인 사용자 인터페이스를 가능하게 합니다. 프로젝트 복잡도에 맞는 패턴을 선택하세요. diff --git a/docs/ko-KR/skills/golang-patterns/SKILL.md b/docs/ko-KR/skills/golang-patterns/SKILL.md new file mode 100644 index 00000000..63548e87 --- /dev/null +++ b/docs/ko-KR/skills/golang-patterns/SKILL.md @@ -0,0 +1,674 @@ +--- +name: golang-patterns +description: 견고하고 효율적이며 유지보수 가능한 Go 애플리케이션 구축을 위한 관용적 Go 패턴, 모범 사례 및 규칙. +origin: ECC +--- + +# Go 개발 패턴 + +견고하고 효율적이며 유지보수 가능한 애플리케이션 구축을 위한 관용적 Go 패턴과 모범 사례. + +## 활성화 시점 + +- 새로운 Go 코드 작성 시 +- Go 코드 리뷰 시 +- 기존 Go 코드 리팩토링 시 +- Go 패키지/모듈 설계 시 + +## 핵심 원칙 + +### 1. 단순성과 명확성 + +Go는 영리함보다 단순성을 선호합니다. 코드는 명확하고 읽기 쉬워야 합니다. + +```go +// Good: Clear and direct +func GetUser(id string) (*User, error) { + user, err := db.FindUser(id) + if err != nil { + return nil, fmt.Errorf("get user %s: %w", id, err) + } + return user, nil +} + +// Bad: Overly clever +func GetUser(id string) (*User, error) { + return func() (*User, error) { + if u, e := db.FindUser(id); e == nil { + return u, nil + } else { + return nil, e + } + }() +} +``` + +### 2. 제로 값을 유용하게 만들기 + +제로 값이 초기화 없이 즉시 사용 가능하도록 타입을 설계하세요. + +```go +// Good: Zero value is useful +type Counter struct { + mu sync.Mutex + count int // zero value is 0, ready to use +} + +func (c *Counter) Inc() { + c.mu.Lock() + c.count++ + c.mu.Unlock() +} + +// Good: bytes.Buffer works with zero value +var buf bytes.Buffer +buf.WriteString("hello") + +// Bad: Requires initialization +type BadCounter struct { + counts map[string]int // nil map will panic +} +``` + +### 3. 인터페이스를 받고 구조체를 반환하기 + +함수는 인터페이스 매개변수를 받고 구체적 타입을 반환해야 합니다. + +```go +// Good: Accepts interface, returns concrete type +func ProcessData(r io.Reader) (*Result, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + return &Result{Data: data}, nil +} + +// Bad: Returns interface (hides implementation details unnecessarily) +func ProcessData(r io.Reader) (io.Reader, error) { + // ... +} +``` + +## 에러 처리 패턴 + +### 컨텍스트가 있는 에러 래핑 + +```go +// Good: Wrap errors with context +func LoadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("load config %s: %w", path, err) + } + + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse config %s: %w", path, err) + } + + return &cfg, nil +} +``` + +### 커스텀 에러 타입 + +```go +// Define domain-specific errors +type ValidationError struct { + Field string + Message string +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) +} + +// Sentinel errors for common cases +var ( + ErrNotFound = errors.New("resource not found") + ErrUnauthorized = errors.New("unauthorized") + ErrInvalidInput = errors.New("invalid input") +) +``` + +### errors.Is와 errors.As를 사용한 에러 확인 + +```go +func HandleError(err error) { + // Check for specific error + if errors.Is(err, sql.ErrNoRows) { + log.Println("No records found") + return + } + + // Check for error type + var validationErr *ValidationError + if errors.As(err, &validationErr) { + log.Printf("Validation error on field %s: %s", + validationErr.Field, validationErr.Message) + return + } + + // Unknown error + log.Printf("Unexpected error: %v", err) +} +``` + +### 에러를 절대 무시하지 말 것 + +```go +// Bad: Ignoring error with blank identifier +result, _ := doSomething() + +// Good: Handle or explicitly document why it's safe to ignore +result, err := doSomething() +if err != nil { + return err +} + +// Acceptable: When error truly doesn't matter (rare) +_ = writer.Close() // Best-effort cleanup, error logged elsewhere +``` + +## 동시성 패턴 + +### 워커 풀 + +```go +func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) { + var wg sync.WaitGroup + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for job := range jobs { + results <- process(job) + } + }() + } + + wg.Wait() + close(results) +} +``` + +### 취소 및 타임아웃을 위한 Context + +```go +func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("fetch %s: %w", url, err) + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) +} +``` + +### 우아한 종료 + +```go +func GracefulShutdown(server *http.Server) { + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + <-quit + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exited") +} +``` + +### 조율된 고루틴을 위한 errgroup + +```go +import "golang.org/x/sync/errgroup" + +func FetchAll(ctx context.Context, urls []string) ([][]byte, error) { + g, ctx := errgroup.WithContext(ctx) + results := make([][]byte, len(urls)) + + for i, url := range urls { + i, url := i, url // Capture loop variables + g.Go(func() error { + data, err := FetchWithTimeout(ctx, url) + if err != nil { + return err + } + results[i] = data + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, err + } + return results, nil +} +``` + +### 고루틴 누수 방지 + +```go +// Bad: Goroutine leak if context is cancelled +func leakyFetch(ctx context.Context, url string) <-chan []byte { + ch := make(chan []byte) + go func() { + data, _ := fetch(url) + ch <- data // Blocks forever if no receiver + }() + return ch +} + +// Good: Properly handles cancellation +func safeFetch(ctx context.Context, url string) <-chan []byte { + ch := make(chan []byte, 1) // Buffered channel + go func() { + data, err := fetch(url) + if err != nil { + return + } + select { + case ch <- data: + case <-ctx.Done(): + } + }() + return ch +} +``` + +## 인터페이스 설계 + +### 작고 집중된 인터페이스 + +```go +// Good: Single-method interfaces +type Reader interface { + Read(p []byte) (n int, err error) +} + +type Writer interface { + Write(p []byte) (n int, err error) +} + +type Closer interface { + Close() error +} + +// Compose interfaces as needed +type ReadWriteCloser interface { + Reader + Writer + Closer +} +``` + +### 사용되는 곳에서 인터페이스 정의 + +```go +// In the consumer package, not the provider +package service + +// UserStore defines what this service needs +type UserStore interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} + +type Service struct { + store UserStore +} + +// Concrete implementation can be in another package +// It doesn't need to know about this interface +``` + +### 타입 어서션을 통한 선택적 동작 + +```go +type Flusher interface { + Flush() error +} + +func WriteAndFlush(w io.Writer, data []byte) error { + if _, err := w.Write(data); err != nil { + return err + } + + // Flush if supported + if f, ok := w.(Flusher); ok { + return f.Flush() + } + return nil +} +``` + +## 패키지 구성 + +### 표준 프로젝트 레이아웃 + +```text +myproject/ +├── cmd/ +│ └── myapp/ +│ └── main.go # Entry point +├── internal/ +│ ├── handler/ # HTTP handlers +│ ├── service/ # Business logic +│ ├── repository/ # Data access +│ └── config/ # Configuration +├── pkg/ +│ └── client/ # Public API client +├── api/ +│ └── v1/ # API definitions (proto, OpenAPI) +├── testdata/ # Test fixtures +├── go.mod +├── go.sum +└── Makefile +``` + +### 패키지 명명 + +```go +// Good: Short, lowercase, no underscores +package http +package json +package user + +// Bad: Verbose, mixed case, or redundant +package httpHandler +package json_parser +package userService // Redundant 'Service' suffix +``` + +### 패키지 수준 상태 피하기 + +```go +// Bad: Global mutable state +var db *sql.DB + +func init() { + db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL")) +} + +// Good: Dependency injection +type Server struct { + db *sql.DB +} + +func NewServer(db *sql.DB) *Server { + return &Server{db: db} +} +``` + +## 구조체 설계 + +### 함수형 옵션 패턴 + +```go +type Server struct { + addr string + timeout time.Duration + logger *log.Logger +} + +type Option func(*Server) + +func WithTimeout(d time.Duration) Option { + return func(s *Server) { + s.timeout = d + } +} + +func WithLogger(l *log.Logger) Option { + return func(s *Server) { + s.logger = l + } +} + +func NewServer(addr string, opts ...Option) *Server { + s := &Server{ + addr: addr, + timeout: 30 * time.Second, // default + logger: log.Default(), // default + } + for _, opt := range opts { + opt(s) + } + return s +} + +// Usage +server := NewServer(":8080", + WithTimeout(60*time.Second), + WithLogger(customLogger), +) +``` + +### 합성을 위한 임베딩 + +```go +type Logger struct { + prefix string +} + +func (l *Logger) Log(msg string) { + fmt.Printf("[%s] %s\n", l.prefix, msg) +} + +type Server struct { + *Logger // Embedding - Server gets Log method + addr string +} + +func NewServer(addr string) *Server { + return &Server{ + Logger: &Logger{prefix: "SERVER"}, + addr: addr, + } +} + +// Usage +s := NewServer(":8080") +s.Log("Starting...") // Calls embedded Logger.Log +``` + +## 메모리 및 성능 + +### 크기를 알 때 슬라이스 미리 할당 + +```go +// Bad: Grows slice multiple times +func processItems(items []Item) []Result { + var results []Result + for _, item := range items { + results = append(results, process(item)) + } + return results +} + +// Good: Single allocation +func processItems(items []Item) []Result { + results := make([]Result, 0, len(items)) + for _, item := range items { + results = append(results, process(item)) + } + return results +} +``` + +### 빈번한 할당에 sync.Pool 사용 + +```go +var bufferPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +func ProcessRequest(data []byte) []byte { + buf := bufferPool.Get().(*bytes.Buffer) + defer func() { + buf.Reset() + bufferPool.Put(buf) + }() + + buf.Write(data) + // Process... + return buf.Bytes() +} +``` + +### 루프에서 문자열 연결 피하기 + +```go +// Bad: Creates many string allocations +func join(parts []string) string { + var result string + for _, p := range parts { + result += p + "," + } + return result +} + +// Good: Single allocation with strings.Builder +func join(parts []string) string { + var sb strings.Builder + for i, p := range parts { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(p) + } + return sb.String() +} + +// Best: Use standard library +func join(parts []string) string { + return strings.Join(parts, ",") +} +``` + +## Go 도구 통합 + +### 필수 명령어 + +```bash +# Build and run +go build ./... +go run ./cmd/myapp + +# Testing +go test ./... +go test -race ./... +go test -cover ./... + +# Static analysis +go vet ./... +staticcheck ./... +golangci-lint run + +# Module management +go mod tidy +go mod verify + +# Formatting +gofmt -w . +goimports -w . +``` + +### 권장 린터 구성 (.golangci.yml) + +```yaml +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - gofmt + - goimports + - misspell + - unconvert + - unparam + +linters-settings: + errcheck: + check-type-assertions: true + govet: + check-shadowing: true + +issues: + exclude-use-default: false +``` + +## 빠른 참조: Go 관용구 + +| 관용구 | 설명 | +|-------|-------------| +| Accept interfaces, return structs | 함수는 인터페이스 매개변수를 받고 구체적 타입을 반환 | +| Errors are values | 에러를 예외가 아닌 일급 값으로 취급 | +| Don't communicate by sharing memory | 고루틴 간 조율에 채널 사용 | +| Make the zero value useful | 타입이 명시적 초기화 없이 작동해야 함 | +| A little copying is better than a little dependency | 불필요한 외부 의존성 피하기 | +| Clear is better than clever | 영리함보다 가독성 우선 | +| gofmt is no one's favorite but everyone's friend | 항상 gofmt/goimports로 포맷팅 | +| Return early | 에러를 먼저 처리하고 정상 경로는 들여쓰기 없이 유지 | + +## 피해야 할 안티패턴 + +```go +// Bad: Naked returns in long functions +func process() (result int, err error) { + // ... 50 lines ... + return // What is being returned? +} + +// Bad: Using panic for control flow +func GetUser(id string) *User { + user, err := db.Find(id) + if err != nil { + panic(err) // Don't do this + } + return user +} + +// Bad: Passing context in struct +type Request struct { + ctx context.Context // Context should be first param + ID string +} + +// Good: Context as first parameter +func ProcessRequest(ctx context.Context, id string) error { + // ... +} + +// Bad: Mixing value and pointer receivers +type Counter struct{ n int } +func (c Counter) Value() int { return c.n } // Value receiver +func (c *Counter) Increment() { c.n++ } // Pointer receiver +// Pick one style and be consistent +``` + +**기억하세요**: Go 코드는 최고의 의미에서 지루해야 합니다 - 예측 가능하고, 일관적이며, 이해하기 쉽게. 의심스러울 때는 단순하게 유지하세요. diff --git a/docs/ko-KR/skills/golang-testing/SKILL.md b/docs/ko-KR/skills/golang-testing/SKILL.md new file mode 100644 index 00000000..05ce0e48 --- /dev/null +++ b/docs/ko-KR/skills/golang-testing/SKILL.md @@ -0,0 +1,720 @@ +--- +name: golang-testing +description: 테이블 주도 테스트, 서브테스트, 벤치마크, 퍼징, 테스트 커버리지를 포함한 Go 테스팅 패턴. 관용적 Go 관행과 함께 TDD 방법론을 따릅니다. +origin: ECC +--- + +# Go 테스팅 패턴 + +TDD 방법론을 따르는 신뢰할 수 있고 유지보수 가능한 테스트 작성을 위한 포괄적인 Go 테스팅 패턴. + +## 활성화 시점 + +- 새로운 Go 함수나 메서드 작성 시 +- 기존 코드에 테스트 커버리지 추가 시 +- 성능이 중요한 코드에 벤치마크 생성 시 +- 입력 유효성 검사를 위한 퍼즈 테스트 구현 시 +- Go 프로젝트에서 TDD 워크플로우 따를 시 + +## Go에서의 TDD 워크플로우 + +### RED-GREEN-REFACTOR 사이클 + +``` +RED → Write a failing test first +GREEN → Write minimal code to pass the test +REFACTOR → Improve code while keeping tests green +REPEAT → Continue with next requirement +``` + +### Go에서의 단계별 TDD + +```go +// Step 1: Define the interface/signature +// calculator.go +package calculator + +func Add(a, b int) int { + panic("not implemented") // Placeholder +} + +// Step 2: Write failing test (RED) +// calculator_test.go +package calculator + +import "testing" + +func TestAdd(t *testing.T) { + got := Add(2, 3) + want := 5 + if got != want { + t.Errorf("Add(2, 3) = %d; want %d", got, want) + } +} + +// Step 3: Run test - verify FAIL +// $ go test +// --- FAIL: TestAdd (0.00s) +// panic: not implemented + +// Step 4: Implement minimal code (GREEN) +func Add(a, b int) int { + return a + b +} + +// Step 5: Run test - verify PASS +// $ go test +// PASS + +// Step 6: Refactor if needed, verify tests still pass +``` + +## 테이블 주도 테스트 + +Go 테스트의 표준 패턴. 최소한의 코드로 포괄적인 커버리지를 가능하게 합니다. + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive numbers", 2, 3, 5}, + {"negative numbers", -1, -2, -3}, + {"zero values", 0, 0, 0}, + {"mixed signs", -1, 1, 0}, + {"large numbers", 1000000, 2000000, 3000000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Add(tt.a, tt.b) + if got != tt.expected { + t.Errorf("Add(%d, %d) = %d; want %d", + tt.a, tt.b, got, tt.expected) + } + }) + } +} +``` + +### 에러 케이스가 있는 테이블 주도 테스트 + +```go +func TestParseConfig(t *testing.T) { + tests := []struct { + name string + input string + want *Config + wantErr bool + }{ + { + name: "valid config", + input: `{"host": "localhost", "port": 8080}`, + want: &Config{Host: "localhost", Port: 8080}, + }, + { + name: "invalid JSON", + input: `{invalid}`, + wantErr: true, + }, + { + name: "empty input", + input: "", + wantErr: true, + }, + { + name: "minimal config", + input: `{}`, + want: &Config{}, // Zero value config + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseConfig(tt.input) + + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("got %+v; want %+v", got, tt.want) + } + }) + } +} +``` + +## 서브테스트 및 서브벤치마크 + +### 관련 테스트 구성 + +```go +func TestUser(t *testing.T) { + // Setup shared by all subtests + db := setupTestDB(t) + + t.Run("Create", func(t *testing.T) { + user := &User{Name: "Alice"} + err := db.CreateUser(user) + if err != nil { + t.Fatalf("CreateUser failed: %v", err) + } + if user.ID == "" { + t.Error("expected user ID to be set") + } + }) + + t.Run("Get", func(t *testing.T) { + user, err := db.GetUser("alice-id") + if err != nil { + t.Fatalf("GetUser failed: %v", err) + } + if user.Name != "Alice" { + t.Errorf("got name %q; want %q", user.Name, "Alice") + } + }) + + t.Run("Update", func(t *testing.T) { + // ... + }) + + t.Run("Delete", func(t *testing.T) { + // ... + }) +} +``` + +### 병렬 서브테스트 + +```go +func TestParallel(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"case1", "input1"}, + {"case2", "input2"}, + {"case3", "input3"}, + } + + for _, tt := range tests { + tt := tt // Capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() // Run subtests in parallel + result := Process(tt.input) + // assertions... + _ = result + }) + } +} +``` + +## 테스트 헬퍼 + +### 헬퍼 함수 + +```go +func setupTestDB(t *testing.T) *sql.DB { + t.Helper() // Marks this as a helper function + + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + + // Cleanup when test finishes + t.Cleanup(func() { + db.Close() + }) + + // Run migrations + if _, err := db.Exec(schema); err != nil { + t.Fatalf("failed to create schema: %v", err) + } + + return db +} + +func assertNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func assertEqual[T comparable](t *testing.T, got, want T) { + t.Helper() + if got != want { + t.Errorf("got %v; want %v", got, want) + } +} +``` + +### 임시 파일 및 디렉터리 + +```go +func TestFileProcessing(t *testing.T) { + // Create temp directory - automatically cleaned up + tmpDir := t.TempDir() + + // Create test file + testFile := filepath.Join(tmpDir, "test.txt") + err := os.WriteFile(testFile, []byte("test content"), 0644) + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + // Run test + result, err := ProcessFile(testFile) + if err != nil { + t.Fatalf("ProcessFile failed: %v", err) + } + + // Assert... + _ = result +} +``` + +## 골든 파일 + +`testdata/`에 저장된 예상 출력 파일에 대한 테스트. + +```go +var update = flag.Bool("update", false, "update golden files") + +func TestRender(t *testing.T) { + tests := []struct { + name string + input Template + }{ + {"simple", Template{Name: "test"}}, + {"complex", Template{Name: "test", Items: []string{"a", "b"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Render(tt.input) + + golden := filepath.Join("testdata", tt.name+".golden") + + if *update { + // Update golden file: go test -update + err := os.WriteFile(golden, got, 0644) + if err != nil { + t.Fatalf("failed to update golden file: %v", err) + } + } + + want, err := os.ReadFile(golden) + if err != nil { + t.Fatalf("failed to read golden file: %v", err) + } + + if !bytes.Equal(got, want) { + t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want) + } + }) + } +} +``` + +## 인터페이스를 사용한 모킹 + +### 인터페이스 기반 모킹 + +```go +// Define interface for dependencies +type UserRepository interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} + +// Production implementation +type PostgresUserRepository struct { + db *sql.DB +} + +func (r *PostgresUserRepository) GetUser(id string) (*User, error) { + // Real database query +} + +// Mock implementation for tests +type MockUserRepository struct { + GetUserFunc func(id string) (*User, error) + SaveUserFunc func(user *User) error +} + +func (m *MockUserRepository) GetUser(id string) (*User, error) { + return m.GetUserFunc(id) +} + +func (m *MockUserRepository) SaveUser(user *User) error { + return m.SaveUserFunc(user) +} + +// Test using mock +func TestUserService(t *testing.T) { + mock := &MockUserRepository{ + GetUserFunc: func(id string) (*User, error) { + if id == "123" { + return &User{ID: "123", Name: "Alice"}, nil + } + return nil, ErrNotFound + }, + } + + service := NewUserService(mock) + + user, err := service.GetUserProfile("123") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if user.Name != "Alice" { + t.Errorf("got name %q; want %q", user.Name, "Alice") + } +} +``` + +## 벤치마크 + +### 기본 벤치마크 + +```go +func BenchmarkProcess(b *testing.B) { + data := generateTestData(1000) + b.ResetTimer() // Don't count setup time + + for i := 0; i < b.N; i++ { + Process(data) + } +} + +// Run: go test -bench=BenchmarkProcess -benchmem +// Output: BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op +``` + +### 다양한 크기의 벤치마크 + +```go +func BenchmarkSort(b *testing.B) { + sizes := []int{100, 1000, 10000, 100000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + data := generateRandomSlice(size) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + // Make a copy to avoid sorting already sorted data + tmp := make([]int, len(data)) + copy(tmp, data) + sort.Ints(tmp) + } + }) + } +} +``` + +### 메모리 할당 벤치마크 + +```go +func BenchmarkStringConcat(b *testing.B) { + parts := []string{"hello", "world", "foo", "bar", "baz"} + + b.Run("plus", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var s string + for _, p := range parts { + s += p + } + _ = s + } + }) + + b.Run("builder", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var sb strings.Builder + for _, p := range parts { + sb.WriteString(p) + } + _ = sb.String() + } + }) + + b.Run("join", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = strings.Join(parts, "") + } + }) +} +``` + +## 퍼징 (Go 1.18+) + +### 기본 퍼즈 테스트 + +```go +func FuzzParseJSON(f *testing.F) { + // Add seed corpus + f.Add(`{"name": "test"}`) + f.Add(`{"count": 123}`) + f.Add(`[]`) + f.Add(`""`) + + f.Fuzz(func(t *testing.T, input string) { + var result map[string]interface{} + err := json.Unmarshal([]byte(input), &result) + + if err != nil { + // Invalid JSON is expected for random input + return + } + + // If parsing succeeded, re-encoding should work + _, err = json.Marshal(result) + if err != nil { + t.Errorf("Marshal failed after successful Unmarshal: %v", err) + } + }) +} + +// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s +``` + +### 다중 입력 퍼즈 테스트 + +```go +func FuzzCompare(f *testing.F) { + f.Add("hello", "world") + f.Add("", "") + f.Add("abc", "abc") + + f.Fuzz(func(t *testing.T, a, b string) { + result := Compare(a, b) + + // Property: Compare(a, a) should always equal 0 + if a == b && result != 0 { + t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result) + } + + // Property: Compare(a, b) and Compare(b, a) should have opposite signs + reverse := Compare(b, a) + if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) { + if result != 0 || reverse != 0 { + t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent", + a, b, result, b, a, reverse) + } + } + }) +} +``` + +## 테스트 커버리지 + +### 커버리지 실행 + +```bash +# Basic coverage +go test -cover ./... + +# Generate coverage profile +go test -coverprofile=coverage.out ./... + +# View coverage in browser +go tool cover -html=coverage.out + +# View coverage by function +go tool cover -func=coverage.out + +# Coverage with race detection +go test -race -coverprofile=coverage.out ./... +``` + +### 커버리지 목표 + +| 코드 유형 | 목표 | +|-----------|--------| +| 핵심 비즈니스 로직 | 100% | +| 공개 API | 90%+ | +| 일반 코드 | 80%+ | +| 생성된 코드 | 제외 | + +### 생성된 코드를 커버리지에서 제외 + +```go +//go:generate mockgen -source=interface.go -destination=mock_interface.go + +// In coverage profile, exclude with build tags: +// go test -cover -tags=!generate ./... +``` + +## HTTP 핸들러 테스팅 + +```go +func TestHealthHandler(t *testing.T) { + // Create request + req := httptest.NewRequest(http.MethodGet, "/health", nil) + w := httptest.NewRecorder() + + // Call handler + HealthHandler(w, req) + + // Check response + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK) + } + + body, _ := io.ReadAll(resp.Body) + if string(body) != "OK" { + t.Errorf("got body %q; want %q", body, "OK") + } +} + +func TestAPIHandler(t *testing.T) { + tests := []struct { + name string + method string + path string + body string + wantStatus int + wantBody string + }{ + { + name: "get user", + method: http.MethodGet, + path: "/users/123", + wantStatus: http.StatusOK, + wantBody: `{"id":"123","name":"Alice"}`, + }, + { + name: "not found", + method: http.MethodGet, + path: "/users/999", + wantStatus: http.StatusNotFound, + }, + { + name: "create user", + method: http.MethodPost, + path: "/users", + body: `{"name":"Bob"}`, + wantStatus: http.StatusCreated, + }, + } + + handler := NewAPIHandler() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var body io.Reader + if tt.body != "" { + body = strings.NewReader(tt.body) + } + + req := httptest.NewRequest(tt.method, tt.path, body) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + if w.Code != tt.wantStatus { + t.Errorf("got status %d; want %d", w.Code, tt.wantStatus) + } + + if tt.wantBody != "" && w.Body.String() != tt.wantBody { + t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody) + } + }) + } +} +``` + +## 테스팅 명령어 + +```bash +# Run all tests +go test ./... + +# Run tests with verbose output +go test -v ./... + +# Run specific test +go test -run TestAdd ./... + +# Run tests matching pattern +go test -run "TestUser/Create" ./... + +# Run tests with race detector +go test -race ./... + +# Run tests with coverage +go test -cover -coverprofile=coverage.out ./... + +# Run short tests only +go test -short ./... + +# Run tests with timeout +go test -timeout 30s ./... + +# Run benchmarks +go test -bench=. -benchmem ./... + +# Run fuzzing +go test -fuzz=FuzzParse -fuzztime=30s ./... + +# Count test runs (for flaky test detection) +go test -count=10 ./... +``` + +## 모범 사례 + +**해야 할 것:** +- 테스트를 먼저 작성 (TDD) +- 포괄적인 커버리지를 위해 테이블 주도 테스트 사용 +- 구현이 아닌 동작을 테스트 +- 헬퍼 함수에서 `t.Helper()` 사용 +- 독립적인 테스트에 `t.Parallel()` 사용 +- `t.Cleanup()`으로 리소스 정리 +- 시나리오를 설명하는 의미 있는 테스트 이름 사용 + +**하지 말아야 할 것:** +- 비공개 함수를 직접 테스트 (공개 API를 통해 테스트) +- 테스트에서 `time.Sleep()` 사용 (채널이나 조건 사용) +- 불안정한 테스트 무시 (수정하거나 제거) +- 모든 것을 모킹 (가능하면 통합 테스트 선호) +- 에러 경로 테스트 생략 + +## CI/CD 통합 + +```yaml +# GitHub Actions example +test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Run tests + run: go test -race -coverprofile=coverage.out ./... + + - name: Check coverage + run: | + go tool cover -func=coverage.out | grep total | awk '{print $3}' | \ + awk -F'%' '{if ($1 < 80) exit 1}' +``` + +**기억하세요**: 테스트는 문서입니다. 코드가 어떻게 사용되어야 하는지를 보여줍니다. 명확하게 작성하고 최신 상태로 유지하세요. diff --git a/docs/ko-KR/skills/iterative-retrieval/SKILL.md b/docs/ko-KR/skills/iterative-retrieval/SKILL.md new file mode 100644 index 00000000..2ab52325 --- /dev/null +++ b/docs/ko-KR/skills/iterative-retrieval/SKILL.md @@ -0,0 +1,211 @@ +--- +name: iterative-retrieval +description: 서브에이전트 컨텍스트 문제를 해결하기 위한 점진적 컨텍스트 검색 개선 패턴 +origin: ECC +--- + +# 반복적 검색 패턴 + +서브에이전트가 작업을 시작하기 전까지 필요한 컨텍스트를 알 수 없는 멀티 에이전트 워크플로우의 "컨텍스트 문제"를 해결합니다. + +## 활성화 시점 + +- 사전에 예측할 수 없는 코드베이스 컨텍스트가 필요한 서브에이전트를 생성할 때 +- 컨텍스트가 점진적으로 개선되는 멀티 에이전트 워크플로우를 구축할 때 +- 에이전트 작업에서 "컨텍스트 초과" 또는 "컨텍스트 누락" 실패를 겪을 때 +- 코드 탐색을 위한 RAG 유사 검색 파이프라인을 설계할 때 +- 에이전트 오케스트레이션에서 토큰 사용량을 최적화할 때 + +## 문제 + +서브에이전트는 제한된 컨텍스트로 생성됩니다. 다음을 알 수 없습니다: +- 관련 코드가 포함된 파일 +- 코드베이스에 존재하는 패턴 +- 프로젝트에서 사용하는 용어 + +표준 접근법의 실패: +- **모든 것을 전송**: 컨텍스트 제한 초과 +- **아무것도 전송하지 않음**: 에이전트가 중요한 정보를 갖지 못함 +- **필요한 것을 추측**: 종종 잘못됨 + +## 해결책: 반복적 검색 + +컨텍스트를 점진적으로 개선하는 4단계 루프: + +``` +┌─────────────────────────────────────────────┐ +│ │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ DISPATCH │─────▶│ EVALUATE │ │ +│ └──────────┘ └──────────┘ │ +│ ▲ │ │ +│ │ ▼ │ +│ ┌──────────┐ ┌──────────┐ │ +│ │ LOOP │◀─────│ REFINE │ │ +│ └──────────┘ └──────────┘ │ +│ │ +│ Max 3 cycles, then proceed │ +└─────────────────────────────────────────────┘ +``` + +### 1단계: DISPATCH + +후보 파일을 수집하기 위한 초기 광범위 쿼리: + +```javascript +// Start with high-level intent +const initialQuery = { + patterns: ['src/**/*.ts', 'lib/**/*.ts'], + keywords: ['authentication', 'user', 'session'], + excludes: ['*.test.ts', '*.spec.ts'] +}; + +// Dispatch to retrieval agent +const candidates = await retrieveFiles(initialQuery); +``` + +### 2단계: EVALUATE + +검색된 콘텐츠의 관련성 평가: + +```javascript +function evaluateRelevance(files, task) { + return files.map(file => ({ + path: file.path, + relevance: scoreRelevance(file.content, task), + reason: explainRelevance(file.content, task), + missingContext: identifyGaps(file.content, task) + })); +} +``` + +점수 기준: +- **높음 (0.8-1.0)**: 대상 기능을 직접 구현 +- **중간 (0.5-0.7)**: 관련 패턴이나 타입을 포함 +- **낮음 (0.2-0.4)**: 간접적으로 관련 +- **없음 (0-0.2)**: 관련 없음, 제외 + +### 3단계: REFINE + +평가를 기반으로 검색 기준 업데이트: + +```javascript +function refineQuery(evaluation, previousQuery) { + return { + // Add new patterns discovered in high-relevance files + patterns: [...previousQuery.patterns, ...extractPatterns(evaluation)], + + // Add terminology found in codebase + keywords: [...previousQuery.keywords, ...extractKeywords(evaluation)], + + // Exclude confirmed irrelevant paths + excludes: [...previousQuery.excludes, ...evaluation + .filter(e => e.relevance < 0.2) + .map(e => e.path) + ], + + // Target specific gaps + focusAreas: evaluation + .flatMap(e => e.missingContext) + .filter(unique) + }; +} +``` + +### 4단계: LOOP + +개선된 기준으로 반복 (최대 3회): + +```javascript +async function iterativeRetrieve(task, maxCycles = 3) { + let query = createInitialQuery(task); + let bestContext = []; + + for (let cycle = 0; cycle < maxCycles; cycle++) { + const candidates = await retrieveFiles(query); + const evaluation = evaluateRelevance(candidates, task); + + // Check if we have sufficient context + const highRelevance = evaluation.filter(e => e.relevance >= 0.7); + if (highRelevance.length >= 3 && !hasCriticalGaps(evaluation)) { + return highRelevance; + } + + // Refine and continue + query = refineQuery(evaluation, query); + bestContext = mergeContext(bestContext, highRelevance); + } + + return bestContext; +} +``` + +## 실용적인 예시 + +### 예시 1: 버그 수정 컨텍스트 + +``` +Task: "Fix the authentication token expiry bug" + +Cycle 1: + DISPATCH: Search for "token", "auth", "expiry" in src/** + EVALUATE: Found auth.ts (0.9), tokens.ts (0.8), user.ts (0.3) + REFINE: Add "refresh", "jwt" keywords; exclude user.ts + +Cycle 2: + DISPATCH: Search refined terms + EVALUATE: Found session-manager.ts (0.95), jwt-utils.ts (0.85) + REFINE: Sufficient context (2 high-relevance files) + +Result: auth.ts, tokens.ts, session-manager.ts, jwt-utils.ts +``` + +### 예시 2: 기능 구현 + +``` +Task: "Add rate limiting to API endpoints" + +Cycle 1: + DISPATCH: Search "rate", "limit", "api" in routes/** + EVALUATE: No matches - codebase uses "throttle" terminology + REFINE: Add "throttle", "middleware" keywords + +Cycle 2: + DISPATCH: Search refined terms + EVALUATE: Found throttle.ts (0.9), middleware/index.ts (0.7) + REFINE: Need router patterns + +Cycle 3: + DISPATCH: Search "router", "express" patterns + EVALUATE: Found router-setup.ts (0.8) + REFINE: Sufficient context + +Result: throttle.ts, middleware/index.ts, router-setup.ts +``` + +## 에이전트와의 통합 + +에이전트 프롬프트에서 사용: + +```markdown +When retrieving context for this task: +1. Start with broad keyword search +2. Evaluate each file's relevance (0-1 scale) +3. Identify what context is still missing +4. Refine search criteria and repeat (max 3 cycles) +5. Return files with relevance >= 0.7 +``` + +## 모범 사례 + +1. **광범위하게 시작하여 점진적으로 좁히기** - 초기 쿼리를 과도하게 지정하지 않기 +2. **코드베이스 용어 학습** - 첫 번째 사이클에서 주로 네이밍 컨벤션이 드러남 +3. **누락된 것 추적** - 명시적 격차 식별이 개선을 주도 +4. **"충분히 좋은" 수준에서 중단** - 관련성 높은 파일 3개가 보통 수준의 파일 10개보다 나음 +5. **자신 있게 제외** - 관련성 낮은 파일은 관련성이 높아지지 않음 + +## 관련 항목 + +- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) - 서브에이전트 오케스트레이션 섹션 +- `continuous-learning` 스킬 - 시간이 지남에 따라 개선되는 패턴 +- `~/.claude/agents/`의 에이전트 정의 diff --git a/docs/ko-KR/skills/postgres-patterns/SKILL.md b/docs/ko-KR/skills/postgres-patterns/SKILL.md new file mode 100644 index 00000000..1007fdbb --- /dev/null +++ b/docs/ko-KR/skills/postgres-patterns/SKILL.md @@ -0,0 +1,147 @@ +--- +name: postgres-patterns +description: 쿼리 최적화, 스키마 설계, 인덱싱, 보안을 위한 PostgreSQL 데이터베이스 패턴. Supabase 모범 사례 기반. +origin: ECC +--- + +# PostgreSQL 패턴 + +PostgreSQL 모범 사례 빠른 참조. 자세한 가이드는 `database-reviewer` 에이전트를 사용하세요. + +## 활성화 시점 + +- SQL 쿼리 또는 마이그레이션을 작성할 때 +- 데이터베이스 스키마를 설계할 때 +- 느린 쿼리를 문제 해결할 때 +- Row Level Security를 구현할 때 +- 커넥션 풀링을 설정할 때 + +## 빠른 참조 + +### 인덱스 치트 시트 + +| 쿼리 패턴 | 인덱스 유형 | 예시 | +|--------------|------------|---------| +| `WHERE col = value` | B-tree (기본값) | `CREATE INDEX idx ON t (col)` | +| `WHERE col > value` | B-tree | `CREATE INDEX idx ON t (col)` | +| `WHERE a = x AND b > y` | Composite | `CREATE INDEX idx ON t (a, b)` | +| `WHERE jsonb @> '{}'` | GIN | `CREATE INDEX idx ON t USING gin (col)` | +| `WHERE tsv @@ query` | GIN | `CREATE INDEX idx ON t USING gin (col)` | +| 시계열 범위 | BRIN | `CREATE INDEX idx ON t USING brin (col)` | + +### 데이터 타입 빠른 참조 + +| 사용 사례 | 올바른 타입 | 지양 | +|----------|-------------|-------| +| ID | `bigint` | `int`, random UUID | +| 문자열 | `text` | `varchar(255)` | +| 타임스탬프 | `timestamptz` | `timestamp` | +| 금액 | `numeric(10,2)` | `float` | +| 플래그 | `boolean` | `varchar`, `int` | + +### 일반 패턴 + +**복합 인덱스 순서:** +```sql +-- Equality columns first, then range columns +CREATE INDEX idx ON orders (status, created_at); +-- Works for: WHERE status = 'pending' AND created_at > '2024-01-01' +``` + +**커버링 인덱스:** +```sql +CREATE INDEX idx ON users (email) INCLUDE (name, created_at); +-- Avoids table lookup for SELECT email, name, created_at +``` + +**부분 인덱스:** +```sql +CREATE INDEX idx ON users (email) WHERE deleted_at IS NULL; +-- Smaller index, only includes active users +``` + +**RLS 정책 (최적화):** +```sql +CREATE POLICY policy ON orders + USING ((SELECT auth.uid()) = user_id); -- Wrap in SELECT! +``` + +**UPSERT:** +```sql +INSERT INTO settings (user_id, key, value) +VALUES (123, 'theme', 'dark') +ON CONFLICT (user_id, key) +DO UPDATE SET value = EXCLUDED.value; +``` + +**커서 페이지네이션:** +```sql +SELECT * FROM products WHERE id > $last_id ORDER BY id LIMIT 20; +-- O(1) vs OFFSET which is O(n) +``` + +**큐 처리:** +```sql +UPDATE jobs SET status = 'processing' +WHERE id = ( + SELECT id FROM jobs WHERE status = 'pending' + ORDER BY created_at LIMIT 1 + FOR UPDATE SKIP LOCKED +) RETURNING *; +``` + +### 안티패턴 감지 + +```sql +-- Find unindexed foreign keys +SELECT conrelid::regclass, a.attname +FROM pg_constraint c +JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey) +WHERE c.contype = 'f' + AND NOT EXISTS ( + SELECT 1 FROM pg_index i + WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey) + ); + +-- Find slow queries +SELECT query, mean_exec_time, calls +FROM pg_stat_statements +WHERE mean_exec_time > 100 +ORDER BY mean_exec_time DESC; + +-- Check table bloat +SELECT relname, n_dead_tup, last_vacuum +FROM pg_stat_user_tables +WHERE n_dead_tup > 1000 +ORDER BY n_dead_tup DESC; +``` + +### 구성 템플릿 + +```sql +-- Connection limits (adjust for RAM) +ALTER SYSTEM SET max_connections = 100; +ALTER SYSTEM SET work_mem = '8MB'; + +-- Timeouts +ALTER SYSTEM SET idle_in_transaction_session_timeout = '30s'; +ALTER SYSTEM SET statement_timeout = '30s'; + +-- Monitoring +CREATE EXTENSION IF NOT EXISTS pg_stat_statements; + +-- Security defaults +REVOKE ALL ON SCHEMA public FROM public; + +SELECT pg_reload_conf(); +``` + +## 관련 항목 + +- 에이전트: `database-reviewer` - 전체 데이터베이스 리뷰 워크플로우 +- 스킬: `clickhouse-io` - ClickHouse 분석 패턴 +- 스킬: `backend-patterns` - API 및 백엔드 패턴 + +--- + +*Supabase Agent Skills 기반 (크레딧: Supabase 팀) (MIT License)* diff --git a/docs/ko-KR/skills/project-guidelines-example/SKILL.md b/docs/ko-KR/skills/project-guidelines-example/SKILL.md new file mode 100644 index 00000000..a70dcc3a --- /dev/null +++ b/docs/ko-KR/skills/project-guidelines-example/SKILL.md @@ -0,0 +1,349 @@ +--- +name: project-guidelines-example +description: "실제 프로덕션 애플리케이션을 기반으로 한 프로젝트별 스킬 템플릿 예시." +origin: ECC +--- + +# 프로젝트 가이드라인 스킬 (예시) + +이것은 프로젝트별 스킬의 예시입니다. 자신의 프로젝트에 맞는 템플릿으로 사용하세요. + +실제 프로덕션 애플리케이션을 기반으로 합니다: [Zenith](https://zenith.chat) - AI 기반 고객 발견 플랫폼. + +## 사용 시점 + +이 스킬이 설계된 특정 프로젝트에서 작업할 때 참조하세요. 프로젝트 스킬에는 다음이 포함됩니다: +- 아키텍처 개요 +- 파일 구조 +- 코드 패턴 +- 테스팅 요구사항 +- 배포 워크플로우 + +--- + +## 아키텍처 개요 + +**기술 스택:** +- **Frontend**: Next.js 15 (App Router), TypeScript, React +- **Backend**: FastAPI (Python), Pydantic 모델 +- **Database**: Supabase (PostgreSQL) +- **AI**: Claude API (도구 호출 및 구조화된 출력) +- **Deployment**: Google Cloud Run +- **Testing**: Playwright (E2E), pytest (백엔드), React Testing Library + +**서비스:** +``` +┌─────────────────────────────────────────────────────────────┐ +│ Frontend │ +│ Next.js 15 + TypeScript + TailwindCSS │ +│ Deployed: Vercel / Cloud Run │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Backend │ +│ FastAPI + Python 3.11 + Pydantic │ +│ Deployed: Cloud Run │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ Supabase │ │ Claude │ │ Redis │ + │ Database │ │ API │ │ Cache │ + └──────────┘ └──────────┘ └──────────┘ +``` + +--- + +## 파일 구조 + +``` +project/ +├── frontend/ +│ └── src/ +│ ├── app/ # Next.js app router 페이지 +│ │ ├── api/ # API 라우트 +│ │ ├── (auth)/ # 인증 보호 라우트 +│ │ └── workspace/ # 메인 앱 워크스페이스 +│ ├── components/ # React 컴포넌트 +│ │ ├── ui/ # 기본 UI 컴포넌트 +│ │ ├── forms/ # 폼 컴포넌트 +│ │ └── layouts/ # 레이아웃 컴포넌트 +│ ├── hooks/ # 커스텀 React hooks +│ ├── lib/ # 유틸리티 +│ ├── types/ # TypeScript 정의 +│ └── config/ # 설정 +│ +├── backend/ +│ ├── routers/ # FastAPI 라우트 핸들러 +│ ├── models.py # Pydantic 모델 +│ ├── main.py # FastAPI 앱 엔트리 +│ ├── auth_system.py # 인증 +│ ├── database.py # 데이터베이스 작업 +│ ├── services/ # 비즈니스 로직 +│ └── tests/ # pytest 테스트 +│ +├── deploy/ # 배포 설정 +├── docs/ # 문서 +└── scripts/ # 유틸리티 스크립트 +``` + +--- + +## 코드 패턴 + +### API 응답 형식 (FastAPI) + +```python +from pydantic import BaseModel +from typing import Generic, TypeVar, Optional + +T = TypeVar('T') + +class ApiResponse(BaseModel, Generic[T]): + success: bool + data: Optional[T] = None + error: Optional[str] = None + + @classmethod + def ok(cls, data: T) -> "ApiResponse[T]": + return cls(success=True, data=data) + + @classmethod + def fail(cls, error: str) -> "ApiResponse[T]": + return cls(success=False, error=error) +``` + +### Frontend API 호출 (TypeScript) + +```typescript +interface ApiResponse { + success: boolean + data?: T + error?: string +} + +async function fetchApi( + endpoint: string, + options?: RequestInit +): Promise> { + try { + const response = await fetch(`/api${endpoint}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }) + + if (!response.ok) { + return { success: false, error: `HTTP ${response.status}` } + } + + return await response.json() + } catch (error) { + return { success: false, error: String(error) } + } +} +``` + +### Claude AI 통합 (구조화된 출력) + +```python +from anthropic import Anthropic +from pydantic import BaseModel + +class AnalysisResult(BaseModel): + summary: str + key_points: list[str] + confidence: float + +async def analyze_with_claude(content: str) -> AnalysisResult: + client = Anthropic() + + response = client.messages.create( + model="claude-sonnet-4-5-20250514", + max_tokens=1024, + messages=[{"role": "user", "content": content}], + tools=[{ + "name": "provide_analysis", + "description": "Provide structured analysis", + "input_schema": AnalysisResult.model_json_schema() + }], + tool_choice={"type": "tool", "name": "provide_analysis"} + ) + + # Extract tool use result + tool_use = next( + block for block in response.content + if block.type == "tool_use" + ) + + return AnalysisResult(**tool_use.input) +``` + +### 커스텀 Hooks (React) + +```typescript +import { useState, useCallback } from 'react' + +interface UseApiState { + data: T | null + loading: boolean + error: string | null +} + +export function useApi( + fetchFn: () => Promise> +) { + const [state, setState] = useState>({ + data: null, + loading: false, + error: null, + }) + + const execute = useCallback(async () => { + setState(prev => ({ ...prev, loading: true, error: null })) + + const result = await fetchFn() + + if (result.success) { + setState({ data: result.data!, loading: false, error: null }) + } else { + setState({ data: null, loading: false, error: result.error! }) + } + }, [fetchFn]) + + return { ...state, execute } +} +``` + +--- + +## 테스팅 요구사항 + +### Backend (pytest) + +```bash +# Run all tests +poetry run pytest tests/ + +# Run with coverage +poetry run pytest tests/ --cov=. --cov-report=html + +# Run specific test file +poetry run pytest tests/test_auth.py -v +``` + +**테스트 구조:** +```python +import pytest +from httpx import AsyncClient +from main import app + +@pytest.fixture +async def client(): + async with AsyncClient(app=app, base_url="http://test") as ac: + yield ac + +@pytest.mark.asyncio +async def test_health_check(client: AsyncClient): + response = await client.get("/health") + assert response.status_code == 200 + assert response.json()["status"] == "healthy" +``` + +### Frontend (React Testing Library) + +```bash +# Run tests +npm run test + +# Run with coverage +npm run test -- --coverage + +# Run E2E tests +npm run test:e2e +``` + +**테스트 구조:** +```typescript +import { render, screen, fireEvent } from '@testing-library/react' +import { WorkspacePanel } from './WorkspacePanel' + +describe('WorkspacePanel', () => { + it('renders workspace correctly', () => { + render() + expect(screen.getByRole('main')).toBeInTheDocument() + }) + + it('handles session creation', async () => { + render() + fireEvent.click(screen.getByText('New Session')) + expect(await screen.findByText('Session created')).toBeInTheDocument() + }) +}) +``` + +--- + +## 배포 워크플로우 + +### 배포 전 체크리스트 + +- [ ] 모든 테스트가 로컬에서 통과 +- [ ] `npm run build` 성공 (frontend) +- [ ] `poetry run pytest` 통과 (backend) +- [ ] 하드코딩된 시크릿 없음 +- [ ] 환경 변수 문서화됨 +- [ ] 데이터베이스 마이그레이션 준비됨 + +### 배포 명령어 + +```bash +# Build and deploy frontend +cd frontend && npm run build +gcloud run deploy frontend --source . + +# Build and deploy backend +cd backend +gcloud run deploy backend --source . +``` + +### 환경 변수 + +```bash +# Frontend (.env.local) +NEXT_PUBLIC_API_URL=https://api.example.com +NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... + +# Backend (.env) +DATABASE_URL=postgresql://... +ANTHROPIC_API_KEY=sk-ant-... +SUPABASE_URL=https://xxx.supabase.co +SUPABASE_KEY=eyJ... +``` + +--- + +## 핵심 규칙 + +1. **코드, 주석, 문서에 이모지 없음** +2. **불변성** - 객체나 배열을 절대 변형하지 않음 +3. **TDD** - 구현 전에 테스트 작성 +4. **80% 커버리지** 최소 +5. **작은 파일 여러 개** - 200-400줄이 일반적, 800줄 최대 +6. **프로덕션 코드에 console.log 없음** +7. **적절한 에러 처리** (try/catch 사용) +8. **입력 유효성 검사** (Pydantic/Zod 사용) + +--- + +## 관련 스킬 + +- `coding-standards.md` - 일반 코딩 모범 사례 +- `backend-patterns.md` - API 및 데이터베이스 패턴 +- `frontend-patterns.md` - React 및 Next.js 패턴 +- `tdd-workflow/` - 테스트 주도 개발 방법론 diff --git a/docs/ko-KR/skills/security-review/SKILL.md b/docs/ko-KR/skills/security-review/SKILL.md new file mode 100644 index 00000000..6b6878cd --- /dev/null +++ b/docs/ko-KR/skills/security-review/SKILL.md @@ -0,0 +1,495 @@ +--- +name: security-review +description: 인증 추가, 사용자 입력 처리, 시크릿 관리, API 엔드포인트 생성, 결제/민감한 기능 구현 시 이 스킬을 사용하세요. 포괄적인 보안 체크리스트와 패턴을 제공합니다. +origin: ECC +--- + +# 보안 리뷰 스킬 + +이 스킬은 모든 코드가 보안 모범 사례를 따르고 잠재적 취약점을 식별하도록 보장합니다. + +## 활성화 시점 + +- 인증 또는 권한 부여 구현 시 +- 사용자 입력 또는 파일 업로드 처리 시 +- 새로운 API 엔드포인트 생성 시 +- 시크릿 또는 자격 증명 작업 시 +- 결제 기능 구현 시 +- 민감한 데이터 저장 또는 전송 시 +- 서드파티 API 통합 시 + +## 보안 체크리스트 + +### 1. 시크릿 관리 + +#### 절대 하지 말아야 할 것 +```typescript +const apiKey = "sk-proj-xxxxx" // Hardcoded secret +const dbPassword = "password123" // In source code +``` + +#### 반드시 해야 할 것 +```typescript +const apiKey = process.env.OPENAI_API_KEY +const dbUrl = process.env.DATABASE_URL + +// Verify secrets exist +if (!apiKey) { + throw new Error('OPENAI_API_KEY not configured') +} +``` + +#### 확인 단계 +- [ ] 하드코딩된 API 키, 토큰, 비밀번호 없음 +- [ ] 모든 시크릿이 환경 변수에 저장됨 +- [ ] `.env.local`이 .gitignore에 포함됨 +- [ ] git 히스토리에 시크릿 없음 +- [ ] 프로덕션 시크릿이 호스팅 플랫폼(Vercel, Railway)에 저장됨 + +### 2. 입력 유효성 검사 + +#### 항상 사용자 입력을 검증할 것 +```typescript +import { z } from 'zod' + +// Define validation schema +const CreateUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(1).max(100), + age: z.number().int().min(0).max(150) +}) + +// Validate before processing +export async function createUser(input: unknown) { + try { + const validated = CreateUserSchema.parse(input) + return await db.users.create(validated) + } catch (error) { + if (error instanceof z.ZodError) { + return { success: false, errors: error.errors } + } + throw error + } +} +``` + +#### 파일 업로드 유효성 검사 +```typescript +function validateFileUpload(file: File) { + // Size check (5MB max) + const maxSize = 5 * 1024 * 1024 + if (file.size > maxSize) { + throw new Error('File too large (max 5MB)') + } + + // Type check + const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'] + if (!allowedTypes.includes(file.type)) { + throw new Error('Invalid file type') + } + + // Extension check + const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'] + const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0] + if (!extension || !allowedExtensions.includes(extension)) { + throw new Error('Invalid file extension') + } + + return true +} +``` + +#### 확인 단계 +- [ ] 모든 사용자 입력이 스키마로 검증됨 +- [ ] 파일 업로드가 제한됨 (크기, 타입, 확장자) +- [ ] 사용자 입력이 쿼리에 직접 사용되지 않음 +- [ ] 화이트리스트 검증 사용 (블랙리스트가 아닌) +- [ ] 에러 메시지가 민감한 정보를 노출하지 않음 + +### 3. SQL Injection 방지 + +#### 절대 SQL을 연결하지 말 것 +```typescript +// DANGEROUS - SQL Injection vulnerability +const query = `SELECT * FROM users WHERE email = '${userEmail}'` +await db.query(query) +``` + +#### 반드시 파라미터화된 쿼리를 사용할 것 +```typescript +// Safe - parameterized query +const { data } = await supabase + .from('users') + .select('*') + .eq('email', userEmail) + +// Or with raw SQL +await db.query( + 'SELECT * FROM users WHERE email = $1', + [userEmail] +) +``` + +#### 확인 단계 +- [ ] 모든 데이터베이스 쿼리가 파라미터화된 쿼리 사용 +- [ ] SQL에서 문자열 연결 없음 +- [ ] ORM/쿼리 빌더가 올바르게 사용됨 +- [ ] Supabase 쿼리가 적절히 새니타이징됨 + +### 4. 인증 및 권한 부여 + +#### JWT 토큰 처리 +```typescript +// ❌ WRONG: localStorage (vulnerable to XSS) +localStorage.setItem('token', token) + +// ✅ CORRECT: httpOnly cookies +res.setHeader('Set-Cookie', + `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`) +``` + +#### 권한 부여 확인 +```typescript +export async function deleteUser(userId: string, requesterId: string) { + // ALWAYS verify authorization first + const requester = await db.users.findUnique({ + where: { id: requesterId } + }) + + if (requester.role !== 'admin') { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 403 } + ) + } + + // Proceed with deletion + await db.users.delete({ where: { id: userId } }) +} +``` + +#### Row Level Security (Supabase) +```sql +-- Enable RLS on all tables +ALTER TABLE users ENABLE ROW LEVEL SECURITY; + +-- Users can only view their own data +CREATE POLICY "Users view own data" + ON users FOR SELECT + USING (auth.uid() = id); + +-- Users can only update their own data +CREATE POLICY "Users update own data" + ON users FOR UPDATE + USING (auth.uid() = id); +``` + +#### 확인 단계 +- [ ] 토큰이 httpOnly 쿠키에 저장됨 (localStorage가 아닌) +- [ ] 민감한 작업 전에 권한 부여 확인 +- [ ] Supabase에서 Row Level Security 활성화됨 +- [ ] 역할 기반 접근 제어 구현됨 +- [ ] 세션 관리가 안전함 + +### 5. XSS 방지 + +#### HTML 새니타이징 +```typescript +import DOMPurify from 'isomorphic-dompurify' + +// ALWAYS sanitize user-provided HTML +function renderUserContent(html: string) { + const clean = DOMPurify.sanitize(html, { + ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'], + ALLOWED_ATTR: [] + }) + return
+} +``` + +#### Content Security Policy +```typescript +// next.config.js +const securityHeaders = [ + { + key: 'Content-Security-Policy', + value: ` + default-src 'self'; + script-src 'self' 'unsafe-eval' 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; + img-src 'self' data: https:; + font-src 'self'; + connect-src 'self' https://api.example.com; + `.replace(/\s{2,}/g, ' ').trim() + } +] +``` + +#### 확인 단계 +- [ ] 사용자 제공 HTML이 새니타이징됨 +- [ ] CSP 헤더가 구성됨 +- [ ] 검증되지 않은 동적 콘텐츠 렌더링 없음 +- [ ] React의 내장 XSS 보호가 사용됨 + +### 6. CSRF 보호 + +#### CSRF 토큰 +```typescript +import { csrf } from '@/lib/csrf' + +export async function POST(request: Request) { + const token = request.headers.get('X-CSRF-Token') + + if (!csrf.verify(token)) { + return NextResponse.json( + { error: 'Invalid CSRF token' }, + { status: 403 } + ) + } + + // Process request +} +``` + +#### SameSite 쿠키 +```typescript +res.setHeader('Set-Cookie', + `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`) +``` + +#### 확인 단계 +- [ ] 상태 변경 작업에 CSRF 토큰 적용 +- [ ] 모든 쿠키에 SameSite=Strict 설정 +- [ ] Double-submit 쿠키 패턴 구현 + +### 7. 속도 제한 + +#### API 속도 제한 +```typescript +import rateLimit from 'express-rate-limit' + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // 100 requests per window + message: 'Too many requests' +}) + +// Apply to routes +app.use('/api/', limiter) +``` + +#### 비용이 높은 작업 +```typescript +// Aggressive rate limiting for searches +const searchLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 10, // 10 requests per minute + message: 'Too many search requests' +}) + +app.use('/api/search', searchLimiter) +``` + +#### 확인 단계 +- [ ] 모든 API 엔드포인트에 속도 제한 적용 +- [ ] 비용이 높은 작업에 더 엄격한 제한 +- [ ] IP 기반 속도 제한 +- [ ] 사용자 기반 속도 제한 (인증된 사용자) + +### 8. 민감한 데이터 노출 + +#### 로깅 +```typescript +// ❌ WRONG: Logging sensitive data +console.log('User login:', { email, password }) +console.log('Payment:', { cardNumber, cvv }) + +// ✅ CORRECT: Redact sensitive data +console.log('User login:', { email, userId }) +console.log('Payment:', { last4: card.last4, userId }) +``` + +#### 에러 메시지 +```typescript +// ❌ WRONG: Exposing internal details +catch (error) { + return NextResponse.json( + { error: error.message, stack: error.stack }, + { status: 500 } + ) +} + +// ✅ CORRECT: Generic error messages +catch (error) { + console.error('Internal error:', error) + return NextResponse.json( + { error: 'An error occurred. Please try again.' }, + { status: 500 } + ) +} +``` + +#### 확인 단계 +- [ ] 로그에 비밀번호, 토큰, 시크릿 없음 +- [ ] 사용자에게 표시되는 에러 메시지가 일반적임 +- [ ] 상세 에러는 서버 로그에만 기록 +- [ ] 사용자에게 스택 트레이스가 노출되지 않음 + +### 9. 블록체인 보안 (Solana) + +#### 지갑 검증 +```typescript +import { verify } from '@solana/web3.js' + +async function verifyWalletOwnership( + publicKey: string, + signature: string, + message: string +) { + try { + const isValid = verify( + Buffer.from(message), + Buffer.from(signature, 'base64'), + Buffer.from(publicKey, 'base64') + ) + return isValid + } catch (error) { + return false + } +} +``` + +#### 트랜잭션 검증 +```typescript +async function verifyTransaction(transaction: Transaction) { + // Verify recipient + if (transaction.to !== expectedRecipient) { + throw new Error('Invalid recipient') + } + + // Verify amount + if (transaction.amount > maxAmount) { + throw new Error('Amount exceeds limit') + } + + // Verify user has sufficient balance + const balance = await getBalance(transaction.from) + if (balance < transaction.amount) { + throw new Error('Insufficient balance') + } + + return true +} +``` + +#### 확인 단계 +- [ ] 지갑 서명 검증됨 +- [ ] 트랜잭션 세부 정보 유효성 검사됨 +- [ ] 트랜잭션 전 잔액 확인 +- [ ] 블라인드 트랜잭션 서명 없음 + +### 10. 의존성 보안 + +#### 정기 업데이트 +```bash +# Check for vulnerabilities +npm audit + +# Fix automatically fixable issues +npm audit fix + +# Update dependencies +npm update + +# Check for outdated packages +npm outdated +``` + +#### 잠금 파일 +```bash +# ALWAYS commit lock files +git add package-lock.json + +# Use in CI/CD for reproducible builds +npm ci # Instead of npm install +``` + +#### 확인 단계 +- [ ] 의존성이 최신 상태 +- [ ] 알려진 취약점 없음 (npm audit 클린) +- [ ] 잠금 파일 커밋됨 +- [ ] GitHub에서 Dependabot 활성화됨 +- [ ] 정기적인 보안 업데이트 + +## 보안 테스트 + +### 자동화된 보안 테스트 +```typescript +// Test authentication +test('requires authentication', async () => { + const response = await fetch('/api/protected') + expect(response.status).toBe(401) +}) + +// Test authorization +test('requires admin role', async () => { + const response = await fetch('/api/admin', { + headers: { Authorization: `Bearer ${userToken}` } + }) + expect(response.status).toBe(403) +}) + +// Test input validation +test('rejects invalid input', async () => { + const response = await fetch('/api/users', { + method: 'POST', + body: JSON.stringify({ email: 'not-an-email' }) + }) + expect(response.status).toBe(400) +}) + +// Test rate limiting +test('enforces rate limits', async () => { + const requests = Array(101).fill(null).map(() => + fetch('/api/endpoint') + ) + + const responses = await Promise.all(requests) + const tooManyRequests = responses.filter(r => r.status === 429) + + expect(tooManyRequests.length).toBeGreaterThan(0) +}) +``` + +## 배포 전 보안 체크리스트 + +모든 프로덕션 배포 전: + +- [ ] **시크릿**: 하드코딩된 시크릿 없음, 모두 환경 변수에 저장 +- [ ] **입력 유효성 검사**: 모든 사용자 입력 검증됨 +- [ ] **SQL Injection**: 모든 쿼리 파라미터화됨 +- [ ] **XSS**: 사용자 콘텐츠 새니타이징됨 +- [ ] **CSRF**: 보호 활성화됨 +- [ ] **인증**: 적절한 토큰 처리 +- [ ] **권한 부여**: 역할 확인 적용됨 +- [ ] **속도 제한**: 모든 엔드포인트에서 활성화됨 +- [ ] **HTTPS**: 프로덕션에서 강제 적용 +- [ ] **보안 헤더**: CSP, X-Frame-Options 구성됨 +- [ ] **에러 처리**: 에러에 민감한 데이터 없음 +- [ ] **로깅**: 민감한 데이터가 로그에 없음 +- [ ] **의존성**: 최신 상태, 취약점 없음 +- [ ] **Row Level Security**: Supabase에서 활성화됨 +- [ ] **CORS**: 적절히 구성됨 +- [ ] **파일 업로드**: 유효성 검사됨 (크기, 타입) +- [ ] **지갑 서명**: 검증됨 (블록체인인 경우) + +## 참고 자료 + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [Next.js Security](https://nextjs.org/docs/security) +- [Supabase Security](https://supabase.com/docs/guides/auth) +- [Web Security Academy](https://portswigger.net/web-security) + +--- + +**기억하세요**: 보안은 선택 사항이 아닙니다. 하나의 취약점이 전체 플랫폼을 침해할 수 있습니다. 의심스러울 때는 보수적으로 대응하세요. diff --git a/docs/ko-KR/skills/security-review/cloud-infrastructure-security.md b/docs/ko-KR/skills/security-review/cloud-infrastructure-security.md new file mode 100644 index 00000000..b6051418 --- /dev/null +++ b/docs/ko-KR/skills/security-review/cloud-infrastructure-security.md @@ -0,0 +1,361 @@ +| name | description | +|------|-------------| +| cloud-infrastructure-security | 클라우드 플랫폼 배포, 인프라 구성, IAM 정책 관리, 로깅/모니터링 설정, CI/CD 파이프라인 구현 시 이 스킬을 사용하세요. 모범 사례에 맞춘 클라우드 보안 체크리스트를 제공합니다. | + +# 클라우드 및 인프라 보안 스킬 + +이 스킬은 클라우드 인프라, CI/CD 파이프라인, 배포 구성이 보안 모범 사례를 따르고 업계 표준을 준수하도록 보장합니다. + +## 활성화 시점 + +- 클라우드 플랫폼(AWS, Vercel, Railway, Cloudflare)에 애플리케이션 배포 시 +- IAM 역할 및 권한 구성 시 +- CI/CD 파이프라인 설정 시 +- Infrastructure as Code(Terraform, CloudFormation) 구현 시 +- 로깅 및 모니터링 구성 시 +- 클라우드 환경에서 시크릿 관리 시 +- CDN 및 엣지 보안 설정 시 +- 재해 복구 및 백업 전략 구현 시 + +## 클라우드 보안 체크리스트 + +### 1. IAM 및 접근 제어 + +#### 최소 권한 원칙 + +```yaml +# ✅ CORRECT: Minimal permissions +iam_role: + permissions: + - s3:GetObject # Only read access + - s3:ListBucket + resources: + - arn:aws:s3:::my-bucket/* # Specific bucket only + +# ❌ WRONG: Overly broad permissions +iam_role: + permissions: + - s3:* # All S3 actions + resources: + - "*" # All resources +``` + +#### 다중 인증 (MFA) + +```bash +# ALWAYS enable MFA for root/admin accounts +aws iam enable-mfa-device \ + --user-name admin \ + --serial-number arn:aws:iam::123456789:mfa/admin \ + --authentication-code1 123456 \ + --authentication-code2 789012 +``` + +#### 확인 단계 + +- [ ] 프로덕션에서 루트 계정 사용 없음 +- [ ] 모든 권한 있는 계정에 MFA 활성화됨 +- [ ] 서비스 계정이 장기 자격 증명이 아닌 역할을 사용 +- [ ] IAM 정책이 최소 권한을 따름 +- [ ] 정기적인 접근 검토 수행 +- [ ] 사용하지 않는 자격 증명 교체 또는 제거 + +### 2. 시크릿 관리 + +#### 클라우드 시크릿 매니저 + +```typescript +// ✅ CORRECT: Use cloud secrets manager +import { SecretsManager } from '@aws-sdk/client-secrets-manager'; + +const client = new SecretsManager({ region: 'us-east-1' }); +const secret = await client.getSecretValue({ SecretId: 'prod/api-key' }); +const apiKey = JSON.parse(secret.SecretString).key; + +// ❌ WRONG: Hardcoded or in environment variables only +const apiKey = process.env.API_KEY; // Not rotated, not audited +``` + +#### 시크릿 교체 + +```bash +# Set up automatic rotation for database credentials +aws secretsmanager rotate-secret \ + --secret-id prod/db-password \ + --rotation-lambda-arn arn:aws:lambda:region:account:function:rotate \ + --rotation-rules AutomaticallyAfterDays=30 +``` + +#### 확인 단계 + +- [ ] 모든 시크릿이 클라우드 시크릿 매니저에 저장됨 (AWS Secrets Manager, Vercel Secrets) +- [ ] 데이터베이스 자격 증명에 대한 자동 교체 활성화됨 +- [ ] API 키가 최소 분기별로 교체됨 +- [ ] 코드, 로그, 에러 메시지에 시크릿 없음 +- [ ] 시크릿 접근에 대한 감사 로깅 활성화됨 + +### 3. 네트워크 보안 + +#### VPC 및 방화벽 구성 + +```terraform +# ✅ CORRECT: Restricted security group +resource "aws_security_group" "app" { + name = "app-sg" + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["10.0.0.0/16"] # Internal VPC only + } + + egress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] # Only HTTPS outbound + } +} + +# ❌ WRONG: Open to the internet +resource "aws_security_group" "bad" { + ingress { + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] # All ports, all IPs! + } +} +``` + +#### 확인 단계 + +- [ ] 데이터베이스가 공개적으로 접근 불가 +- [ ] SSH/RDP 포트가 VPN/배스천에만 제한됨 +- [ ] 보안 그룹이 최소 권한을 따름 +- [ ] 네트워크 ACL이 구성됨 +- [ ] VPC 플로우 로그가 활성화됨 + +### 4. 로깅 및 모니터링 + +#### CloudWatch/로깅 구성 + +```typescript +// ✅ CORRECT: Comprehensive logging +import { CloudWatchLogsClient, CreateLogStreamCommand } from '@aws-sdk/client-cloudwatch-logs'; + +const logSecurityEvent = async (event: SecurityEvent) => { + await cloudwatch.putLogEvents({ + logGroupName: '/aws/security/events', + logStreamName: 'authentication', + logEvents: [{ + timestamp: Date.now(), + message: JSON.stringify({ + type: event.type, + userId: event.userId, + ip: event.ip, + result: event.result, + // Never log sensitive data + }) + }] + }); +}; +``` + +#### 확인 단계 + +- [ ] 모든 서비스에 CloudWatch/로깅 활성화됨 +- [ ] 실패한 인증 시도가 로깅됨 +- [ ] 관리자 작업이 감사됨 +- [ ] 로그 보존 기간이 구성됨 (규정 준수를 위해 90일 이상) +- [ ] 의심스러운 활동에 대한 알림 구성됨 +- [ ] 로그가 중앙 집중화되고 변조 방지됨 + +### 5. CI/CD 파이프라인 보안 + +#### 보안 파이프라인 구성 + +```yaml +# ✅ CORRECT: Secure GitHub Actions workflow +name: Deploy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read # Minimal permissions + + steps: + - uses: actions/checkout@v4 + + # Scan for secrets + - name: Secret scanning + uses: trufflesecurity/trufflehog@main + + # Dependency audit + - name: Audit dependencies + run: npm audit --audit-level=high + + # Use OIDC, not long-lived tokens + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole + aws-region: us-east-1 +``` + +#### 공급망 보안 + +```json +// package.json - Use lock files and integrity checks +{ + "scripts": { + "install": "npm ci", // Use ci for reproducible builds + "audit": "npm audit --audit-level=moderate", + "check": "npm outdated" + } +} +``` + +#### 확인 단계 + +- [ ] 장기 자격 증명 대신 OIDC 사용 +- [ ] 파이프라인에서 시크릿 스캐닝 +- [ ] 의존성 취약점 스캐닝 +- [ ] 컨테이너 이미지 스캐닝 (해당하는 경우) +- [ ] 브랜치 보호 규칙 적용됨 +- [ ] 병합 전 코드 리뷰 필수 +- [ ] 서명된 커밋 적용 + +### 6. Cloudflare 및 CDN 보안 + +#### Cloudflare 보안 구성 + +```typescript +// ✅ CORRECT: Cloudflare Workers with security headers +export default { + async fetch(request: Request): Promise { + const response = await fetch(request); + + // Add security headers + const headers = new Headers(response.headers); + headers.set('X-Frame-Options', 'DENY'); + headers.set('X-Content-Type-Options', 'nosniff'); + headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); + headers.set('Permissions-Policy', 'geolocation=(), microphone=()'); + + return new Response(response.body, { + status: response.status, + headers + }); + } +}; +``` + +#### WAF 규칙 + +```bash +# Enable Cloudflare WAF managed rules +# - OWASP Core Ruleset +# - Cloudflare Managed Ruleset +# - Rate limiting rules +# - Bot protection +``` + +#### 확인 단계 + +- [ ] OWASP 규칙으로 WAF 활성화됨 +- [ ] 속도 제한 구성됨 +- [ ] 봇 보호 활성화됨 +- [ ] DDoS 보호 활성화됨 +- [ ] 보안 헤더 구성됨 +- [ ] SSL/TLS 엄격 모드 활성화됨 + +### 7. 백업 및 재해 복구 + +#### 자동 백업 + +```terraform +# ✅ CORRECT: Automated RDS backups +resource "aws_db_instance" "main" { + allocated_storage = 20 + engine = "postgres" + + backup_retention_period = 30 # 30 days retention + backup_window = "03:00-04:00" + maintenance_window = "mon:04:00-mon:05:00" + + enabled_cloudwatch_logs_exports = ["postgresql"] + + deletion_protection = true # Prevent accidental deletion +} +``` + +#### 확인 단계 + +- [ ] 자동 일일 백업 구성됨 +- [ ] 백업 보존 기간이 규정 준수 요구사항을 충족 +- [ ] 특정 시점 복구 활성화됨 +- [ ] 분기별 백업 테스트 수행 +- [ ] 재해 복구 계획 문서화됨 +- [ ] RPO 및 RTO가 정의되고 테스트됨 + +## 배포 전 클라우드 보안 체크리스트 + +모든 프로덕션 클라우드 배포 전: + +- [ ] **IAM**: 루트 계정 미사용, MFA 활성화, 최소 권한 정책 +- [ ] **시크릿**: 모든 시크릿이 클라우드 시크릿 매니저에 교체와 함께 저장됨 +- [ ] **네트워크**: 보안 그룹 제한됨, 공개 데이터베이스 없음 +- [ ] **로깅**: CloudWatch/로깅이 보존 기간과 함께 활성화됨 +- [ ] **모니터링**: 이상 징후에 대한 알림 구성됨 +- [ ] **CI/CD**: OIDC 인증, 시크릿 스캐닝, 의존성 감사 +- [ ] **CDN/WAF**: OWASP 규칙으로 Cloudflare WAF 활성화됨 +- [ ] **암호화**: 저장 및 전송 중 데이터 암호화 +- [ ] **백업**: 테스트된 복구와 함께 자동 백업 +- [ ] **규정 준수**: GDPR/HIPAA 요구사항 충족 (해당하는 경우) +- [ ] **문서화**: 인프라 문서화, 런북 작성됨 +- [ ] **인시던트 대응**: 보안 인시던트 계획 마련 + +## 일반적인 클라우드 보안 잘못된 구성 + +### S3 버킷 노출 + +```bash +# ❌ WRONG: Public bucket +aws s3api put-bucket-acl --bucket my-bucket --acl public-read + +# ✅ CORRECT: Private bucket with specific access +aws s3api put-bucket-acl --bucket my-bucket --acl private +aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json +``` + +### RDS 공개 접근 + +```terraform +# ❌ WRONG +resource "aws_db_instance" "bad" { + publicly_accessible = true # NEVER do this! +} + +# ✅ CORRECT +resource "aws_db_instance" "good" { + publicly_accessible = false + vpc_security_group_ids = [aws_security_group.db.id] +} +``` + +## 참고 자료 + +- [AWS Security Best Practices](https://aws.amazon.com/security/best-practices/) +- [CIS AWS Foundations Benchmark](https://www.cisecurity.org/benchmark/amazon_web_services) +- [Cloudflare Security Documentation](https://developers.cloudflare.com/security/) +- [OWASP Cloud Security](https://owasp.org/www-project-cloud-security/) +- [Terraform Security Best Practices](https://www.terraform.io/docs/cloud/guides/recommended-practices/) + +**기억하세요**: 클라우드 잘못된 구성은 데이터 유출의 주요 원인입니다. 하나의 노출된 S3 버킷이나 과도하게 허용적인 IAM 정책이 전체 인프라를 침해할 수 있습니다. 항상 최소 권한 원칙과 심층 방어를 따르세요. diff --git a/docs/ko-KR/skills/strategic-compact/SKILL.md b/docs/ko-KR/skills/strategic-compact/SKILL.md new file mode 100644 index 00000000..e58f4a66 --- /dev/null +++ b/docs/ko-KR/skills/strategic-compact/SKILL.md @@ -0,0 +1,103 @@ +--- +name: strategic-compact +description: 임의의 자동 컴팩션 대신 논리적 간격에서 수동 컨텍스트 압축을 제안하여 작업 단계를 통해 컨텍스트를 보존합니다. +origin: ECC +--- + +# 전략적 컴팩트 스킬 + +임의의 자동 컴팩션에 의존하지 않고 워크플로우의 전략적 지점에서 수동 `/compact`를 제안합니다. + +## 활성화 시점 + +- 컨텍스트 제한에 근접하는 긴 세션을 실행할 때 (200K+ 토큰) +- 다단계 작업을 수행할 때 (조사 -> 계획 -> 구현 -> 테스트) +- 같은 세션 내에서 관련 없는 작업 간 전환할 때 +- 주요 마일스톤을 완료하고 새 작업을 시작할 때 +- 응답이 느려지거나 일관성이 떨어질 때 (컨텍스트 압박) + +## 전략적 컴팩션이 필요한 이유 + +자동 컴팩션은 임의의 지점에서 실행됩니다: +- 종종 작업 중간에 실행되어 중요한 컨텍스트를 잃음 +- 논리적 작업 경계를 인식하지 못함 +- 복잡한 다단계 작업을 중단할 수 있음 + +논리적 경계에서의 전략적 컴팩션: +- **탐색 후, 실행 전** -- 조사 컨텍스트를 압축하고 구현 계획은 유지 +- **마일스톤 완료 후** -- 다음 단계를 위한 새로운 시작 +- **주요 컨텍스트 전환 전** -- 다른 작업 시작 전에 탐색 컨텍스트 정리 + +## 작동 방식 + +`suggest-compact.js` 스크립트는 PreToolUse (Edit/Write)에서 실행되며 다음을 수행합니다: + +1. **도구 호출 추적** -- 세션 내 도구 호출 횟수를 카운트 +2. **임계값 감지** -- 설정 가능한 임계값에서 제안 (기본값: 50회) +3. **주기적 알림** -- 임계값 이후 25회마다 알림 + +## Hook 설정 + +`~/.claude/settings.json`에 추가합니다: + +```json +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit", + "hooks": [{ "type": "command", "command": "node ~/.claude/skills/strategic-compact/suggest-compact.js" }] + }, + { + "matcher": "Write", + "hooks": [{ "type": "command", "command": "node ~/.claude/skills/strategic-compact/suggest-compact.js" }] + } + ] + } +} +``` + +## 구성 + +환경 변수: +- `COMPACT_THRESHOLD` -- 첫 번째 제안까지의 도구 호출 횟수 (기본값: 50) + +## 컴팩션 결정 가이드 + +컴팩션 시기를 결정하기 위해 이 표를 사용하세요: + +| 단계 전환 | 컴팩션? | 이유 | +|-----------------|----------|-----| +| 조사 -> 계획 | 예 | 조사 컨텍스트는 부피가 크고, 계획이 증류된 결과물 | +| 계획 -> 구현 | 예 | 계획은 TodoWrite 또는 파일에 있으므로 코드를 위한 컨텍스트 확보 | +| 구현 -> 테스트 | 경우에 따라 | 테스트가 최근 코드를 참조하면 유지; 포커스 전환 시 컴팩션 | +| 디버깅 -> 다음 기능 | 예 | 디버그 추적이 관련 없는 작업의 컨텍스트를 오염시킴 | +| 구현 중간 | 아니오 | 변수명, 파일 경로, 부분 상태를 잃는 비용이 큼 | +| 실패한 접근 후 | 예 | 새 접근을 시도하기 전에 막다른 길의 추론을 정리 | + +## 컴팩션에서 유지되는 것 + +무엇이 유지되는지 이해하면 자신 있게 컴팩션할 수 있습니다: + +| 유지됨 | 손실됨 | +|----------|------| +| CLAUDE.md 지침 | 중간 추론 및 분석 | +| TodoWrite 작업 목록 | 이전에 읽은 파일 내용 | +| 메모리 파일 (`~/.claude/memory/`) | 다단계 대화 컨텍스트 | +| Git 상태 (커밋, 브랜치) | 도구 호출 기록 및 횟수 | +| 디스크의 파일 | 구두로 언급된 세밀한 사용자 선호도 | + +## 모범 사례 + +1. **계획 후 컴팩션** -- TodoWrite에서 계획이 확정되면 새로 시작하기 위해 컴팩션 +2. **디버깅 후 컴팩션** -- 계속하기 전에 에러 해결 컨텍스트 정리 +3. **구현 중간에는 컴팩션하지 않기** -- 관련 변경 사항의 컨텍스트 보존 +4. **제안을 읽기** -- Hook이 *언제*를 알려주고, *할지* 여부는 당신이 결정 +5. **컴팩션 전에 기록** -- 컴팩션 전에 중요한 컨텍스트를 파일이나 메모리에 저장 +6. **요약과 함께 `/compact` 사용** -- 커스텀 메시지 추가: `/compact Focus on implementing auth middleware next` + +## 관련 항목 + +- [The Longform Guide](https://x.com/affaanmustafa/status/2014040193557471352) -- 토큰 최적화 섹션 +- 메모리 영속성 Hook -- 컴팩션에서 살아남는 상태를 위해 +- `continuous-learning` 스킬 -- 세션 종료 전 패턴 추출 diff --git a/docs/ko-KR/skills/tdd-workflow/SKILL.md b/docs/ko-KR/skills/tdd-workflow/SKILL.md new file mode 100644 index 00000000..74a29398 --- /dev/null +++ b/docs/ko-KR/skills/tdd-workflow/SKILL.md @@ -0,0 +1,410 @@ +--- +name: tdd-workflow +description: 새 기능 작성, 버그 수정 또는 코드 리팩터링 시 이 스킬을 사용하세요. 단위, 통합, E2E 테스트를 포함한 80% 이상의 커버리지로 테스트 주도 개발을 시행합니다. +origin: ECC +--- + +# 테스트 주도 개발 워크플로우 + +이 스킬은 모든 코드 개발이 포괄적인 테스트 커버리지와 함께 TDD 원칙을 따르도록 보장합니다. + +## 활성화 시점 + +- 새 기능이나 기능성을 작성할 때 +- 버그나 이슈를 수정할 때 +- 기존 코드를 리팩터링할 때 +- API 엔드포인트를 추가할 때 +- 새 컴포넌트를 생성할 때 + +## 핵심 원칙 + +### 1. 코드보다 테스트가 먼저 +항상 테스트를 먼저 작성한 후, 테스트를 통과시키는 코드를 구현합니다. + +### 2. 커버리지 요구 사항 +- 최소 80% 커버리지 (단위 + 통합 + E2E) +- 모든 엣지 케이스 커버 +- 에러 시나리오 테스트 +- 경계 조건 검증 + +### 3. 테스트 유형 + +#### 단위 테스트 +- 개별 함수 및 유틸리티 +- 컴포넌트 로직 +- 순수 함수 +- 헬퍼 및 유틸리티 + +#### 통합 테스트 +- API 엔드포인트 +- 데이터베이스 작업 +- 서비스 상호작용 +- 외부 API 호출 + +#### E2E 테스트 (Playwright) +- 핵심 사용자 플로우 +- 완전한 워크플로우 +- 브라우저 자동화 +- UI 상호작용 + +## TDD 워크플로우 단계 + +### 단계 1: 사용자 여정 작성 +``` +As a [role], I want to [action], so that [benefit] + +Example: +As a user, I want to search for markets semantically, +so that I can find relevant markets even without exact keywords. +``` + +### 단계 2: 테스트 케이스 생성 +각 사용자 여정에 대해 포괄적인 테스트 케이스를 작성합니다: + +```typescript +describe('Semantic Search', () => { + it('returns relevant markets for query', async () => { + // Test implementation + }) + + it('handles empty query gracefully', async () => { + // Test edge case + }) + + it('falls back to substring search when Redis unavailable', async () => { + // Test fallback behavior + }) + + it('sorts results by similarity score', async () => { + // Test sorting logic + }) +}) +``` + +### 단계 3: 테스트 실행 (실패해야 함) +```bash +npm test +# Tests should fail - we haven't implemented yet +``` + +### 단계 4: 코드 구현 +테스트를 통과시키기 위한 최소한의 코드를 작성합니다: + +```typescript +// Implementation guided by tests +export async function searchMarkets(query: string) { + // Implementation here +} +``` + +### 단계 5: 테스트 재실행 +```bash +npm test +# Tests should now pass +``` + +### 단계 6: 리팩터링 +테스트가 통과하는 상태를 유지하면서 코드 품질을 개선합니다: +- 중복 제거 +- 네이밍 개선 +- 성능 최적화 +- 가독성 향상 + +### 단계 7: 커버리지 확인 +```bash +npm run test:coverage +# Verify 80%+ coverage achieved +``` + +## 테스트 패턴 + +### 단위 테스트 패턴 (Jest/Vitest) +```typescript +import { render, screen, fireEvent } from '@testing-library/react' +import { Button } from './Button' + +describe('Button Component', () => { + it('renders with correct text', () => { + render() + expect(screen.getByText('Click me')).toBeInTheDocument() + }) + + it('calls onClick when clicked', () => { + const handleClick = jest.fn() + render() + + fireEvent.click(screen.getByRole('button')) + + expect(handleClick).toHaveBeenCalledTimes(1) + }) + + it('is disabled when disabled prop is true', () => { + render() + expect(screen.getByRole('button')).toBeDisabled() + }) +}) +``` + +### API 통합 테스트 패턴 +```typescript +import { NextRequest } from 'next/server' +import { GET } from './route' + +describe('GET /api/markets', () => { + it('returns markets successfully', async () => { + const request = new NextRequest('http://localhost/api/markets') + const response = await GET(request) + const data = await response.json() + + expect(response.status).toBe(200) + expect(data.success).toBe(true) + expect(Array.isArray(data.data)).toBe(true) + }) + + it('validates query parameters', async () => { + const request = new NextRequest('http://localhost/api/markets?limit=invalid') + const response = await GET(request) + + expect(response.status).toBe(400) + }) + + it('handles database errors gracefully', async () => { + // Mock database failure + const request = new NextRequest('http://localhost/api/markets') + // Test error handling + }) +}) +``` + +### E2E 테스트 패턴 (Playwright) +```typescript +import { test, expect } from '@playwright/test' + +test('user can search and filter markets', async ({ page }) => { + // Navigate to markets page + await page.goto('/') + await page.click('a[href="/markets"]') + + // Verify page loaded + await expect(page.locator('h1')).toContainText('Markets') + + // Search for markets + await page.fill('input[placeholder="Search markets"]', 'election') + + // Wait for debounce and results + await page.waitForTimeout(600) + + // Verify search results displayed + const results = page.locator('[data-testid="market-card"]') + await expect(results).toHaveCount(5, { timeout: 5000 }) + + // Verify results contain search term + const firstResult = results.first() + await expect(firstResult).toContainText('election', { ignoreCase: true }) + + // Filter by status + await page.click('button:has-text("Active")') + + // Verify filtered results + await expect(results).toHaveCount(3) +}) + +test('user can create a new market', async ({ page }) => { + // Login first + await page.goto('/creator-dashboard') + + // Fill market creation form + await page.fill('input[name="name"]', 'Test Market') + await page.fill('textarea[name="description"]', 'Test description') + await page.fill('input[name="endDate"]', '2025-12-31') + + // Submit form + await page.click('button[type="submit"]') + + // Verify success message + await expect(page.locator('text=Market created successfully')).toBeVisible() + + // Verify redirect to market page + await expect(page).toHaveURL(/\/markets\/test-market/) +}) +``` + +## 테스트 파일 구성 + +``` +src/ +├── components/ +│ ├── Button/ +│ │ ├── Button.tsx +│ │ ├── Button.test.tsx # Unit tests +│ │ └── Button.stories.tsx # Storybook +│ └── MarketCard/ +│ ├── MarketCard.tsx +│ └── MarketCard.test.tsx +├── app/ +│ └── api/ +│ └── markets/ +│ ├── route.ts +│ └── route.test.ts # Integration tests +└── e2e/ + ├── markets.spec.ts # E2E tests + ├── trading.spec.ts + └── auth.spec.ts +``` + +## 외부 서비스 모킹 + +### Supabase Mock +```typescript +jest.mock('@/lib/supabase', () => ({ + supabase: { + from: jest.fn(() => ({ + select: jest.fn(() => ({ + eq: jest.fn(() => Promise.resolve({ + data: [{ id: 1, name: 'Test Market' }], + error: null + })) + })) + })) + } +})) +``` + +### Redis Mock +```typescript +jest.mock('@/lib/redis', () => ({ + searchMarketsByVector: jest.fn(() => Promise.resolve([ + { slug: 'test-market', similarity_score: 0.95 } + ])), + checkRedisHealth: jest.fn(() => Promise.resolve({ connected: true })) +})) +``` + +### OpenAI Mock +```typescript +jest.mock('@/lib/openai', () => ({ + generateEmbedding: jest.fn(() => Promise.resolve( + new Array(1536).fill(0.1) // Mock 1536-dim embedding + )) +})) +``` + +## 테스트 커버리지 검증 + +### 커버리지 리포트 실행 +```bash +npm run test:coverage +``` + +### 커버리지 임계값 +```json +{ + "jest": { + "coverageThresholds": { + "global": { + "branches": 80, + "functions": 80, + "lines": 80, + "statements": 80 + } + } + } +} +``` + +## 흔한 테스트 실수 + +### 잘못된 예: 구현 세부사항 테스트 +```typescript +// Don't test internal state +expect(component.state.count).toBe(5) +``` + +### 올바른 예: 사용자에게 보이는 동작 테스트 +```typescript +// Test what users see +expect(screen.getByText('Count: 5')).toBeInTheDocument() +``` + +### 잘못된 예: 취약한 셀렉터 +```typescript +// Breaks easily +await page.click('.css-class-xyz') +``` + +### 올바른 예: 시맨틱 셀렉터 +```typescript +// Resilient to changes +await page.click('button:has-text("Submit")') +await page.click('[data-testid="submit-button"]') +``` + +### 잘못된 예: 테스트 격리 없음 +```typescript +// Tests depend on each other +test('creates user', () => { /* ... */ }) +test('updates same user', () => { /* depends on previous test */ }) +``` + +### 올바른 예: 독립적인 테스트 +```typescript +// Each test sets up its own data +test('creates user', () => { + const user = createTestUser() + // Test logic +}) + +test('updates user', () => { + const user = createTestUser() + // Update logic +}) +``` + +## 지속적 테스트 + +### 개발 중 Watch 모드 +```bash +npm test -- --watch +# Tests run automatically on file changes +``` + +### Pre-Commit Hook +```bash +# Runs before every commit +npm test && npm run lint +``` + +### CI/CD 통합 +```yaml +# GitHub Actions +- name: Run Tests + run: npm test -- --coverage +- name: Upload Coverage + uses: codecov/codecov-action@v3 +``` + +## 모범 사례 + +1. **테스트 먼저 작성** - 항상 TDD +2. **테스트당 하나의 Assert** - 단일 동작에 집중 +3. **설명적인 테스트 이름** - 무엇을 테스트하는지 설명 +4. **Arrange-Act-Assert** - 명확한 테스트 구조 +5. **외부 의존성 모킹** - 단위 테스트 격리 +6. **엣지 케이스 테스트** - null, undefined, 빈 값, 큰 값 +7. **에러 경로 테스트** - 정상 경로만이 아닌 +8. **테스트 속도 유지** - 단위 테스트 각 50ms 미만 +9. **테스트 후 정리** - 부작용 없음 +10. **커버리지 리포트 검토** - 누락 부분 식별 + +## 성공 지표 + +- 80% 이상의 코드 커버리지 달성 +- 모든 테스트 통과 (그린) +- 건너뛴 테스트나 비활성화된 테스트 없음 +- 빠른 테스트 실행 (단위 테스트 30초 미만) +- E2E 테스트가 핵심 사용자 플로우를 커버 +- 테스트가 프로덕션 이전에 버그를 포착 + +--- + +**기억하세요**: 테스트는 선택 사항이 아닙니다. 테스트는 자신감 있는 리팩터링, 빠른 개발, 그리고 프로덕션 안정성을 가능하게 하는 안전망입니다. diff --git a/docs/ko-KR/skills/verification-loop/SKILL.md b/docs/ko-KR/skills/verification-loop/SKILL.md new file mode 100644 index 00000000..0571b6a0 --- /dev/null +++ b/docs/ko-KR/skills/verification-loop/SKILL.md @@ -0,0 +1,126 @@ +--- +name: verification-loop +description: "Claude Code 세션을 위한 포괄적인 검증 시스템." +origin: ECC +--- + +# 검증 루프 스킬 + +Claude Code 세션을 위한 포괄적인 검증 시스템. + +## 사용 시점 + +다음 상황에서 이 스킬을 호출하세요: +- 기능 또는 주요 코드 변경을 완료한 후 +- PR을 생성하기 전 +- 품질 게이트가 통과하는지 확인하고 싶을 때 +- 리팩터링 후 + +## 검증 단계 + +### 단계 1: 빌드 검증 +```bash +# Check if project builds +npm run build 2>&1 | tail -20 +# OR +pnpm build 2>&1 | tail -20 +``` + +빌드가 실패하면 계속하기 전에 중단하고 수정합니다. + +### 단계 2: 타입 검사 +```bash +# TypeScript projects +npx tsc --noEmit 2>&1 | head -30 + +# Python projects +pyright . 2>&1 | head -30 +``` + +모든 타입 에러를 보고합니다. 중요한 것은 계속하기 전에 수정합니다. + +### 단계 3: 린트 검사 +```bash +# JavaScript/TypeScript +npm run lint 2>&1 | head -30 + +# Python +ruff check . 2>&1 | head -30 +``` + +### 단계 4: 테스트 스위트 +```bash +# Run tests with coverage +npm run test -- --coverage 2>&1 | tail -50 + +# Check coverage threshold +# Target: 80% minimum +``` + +보고 항목: +- 전체 테스트: X +- 통과: X +- 실패: X +- 커버리지: X% + +### 단계 5: 보안 스캔 +```bash +# Check for secrets +grep -rn "sk-" --include="*.ts" --include="*.js" . 2>/dev/null | head -10 +grep -rn "api_key" --include="*.ts" --include="*.js" . 2>/dev/null | head -10 + +# Check for console.log +grep -rn "console.log" --include="*.ts" --include="*.tsx" src/ 2>/dev/null | head -10 +``` + +### 단계 6: Diff 리뷰 +```bash +# Show what changed +git diff --stat +git diff HEAD~1 --name-only +``` + +각 변경된 파일에서 다음을 검토합니다: +- 의도하지 않은 변경 +- 누락된 에러 처리 +- 잠재적 엣지 케이스 + +## 출력 형식 + +모든 단계를 실행한 후 검증 보고서를 생성합니다: + +``` +VERIFICATION REPORT +================== + +Build: [PASS/FAIL] +Types: [PASS/FAIL] (X errors) +Lint: [PASS/FAIL] (X warnings) +Tests: [PASS/FAIL] (X/Y passed, Z% coverage) +Security: [PASS/FAIL] (X issues) +Diff: [X files changed] + +Overall: [READY/NOT READY] for PR + +Issues to Fix: +1. ... +2. ... +``` + +## 연속 모드 + +긴 세션에서는 15분마다 또는 주요 변경 후에 검증을 실행합니다: + +```markdown +Set a mental checkpoint: +- After completing each function +- After finishing a component +- Before moving to next task + +Run: /verify +``` + +## Hook과의 통합 + +이 스킬은 PostToolUse Hook을 보완하지만 더 깊은 검증을 제공합니다. +Hook은 즉시 문제를 포착하고, 이 스킬은 포괄적인 검토를 제공합니다.