-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from Together42/10-together-event-refactoring
feat: together event refactoring
- Loading branch information
Showing
45 changed files
with
2,616 additions
and
399 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { ScheduleModule } from '@nestjs/schedule'; | ||
import { BatchService } from './batch.service'; | ||
import { MeetupsService } from 'src/meetups/meetups.service'; | ||
import { TypeOrmModule } from '@nestjs/typeorm'; | ||
import { MeetupEntity } from 'src/meetups/entity/meetup.entity'; | ||
import { MeetupAttendeeEntity } from 'src/meetups/entity/meetup-attendee.entity'; | ||
|
||
@Module({ | ||
imports: [ | ||
ScheduleModule.forRoot(), | ||
TypeOrmModule.forFeature([MeetupEntity, MeetupAttendeeEntity]), | ||
], | ||
providers: [BatchService, MeetupsService], | ||
}) | ||
export class BatchModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { Injectable, Logger } from '@nestjs/common'; | ||
import { Cron, SchedulerRegistry } from '@nestjs/schedule'; | ||
import { CreateMeetupDto } from 'src/meetups/dto/create-meetup.dto'; | ||
import { MeetupsService } from 'src/meetups/meetups.service'; | ||
import { CronJob } from 'cron'; | ||
import { MeetupCategory } from 'src/meetups/enum/meetup-category.enum'; | ||
|
||
@Injectable() | ||
export class BatchService { | ||
constructor( | ||
private schedulerReistry: SchedulerRegistry, | ||
private meetupsService: MeetupsService, | ||
) {} | ||
private readonly logger = new Logger(BatchService.name); | ||
|
||
@Cron('0 14 * * 1', { name: 'createWeeklyMeeting', timeZone: 'Asia/Seoul' }) | ||
async createWeeklyMeeting() { | ||
const meetingDate = new Date(); | ||
meetingDate.setDate(meetingDate.getDate() + 2); | ||
const meetingMonth = meetingDate.getMonth() + 1; | ||
const meetingDay = meetingDate.getDate(); | ||
|
||
const createMeetupDto: CreateMeetupDto = { | ||
title: `[주간회의] ${meetingMonth}월 ${meetingDay}일`, | ||
description: '매 주 생성되는 정기 회의입니다.', | ||
categoryId: MeetupCategory.WEEKLY_MEETUP, | ||
}; | ||
const { meetupId } = await this.meetupsService.create(createMeetupDto); | ||
|
||
// 생성한 이벤트 마감 크론잡 등록 및 실행 | ||
const cronJob = CronJob.from({ | ||
cronTime: '0 17 * * 3', | ||
timeZone: 'Asia/Seoul', | ||
onTick: async () => { | ||
try { | ||
await this.meetupsService.createMatching({ meetupId, teamNum: 1 }); | ||
} catch (e) { | ||
this.logger.error('[createWeeklyMeeting]', e); | ||
} | ||
}, | ||
}); | ||
this.schedulerReistry.addCronJob('matchWeeklyMeeting', cronJob); | ||
cronJob.start(); | ||
} | ||
|
||
@Cron('0 14 * * 3', { name: 'createWeeklyDinner', timeZone: 'Asia/Seoul' }) | ||
async createWeeklyDinner() { | ||
const createMeetupDto: CreateMeetupDto = { | ||
title: '[주간 식사] 회의 끝나고 같이 저녁 드실 분~', | ||
description: '금일 오후 6시에 자동 마감됩니다.', | ||
categoryId: MeetupCategory.WEEKLY_MEETUP, | ||
}; | ||
const { meetupId } = await this.meetupsService.create(createMeetupDto); | ||
|
||
// 생성한 이벤트 마감 크론잡 등록 및 실행 | ||
const cronJob = CronJob.from({ | ||
cronTime: '0 18 * * 3', | ||
timeZone: 'Asia/Seoul', | ||
onTick: async () => { | ||
try { | ||
await this.meetupsService.createMatching({ meetupId, teamNum: 1 }); | ||
} catch (e) { | ||
this.logger.error('[createWeeklyDinner]', e); | ||
} | ||
}, | ||
}); | ||
this.schedulerReistry.addCronJob('matchWeeklyDinner', cronJob); | ||
cronJob.start(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import { | ||
HttpExceptionBody, | ||
HttpExceptionBodyMessage, | ||
HttpStatus, | ||
} from '@nestjs/common'; | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
import { STATUS_CODES } from 'http'; | ||
|
||
/** | ||
* 5XX번대 서버 에러 | ||
*/ | ||
export class ServerExceptionBody implements Omit<HttpExceptionBody, 'error'> { | ||
@ApiProperty({ | ||
oneOf: [ | ||
{ type: 'string', example: '에러 내용' }, | ||
{ type: 'string[]', example: ['에러내용1', '에러내용2'] }, | ||
], | ||
}) | ||
message: HttpExceptionBodyMessage; | ||
|
||
@ApiProperty() | ||
statusCode: number; | ||
} | ||
|
||
/** | ||
* 4XX번대 클라이언트 에러 | ||
*/ | ||
export class ClientExceptionBody implements HttpExceptionBody { | ||
@ApiProperty({ | ||
oneOf: [ | ||
{ type: 'string', example: '에러 내용' }, | ||
{ type: 'string[]', example: ['에러내용1', '에러내용2'] }, | ||
], | ||
}) | ||
message: HttpExceptionBodyMessage; | ||
|
||
@ApiProperty() | ||
statusCode: number; | ||
|
||
@ApiProperty() | ||
error: string; | ||
} | ||
|
||
export class InternalServerExceptionBody extends ServerExceptionBody { | ||
@ApiProperty({ | ||
type: 'string', | ||
example: STATUS_CODES[HttpStatus.INTERNAL_SERVER_ERROR], | ||
}) | ||
message = STATUS_CODES[HttpStatus.INTERNAL_SERVER_ERROR]!; | ||
|
||
@ApiProperty({ type: 'number', example: HttpStatus.INTERNAL_SERVER_ERROR }) | ||
statusCode = HttpStatus.INTERNAL_SERVER_ERROR; | ||
} | ||
|
||
export class NotFoundExceptionBody extends ClientExceptionBody { | ||
@ApiProperty({ | ||
type: 'string', | ||
example: STATUS_CODES[HttpStatus.NOT_FOUND], | ||
}) | ||
error = STATUS_CODES[HttpStatus.FORBIDDEN]!; | ||
|
||
@ApiProperty({ type: Number, example: HttpStatus.NOT_FOUND }) | ||
statusCode = HttpStatus.NOT_FOUND; | ||
} | ||
|
||
export class ForbiddenExceptionBody extends ClientExceptionBody { | ||
@ApiProperty({ | ||
type: 'string', | ||
example: STATUS_CODES[HttpStatus.FORBIDDEN], | ||
}) | ||
error = STATUS_CODES[HttpStatus.FORBIDDEN]!; | ||
|
||
@ApiProperty({ type: Number, example: HttpStatus.FORBIDDEN }) | ||
statusCode = HttpStatus.FORBIDDEN; | ||
} | ||
|
||
export class BadRequestExceptionBody extends ClientExceptionBody { | ||
@ApiProperty({ | ||
type: 'string', | ||
example: STATUS_CODES[HttpStatus.BAD_REQUEST], | ||
}) | ||
error = STATUS_CODES[HttpStatus.BAD_REQUEST]!; | ||
|
||
@ApiProperty({ type: 'number', example: HttpStatus.BAD_REQUEST }) | ||
statusCode = HttpStatus.BAD_REQUEST; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export enum ErrorMessage { | ||
NO_PERMISSION = '권한이 없습니다.', | ||
MEETUP_NOT_FOUND = '존재하지 않는 이벤트입니다.', | ||
MEETUP_NOT_FOUND_OR_CLOSED = '존재하지 않거나 마감된 이벤트입니다.', | ||
MEETUP_REGISTRATION_NOT_FOUND = '해당 이벤트에 신청한 내역이 없습니다.', | ||
MEETUP_REGISTRATION_ALREADY_EXIST = '이미 신청한 이벤트 입니다.', | ||
TOO_MANY_MEETUP_TEAM_NUMBER = '신청 인원보다 팀 개수가 많습니다.', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { HttpException } from '@nestjs/common'; | ||
import { Message, Blocks, bold, Attachment } from 'slack-block-builder'; | ||
import { MeetupDetailDto } from '../meetups/dto/meetup-detail.dto'; | ||
import { MeetupDto } from 'src/meetups/dto/meetup.dto'; | ||
|
||
/** | ||
* Fisher-Yates shuffle 방식으로 배열의 요소들을 랜덤으로 섞어줍니다. | ||
* @param array 모든 타입의 배열을 받습니다. | ||
*/ | ||
export const shuffleArray = (array: any[]) => { | ||
for (let idx = 0; idx < array.length; idx++) { | ||
const randomIdx = Math.floor(Math.random() * (idx + 1)); | ||
[array[idx], array[randomIdx]] = [array[randomIdx], array[idx]]; | ||
} | ||
}; | ||
|
||
export const isHttpException = (error: any): error is HttpException => { | ||
if ('response' in error && 'status' in error) { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
|
||
/** | ||
* 이벤트 생성시 보내지는 슬랙봇 메시지 | ||
*/ | ||
export const meetupCreatedMessage = ({ | ||
channel, | ||
meetup, | ||
}: { | ||
channel: string; | ||
meetup: Pick<MeetupDto, 'title' | 'description'>; | ||
}) => { | ||
const { title, description } = meetup; | ||
return Message({ text: `[친해지길 바라] ${title}`, channel }) | ||
.blocks( | ||
Blocks.Header({ | ||
text: ':fire: 친해지길 바라 :fire:', | ||
}), | ||
Blocks.Divider(), | ||
Blocks.Section({ | ||
text: `${bold( | ||
title, | ||
)}\n${description}\n이벤트가 생성되었습니다. 서둘러 참석해주세요!`, | ||
}), | ||
Blocks.Section({ | ||
text: process.env.TOGETHER_HOME_URL, | ||
}), | ||
) | ||
.buildToObject(); | ||
}; | ||
|
||
/** | ||
* 이벤트 매칭시 보내지는 슬랙봇 메시지 | ||
*/ | ||
export const meetupMatchedMessage = ({ | ||
channel, | ||
meetupDetail, | ||
}: { | ||
channel: string; | ||
meetupDetail: MeetupDetailDto; | ||
}) => { | ||
const { event, teamList } = meetupDetail; | ||
const teams = Object.entries(teamList).map(([teamNum, teamMembers]) => { | ||
const teamMembersNickname = teamMembers.map((member) => member.intraId); | ||
return `${teamNum}팀 : ${teamMembersNickname.join(', ')}`; | ||
}); | ||
return Message({ text: `[친해지길 바라] ${event.title}`, channel }) | ||
.blocks( | ||
Blocks.Header({ | ||
text: ':fire: 친해지길 바라 :fire:', | ||
}), | ||
Blocks.Divider(), | ||
Blocks.Section({ | ||
text: `${bold(event.title)}`, | ||
}), | ||
Blocks.Section({ | ||
text: `${teams.join('\n')}`, | ||
}), | ||
Blocks.Section({ | ||
text: `팀 매칭이 완료되었습니다. 자신의 팀원들과 연락해보세요! :raised_hands:`, | ||
}), | ||
) | ||
.buildToObject(); | ||
}; | ||
|
||
/** | ||
* 이벤트 신청시 보내지는 슬랙봇 메시지 | ||
*/ | ||
export const meetupRegisteredMessage = ({ | ||
channel, | ||
meetup, | ||
}: { | ||
channel: string; | ||
meetup: Pick<MeetupDto, 'title' | 'description'>; | ||
}) => { | ||
const { title } = meetup; | ||
return Message({ channel, text: '[친해지길 바라] 이벤트 신청 완료' }) | ||
.blocks(Blocks.Header({ text: '친해지길 바라' })) | ||
.attachments( | ||
Attachment() | ||
.color('#36a64f') | ||
.blocks( | ||
Blocks.Section({ | ||
text: `${bold(title)}`, | ||
}), | ||
Blocks.Section({ | ||
text: `이벤트 신청이 완료 되었습니다.\n참여 감사합니다 :laughing:`, | ||
}), | ||
), | ||
) | ||
.buildToObject(); | ||
}; | ||
|
||
/** | ||
* 이벤트 신청 취소시 보내지는 슬랙봇 메시지 | ||
*/ | ||
export const meetupUnregisteredMessage = ({ | ||
channel, | ||
meetup, | ||
}: { | ||
channel: string; | ||
meetup: Pick<MeetupDto, 'title' | 'description'>; | ||
}) => { | ||
const { title } = meetup; | ||
return Message({ channel, text: '[친해지길 바라] 이벤트 신청 취소' }) | ||
.blocks(Blocks.Header({ text: '친해지길 바라' })) | ||
.attachments( | ||
Attachment() | ||
.color('#f2c744') | ||
.blocks( | ||
Blocks.Section({ | ||
text: `${bold(title)}`, | ||
}), | ||
Blocks.Section({ | ||
text: `이벤트 신청이 취소 되었습니다.\n아쉽네요.. 다음에 다시 만나요! :face_holding_back_tears:`, | ||
}), | ||
), | ||
) | ||
.buildToObject(); | ||
}; |
Oops, something went wrong.