-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/#12 - 알림 구현 #313
Merged
Merged
Feature/#12 - 알림 구현 #313
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
290c5ab
✨ feat: 알람 엔티티 추가 및 모듈 뼈대 구현
demian-m00n 57cee7f
✨ feat: 알람 CRUD 기능 구현
demian-m00n 3b222d2
✨ feat: 알람 요청 DTO 추가
demian-m00n 2fca19d
✨ feat: 유저 ID, 주식 ID 기반 알람 리스트 조회 기능 추가
demian-m00n fa22d83
✨ feat: subscription 엔티티 추가
demian-m00n bcf7acc
✨ feat: 알람 매칭 서브스크라이버 추가
demian-m00n a5caa36
✨ feat: 웹 푸시 서비스 구현
demian-m00n 0b7cfba
🚚 chore: web-push 의존성 설치
demian-m00n 24a7843
✨ feat: 웹 푸시 서비스 구현
demian-m00n 3429cee
✨ feat: 푸시 구독 기능 구현
demian-m00n 4b4e923
💄 style: merge dev-be, 알림 서비스 인수인계
swkim12345 6557b32
Merge branch 'dev-be' into feature/#12
swkim12345 6812061
♻️ refactor: 의존성 수정
swkim12345 cc3e1c6
💄 style: discribe todo 추가
swkim12345 a87b719
♻️ refactor: token의 경우 global하게 필요 없으므로 데코레이터 삭제
swkim12345 db5a481
♻️ refactor: alarm service와 서비스 의존 해결
swkim12345 5110889
♻️ refactor: live data 모듈간 의존성 리팩토링
swkim12345 43af7e6
💄 style: 불필요한 주석 삭제
swkim12345 4f54cec
💄 style: 테스트 코드 삭제
swkim12345 39267aa
✨ feat: push 서비스 완성 및 dto 추가
swkim12345 cb4a2ce
📝 docs: alarm request swagger 추가
swkim12345 af61d20
📝 docs: 알림 swagger 추가
swkim12345 bebbb0a
🐛 fix: 알람 모듈 수정, swagger 추가
swkim12345 47ae8b8
🐛 fix: push 모듈 수정 및 swagger 추가
swkim12345 1ad805f
💄 style: merge from dev-be
swkim12345 480c6e1
🐛 fix: localhost cors에러 해결. 이건...
swkim12345 d0affc6
✨ feat: subscriber 추가
swkim12345 00a4b41
🐛 fix: userId가 저장되지 않는 문제 해결
swkim12345 a6fc5b8
🐛 fix: userId를 명시적으로받지 않음. swagger 보강
swkim12345 7e3a2b3
📝 docs: swagger param 애매한 거 수정
swkim12345 1beebb5
📝 docs: live data logger 추가
swkim12345 04b411c
🐛 fix: alarm subscribe 찾지 못해 보내지 못하는 에러 수정
swkim12345 f98e749
Merge branch 'dev-be' into feature/#12
swkim12345 7a802f7
📝 docs: alarm response 추가, subscribe response 중 필요 없는 거 삭제
swkim12345 6a8ada6
📝 docs: swagger message response 추가
swkim12345 ec589ea
🐛 fix: subscriber 조건 변경
swkim12345 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { | ||
Controller, | ||
Get, | ||
Post, | ||
Param, | ||
Body, | ||
Put, | ||
Delete, | ||
UseGuards, | ||
} from '@nestjs/common'; | ||
import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; | ||
import { AlarmService } from './alarm.service'; | ||
import { AlarmRequest } from './dto/alarm.request'; | ||
import { AlarmResponse, AlarmSuccessResponse } from './dto/alarm.response'; | ||
import SessionGuard from '@/auth/session/session.guard'; | ||
import { GetUser } from '@/common/decorator/user.decorator'; | ||
import { User } from '@/user/domain/user.entity'; | ||
|
||
@Controller('alarm') | ||
export class AlarmController { | ||
constructor(private readonly alarmService: AlarmService) {} | ||
|
||
@Post() | ||
@ApiOperation({ | ||
summary: '알림 생성', | ||
description: '각 정보에 맞는 알림을 생성한다.', | ||
}) | ||
@ApiOkResponse({ | ||
description: '알림 생성 완료', | ||
type: AlarmResponse, | ||
}) | ||
@UseGuards(SessionGuard) | ||
async create( | ||
@Body() alarmRequest: AlarmRequest, | ||
@GetUser() user: User, | ||
): Promise<AlarmResponse> { | ||
const userId = user.id; | ||
|
||
return await this.alarmService.create(alarmRequest, userId); | ||
} | ||
|
||
@Get(':id') | ||
@ApiOperation({ | ||
summary: '등록된 알림 확인', | ||
description: '등록된 알림을 알림 아이디를 기준으로 찾을 수 있다.', | ||
}) | ||
@ApiOkResponse({ | ||
description: '알림 아이디와 동일한 알림 찾음', | ||
type: AlarmResponse, | ||
}) | ||
@ApiParam({ | ||
name: 'id', | ||
type: Number, | ||
description: '알림 아이디', | ||
example: 1, | ||
}) | ||
@UseGuards(SessionGuard) | ||
async findOne(@Param('id') alarmId: number): Promise<AlarmResponse> { | ||
return this.alarmService.findOne(alarmId); | ||
} | ||
|
||
@Put(':id') | ||
@ApiOperation({ | ||
summary: '등록된 알림 업데이트', | ||
description: '알림 아이디 기준으로 업데이트를 할 수 있다.', | ||
}) | ||
@ApiOkResponse({ | ||
description: '아이디와 동일한 알림 업데이트', | ||
type: AlarmResponse, | ||
}) | ||
@ApiParam({ | ||
name: 'id', | ||
type: Number, | ||
description: '알림 아이디', | ||
example: 1, | ||
}) | ||
@UseGuards(SessionGuard) | ||
async update( | ||
@Param('id') alarmId: number, | ||
@Body() updateData: AlarmRequest, | ||
): Promise<AlarmResponse> { | ||
return this.alarmService.update(alarmId, updateData); | ||
} | ||
|
||
@Delete(':id') | ||
@ApiParam({ | ||
name: 'id', | ||
type: Number, | ||
description: '알림 아이디', | ||
example: 1, | ||
}) | ||
@ApiOperation({ | ||
summary: '등록된 알림 삭제', | ||
description: '알림 아이디 기준으로 삭제를 할 수 있다.', | ||
}) | ||
@ApiOkResponse({ | ||
description: '아이디와 동일한 알림 삭제', | ||
type: AlarmSuccessResponse, | ||
}) | ||
@UseGuards(SessionGuard) | ||
async delete(@Param('id') alarmId: number) { | ||
await this.alarmService.delete(alarmId); | ||
|
||
return new AlarmSuccessResponse('알림 삭제를 성공했습니다.'); | ||
} | ||
|
||
@Get('user') | ||
@ApiOperation({ | ||
summary: '사용자별 알림 조회', | ||
description: '사용자 아이디를 기준으로 모든 알림을 조회한다.', | ||
}) | ||
@ApiOkResponse({ | ||
description: '사용자에게 등록되어 있는 모든 알림 조회', | ||
type: [AlarmResponse], | ||
}) | ||
@UseGuards(SessionGuard) | ||
async getByUserId(@GetUser() user: User) { | ||
const userId = user.id; | ||
|
||
return await this.alarmService.findByUserId(userId); | ||
} | ||
|
||
@Get('stock/:stockId') | ||
@ApiOperation({ | ||
summary: '주식별 알림 조회', | ||
description: '주식 아이디를 기준으로 알림을 조회한다.', | ||
}) | ||
@ApiOkResponse({ | ||
description: | ||
'주식 아이디에 등록되어 있는 알림 중 유저에 해당하는 알림 조회', | ||
type: [AlarmResponse], | ||
}) | ||
@ApiParam({ | ||
name: 'id', | ||
type: String, | ||
description: '주식 아이디', | ||
example: '005930', | ||
}) | ||
@UseGuards(SessionGuard) | ||
async getByStockId(@Param('stockId') stockId: string, @GetUser() user: User) { | ||
const userId = user.id; | ||
|
||
return await this.alarmService.findByStockId(stockId, userId); | ||
} | ||
} |
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,17 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { TypeOrmModule } from '@nestjs/typeorm'; | ||
import { AlarmController } from './alarm.controller'; | ||
import { AlarmService } from './alarm.service'; | ||
import { AlarmSubscriber } from './alarm.subscriber'; | ||
import { Alarm } from './domain/alarm.entity'; | ||
import { PushSubscription } from './domain/subscription.entity'; | ||
import { PushController } from './push.controller'; | ||
import { PushService } from './push.service'; | ||
|
||
@Module({ | ||
imports: [TypeOrmModule.forFeature([Alarm, PushSubscription])], | ||
controllers: [AlarmController, PushController], | ||
providers: [AlarmService, PushService, AlarmSubscriber], | ||
exports: [AlarmService], | ||
}) | ||
export class AlarmModule {} |
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,115 @@ | ||
import { | ||
ForbiddenException, | ||
Injectable, | ||
NotFoundException, | ||
} from '@nestjs/common'; | ||
import { InjectRepository } from '@nestjs/typeorm'; | ||
import { DataSource, Repository } from 'typeorm'; | ||
import { Alarm } from './domain/alarm.entity'; | ||
import { PushSubscription } from './domain/subscription.entity'; | ||
import { AlarmRequest } from './dto/alarm.request'; | ||
import { AlarmResponse } from './dto/alarm.response'; | ||
import { PushService } from './push.service'; | ||
import { User } from '@/user/domain/user.entity'; | ||
|
||
@Injectable() | ||
export class AlarmService { | ||
constructor( | ||
@InjectRepository(Alarm) | ||
private readonly alarmRepository: Repository<Alarm>, | ||
private readonly dataSource: DataSource, | ||
private readonly pushService: PushService, | ||
) {} | ||
|
||
async create(alarmData: AlarmRequest, userId: number) { | ||
return await this.dataSource.transaction(async (manager) => { | ||
const repository = manager.getRepository(Alarm); | ||
const user = await manager.findOne(User, { where: { id: userId } }); | ||
if (!user) { | ||
throw new ForbiddenException('유저를 찾을 수 없습니다.'); | ||
} | ||
|
||
const newAlarm = repository.create({ | ||
...alarmData, | ||
user, | ||
stock: { id: alarmData.stockId }, | ||
}); | ||
const result = await repository.save(newAlarm); | ||
return new AlarmResponse(result); | ||
}); | ||
} | ||
|
||
async findByUserId(userId: number) { | ||
const result = await this.alarmRepository.find({ | ||
where: { user: { id: userId } }, | ||
relations: ['user', 'stock'], | ||
}); | ||
return result.map((val) => new AlarmResponse(val)); | ||
} | ||
|
||
async findByStockId(stockId: string, userId: number): Promise<Alarm[]> { | ||
return await this.alarmRepository.find({ | ||
where: { stock: { id: stockId }, user: { id: userId } }, | ||
relations: ['user', 'stock'], | ||
}); | ||
} | ||
|
||
async findOne(id: number) { | ||
const result = await this.alarmRepository.findOne({ | ||
where: { id }, | ||
relations: ['user', 'stock'], | ||
}); | ||
if (result) return new AlarmResponse(result); | ||
else throw new NotFoundException('등록된 알림을 찾을 수 없습니다.'); | ||
} | ||
|
||
async update(id: number, updateData: AlarmRequest) { | ||
const alarm = await this.alarmRepository.findOne({ where: { id } }); | ||
if (!alarm) { | ||
throw new NotFoundException('등록된 알림을 찾을 수 없습니다.'); | ||
} | ||
|
||
await this.alarmRepository.update(id, updateData); | ||
const updatedAlarm = await this.alarmRepository.findOne({ | ||
where: { id }, | ||
relations: ['user', 'stock'], | ||
}); | ||
if (updatedAlarm) return new AlarmResponse(updatedAlarm); | ||
else | ||
throw new NotFoundException( | ||
`${id} : 업데이트할 알림을 찾을 수 없습니다.`, | ||
); | ||
} | ||
|
||
async delete(id: number) { | ||
const alarm = await this.alarmRepository.findOne({ where: { id } }); | ||
if (!alarm) { | ||
throw new NotFoundException(`${id} : 삭제할 알림을 찾을 수 없습니다.`); | ||
} | ||
|
||
await this.alarmRepository.delete(id); | ||
} | ||
|
||
async sendPushNotification(alarm: Alarm): Promise<void> { | ||
const { user, stock, targetPrice, targetVolume } = alarm; | ||
|
||
const payload = { | ||
title: '주식 알림', | ||
body: `${stock.name}: ${ | ||
targetPrice ? `가격이 ${targetPrice}에 도달했습니다.` : '' | ||
} ${targetVolume ? `거래량이 ${targetVolume}에 도달했습니다.` : ''}`, | ||
stockId: stock.id, | ||
}; | ||
|
||
const subscriptions = await this.dataSource.manager.findBy( | ||
PushSubscription, | ||
{ | ||
user: { id: user.id }, | ||
}, | ||
); | ||
|
||
for (const subscription of subscriptions) { | ||
await this.pushService.sendPushNotification(subscription, payload); | ||
} | ||
} | ||
} |
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,62 @@ | ||
import { Inject, Injectable } from '@nestjs/common'; | ||
import { | ||
DataSource, | ||
EntitySubscriberInterface, | ||
EventSubscriber, | ||
InsertEvent, | ||
} from 'typeorm'; | ||
import { Logger } from 'winston'; | ||
import { AlarmService } from './alarm.service'; | ||
import { Alarm } from './domain/alarm.entity'; | ||
import { StockMinutely } from '@/stock/domain/stockData.entity'; | ||
|
||
@Injectable() | ||
@EventSubscriber() | ||
export class AlarmSubscriber | ||
implements EntitySubscriberInterface<StockMinutely> | ||
{ | ||
constructor( | ||
private readonly datasource: DataSource, | ||
private readonly alarmService: AlarmService, | ||
@Inject('winston') private readonly logger: Logger, | ||
) { | ||
this.datasource.subscribers.push(this); | ||
} | ||
|
||
listenTo() { | ||
return StockMinutely; | ||
} | ||
|
||
isValidAlarm(alarm: Alarm, entity: StockMinutely) { | ||
if (alarm.alarmDate && alarm.alarmDate > entity.createdAt) { | ||
return false; | ||
} else { | ||
if (alarm.targetPrice && alarm.targetPrice >= entity.open) { | ||
return true; | ||
Comment on lines
+33
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. else가 없어도 될 것 같아요! |
||
} | ||
if (alarm.targetVolume && alarm.targetVolume >= entity.volume) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
} | ||
|
||
async afterInsert(event: InsertEvent<StockMinutely>) { | ||
try { | ||
const stockMinutely = event.entity; | ||
const rawAlarms = await this.datasource.manager.find(Alarm, { | ||
where: { stock: { id: stockMinutely.stock.id } }, | ||
relations: ['user', 'stock'], | ||
}); | ||
|
||
const alarms = rawAlarms.filter((val) => | ||
this.isValidAlarm(val, stockMinutely), | ||
); | ||
for (const alarm of alarms) { | ||
await this.alarmService.sendPushNotification(alarm); | ||
} | ||
} catch (error) { | ||
this.logger.warn(`Failed to handle alarm afterInsert event : ${error}`); | ||
} | ||
} | ||
} |
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,40 @@ | ||
import { | ||
Entity, | ||
PrimaryGeneratedColumn, | ||
Column, | ||
CreateDateColumn, | ||
UpdateDateColumn, | ||
ManyToOne, | ||
JoinColumn, | ||
} from 'typeorm'; | ||
import { Stock } from '@/stock/domain/stock.entity'; | ||
import { User } from '@/user/domain/user.entity'; | ||
|
||
@Entity() | ||
export class Alarm { | ||
@PrimaryGeneratedColumn() | ||
id: number; | ||
|
||
@ManyToOne(() => User, (user) => user.alarms, { onDelete: 'CASCADE' }) | ||
@JoinColumn({ name: 'user_id' }) | ||
user: User; | ||
|
||
@ManyToOne(() => Stock, (stock) => stock.alarms, { onDelete: 'CASCADE' }) | ||
@JoinColumn({ name: 'stock_id' }) | ||
stock: Stock; | ||
|
||
@Column({ type: 'int', name: 'target_price', nullable: true }) | ||
targetPrice?: number; | ||
|
||
@Column({ type: 'bigint', name: 'target_volume', nullable: true }) | ||
targetVolume?: number; | ||
|
||
@Column({ type: 'timestamp', name: 'alarm_date', nullable: true }) | ||
alarmDate?: Date; | ||
|
||
@CreateDateColumn({ type: 'timestamp', name: 'created_at' }) | ||
createdAt: Date; | ||
|
||
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at' }) | ||
updatedAt: Date; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
유저가 주인인지 확인하는 과정이 필요하다고 생각합니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 감사합니다! 이거 추가해서 올릴게요!