diff --git a/index.js b/index.js index 3aff254..839f538 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,11 @@ require('dotenv').config(); -const { Client, GatewayIntentBits, Partials, Collection } = require('discord.js'); +const { Client, GatewayIntentBits, Partials } = require('discord.js'); const fs = require('fs'); const path = require('path'); const logger = require('./modules/colorfulLogger'); const { loadData } = require('./src/data/dataManager'); const { setupProcessHandlers } = require('./src/handlers/processHandlers'); +const { loadCommands } = require('./src/handlers/commandHandler'); const client = new Client({ intents: [ @@ -16,39 +17,13 @@ const client = new Client({ partials: [Partials.Channel], }); -// Load data and setup process handlers +// Load data and commands loadData(); +loadCommands(); + +// Setup process handlers setupProcessHandlers(client); -// Load commands -client.commands = new Collection(); -client.legacyCommands = new Collection(); - -const commandFolders = fs.readdirSync(path.join(__dirname, 'src', 'commands')); - -for (const folder of commandFolders) { - const commandFiles = fs.readdirSync(path.join(__dirname, 'src', 'commands', folder)).filter(file => file.endsWith('.js')); - for (const file of commandFiles) { - const filePath = path.join(__dirname, 'src', 'commands', folder, file); - const command = require(filePath); - - // Load slash command part - if (command.data && command.execute) { - client.commands.set(command.data.name, command); - } - - // Load legacy command part - if (command.legacy) { - client.legacyCommands.set(command.legacy.name, command.legacy); - if (command.legacy.aliases) { - command.legacy.aliases.forEach(alias => { - client.legacyCommands.set(alias, command.legacy); - }); - } - } - } -} - // Load event handlers const eventsPath = path.join(__dirname, 'src', 'events'); const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js')); @@ -57,9 +32,9 @@ for (const file of eventFiles) { const filePath = path.join(eventsPath, file); const event = require(filePath); if (event.once) { - client.once(event.name, (...args) => event.execute(...args)); + client.once(event.name, (...args) => event.execute(...args, client)); } else { - client.on(event.name, (...args) => event.execute(...args)); + client.on(event.name, (...args) => event.execute(...args, client)); } } diff --git a/src/data/dataManager.js b/src/data/dataManager.js index 4e0bfdd..d7236dd 100644 --- a/src/data/dataManager.js +++ b/src/data/dataManager.js @@ -7,10 +7,9 @@ const { notificationHistoryPath, dataDir } = require('../../config/constants'); +const cacheManager = require('../utils/cacheManager'); -let ipdaeDatas = []; -let celebrationConfig = {}; -let notificationHistory = {}; +const CACHE_TTL = 300000; // 5 minutes async function loadData() { try { @@ -19,36 +18,47 @@ async function loadData() { logger.error(`Failed to create data directory ${dataDir}: ${e.message}`); } + await loadIpdaeData(); + await loadNotificationHistory(); + await loadCelebrationConfig(); + await migrateData(); +} + +async function loadIpdaeData() { try { const data = await fs.readFile(ipdaeDataDirectory, 'utf8'); - ipdaeDatas = JSON.parse(data); + const ipdaeDatas = JSON.parse(data); ipdaeDatas.sort((a, b) => a.id.localeCompare(b.id)); - logger.info('ipdaeData.json loaded and parsed.'); + cacheManager.set('ipdaeData', ipdaeDatas, CACHE_TTL); + logger.info('ipdaeData.json loaded and cached.'); } catch (e) { logger.warn(`Failed to load ipdaeData.json: ${e.message}. Initializing as empty array.`); - ipdaeDatas = []; + cacheManager.set('ipdaeData', [], CACHE_TTL); if (e.code === 'ENOENT') { await saveData('ipdae'); } } +} +async function loadNotificationHistory() { try { const historyData = await fs.readFile(notificationHistoryPath, 'utf8'); - notificationHistory = JSON.parse(historyData); - logger.info('notificationHistory.json loaded and parsed.'); + const notificationHistory = JSON.parse(historyData); + cacheManager.set('notificationHistory', notificationHistory, CACHE_TTL); + logger.info('notificationHistory.json loaded and cached.'); } catch (e) { logger.warn(`notificationHistory.json not found or invalid: ${e.message}. Initializing as empty object.`); - notificationHistory = {}; + cacheManager.set('notificationHistory', {}, CACHE_TTL); if (e.code === 'ENOENT') { await saveData('history'); } } +} - await migrateData(); - +async function loadCelebrationConfig() { try { const configData = await fs.readFile(celebrationConfigPath, 'utf8'); - celebrationConfig = JSON.parse(configData); + const celebrationConfig = JSON.parse(configData); if (celebrationConfig.milestones && Array.isArray(celebrationConfig.milestones)) { celebrationConfig.milestones.forEach(m => { m.mentionUser = m.mentionUser ?? true; @@ -57,10 +67,11 @@ async function loadData() { } else { throw new Error("Milestones data is missing or invalid in celebrationConfig.json"); } - logger.info('celebrationConfig.json loaded and parsed.'); + cacheManager.set('celebrationConfig', celebrationConfig, CACHE_TTL); + logger.info('celebrationConfig.json loaded and cached.'); } catch (e) { logger.warn(`celebrationConfig.json error: ${e.message}. Creating/Using default config.`); - celebrationConfig = { + const defaultConfig = { channelTopicKeyword: "전역축하알림", milestones: [ { days: 100, message: "🎉 {userName}님, 전역까지 D-100일! 영광의 그날까지 파이팅입니다! 🎉", mentionUser: true, mentionEveryone: false }, @@ -71,16 +82,19 @@ async function loadData() { { days: 0, message: "🫡 {userName}님, 🎉축 전역🎉 대한민국의 자랑스러운 아들! 그동안 정말 고 생 많으셨습니다! 🫡", mentionUser: true, mentionEveryone: true } ] }; + cacheManager.set('celebrationConfig', defaultConfig, CACHE_TTL); await saveData('config'); } } async function migrateData() { + let ipdaeData = getIpdaeData(); + let notificationHistory = getNotificationHistory(); let ipdaeDataNeedsSave = false; let notificationHistoryNeedsSave = false; - for (let i = 0; i < ipdaeDatas.length; i++) { - const user = ipdaeDatas[i]; + for (let i = 0; i < ipdaeData.length; i++) { + const user = ipdaeData[i]; if (user.hasOwnProperty('notifiedMilestones')) { if (Array.isArray(user.notifiedMilestones) && user.notifiedMilestones.length > 0) { if (!notificationHistory[user.id] || Object.keys(notificationHistory[user.id]).length === 0) { @@ -97,9 +111,11 @@ async function migrateData() { } if (ipdaeDataNeedsSave) { + cacheManager.set('ipdaeData', ipdaeData, CACHE_TTL); await saveData('ipdae'); } if (notificationHistoryNeedsSave) { + cacheManager.set('notificationHistory', notificationHistory, CACHE_TTL); await saveData('history'); } } @@ -108,16 +124,22 @@ async function saveData(type) { try { switch (type) { case 'ipdae': - await fs.writeFile(ipdaeDataDirectory, JSON.stringify(ipdaeDatas, null, 4), 'utf8'); - logger.info('ipdaeData.json saved.'); + const ipdaeData = getIpdaeData(); + await fs.writeFile(ipdaeDataDirectory, JSON.stringify(ipdaeData, null, 4), 'utf8'); + cacheManager.set('ipdaeData', ipdaeData, CACHE_TTL); + logger.info('ipdaeData.json saved and cache updated.'); break; case 'config': + const celebrationConfig = getCelebrationConfig(); await fs.writeFile(celebrationConfigPath, JSON.stringify(celebrationConfig, null, 4), 'utf8'); - logger.info('celebrationConfig.json saved.'); + cacheManager.set('celebrationConfig', celebrationConfig, CACHE_TTL); + logger.info('celebrationConfig.json saved and cache updated.'); break; case 'history': + const notificationHistory = getNotificationHistory(); await fs.writeFile(notificationHistoryPath, JSON.stringify(notificationHistory, null, 4), 'utf8'); - logger.info('notificationHistory.json saved.'); + cacheManager.set('notificationHistory', notificationHistory, CACHE_TTL); + logger.info('notificationHistory.json saved and cache updated.'); break; default: logger.warn(`Unknown data type for saving: ${type}`); @@ -128,18 +150,38 @@ async function saveData(type) { } function getIpdaeData() { - return ipdaeDatas; + let data = cacheManager.get('ipdaeData'); + if (!data) { + logger.info("ipdaeData not in cache or expired, reloading from file."); + loadIpdaeData(); + data = cacheManager.get('ipdaeData'); + } + return data || []; } function getCelebrationConfig() { - return celebrationConfig; + let data = cacheManager.get('celebrationConfig'); + if (!data) { + logger.info("celebrationConfig not in cache or expired, reloading from file."); + loadCelebrationConfig(); + data = cacheManager.get('celebrationConfig'); + } + return data || {}; } function getNotificationHistory() { - return notificationHistory; + let data = cacheManager.get('notificationHistory'); + if (!data) { + logger.info("notificationHistory not in cache or expired, reloading from file."); + loadNotificationHistory(); + data = cacheManager.get('notificationHistory'); + } + return data || {}; } async function updateEnlistmentData(userId, date, type) { + let ipdaeDatas = getIpdaeData(); + let notificationHistory = getNotificationHistory(); let notificationHistoryModified = false; const index = binarySearch(ipdaeDatas, userId); @@ -161,8 +203,10 @@ async function updateEnlistmentData(userId, date, type) { notificationHistoryModified = true; } + cacheManager.set('ipdaeData', ipdaeDatas, CACHE_TTL); await saveData('ipdae'); if (notificationHistoryModified) { + cacheManager.set('notificationHistory', notificationHistory, CACHE_TTL); await saveData('history'); } @@ -176,7 +220,4 @@ module.exports = { getCelebrationConfig, getNotificationHistory, updateEnlistmentData, - ipdaeDatas, - celebrationConfig, - notificationHistory }; \ No newline at end of file diff --git a/src/handlers/commandHandler.js b/src/handlers/commandHandler.js index eed31eb..18ea2c1 100644 --- a/src/handlers/commandHandler.js +++ b/src/handlers/commandHandler.js @@ -4,29 +4,43 @@ const path = require('path'); const logger = require('../../modules/colorfulLogger'); const { commandPrefix } = require('../../config/constants'); const { getNameById } = require('../utils/discordUtils'); +const cacheManager = require('../utils/cacheManager'); -function loadLegacyCommands(client) { - client.legacyCommands = new Collection(); +function loadCommands() { + const slashCommands = new Collection(); + const legacyCommands = new Collection(); const commandFolders = fs.readdirSync(path.join(__dirname, '..', 'commands')); for (const folder of commandFolders) { const commandFiles = fs.readdirSync(path.join(__dirname, '..', 'commands', folder)).filter(file => file.endsWith('.js')); for (const file of commandFiles) { try { - const command = require(path.join(__dirname, '..', 'commands', folder, file)); - + const filePath = path.join(__dirname, '..', 'commands', folder, file); + const command = require(filePath); + + // Load slash command part + if (command.data && command.execute) { + slashCommands.set(command.data.name, command); + } + + // Load legacy command part if (command.legacy) { - const legacyCommand = command.legacy; - client.legacyCommands.set(legacyCommand.name, legacyCommand); - if (legacyCommand.aliases && legacyCommand.aliases.length > 0) { - legacyCommand.aliases.forEach(alias => client.legacyCommands.set(alias, legacyCommand)); + legacyCommands.set(command.legacy.name, command.legacy); + if (command.legacy.aliases) { + command.legacy.aliases.forEach(alias => { + legacyCommands.set(alias, command.legacy); + }); } } } catch (error) { - logger.error(`Error loading legacy command from ${file}:`, error); + logger.error(`Error loading command from ${file}:`, error); } } } + + cacheManager.set('slashCommands', slashCommands); + cacheManager.set('legacyCommands', legacyCommands); + logger.info('All commands loaded and cached.'); } async function handleCommand(message) { @@ -35,7 +49,13 @@ async function handleCommand(message) { const args = message.content.slice(commandPrefix.length).trim().split(/\s+/); const commandName = args.shift().toLowerCase(); - const command = message.client.legacyCommands.get(commandName); + const legacyCommands = cacheManager.get('legacyCommands'); + if (!legacyCommands) { + logger.error('Legacy commands not found in cache.'); + return; + } + + const command = legacyCommands.get(commandName); if (!command) return; @@ -52,4 +72,4 @@ async function handleCommand(message) { } } -module.exports = { loadLegacyCommands, handleCommand }; +module.exports = { loadCommands, handleCommand }; diff --git a/src/handlers/slashCommandHandler.js b/src/handlers/slashCommandHandler.js index c4ed1e0..1978050 100644 --- a/src/handlers/slashCommandHandler.js +++ b/src/handlers/slashCommandHandler.js @@ -1,33 +1,15 @@ -const { Collection } = require('discord.js'); -const fs = require('fs'); -const path = require('path'); -const logger = require('../../modules/colorfulLogger'); const { handleCommandError } = require('../../utils/errorHandler'); - -function loadCommands() { - const commands = new Collection(); - const commandFolders = fs.readdirSync(path.join(__dirname, '..', 'commands')); - - for (const folder of commandFolders) { - const commandFiles = fs.readdirSync(path.join(__dirname, '..', 'commands', folder)).filter(file => file.endsWith('.js')); - for (const file of commandFiles) { - try { - const command = require(path.join(__dirname, '..', 'commands', folder, file)); - if (command.data && command.execute) { - commands.set(command.data.name, command); - } else { - logger.warn(`Command file ${file} is missing a data or execute export.`); - } - } catch (error) { - logger.error(`Error loading command from ${file}:`, error); - } - } - } - return commands; -} +const cacheManager = require('../utils/cacheManager'); +const logger = require('../../modules/colorfulLogger'); async function onSlashCommand(interaction) { - const command = interaction.client.commands.get(interaction.commandName); + const slashCommands = cacheManager.get('slashCommands'); + if (!slashCommands) { + logger.error('Slash commands not found in cache.'); + return; + } + + const command = slashCommands.get(interaction.commandName); if (!command) { logger.warn(`No command matching ${interaction.commandName} was found.`); @@ -43,4 +25,4 @@ async function onSlashCommand(interaction) { } } -module.exports = { onSlashCommand, loadCommands }; \ No newline at end of file +module.exports = { onSlashCommand }; \ No newline at end of file diff --git a/src/utils/cacheManager.js b/src/utils/cacheManager.js new file mode 100644 index 0000000..d9fc453 --- /dev/null +++ b/src/utils/cacheManager.js @@ -0,0 +1,54 @@ +const logger = require('../../modules/colorfulLogger'); + +class CacheManager { + constructor() { + this.cache = new Map(); + logger.info('CacheManager initialized.'); + } + + get(key) { + const cachedItem = this.cache.get(key); + if (!cachedItem) { + logger.debug(`Cache MISS for key: ${key}`); + return null; + } + + if (cachedItem.expire && Date.now() > cachedItem.expire) { + logger.info(`Cache EXPIRED for key: ${key}`); + this.cache.delete(key); + return null; + } + + logger.debug(`Cache HIT for key: ${key}`); + return cachedItem.value; + } + + set(key, value, ttl = 0) { + logger.debug(`Caching data for key: ${key} with TTL: ${ttl}ms`); + this.cache.set(key, { + value: value, + expire: ttl > 0 ? Date.now() + ttl : null, + }); + } + + has(key) { + return this.cache.has(key); + } + + delete(key) { + logger.debug(`Deleting cache for key: ${key}`); + return this.cache.delete(key); + } + + flush() { + logger.info('Flushing all caches.'); + this.cache.clear(); + } + + get size() { + return this.cache.size; + } +} + +const cacheManager = new CacheManager(); +module.exports = cacheManager;