/** * SQL 쿼리 빌더 유틸리티 * * Raw Query 방식에서 안전하고 효율적인 쿼리 생성을 위한 헬퍼 */ export interface SelectOptions { columns?: string[]; where?: Record; joins?: JoinClause[]; orderBy?: string; limit?: number; offset?: number; groupBy?: string[]; having?: Record; } export interface JoinClause { type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL'; table: string; on: string; } export interface InsertOptions { returning?: string[]; onConflict?: { columns: string[]; action: 'DO NOTHING' | 'DO UPDATE'; updateSet?: string[]; }; } export interface UpdateOptions { returning?: string[]; } export interface QueryResult { query: string; params: any[]; } export class QueryBuilder { /** * SELECT 쿼리 생성 */ static select(table: string, options: SelectOptions = {}): QueryResult { const { columns = ['*'], where = {}, joins = [], orderBy, limit, offset, groupBy = [], having = {}, } = options; let query = `SELECT ${columns.join(', ')} FROM ${table}`; const params: any[] = []; let paramIndex = 1; // JOIN 절 추가 for (const join of joins) { query += ` ${join.type} JOIN ${join.table} ON ${join.on}`; } // WHERE 절 추가 const whereConditions = Object.keys(where); if (whereConditions.length > 0) { const whereClause = whereConditions .map((key) => { params.push(where[key]); return `${key} = $${paramIndex++}`; }) .join(' AND '); query += ` WHERE ${whereClause}`; } // GROUP BY 절 추가 if (groupBy.length > 0) { query += ` GROUP BY ${groupBy.join(', ')}`; } // HAVING 절 추가 const havingConditions = Object.keys(having); if (havingConditions.length > 0) { const havingClause = havingConditions .map((key) => { params.push(having[key]); return `${key} = $${paramIndex++}`; }) .join(' AND '); query += ` HAVING ${havingClause}`; } // ORDER BY 절 추가 if (orderBy) { query += ` ORDER BY ${orderBy}`; } // LIMIT 절 추가 if (limit !== undefined) { params.push(limit); query += ` LIMIT $${paramIndex++}`; } // OFFSET 절 추가 if (offset !== undefined) { params.push(offset); query += ` OFFSET $${paramIndex++}`; } return { query, params }; } /** * INSERT 쿼리 생성 */ static insert( table: string, data: Record, options: InsertOptions = {} ): QueryResult { const { returning = [], onConflict } = options; const columns = Object.keys(data); const values = Object.values(data); const placeholders = values.map((_, index) => `$${index + 1}`).join(', '); let query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`; // ON CONFLICT 절 추가 if (onConflict) { query += ` ON CONFLICT (${onConflict.columns.join(', ')})`; if (onConflict.action === 'DO NOTHING') { query += ' DO NOTHING'; } else if (onConflict.action === 'DO UPDATE' && onConflict.updateSet) { const updateSet = onConflict.updateSet .map(col => `${col} = EXCLUDED.${col}`) .join(', '); query += ` DO UPDATE SET ${updateSet}`; } } // RETURNING 절 추가 if (returning.length > 0) { query += ` RETURNING ${returning.join(', ')}`; } return { query, params: values }; } /** * UPDATE 쿼리 생성 */ static update( table: string, data: Record, where: Record, options: UpdateOptions = {} ): QueryResult { const { returning = [] } = options; const dataKeys = Object.keys(data); const dataValues = Object.values(data); const whereKeys = Object.keys(where); const whereValues = Object.values(where); let paramIndex = 1; // SET 절 생성 const setClause = dataKeys .map((key) => `${key} = $${paramIndex++}`) .join(', '); // WHERE 절 생성 const whereClause = whereKeys .map((key) => `${key} = $${paramIndex++}`) .join(' AND '); let query = `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`; // RETURNING 절 추가 if (returning.length > 0) { query += ` RETURNING ${returning.join(', ')}`; } const params = [...dataValues, ...whereValues]; return { query, params }; } /** * DELETE 쿼리 생성 */ static delete(table: string, where: Record): QueryResult { const whereKeys = Object.keys(where); const whereValues = Object.values(where); const whereClause = whereKeys .map((key, index) => `${key} = $${index + 1}`) .join(' AND '); const query = `DELETE FROM ${table} WHERE ${whereClause}`; return { query, params: whereValues }; } /** * COUNT 쿼리 생성 */ static count(table: string, where: Record = {}): QueryResult { const whereKeys = Object.keys(where); const whereValues = Object.values(where); let query = `SELECT COUNT(*) as count FROM ${table}`; if (whereKeys.length > 0) { const whereClause = whereKeys .map((key, index) => `${key} = $${index + 1}`) .join(' AND '); query += ` WHERE ${whereClause}`; } return { query, params: whereValues }; } /** * EXISTS 쿼리 생성 */ static exists(table: string, where: Record): QueryResult { const whereKeys = Object.keys(where); const whereValues = Object.values(where); const whereClause = whereKeys .map((key, index) => `${key} = $${index + 1}`) .join(' AND '); const query = `SELECT EXISTS(SELECT 1 FROM ${table} WHERE ${whereClause}) as exists`; return { query, params: whereValues }; } /** * 동적 WHERE 절 생성 (복잡한 조건) */ static buildWhereClause( conditions: Record, startParamIndex: number = 1 ): { clause: string; params: any[]; nextParamIndex: number } { const keys = Object.keys(conditions); const params: any[] = []; let paramIndex = startParamIndex; if (keys.length === 0) { return { clause: '', params: [], nextParamIndex: paramIndex }; } const clause = keys .map((key) => { const value = conditions[key]; // 특수 연산자 처리 if (key.includes('>>') || key.includes('->')) { // JSON 쿼리 params.push(value); return `${key} = $${paramIndex++}`; } else if (Array.isArray(value)) { // IN 절 const placeholders = value.map(() => `$${paramIndex++}`).join(', '); params.push(...value); return `${key} IN (${placeholders})`; } else if (value === null) { // NULL 체크 return `${key} IS NULL`; } else { // 일반 조건 params.push(value); return `${key} = $${paramIndex++}`; } }) .join(' AND '); return { clause, params, nextParamIndex: paramIndex }; } }