import { query, queryOne, transaction } from "../database/db"; export interface ComponentStandardData { component_code: string; component_name: string; component_name_eng?: string; description?: string; category: string; icon_name?: string; default_size?: any; component_config: any; preview_image?: string; sort_order?: number; is_active?: string; is_public?: string; company_code: string; created_by?: string; updated_by?: string; } export interface ComponentQueryParams { category?: string; active?: string; is_public?: string; company_code?: string; search?: string; sort?: string; order?: "asc" | "desc"; limit?: number; offset?: number; } class ComponentStandardService { /** * 컴포넌트 목록 조회 */ async getComponents(params: ComponentQueryParams = {}) { const { category, active = "Y", is_public, company_code, search, sort = "sort_order", order = "asc", limit, offset = 0, } = params; const whereConditions: string[] = []; const values: any[] = []; let paramIndex = 1; // 활성화 상태 필터 if (active) { whereConditions.push(`is_active = $${paramIndex++}`); values.push(active); } // 카테고리 필터 if (category && category !== "all") { whereConditions.push(`category = $${paramIndex++}`); values.push(category); } // 공개 여부 필터 if (is_public) { whereConditions.push(`is_public = $${paramIndex++}`); values.push(is_public); } // 회사별 필터 (공개 컴포넌트 + 해당 회사 컴포넌트) if (company_code) { whereConditions.push( `(is_public = 'Y' OR company_code = $${paramIndex++})` ); values.push(company_code); } // 검색 조건 if (search) { whereConditions.push( `(component_name ILIKE $${paramIndex} OR component_name_eng ILIKE $${paramIndex} OR description ILIKE $${paramIndex})` ); values.push(`%${search}%`); paramIndex++; } const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : ""; // 정렬 컬럼 검증 (SQL 인젝션 방지) const validSortColumns = [ "sort_order", "component_name", "category", "created_date", "updated_date", ]; const sortColumn = validSortColumns.includes(sort) ? sort : "sort_order"; const sortOrder = order === "desc" ? "DESC" : "ASC"; // 컴포넌트 조회 const components = await query( `SELECT * FROM component_standards ${whereClause} ORDER BY ${sortColumn} ${sortOrder} ${limit ? `LIMIT $${paramIndex++}` : ""} ${limit ? `OFFSET $${paramIndex++}` : ""}`, limit ? [...values, limit, offset] : values ); // 전체 개수 조회 const countResult = await queryOne<{ count: string }>( `SELECT COUNT(*) as count FROM component_standards ${whereClause}`, values ); const total = parseInt(countResult?.count || "0"); return { components, total, limit, offset, }; } /** * 컴포넌트 상세 조회 */ async getComponent(component_code: string) { const component = await queryOne( `SELECT * FROM component_standards WHERE component_code = $1`, [component_code] ); if (!component) { throw new Error(`컴포넌트를 찾을 수 없습니다: ${component_code}`); } return component; } /** * 컴포넌트 생성 */ async createComponent(data: ComponentStandardData) { // 중복 코드 확인 const existing = await queryOne( `SELECT * FROM component_standards WHERE component_code = $1`, [data.component_code] ); if (existing) { throw new Error( `이미 존재하는 컴포넌트 코드입니다: ${data.component_code}` ); } // 'active' 필드를 'is_active'로 변환 const createData = { ...data }; if ("active" in createData) { createData.is_active = (createData as any).active; delete (createData as any).active; } const component = await queryOne( `INSERT INTO component_standards (component_code, component_name, component_name_eng, description, category, icon_name, default_size, component_config, preview_image, sort_order, is_active, is_public, company_code, created_by, updated_by, created_date, updated_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW(), NOW()) RETURNING *`, [ createData.component_code, createData.component_name, createData.component_name_eng || null, createData.description || null, createData.category, createData.icon_name || null, createData.default_size || null, createData.component_config, createData.preview_image || null, createData.sort_order || 0, createData.is_active || "Y", createData.is_public || "N", createData.company_code, createData.created_by || null, createData.updated_by || null, ] ); return component; } /** * 컴포넌트 수정 */ async updateComponent( component_code: string, data: Partial ) { const existing = await this.getComponent(component_code); // 'active' 필드를 'is_active'로 변환 const updateData = { ...data }; if ("active" in updateData) { updateData.is_active = (updateData as any).active; delete (updateData as any).active; } // 동적 UPDATE 쿼리 생성 const updateFields: string[] = ["updated_date = NOW()"]; const values: any[] = []; let paramIndex = 1; const fieldMapping: { [key: string]: string } = { component_name: "component_name", component_name_eng: "component_name_eng", description: "description", category: "category", icon_name: "icon_name", default_size: "default_size", component_config: "component_config", preview_image: "preview_image", sort_order: "sort_order", is_active: "is_active", is_public: "is_public", company_code: "company_code", updated_by: "updated_by", }; for (const [key, dbField] of Object.entries(fieldMapping)) { if (key in updateData) { updateFields.push(`${dbField} = $${paramIndex++}`); values.push((updateData as any)[key]); } } const component = await queryOne( `UPDATE component_standards SET ${updateFields.join(", ")} WHERE component_code = $${paramIndex} RETURNING *`, [...values, component_code] ); return component; } /** * 컴포넌트 삭제 */ async deleteComponent(component_code: string) { const existing = await this.getComponent(component_code); await query(`DELETE FROM component_standards WHERE component_code = $1`, [ component_code, ]); return { message: `컴포넌트가 삭제되었습니다: ${component_code}` }; } /** * 컴포넌트 정렬 순서 업데이트 */ async updateSortOrder( updates: Array<{ component_code: string; sort_order: number }> ) { await transaction(async (client) => { for (const { component_code, sort_order } of updates) { await client.query( `UPDATE component_standards SET sort_order = $1, updated_date = NOW() WHERE component_code = $2`, [sort_order, component_code] ); } }); return { message: "정렬 순서가 업데이트되었습니다." }; } /** * 컴포넌트 복제 */ async duplicateComponent( source_code: string, new_code: string, new_name: string ) { const source = await this.getComponent(source_code); // 새 코드 중복 확인 const existing = await queryOne( `SELECT * FROM component_standards WHERE component_code = $1`, [new_code] ); if (existing) { throw new Error(`이미 존재하는 컴포넌트 코드입니다: ${new_code}`); } const component = await queryOne( `INSERT INTO component_standards (component_code, component_name, component_name_eng, description, category, icon_name, default_size, component_config, preview_image, sort_order, is_active, is_public, company_code, created_date, updated_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), NOW()) RETURNING *`, [ new_code, new_name, source?.component_name_eng, source?.description, source?.category, source?.icon_name, source?.default_size, source?.component_config, source?.preview_image, source?.sort_order, source?.is_active, source?.is_public, source?.company_code || "DEFAULT", ] ); return component; } /** * 카테고리 목록 조회 */ async getCategories(company_code?: string) { const whereConditions: string[] = ["is_active = 'Y'"]; const values: any[] = []; if (company_code) { whereConditions.push(`(is_public = 'Y' OR company_code = $1)`); values.push(company_code); } const whereClause = `WHERE ${whereConditions.join(" AND ")}`; const categories = await query<{ category: string }>( `SELECT DISTINCT category FROM component_standards ${whereClause} ORDER BY category`, values ); return categories .map((item) => item.category) .filter((category) => category !== null); } /** * 컴포넌트 통계 */ async getStatistics(company_code?: string) { const whereConditions: string[] = ["is_active = 'Y'"]; const values: any[] = []; if (company_code) { whereConditions.push(`(is_public = 'Y' OR company_code = $1)`); values.push(company_code); } const whereClause = `WHERE ${whereConditions.join(" AND ")}`; // 전체 개수 const totalResult = await queryOne<{ count: string }>( `SELECT COUNT(*) as count FROM component_standards ${whereClause}`, values ); const total = parseInt(totalResult?.count || "0"); // 카테고리별 집계 const byCategory = await query<{ category: string; count: string }>( `SELECT category, COUNT(*) as count FROM component_standards ${whereClause} GROUP BY category`, values ); // 상태별 집계 const byStatus = await query<{ is_active: string; count: string }>( `SELECT is_active, COUNT(*) as count FROM component_standards GROUP BY is_active` ); return { total, byCategory: byCategory.map((item) => ({ category: item.category, count: parseInt(item.count), })), byStatus: byStatus.map((item) => ({ status: item.is_active, count: parseInt(item.count), })), }; } /** * 컴포넌트 코드 중복 체크 */ async checkDuplicate( component_code: string, company_code?: string ): Promise { const whereConditions: string[] = ["component_code = $1"]; const values: any[] = [component_code]; // 회사 코드가 있고 "*"가 아닌 경우에만 조건 추가 if (company_code && company_code !== "*") { whereConditions.push("company_code = $2"); values.push(company_code); } const whereClause = `WHERE ${whereConditions.join(" AND ")}`; const existingComponent = await queryOne( `SELECT * FROM component_standards ${whereClause} LIMIT 1`, values ); return !!existingComponent; } } export default new ComponentStandardService();