Skip to content

Commit 76dd518

Browse files
authored
Merge pull request #87 from Together42/82-rotation-bot
Feat: 다음 날 사서 로테이션 알림 슬랙봇 추가
2 parents d453e1d + f4a8a6d commit 76dd518

File tree

3 files changed

+204
-85
lines changed

3 files changed

+204
-85
lines changed

Diff for: src/rotation/rotations.controller.ts

-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export class RotationsController {
5353
* 당일 사서 조회 (달력)
5454
* 구글 시트를 위한 API
5555
* Auth : None
56-
* 먼가 잘 안되는 것 같기도...
5756
*/
5857
@Get('/today')
5958
@ApiOperation({

Diff for: src/rotation/rotations.service.ts

+190-77
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,37 @@ import {
77
forwardRef,
88
} from '@nestjs/common';
99
import { Cron } from '@nestjs/schedule';
10+
import { ConfigService } from '@nestjs/config';
1011
import { CreateRegistrationDto } from './dto/create-registration.dto';
1112
import { CreateRotationDto } from './dto/create-rotation.dto';
1213
import { UpdateRotationDto } from './dto/update-rotation.dto';
1314
import { RotationEntity } from './entity/rotation.entity';
1415
import { RotationAttendeeEntity } from './entity/rotation-attendee.entity';
1516
import { UserService } from 'src/user/user.service';
1617
import { RotationRepository } from './repository/rotations.repository';
17-
import { getFourthWeekdaysOfMonth, getNextYearAndMonth, getTodayDate } from './utils/date';
18+
import {
19+
getFourthFridayOfMonth,
20+
getFourthMondayOfMonth,
21+
getFourthWeekdaysOfMonth,
22+
getNextYearAndMonth,
23+
getTodayDay,
24+
getTomorrowDate,
25+
} from './utils/date';
1826
import { RotationAttendeeRepository } from './repository/rotation-attendees.repository';
1927
import { DayObject, RotationAttendeeInfo } from './utils/types';
2028
import { HolidayService } from 'src/holiday/holiday.service';
2129
import { createRotation } from './utils/rotation';
30+
import { SlackService } from 'nestjs-slack';
31+
import { Message } from 'slack-block-builder';
2232
import { FindTodayRotationDto } from './dto/find-today-rotation.dto';
2333
import { FindRegistrationDto } from './dto/find-registration.dto';
2434
import { FindAllRotationDto } from './dto/find-all-rotation.dto';
2535

2636
function getRotationCronTime() {
2737
if (process.env.NODE_ENV === 'production') {
28-
return '59 23 * * 5';
38+
return '42 4 27 * *';
2939
}
30-
return '0 0 * * 5';
40+
return '0 0 27 * *';
3141
}
3242

3343
@Injectable()
@@ -39,8 +49,114 @@ export class RotationsService {
3949
private rotationAttendeeRepository: RotationAttendeeRepository,
4050
@Inject(forwardRef(() => UserService)) private userService: UserService,
4151
private holidayService: HolidayService,
52+
private slackService: SlackService,
53+
private configService: ConfigService,
4254
) {}
4355

56+
/*
57+
* 매일 9시 42분에 내일 사서에게 메세지를 전송하는 cron job
58+
*/
59+
@Cron('42 9 * * *', {
60+
name: 'sendMessageTomorrowLibrarian',
61+
timeZone: 'Asia/Seoul',
62+
})
63+
async sendMessageTomorrowLibrarian(): Promise<void> {
64+
const tomorrow = getTomorrowDate();
65+
const tomorrowYear = tomorrow.getFullYear();
66+
const tomorrowMonth = tomorrow.getMonth() + 1;
67+
const tomorrowDay = tomorrow.getDate();
68+
69+
const tomorrowLibrarian = await this.rotationRepository.find({
70+
where: {
71+
year: tomorrowYear,
72+
month: tomorrowMonth,
73+
day: tomorrowDay,
74+
},
75+
relations: ['user'],
76+
});
77+
78+
if (tomorrowLibrarian.length === 0) {
79+
return;
80+
}
81+
82+
for (const item of tomorrowLibrarian) {
83+
if (!item.user) {
84+
this.logger.warn('Failed to get tomorrow librarian information. User not found.');
85+
return;
86+
}
87+
88+
const message = `[알림] 안녕하세요 ${item.user.nickname}님! 내일 사서 업무가 있습니다.`;
89+
90+
try {
91+
await this.slackService.postMessage(
92+
Message({
93+
text: message,
94+
channel: item.user.slackMemberId,
95+
}).buildToObject(),
96+
);
97+
} catch (error) {
98+
this.logger.error(`Error sending message to ${item.user.nickname}: ` + error);
99+
}
100+
}
101+
}
102+
103+
/*
104+
* 매월 23일, 집현전 슬랙 채널에 메세지를 보내는 cron job
105+
*/
106+
@Cron('42 15 23 * *', {
107+
name: 'sendMessageRotationDeadlineFirst',
108+
timeZone: 'Asia/Seoul',
109+
})
110+
async sendMessageRotationDeadlineFirst(): Promise<void> {
111+
const message =
112+
'[알림] 4일 후 로테이션 신청이 마감됩니다.\n[신청하러 가기]: https://together.42jip.net/';
113+
114+
await this.slackService.postMessage(
115+
Message({
116+
text: message,
117+
channel: this.configService.get('slack.jiphyeonjeonChannel'),
118+
}).buildToObject(),
119+
);
120+
}
121+
122+
/*
123+
* 매월 26일, 집현전 슬랙 채널에 메세지를 보내는 cron job
124+
*/
125+
@Cron('42 15 26 * *', {
126+
name: 'sendMessageRotationDeadlineLast',
127+
timeZone: 'Asia/Seoul',
128+
})
129+
async sendMessageRotationDeadlineLast(): Promise<void> {
130+
const message =
131+
'[알림] 로테이션 신청 기간이 내일 마감됩니다. 오늘까지 신청해주세요!\n[신청하러 가기]: https://together.42jip.net/';
132+
133+
await this.slackService.postMessage(
134+
Message({
135+
text: message,
136+
channel: this.configService.get('slack.jiphyeonjeonChannel'),
137+
}).buildToObject(),
138+
);
139+
}
140+
141+
/*
142+
* 매월 27일 사서 로테이션 설정이 완료된 후, 집현전 슬랙 채널에 메세지를 보내는 cron job
143+
*/
144+
@Cron('42 15 27 * *', {
145+
name: 'sendMessageRotationFinished',
146+
timeZone: 'Asia/Seoul',
147+
})
148+
async sendMessageRotationFinished(): Promise<void> {
149+
const message =
150+
'[알림] 다음 달 로테이션이 확정되었습니다! 친바 사이트에서 확인해주세요!\n[확인하러 가기]: https://together.42jip.net/';
151+
152+
await this.slackService.postMessage(
153+
Message({
154+
text: message,
155+
channel: this.configService.get('slack.jiphyeonjeonChannel'),
156+
}).buildToObject(),
157+
);
158+
}
159+
44160
/*
45161
* 4주차 월요일에 유저를 모두 DB에 담아놓는 작업 필요
46162
* [update 20231219] - 매 달 1일에 유저를 모두 DB에 담아놓는 작업으로 변경
@@ -72,101 +188,98 @@ export class RotationsService {
72188
* 매주 금요일을 체크하여, 만약 4주차 금요일인 경우,
73189
* 23시 59분에 로테이션을 돌린다.
74190
* 다음 달 로테이션 참석자를 바탕으로 로테이션 결과 반환
191+
* [update 20240202] - 매월 4주차 금요일이 아닌, 매월 27일 새벽에 로테이션을 돌린다.
75192
*/
76193
@Cron(`${getRotationCronTime()}`, {
77194
name: 'setRotation',
78195
timeZone: 'Asia/Seoul',
79196
})
80197
async setRotation(): Promise<void> {
81-
if (getFourthWeekdaysOfMonth().indexOf(getTodayDate()) > 0) {
82-
this.logger.log('Setting rotation...');
198+
this.logger.log('Setting rotation...');
83199

84-
const { year, month } = getNextYearAndMonth();
85-
const attendeeArray: Partial<RotationAttendeeEntity>[] = await this.getAllRegistration();
86-
const monthArrayInfo: DayObject[][] = await this.getInitMonthArray(year, month);
200+
const { year, month } = getNextYearAndMonth();
201+
const attendeeArray: Partial<RotationAttendeeEntity>[] = await this.getAllRegistration();
202+
const monthArrayInfo: DayObject[][] = await this.getInitMonthArray(year, month);
87203

88-
if (!attendeeArray || attendeeArray.length === 0) {
89-
this.logger.warn('No attendees participated in the rotation');
90-
return;
91-
}
204+
if (!attendeeArray || attendeeArray.length === 0) {
205+
this.logger.warn('No attendees participated in the rotation');
206+
return;
207+
}
92208

93-
const rotationAttendeeInfo: RotationAttendeeInfo[] = attendeeArray.map((attendee) => {
94-
const parsedAttendLimit: number[] = Array.isArray(attendee.attendLimit)
95-
? JSON.parse(JSON.stringify(attendee.attendLimit))
96-
: [];
97-
return {
98-
userId: attendee.userId,
99-
year: attendee.year,
100-
month: attendee.month,
101-
attendLimit: parsedAttendLimit,
102-
attended: 0,
103-
};
104-
});
209+
const rotationAttendeeInfo: RotationAttendeeInfo[] = attendeeArray.map((attendee) => {
210+
const parsedAttendLimit: number[] = Array.isArray(attendee.attendLimit)
211+
? JSON.parse(JSON.stringify(attendee.attendLimit))
212+
: [];
213+
return {
214+
userId: attendee.userId,
215+
year: attendee.year,
216+
month: attendee.month,
217+
attendLimit: parsedAttendLimit,
218+
attended: 0,
219+
};
220+
});
221+
222+
// 만약 year & month에 해당하는 로테이션 정보가 이미 존재한다면,
223+
// 해당 로테이션 정보를 삭제하고 다시 생성한다.
224+
const hasInfo = await this.rotationRepository.find({
225+
where: {
226+
year: year,
227+
month: month,
228+
},
229+
});
230+
231+
if (hasInfo.length > 0) {
232+
this.logger.log('Rotation info already exists. Deleting...');
233+
await this.rotationRepository.softRemove(hasInfo);
234+
}
105235

106-
// 만약 year & month에 해당하는 로테이션 정보가 이미 존재한다면,
107-
// 해당 로테이션 정보를 삭제하고 다시 생성한다.
108-
const hasInfo = await this.rotationRepository.find({
236+
const rotationResultArray: DayObject[] = createRotation(rotationAttendeeInfo, monthArrayInfo);
237+
238+
for (const item of rotationResultArray) {
239+
const [userId1, userId2] = item.arr;
240+
241+
const attendeeOneExist = await this.rotationRepository.findOne({
109242
where: {
243+
userId: userId1,
110244
year: year,
111245
month: month,
246+
day: item.day,
112247
},
113248
});
114249

115-
if (hasInfo.length > 0) {
116-
this.logger.log('Rotation info already exists. Deleting...');
117-
await this.rotationRepository.softRemove(hasInfo);
118-
}
119-
120-
const rotationResultArray: DayObject[] = createRotation(rotationAttendeeInfo, monthArrayInfo);
121-
122-
for (const item of rotationResultArray) {
123-
const [userId1, userId2] = item.arr;
250+
if (!attendeeOneExist) {
251+
const rotation1 = new RotationEntity();
252+
rotation1.userId = userId1;
253+
rotation1.updateUserId = userId1;
254+
rotation1.year = year;
255+
rotation1.month = month;
256+
rotation1.day = item.day;
124257

125-
const attendeeOneExist = await this.rotationRepository.findOne({
126-
where: {
127-
userId: userId1,
128-
year: year,
129-
month: month,
130-
day: item.day,
131-
},
132-
});
133-
134-
if (!attendeeOneExist) {
135-
const rotation1 = new RotationEntity();
136-
rotation1.userId = userId1;
137-
rotation1.updateUserId = userId1;
138-
rotation1.year = year;
139-
rotation1.month = month;
140-
rotation1.day = item.day;
141-
142-
await this.rotationRepository.save(rotation1);
143-
}
258+
await this.rotationRepository.save(rotation1);
259+
}
144260

145-
const attendeeTwoExist = await this.rotationRepository.findOne({
146-
where: {
147-
userId: userId2,
148-
year: year,
149-
month: month,
150-
day: item.day,
151-
},
152-
});
261+
const attendeeTwoExist = await this.rotationRepository.findOne({
262+
where: {
263+
userId: userId2,
264+
year: year,
265+
month: month,
266+
day: item.day,
267+
},
268+
});
153269

154-
if (!attendeeTwoExist) {
155-
const rotation2 = new RotationEntity();
156-
rotation2.userId = userId2;
157-
rotation2.updateUserId = userId2;
158-
rotation2.year = year;
159-
rotation2.month = month;
160-
rotation2.day = item.day;
270+
if (!attendeeTwoExist) {
271+
const rotation2 = new RotationEntity();
272+
rotation2.userId = userId2;
273+
rotation2.updateUserId = userId2;
274+
rotation2.year = year;
275+
rotation2.month = month;
276+
rotation2.day = item.day;
161277

162-
await this.rotationRepository.save(rotation2);
163-
}
278+
await this.rotationRepository.save(rotation2);
164279
}
165-
166-
this.logger.log('Successfully set rotation!');
167-
} else {
168-
// skipped...
169280
}
281+
282+
this.logger.log('Successfully set rotation!');
170283
}
171284

172285
/*
@@ -264,7 +377,7 @@ export class RotationsService {
264377
const { year, month } = getNextYearAndMonth();
265378

266379
/* 4주차인지 확인 */
267-
// if (getFourthWeekdaysOfMonth().indexOf(getTodayDate()) < 0) {
380+
// if (getFourthWeekdaysOfMonth().indexOfDay()) < 0) {
268381
// throw new BadRequestException(
269382
// 'Invalid date: Today is not a fourth weekday of the month.',
270383
// );

Diff for: src/rotation/utils/date.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ export function getNextYearAndMonth(): { year: number; month: number } {
1111
return { year, month };
1212
}
1313

14-
export function getTodayDate(): number {
14+
export function getTomorrowDate(): Date {
15+
return new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
16+
}
17+
18+
export function getTodayDay(): number {
1519
return new Date().getDate();
1620
}
1721

@@ -33,8 +37,7 @@ const getFourthWeekPeriod = (date = new Date()): number[] => {
3337
dateOfThursdayOnFirstWeek = 1 + DAY_IN_WEEK + DAY_OF_THURSDAY - firstDay;
3438
}
3539

36-
const dateOfThursdayOfFourthWeek =
37-
dateOfThursdayOnFirstWeek + 3 * DAY_IN_WEEK;
40+
const dateOfThursdayOfFourthWeek = dateOfThursdayOnFirstWeek + 3 * DAY_IN_WEEK;
3841
const dateOfMondayOnFourthWeek = dateOfThursdayOfFourthWeek - 3;
3942
const dateOfSundayOnFourthWeek = dateOfThursdayOfFourthWeek + 3;
4043

@@ -43,8 +46,7 @@ const getFourthWeekPeriod = (date = new Date()): number[] => {
4346

4447
export const getFourthWeekdaysOfMonth = (date = new Date()): number[] => {
4548
// eslint-disable-next-line @typescript-eslint/no-unused-vars
46-
const [dateOfMondayOnFourthWeek, dateOfSundayOnFourthWeek] =
47-
getFourthWeekPeriod(date);
49+
const [dateOfMondayOnFourthWeek, dateOfSundayOnFourthWeek] = getFourthWeekPeriod(date);
4850
const fourthWeekdays: number[] = [];
4951

5052
for (let i = 0; i < 5; i++) {
@@ -55,10 +57,15 @@ export const getFourthWeekdaysOfMonth = (date = new Date()): number[] => {
5557
return fourthWeekdays;
5658
};
5759

60+
export const getFourthMondayOfMonth = (date = new Date()): number => {
61+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
62+
const [dateOfMondayOnFourthWeek] = getFourthWeekPeriod(date);
63+
return dateOfMondayOnFourthWeek;
64+
};
65+
5866
export const getFourthFridayOfMonth = (date = new Date()): number => {
5967
// eslint-disable-next-line @typescript-eslint/no-unused-vars
60-
const [dateOfMondayOnFourthWeek, dateOfSundayOnFourthWeek] =
61-
getFourthWeekPeriod(date);
68+
const [dateOfMondayOnFourthWeek, dateOfSundayOnFourthWeek] = getFourthWeekPeriod(date);
6269
const dateOfFridayOnFourthWeek = dateOfSundayOnFourthWeek - 2;
6370

6471
return dateOfFridayOnFourthWeek;

0 commit comments

Comments
 (0)