first commit
This commit is contained in:
119
src/commands/military/celebration.js
Normal file
119
src/commands/military/celebration.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const { PermissionsBitField } = require('discord.js');
|
||||
const logger = require('../../../modules/colorfulLogger');
|
||||
const { getCelebrationConfig, getIpdaeData, getNotificationHistory, saveData } = require('../../data/dataManager');
|
||||
const { addMonthsToDate, calculateDaysFromNow } = require('../../utils/dateUtils');
|
||||
const { handleCommandError } = require('../../../utils/errorHandler');
|
||||
|
||||
async function checkAndSendCelebrationMessages(client) {
|
||||
logger.info('Starting celebration check...');
|
||||
try {
|
||||
const celebrationConfig = getCelebrationConfig();
|
||||
const ipdaeDatas = getIpdaeData();
|
||||
const notificationHistory = getNotificationHistory();
|
||||
|
||||
if (!celebrationConfig?.milestones || !celebrationConfig.channelTopicKeyword) {
|
||||
logger.warn('Celebration config is missing or incomplete. Skipping celebration check.');
|
||||
return;
|
||||
}
|
||||
|
||||
let notificationHistoryModifiedThisRun = false;
|
||||
const today = new Date();
|
||||
|
||||
for (const userData of ipdaeDatas) {
|
||||
if (!userData.id || !userData.date || !userData.type) continue;
|
||||
|
||||
let months;
|
||||
switch (userData.type) {
|
||||
case '육군': case '해병대': months = 18; break;
|
||||
case '해군': months = 20; break;
|
||||
case '공군': case '사회복무요원': months = 21; break;
|
||||
default: continue;
|
||||
}
|
||||
|
||||
const remainingDays = calculateDaysFromNow(addMonthsToDate(userData.date, months), today);
|
||||
const userHistory = notificationHistory[userData.id] || [];
|
||||
|
||||
for (const milestone of celebrationConfig.milestones) {
|
||||
if (remainingDays === milestone.days && !userHistory.includes(milestone.days)) {
|
||||
const userToNotify = await client.users.fetch(userData.id).catch(() => null);
|
||||
if (!userToNotify) continue;
|
||||
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
const member = guild.members.cache.get(userData.id) || await guild.members.fetch(userData.id).catch(() => null);
|
||||
if (member) {
|
||||
const targetChannels = guild.channels.cache.filter(ch =>
|
||||
ch.isTextBased() &&
|
||||
ch.topic?.includes(celebrationConfig.channelTopicKeyword) &&
|
||||
guild.members.me?.permissionsIn(ch).has(PermissionsBitField.Flags.SendMessages)
|
||||
);
|
||||
|
||||
for (const channel of targetChannels.values()) {
|
||||
try {
|
||||
const { mentionUser = true, mentionEveryone = false } = milestone;
|
||||
let messagePrefix = "";
|
||||
const allowEveryoneMention = mentionEveryone && guild.members.me.permissionsIn(channel).has(PermissionsBitField.Flags.MentionEveryone);
|
||||
|
||||
if (allowEveryoneMention) messagePrefix += "@everyone ";
|
||||
if (mentionUser) messagePrefix += `<@${userData.id}> `;
|
||||
|
||||
const displayName = member.displayName || userToNotify.username;
|
||||
const finalMessageToSend = messagePrefix + milestone.message.replace(/{userName}/g, displayName);
|
||||
|
||||
await channel.send({
|
||||
content: finalMessageToSend,
|
||||
allowedMentions: { parse: allowEveryoneMention ? ['everyone'] : [], users: mentionUser ? [userData.id] : [] }
|
||||
});
|
||||
|
||||
logger.info(`Sent ${milestone.days}-day celebration for ${displayName} to #${channel.name}.`);
|
||||
|
||||
if (!notificationHistory[userData.id]) {
|
||||
notificationHistory[userData.id] = [];
|
||||
}
|
||||
notificationHistory[userData.id].push(milestone.days);
|
||||
notificationHistoryModifiedThisRun = true;
|
||||
|
||||
} catch (sendError) {
|
||||
logger.error(`Failed to send celebration message to #${channel.name} for ${member.displayName}: ${sendError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notificationHistoryModifiedThisRun) {
|
||||
await saveData('history');
|
||||
}
|
||||
|
||||
logger.info('Celebration check finished.');
|
||||
} catch (e) {
|
||||
await handleCommandError(e, 'Celebration Check', client);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleDailyCelebrationCheck(client) {
|
||||
try {
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now);
|
||||
tomorrow.setDate(now.getDate() + 1);
|
||||
tomorrow.setHours(0, 0, 5, 0);
|
||||
|
||||
let timeToFirstCheck = tomorrow.getTime() - now.getTime();
|
||||
if (timeToFirstCheck < 0) {
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
timeToFirstCheck = tomorrow.getTime() - now.getTime();
|
||||
}
|
||||
|
||||
logger.info(`Next automatic celebration check scheduled for: ${tomorrow.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })}`);
|
||||
|
||||
setTimeout(() => {
|
||||
checkAndSendCelebrationMessages(client);
|
||||
setInterval(() => checkAndSendCelebrationMessages(client), 24 * 60 * 60 * 1000);
|
||||
}, timeToFirstCheck);
|
||||
} catch (e) {
|
||||
handleCommandError(e, 'Schedule Celebration Check', client);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { checkAndSendCelebrationMessages, scheduleDailyCelebrationCheck };
|
124
src/commands/military/getDischargeInfo.js
Normal file
124
src/commands/military/getDischargeInfo.js
Normal file
@@ -0,0 +1,124 @@
|
||||
const { SlashCommandBuilder, EmbedBuilder, InteractionContextType, ApplicationIntegrationType } = require('discord.js');
|
||||
const logger = require('../../../modules/colorfulLogger');
|
||||
const { getIpdaeData } = require('../../data/dataManager');
|
||||
const {
|
||||
addMonthsToDate,
|
||||
calculateProgress,
|
||||
calculateDateDifference,
|
||||
formatDate,
|
||||
dateFormatter,
|
||||
calculateMonthDifference
|
||||
} = require('../../utils/dateUtils');
|
||||
const { createProgressBar, getNameById } = require('../../utils/discordUtils');
|
||||
const { binarySearch, addSpace } = require('../../utils/helpers');
|
||||
|
||||
const commandData = new SlashCommandBuilder()
|
||||
.setName('전역일')
|
||||
.setDescription('전역일과 관련된 정보들을 확인합니다')
|
||||
.setContexts(InteractionContextType.Guild, InteractionContextType.BotDM, InteractionContextType.PrivateChannel)
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
|
||||
.addUserOption(option => option.setName('유저').setDescription('전역일을 확인할 유저').setRequired(false))
|
||||
.addIntegerOption(option => option.setName('소수점').setDescription('진행도의 소수점').setMinValue(0).setMaxValue(100).setRequired(false))
|
||||
.addBooleanOption(option => option.setName('상세내용').setDescription('상세 내용 표시 여부').setRequired(false));
|
||||
|
||||
function getDischargeInfo(client, targetUserId, targetUserName, decimal, usedFullInfo) {
|
||||
try {
|
||||
const ipdaeDatas = getIpdaeData();
|
||||
const userNameForEmbed = (targetUserName && targetUserName.trim() !== `Unknown User (${targetUserId})` && targetUserName.trim() !== '') ? `${targetUserName}님` : `사용자 (${targetUserId})님`;
|
||||
const index = binarySearch(ipdaeDatas, targetUserId);
|
||||
if (index === -1) {
|
||||
return new EmbedBuilder()
|
||||
.setTitle('저장된 데이터가 없습니다')
|
||||
.setDescription(`먼저 \`!입대일\` 또는 \`/입대일\` 명령어로 데이터를 저장해주세요.`)
|
||||
.addFields({ name: '대상자', value: `${userNameForEmbed} (${targetUserId})` })
|
||||
.setColor('#F44336');
|
||||
}
|
||||
const ipdaeData = ipdaeDatas[index];
|
||||
let months = 0, startText = '입대일', endText = '전역일';
|
||||
switch (ipdaeData.type) {
|
||||
case '육군': case '해병대': months = 18; break;
|
||||
case '해군': months = 20; break;
|
||||
case '공군': case '사회복무요원': months = 21; break;
|
||||
default: throw new Error(`Unknown service type "${ipdaeData.type}" for user ${targetUserId}`);
|
||||
}
|
||||
if (ipdaeData.type === '사회복무요원') { startText = '소집일'; endText = '소집해제일'; }
|
||||
const startDate = ipdaeData.date;
|
||||
const endDate = addMonthsToDate(startDate, months);
|
||||
const now = new Date();
|
||||
const currentProgress = calculateProgress(startDate, endDate, now);
|
||||
const totalDays = calculateDateDifference(startDate, endDate) + 1;
|
||||
const todayFormatted = formatDate(now);
|
||||
let daysServed = new Date(todayFormatted) < new Date(startDate) ? 0 : calculateDateDifference(startDate, todayFormatted) + 1;
|
||||
daysServed = Math.min(daysServed, totalDays);
|
||||
const remainingDays = totalDays - daysServed;
|
||||
const [sYearStr, sMonthStr, sDayStr] = startDate.split('-');
|
||||
let pastMonths = calculateMonthDifference(sYearStr, sMonthStr, String(now.getFullYear()), String(now.getMonth() + 1));
|
||||
if (sDayStr === '01') pastMonths++;
|
||||
let militaryRank = '민간인';
|
||||
if (remainingDays > 0) {
|
||||
if (pastMonths <= 2) militaryRank = `이병 ${pastMonths + 1}호봉`;
|
||||
else if (pastMonths <= 8) militaryRank = `일병 ${pastMonths - 2}호봉`;
|
||||
else if (pastMonths <= 14) militaryRank = `상병 ${pastMonths - 8}호봉`;
|
||||
else militaryRank = `병장 ${pastMonths - 14}호봉`;
|
||||
}
|
||||
const progressBarText = createProgressBar(currentProgress, 10, decimal || 4);
|
||||
|
||||
let dDayText;
|
||||
if (remainingDays === 0) {
|
||||
dDayText = 'D-Day';
|
||||
} else if (remainingDays < 0) {
|
||||
dDayText = `D+${-remainingDays}`;
|
||||
} else {
|
||||
dDayText = `D-${remainingDays}`;
|
||||
}
|
||||
|
||||
if (!usedFullInfo) {
|
||||
return new EmbedBuilder()
|
||||
.setTitle(`${userNameForEmbed}의 ${endText}`)
|
||||
.addFields(
|
||||
{ name: '현재 진행도', value: `${progressBarText}\n${militaryRank}${addSpace(Math.max(0, 14 - militaryRank.length))}${dDayText}` },
|
||||
{ name: '전체 복무일', value: `${totalDays}일` },
|
||||
{ name: '현재 복무일', value: `${daysServed}일` }
|
||||
)
|
||||
.setColor('#007FFF');
|
||||
} else {
|
||||
return new EmbedBuilder()
|
||||
.setTitle(`${userNameForEmbed}의 ${endText} 상세 정보`)
|
||||
.addFields(
|
||||
{ name: startText, value: dateFormatter(startDate) },
|
||||
{ name: endText, value: dateFormatter(endDate) },
|
||||
{ name: '현재 진행도', value: progressBarText },
|
||||
{ name: '복무 형태', value: ipdaeData.type },
|
||||
{ name: '전체 복무일', value: `${totalDays}일` },
|
||||
{ name: '현재 복무일', value: `${daysServed}일` },
|
||||
{ name: '남은 복무일', value: String(remainingDays) },
|
||||
{ name: '현재 계급', value: militaryRank }
|
||||
)
|
||||
.setColor('#007FFF');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Error in getDischargeInfo for ${targetUserId}: ${e.message}`);
|
||||
return new EmbedBuilder()
|
||||
.setTitle('전역일 계산 중 오류')
|
||||
.setDescription(`\`\`\`\n${e.message}\n\`\`\``)
|
||||
.setColor('#F44336');
|
||||
}
|
||||
}
|
||||
|
||||
async function execute(interaction) {
|
||||
const decimalArg = interaction.options.getInteger('소수점') || 4;
|
||||
const targetUserOption = interaction.options.getUser('유저');
|
||||
const targetUserId = targetUserOption ? targetUserOption.id : interaction.user.id;
|
||||
const fullInfoArg = interaction.options.getBoolean('상세내용') || false;
|
||||
const targetUserName = getNameById(interaction.client, targetUserId, interaction.guild);
|
||||
logger.info(`Processing /전역일 for ${targetUserName} (${targetUserId}), requested by ${interaction.user.username} (${interaction.user.id})`);
|
||||
await interaction.deferReply();
|
||||
const resultEmbed = getDischargeInfo(interaction.client, targetUserId, targetUserName, decimalArg, fullInfoArg);
|
||||
await interaction.editReply({ embeds: [resultEmbed] });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data: commandData,
|
||||
getDischargeInfo,
|
||||
execute
|
||||
};
|
116
src/commands/military/setEnlistmentDate.js
Normal file
116
src/commands/military/setEnlistmentDate.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const { SlashCommandBuilder, EmbedBuilder, InteractionContextType, ApplicationIntegrationType } = require('discord.js');
|
||||
const logger = require('../../../modules/colorfulLogger');
|
||||
const {
|
||||
ipdaeDatas,
|
||||
notificationHistory,
|
||||
saveData
|
||||
} = require('../../data/dataManager');
|
||||
const { serviceTypes, adminUserIds } = require('../../../config/constants');
|
||||
const { dateFormatter } = require('../../utils/dateUtils');
|
||||
const { binarySearch, binaryInsert } = require('../../utils/helpers');
|
||||
const { getNameById } = require('../../utils/discordUtils');
|
||||
const { handleCommandError } = require('../../../utils/errorHandler');
|
||||
|
||||
const commandData = new SlashCommandBuilder()
|
||||
.setName('입대일')
|
||||
.setDescription('입대일을 설정합니다')
|
||||
.setContexts(InteractionContextType.Guild, InteractionContextType.BotDM, InteractionContextType.PrivateChannel)
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
|
||||
.addStringOption(option => option.setName('입대일').setDescription('형식: YYYY-MM-DD').setRequired(true))
|
||||
.addStringOption(option => option.setName('복무형태').setDescription('복무 형태').setRequired(true)
|
||||
.addChoices(
|
||||
{ name: '육군', value: '육군' },
|
||||
{ name: '해군', value: '해군' },
|
||||
{ name: '공군', value: '공군' },
|
||||
{ name: '해병대', value: '해병대' },
|
||||
{ name: '사회복무요원', value: '사회복무요원' }
|
||||
));
|
||||
|
||||
async function setEnlistmentDate(client, callerUserId, dateArg, typeArg, commandString, customTargetUserId) {
|
||||
dateArg = String(dateArg || '').trim();
|
||||
typeArg = String(typeArg || '').trim();
|
||||
let targetUserId = customTargetUserId && adminUserIds.includes(callerUserId) ? String(customTargetUserId.replace(/\D/g, '')) : callerUserId;
|
||||
const targetUserName = getNameById(client, targetUserId);
|
||||
const startDateEmbedTitle = customTargetUserId ? `${targetUserName}(${targetUserId}) 님의 입대일이 저장되었습니다` : '입대일이 저장되었습니다';
|
||||
|
||||
try {
|
||||
if (!(/^\d{4}-\d{2}-\d{2}$/.test(dateArg) && serviceTypes.includes(typeArg))) {
|
||||
return new EmbedBuilder()
|
||||
.setTitle('명령어 형식 오류')
|
||||
.setDescription(`입력된 날짜: \`${dateArg}\`, 복무형태: \`${typeArg}\``)
|
||||
.addFields({ name: '올바른 형식', value: `${commandString} YYYY-MM-DD ${serviceTypes.join('/')}` })
|
||||
.setColor('#F44336');
|
||||
}
|
||||
|
||||
const [year, month, day] = dateArg.split('-').map(Number);
|
||||
const enlistDate = new Date(year, month - 1, day);
|
||||
if (isNaN(enlistDate.getTime()) || enlistDate.getFullYear() !== year || enlistDate.getMonth() + 1 !== month || enlistDate.getDate() !== day) {
|
||||
return new EmbedBuilder()
|
||||
.setTitle('유효하지 않은 날짜입니다.')
|
||||
.setDescription(`입력한 날짜 \`${dateArg}\`를 확인해주세요.`)
|
||||
.setColor('#F44336');
|
||||
}
|
||||
|
||||
let notificationHistoryModified = false;
|
||||
const index = binarySearch(ipdaeDatas, targetUserId);
|
||||
|
||||
if (index !== -1) {
|
||||
const oldDate = ipdaeDatas[index].date;
|
||||
const oldType = ipdaeDatas[index].type;
|
||||
|
||||
if (targetUserId === '1107292307025301544' && typeArg !== '사회복무요원') {
|
||||
typeArg = '사회복무요원';
|
||||
logger.info(`User ID 1107292307025301544 type overridden to 사회복무요원.`);
|
||||
}
|
||||
|
||||
ipdaeDatas[index].date = dateArg;
|
||||
ipdaeDatas[index].type = typeArg;
|
||||
|
||||
if (oldDate !== dateArg || oldType !== typeArg) {
|
||||
logger.info(`Enlistment data changed for ${targetUserId}. Resetting their notification history.`);
|
||||
notificationHistory[targetUserId] = [];
|
||||
notificationHistoryModified = true;
|
||||
}
|
||||
} else {
|
||||
binaryInsert(ipdaeDatas, { id: targetUserId, date: dateArg, type: typeArg });
|
||||
notificationHistory[targetUserId] = [];
|
||||
notificationHistoryModified = true;
|
||||
}
|
||||
|
||||
await saveData('ipdae');
|
||||
if (notificationHistoryModified) {
|
||||
await saveData('history');
|
||||
}
|
||||
|
||||
const finalData = ipdaeDatas[binarySearch(ipdaeDatas, targetUserId)];
|
||||
return new EmbedBuilder()
|
||||
.setTitle(index !== -1 ? startDateEmbedTitle.replace('저장', '수정') : startDateEmbedTitle)
|
||||
.addFields(
|
||||
{ name: '입대일', value: dateFormatter(finalData.date) },
|
||||
{ name: '복무 형태', value: finalData.type }
|
||||
)
|
||||
.setColor('#3BB143');
|
||||
|
||||
} catch (e) {
|
||||
await handleCommandError(e, `Data Save for ${targetUserId}`, client);
|
||||
return new EmbedBuilder()
|
||||
.setTitle('데이터 저장/수정 중 오류')
|
||||
.addFields({ name: '에러 메시지', value: e.message })
|
||||
.setColor('#F44336');
|
||||
}
|
||||
}
|
||||
|
||||
async function execute(interaction) {
|
||||
const slashStartDateArg = interaction.options.getString('입대일');
|
||||
const slashStartTypeArg = interaction.options.getString('복무형태');
|
||||
logger.info(`Processing /입대일 for ${interaction.user.username} (${interaction.user.id}) with date: ${slashStartDateArg}, type: ${slashStartTypeArg}`);
|
||||
await interaction.deferReply();
|
||||
const resultEmbed = await setEnlistmentDate(interaction.client, interaction.user.id, slashStartDateArg, slashStartTypeArg, '/입대일', undefined);
|
||||
await interaction.editReply({ embeds: [resultEmbed] });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data: commandData,
|
||||
setEnlistmentDate,
|
||||
execute
|
||||
};
|
Reference in New Issue
Block a user