/** * 채번 규칙 관리 서비스 */ import { getPool } from "../database/db"; import { logger } from "../utils/logger"; interface NumberingRulePart { id?: number; order: number; partType: string; generationMethod: string; autoConfig?: any; manualConfig?: any; generatedValue?: string; } interface NumberingRuleConfig { ruleId: string; ruleName: string; description?: string; parts: NumberingRulePart[]; separator?: string; resetPeriod?: string; currentSequence?: number; tableName?: string; columnName?: string; companyCode?: string; createdAt?: string; updatedAt?: string; createdBy?: string; } class NumberingRuleService { /** * 규칙 목록 조회 */ async getRuleList(companyCode: string): Promise { try { logger.info("채번 규칙 목록 조회 시작", { companyCode }); const pool = getPool(); const query = ` SELECT rule_id AS "ruleId", rule_name AS "ruleName", description, separator, reset_period AS "resetPeriod", current_sequence AS "currentSequence", table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules WHERE company_code = $1 OR company_code = '*' ORDER BY created_at DESC `; const result = await pool.query(query, [companyCode]); // 각 규칙의 파트 정보 조회 for (const rule of result.rows) { const partsQuery = ` SELECT id, part_order AS "order", part_type AS "partType", generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" FROM numbering_rule_parts WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*') ORDER BY part_order `; const partsResult = await pool.query(partsQuery, [rule.ruleId, companyCode]); rule.parts = partsResult.rows; } logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, { companyCode }); return result.rows; } catch (error: any) { logger.error(`채번 규칙 목록 조회 중 에러: ${error.message}`); throw error; } } /** * 특정 규칙 조회 */ async getRuleById(ruleId: string, companyCode: string): Promise { const pool = getPool(); const query = ` SELECT rule_id AS "ruleId", rule_name AS "ruleName", description, separator, reset_period AS "resetPeriod", current_sequence AS "currentSequence", table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" FROM numbering_rules WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*') `; const result = await pool.query(query, [ruleId, companyCode]); if (result.rowCount === 0) return null; const rule = result.rows[0]; const partsQuery = ` SELECT id, part_order AS "order", part_type AS "partType", generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" FROM numbering_rule_parts WHERE rule_id = $1 AND (company_code = $2 OR company_code = '*') ORDER BY part_order `; const partsResult = await pool.query(partsQuery, [ruleId, companyCode]); rule.parts = partsResult.rows; return rule; } /** * 규칙 생성 */ async createRule( config: NumberingRuleConfig, companyCode: string, userId: string ): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); // 마스터 삽입 const insertRuleQuery = ` INSERT INTO numbering_rules ( rule_id, rule_name, description, separator, reset_period, current_sequence, table_name, column_name, company_code, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING rule_id AS "ruleId", rule_name AS "ruleName", description, separator, reset_period AS "resetPeriod", current_sequence AS "currentSequence", table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" `; const ruleResult = await client.query(insertRuleQuery, [ config.ruleId, config.ruleName, config.description || null, config.separator || "-", config.resetPeriod || "none", config.currentSequence || 1, config.tableName || null, config.columnName || null, companyCode, userId, ]); // 파트 삽입 const parts: NumberingRulePart[] = []; for (const part of config.parts) { const insertPartQuery = ` INSERT INTO numbering_rule_parts ( rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code ) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, part_order AS "order", part_type AS "partType", generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" `; const partResult = await client.query(insertPartQuery, [ config.ruleId, part.order, part.partType, part.generationMethod, JSON.stringify(part.autoConfig || {}), JSON.stringify(part.manualConfig || {}), companyCode, ]); parts.push(partResult.rows[0]); } await client.query("COMMIT"); logger.info("채번 규칙 생성 완료", { ruleId: config.ruleId, companyCode }); return { ...ruleResult.rows[0], parts }; } catch (error: any) { await client.query("ROLLBACK"); logger.error("채번 규칙 생성 실패", { error: error.message }); throw error; } finally { client.release(); } } /** * 규칙 수정 */ async updateRule( ruleId: string, updates: Partial, companyCode: string ): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); const updateRuleQuery = ` UPDATE numbering_rules SET rule_name = COALESCE($1, rule_name), description = COALESCE($2, description), separator = COALESCE($3, separator), reset_period = COALESCE($4, reset_period), table_name = COALESCE($5, table_name), column_name = COALESCE($6, column_name), updated_at = NOW() WHERE rule_id = $7 AND company_code = $8 RETURNING rule_id AS "ruleId", rule_name AS "ruleName", description, separator, reset_period AS "resetPeriod", current_sequence AS "currentSequence", table_name AS "tableName", column_name AS "columnName", company_code AS "companyCode", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy" `; const ruleResult = await client.query(updateRuleQuery, [ updates.ruleName, updates.description, updates.separator, updates.resetPeriod, updates.tableName, updates.columnName, ruleId, companyCode, ]); if (ruleResult.rowCount === 0) { throw new Error("규칙을 찾을 수 없거나 권한이 없습니다"); } // 파트 업데이트 let parts: NumberingRulePart[] = []; if (updates.parts) { await client.query( "DELETE FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2", [ruleId, companyCode] ); for (const part of updates.parts) { const insertPartQuery = ` INSERT INTO numbering_rule_parts ( rule_id, part_order, part_type, generation_method, auto_config, manual_config, company_code ) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, part_order AS "order", part_type AS "partType", generation_method AS "generationMethod", auto_config AS "autoConfig", manual_config AS "manualConfig" `; const partResult = await client.query(insertPartQuery, [ ruleId, part.order, part.partType, part.generationMethod, JSON.stringify(part.autoConfig || {}), JSON.stringify(part.manualConfig || {}), companyCode, ]); parts.push(partResult.rows[0]); } } await client.query("COMMIT"); logger.info("채번 규칙 수정 완료", { ruleId, companyCode }); return { ...ruleResult.rows[0], parts }; } catch (error: any) { await client.query("ROLLBACK"); logger.error("채번 규칙 수정 실패", { error: error.message }); throw error; } finally { client.release(); } } /** * 규칙 삭제 */ async deleteRule(ruleId: string, companyCode: string): Promise { const pool = getPool(); const query = ` DELETE FROM numbering_rules WHERE rule_id = $1 AND company_code = $2 `; const result = await pool.query(query, [ruleId, companyCode]); if (result.rowCount === 0) { throw new Error("규칙을 찾을 수 없거나 권한이 없습니다"); } logger.info("채번 규칙 삭제 완료", { ruleId, companyCode }); } /** * 코드 생성 */ async generateCode(ruleId: string, companyCode: string): Promise { const pool = getPool(); const client = await pool.connect(); try { await client.query("BEGIN"); const rule = await this.getRuleById(ruleId, companyCode); if (!rule) throw new Error("규칙을 찾을 수 없습니다"); const parts = rule.parts .sort((a: any, b: any) => a.order - b.order) .map((part: any) => { if (part.generationMethod === "manual") { return part.manualConfig?.value || ""; } const autoConfig = part.autoConfig || {}; switch (part.partType) { case "prefix": return autoConfig.prefix || "PREFIX"; case "sequence": { const length = autoConfig.sequenceLength || 4; return String(rule.currentSequence || 1).padStart(length, "0"); } case "date": return this.formatDate(new Date(), autoConfig.dateFormat || "YYYYMMDD"); case "year": { const format = autoConfig.dateFormat || "YYYY"; const year = new Date().getFullYear(); return format === "YY" ? String(year).slice(-2) : String(year); } case "month": return String(new Date().getMonth() + 1).padStart(2, "0"); case "custom": return autoConfig.value || "CUSTOM"; default: return ""; } }); const generatedCode = parts.join(rule.separator || ""); const hasSequence = rule.parts.some((p: any) => p.partType === "sequence"); if (hasSequence) { await client.query( "UPDATE numbering_rules SET current_sequence = current_sequence + 1 WHERE rule_id = $1 AND company_code = $2", [ruleId, companyCode] ); } await client.query("COMMIT"); logger.info("코드 생성 완료", { ruleId, generatedCode }); return generatedCode; } catch (error: any) { await client.query("ROLLBACK"); logger.error("코드 생성 실패", { error: error.message }); throw error; } finally { client.release(); } } private formatDate(date: Date, format: string): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); switch (format) { case "YYYY": return String(year); case "YY": return String(year).slice(-2); case "YYYYMM": return `${year}${month}`; case "YYMM": return `${String(year).slice(-2)}${month}`; case "YYYYMMDD": return `${year}${month}${day}`; case "YYMMDD": return `${String(year).slice(-2)}${month}${day}`; default: return `${year}${month}${day}`; } } async resetSequence(ruleId: string, companyCode: string): Promise { const pool = getPool(); await pool.query( "UPDATE numbering_rules SET current_sequence = 1, updated_at = NOW() WHERE rule_id = $1 AND company_code = $2", [ruleId, companyCode] ); logger.info("시퀀스 초기화 완료", { ruleId, companyCode }); } } export const numberingRuleService = new NumberingRuleService();