From 290c5abe0a7b4b287c5aa500cde5de891c12f60d Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Tue, 19 Nov 2024 23:18:24 +0900 Subject: [PATCH 01/32] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EB=BC=88=EB=8C=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.controller.ts | 4 ++ packages/backend/src/alarm/alarm.module.ts | 9 +++++ packages/backend/src/alarm/alarm.service.ts | 7 ++++ .../backend/src/alarm/domain/alarm.entity.ts | 40 +++++++++++++++++++ .../backend/src/stock/domain/stock.entity.ts | 8 +++- .../backend/src/user/domain/user.entity.ts | 4 ++ 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 packages/backend/src/alarm/alarm.controller.ts create mode 100644 packages/backend/src/alarm/alarm.module.ts create mode 100644 packages/backend/src/alarm/alarm.service.ts create mode 100644 packages/backend/src/alarm/domain/alarm.entity.ts diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts new file mode 100644 index 00000000..41d63e67 --- /dev/null +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('alarm') +export class AlarmController {} diff --git a/packages/backend/src/alarm/alarm.module.ts b/packages/backend/src/alarm/alarm.module.ts new file mode 100644 index 00000000..10d00a05 --- /dev/null +++ b/packages/backend/src/alarm/alarm.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AlarmController } from './alarm.controller'; +import { AlarmService } from './alarm.service'; + +@Module({ + controllers: [AlarmController], + providers: [AlarmService], +}) +export class AlarmModule {} diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts new file mode 100644 index 00000000..7d5a99e1 --- /dev/null +++ b/packages/backend/src/alarm/alarm.service.ts @@ -0,0 +1,7 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +@Injectable() +export class AlarmService { + constructor(private readonly dataSource: DataSource) {} +} diff --git a/packages/backend/src/alarm/domain/alarm.entity.ts b/packages/backend/src/alarm/domain/alarm.entity.ts new file mode 100644 index 00000000..4d33d215 --- /dev/null +++ b/packages/backend/src/alarm/domain/alarm.entity.ts @@ -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; +} diff --git a/packages/backend/src/stock/domain/stock.entity.ts b/packages/backend/src/stock/domain/stock.entity.ts index 033833bf..ae3fd6c9 100644 --- a/packages/backend/src/stock/domain/stock.entity.ts +++ b/packages/backend/src/stock/domain/stock.entity.ts @@ -1,6 +1,4 @@ import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm'; -import { DateEmbedded } from '@/common/dateEmbedded.entity'; -import { UserStock } from '@/stock/domain/userStock.entity'; import { StockDaily, StockMinutely, @@ -8,6 +6,9 @@ import { StockWeekly, StockYearly, } from './stockData.entity'; +import { Alarm } from '@/alarm/domain/alarm.entity'; +import { DateEmbedded } from '@/common/dateEmbedded.entity'; +import { UserStock } from '@/stock/domain/userStock.entity'; @Entity() export class Stock { @@ -46,4 +47,7 @@ export class Stock { @OneToMany(() => StockYearly, (stockYearly) => stockYearly.stock) stockYearly?: StockYearly[]; + + @OneToMany(() => Alarm, (alarm) => alarm.stock) + alarms?: Alarm[]; } diff --git a/packages/backend/src/user/domain/user.entity.ts b/packages/backend/src/user/domain/user.entity.ts index 26cdb345..e0d43447 100644 --- a/packages/backend/src/user/domain/user.entity.ts +++ b/packages/backend/src/user/domain/user.entity.ts @@ -5,6 +5,7 @@ import { OneToMany, PrimaryGeneratedColumn, } from 'typeorm'; +import { Alarm } from '@/alarm/domain/alarm.entity'; import { DateEmbedded } from '@/common/dateEmbedded.entity'; import { UserStock } from '@/stock/domain/userStock.entity'; import { OauthType } from '@/user/domain/ouathType'; @@ -39,4 +40,7 @@ export class User { @OneToMany(() => UserStock, (userStock) => userStock.user) userStocks: UserStock[]; + + @OneToMany(() => Alarm, (alarm) => alarm.user) + alarms: Alarm[]; } From 57cee7f1ee7607d090c7fc57a061966f1001ab56 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Tue, 19 Nov 2024 23:32:56 +0900 Subject: [PATCH 02/32] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?CRUD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.controller.ts | 47 ++++++++++++- packages/backend/src/alarm/alarm.module.ts | 3 + packages/backend/src/alarm/alarm.service.ts | 69 ++++++++++++++++++- 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts index 41d63e67..b41f8e8e 100644 --- a/packages/backend/src/alarm/alarm.controller.ts +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -1,4 +1,45 @@ -import { Controller } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Param, + Body, + Put, + Delete, +} from '@nestjs/common'; +import { AlarmService } from './alarm.service'; +import { Alarm } from './domain/alarm.entity'; -@Controller('alarm') -export class AlarmController {} +@Controller('alarms') +export class AlarmController { + constructor(private readonly alarmService: AlarmService) {} + + @Post() + async create(@Body() alarmData: Partial): Promise { + return this.alarmService.create(alarmData); + } + + @Get() + async findAll(): Promise { + return this.alarmService.findAll(); + } + + @Get(':id') + async findOne(@Param('id') id: number): Promise { + return this.alarmService.findOne(id); + } + + @Put(':id') + async update( + @Param('id') id: number, + @Body() updateData: Partial, + ): Promise { + return this.alarmService.update(id, updateData); + } + + @Delete(':id') + async delete(@Param('id') id: number): Promise<{ message: string }> { + await this.alarmService.delete(id); + return { message: `Alarm with ID ${id} deleted successfully` }; + } +} diff --git a/packages/backend/src/alarm/alarm.module.ts b/packages/backend/src/alarm/alarm.module.ts index 10d00a05..f6460821 100644 --- a/packages/backend/src/alarm/alarm.module.ts +++ b/packages/backend/src/alarm/alarm.module.ts @@ -1,8 +1,11 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { AlarmController } from './alarm.controller'; import { AlarmService } from './alarm.service'; +import { Alarm } from './domain/alarm.entity'; @Module({ + imports: [TypeOrmModule.forFeature([Alarm])], controllers: [AlarmController], providers: [AlarmService], }) diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index 7d5a99e1..8b3a2994 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -1,7 +1,70 @@ -import { Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { DataSource, Repository } from 'typeorm'; +import { Alarm } from './domain/alarm.entity'; @Injectable() export class AlarmService { - constructor(private readonly dataSource: DataSource) {} + constructor( + @InjectRepository(Alarm) + private readonly alarmRepository: Repository, + private readonly dataSource: DataSource, + ) {} + + async create(alarmData: Partial): Promise { + return await this.dataSource.transaction(async (manager) => { + const repository = manager.getRepository(Alarm); + const newAlarm = repository.create(alarmData); + return repository.save(newAlarm); + }); + } + + async findAll(): Promise { + return await this.dataSource.transaction(async (manager) => { + const repository = manager.getRepository(Alarm); + return repository.find({ relations: ['user', 'stock'] }); + }); + } + + async findOne(id: number): Promise { + return await this.dataSource.transaction(async (manager) => { + const repository = manager.getRepository(Alarm); + return repository.findOne({ + where: { id }, + relations: ['user', 'stock'], + }); + }); + } + + async update(id: number, updateData: Partial): Promise { + return await this.dataSource.transaction(async (manager) => { + const repository = manager.getRepository(Alarm); + + const alarm = await repository.findOne({ where: { id } }); + if (!alarm) { + throw new NotFoundException(`Alarm with ID ${id} not found`); + } + + await repository.update(id, updateData); + return ( + (await this.alarmRepository.findOne({ + where: { id }, + relations: ['user', 'stock'], + })) ?? new Alarm() + ); + }); + } + + async delete(id: number): Promise { + await this.dataSource.transaction(async (manager) => { + const repository = manager.getRepository(Alarm); + + const alarm = await repository.findOne({ where: { id } }); + if (!alarm) { + throw new NotFoundException(`Alarm with ID ${id} not found`); + } + + await repository.delete(id); + }); + } } From 3b222d24088f45c85b752cbf588babfec80e3962 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 20 Nov 2024 22:11:28 +0900 Subject: [PATCH 03/32] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/alarm/dto/alarm.request.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/backend/src/alarm/dto/alarm.request.ts diff --git a/packages/backend/src/alarm/dto/alarm.request.ts b/packages/backend/src/alarm/dto/alarm.request.ts new file mode 100644 index 00000000..72dbd3a5 --- /dev/null +++ b/packages/backend/src/alarm/dto/alarm.request.ts @@ -0,0 +1,6 @@ +export class AlarmRequest { + stock_id: string; + targetPrice?: number; + targetVolum?: number; + alarmDate?: Date; +} From 2fca19db3cfe7f220b4912a5c8ed812b97ebd2f0 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 20 Nov 2024 22:20:25 +0900 Subject: [PATCH 04/32] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?ID,=20=EC=A3=BC=EC=8B=9D=20ID=20=EA=B8=B0=EB=B0=98=20=EC=95=8C?= =?UTF-8?q?=EB=9E=8C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.controller.ts | 26 ++++++++++++------- packages/backend/src/alarm/alarm.service.ts | 20 +++++++++++--- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts index b41f8e8e..255c9d94 100644 --- a/packages/backend/src/alarm/alarm.controller.ts +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -9,19 +9,15 @@ import { } from '@nestjs/common'; import { AlarmService } from './alarm.service'; import { Alarm } from './domain/alarm.entity'; +import { AlarmRequest } from './dto/alarm.request'; -@Controller('alarms') +@Controller('alarm') export class AlarmController { constructor(private readonly alarmService: AlarmService) {} @Post() - async create(@Body() alarmData: Partial): Promise { - return this.alarmService.create(alarmData); - } - - @Get() - async findAll(): Promise { - return this.alarmService.findAll(); + async create(@Body() alarmRequest: AlarmRequest) { + return await this.alarmService.create(alarmRequest); } @Get(':id') @@ -32,14 +28,24 @@ export class AlarmController { @Put(':id') async update( @Param('id') id: number, - @Body() updateData: Partial, + @Body() updateData: AlarmRequest, ): Promise { return this.alarmService.update(id, updateData); } @Delete(':id') - async delete(@Param('id') id: number): Promise<{ message: string }> { + async delete(@Param('id') id: number) { await this.alarmService.delete(id); return { message: `Alarm with ID ${id} deleted successfully` }; } + + @Get('user/:userId') + async getByUserId(@Param('userId') userId: number): Promise { + return await this.alarmService.findByUserId(userId); + } + + @Get('stock/:stockId') + async getByStockId(@Param('stockId') stockId: string): Promise { + return await this.alarmService.findByStockId(stockId); + } } diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index 8b3a2994..ee5a3cf9 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { Alarm } from './domain/alarm.entity'; +import { AlarmRequest } from './dto/alarm.request'; @Injectable() export class AlarmService { @@ -19,10 +20,23 @@ export class AlarmService { }); } - async findAll(): Promise { + async findByUserId(userId: number): Promise { return await this.dataSource.transaction(async (manager) => { const repository = manager.getRepository(Alarm); - return repository.find({ relations: ['user', 'stock'] }); + return repository.find({ + where: { user: { id: userId } }, + relations: ['user', 'stock'], + }); + }); + } + + async findByStockId(stockId: string): Promise { + return await this.dataSource.transaction(async (manager) => { + const repository = manager.getRepository(Alarm); + return repository.find({ + where: { stock: { id: stockId } }, + relations: ['user', 'stock'], + }); }); } @@ -36,7 +50,7 @@ export class AlarmService { }); } - async update(id: number, updateData: Partial): Promise { + async update(id: number, updateData: AlarmRequest): Promise { return await this.dataSource.transaction(async (manager) => { const repository = manager.getRepository(Alarm); From fa22d8314c11732417a050d919c5996dafc894fe Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 20 Nov 2024 23:34:37 +0900 Subject: [PATCH 05/32] =?UTF-8?q?=E2=9C=A8=20feat:=20subscription=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/alarm/domain/subscription.entity.ts | 20 +++++++++++++++++++ .../backend/src/user/domain/user.entity.ts | 4 ++++ 2 files changed, 24 insertions(+) create mode 100644 packages/backend/src/alarm/domain/subscription.entity.ts diff --git a/packages/backend/src/alarm/domain/subscription.entity.ts b/packages/backend/src/alarm/domain/subscription.entity.ts new file mode 100644 index 00000000..f3013973 --- /dev/null +++ b/packages/backend/src/alarm/domain/subscription.entity.ts @@ -0,0 +1,20 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { User } from '@/user/domain/user.entity'; + +@Entity() +export class PushSubscription { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => User, (user) => user.subscriptions, { onDelete: 'CASCADE' }) + user: User; + + @Column({ type: 'text' }) + endpoint: string; + + @Column({ type: 'text' }) + p256dh: string; + + @Column({ type: 'text' }) + auth: string; +} diff --git a/packages/backend/src/user/domain/user.entity.ts b/packages/backend/src/user/domain/user.entity.ts index e0d43447..6c390932 100644 --- a/packages/backend/src/user/domain/user.entity.ts +++ b/packages/backend/src/user/domain/user.entity.ts @@ -6,6 +6,7 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; import { Alarm } from '@/alarm/domain/alarm.entity'; +import { PushSubscription } from '@/alarm/domain/subscription.entity'; import { DateEmbedded } from '@/common/dateEmbedded.entity'; import { UserStock } from '@/stock/domain/userStock.entity'; import { OauthType } from '@/user/domain/ouathType'; @@ -43,4 +44,7 @@ export class User { @OneToMany(() => Alarm, (alarm) => alarm.user) alarms: Alarm[]; + + @OneToMany(() => PushSubscription, (subscription) => subscription.user) + subscriptions: PushSubscription[]; } From bcf7acc6b19ac9cca5f66a8751c6334942fde948 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 20 Nov 2024 23:36:08 +0900 Subject: [PATCH 06/32] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EB=A7=A4=EC=B9=AD=20=EC=84=9C=EB=B8=8C=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B2=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/alarm/alarm.service.ts | 35 ++++++++++++++++++- .../src/stock/stockLiveData.subscriber.ts | 10 ++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index ee5a3cf9..34a64078 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, Repository } from 'typeorm'; +import { DataSource, LessThanOrEqual, Repository } from 'typeorm'; import { Alarm } from './domain/alarm.entity'; import { AlarmRequest } from './dto/alarm.request'; @@ -81,4 +81,37 @@ export class AlarmService { await repository.delete(id); }); } + + async getMatchingAlarms( + stockId: string, + currentPrice: number, + currentVolume: number, + ): Promise { + return this.alarmRepository.find({ + where: [ + { stock: { id: stockId }, targetPrice: LessThanOrEqual(currentPrice) }, + { + stock: { id: stockId }, + targetVolume: LessThanOrEqual(currentVolume), + }, + ], + }); + } + + private async sendAlarmNotification(alarm: Alarm): Promise { + const { user, stock, targetPrice, targetVolume } = alarm; + + const payload = { + title: '주식 알림', + body: `${stock.name}: ${ + targetPrice ? `가격이 ${targetPrice}에 도달했습니다.` : '' + } ${targetVolume ? `거래량이 (${targetVolume}에 도달했습니다.` : ''}`, + }; + + // 웹 푸시 전송 + + await this.handleAlarmAfterNotification(); + } + + private async handleAlarmAfterNotification() {} } diff --git a/packages/backend/src/stock/stockLiveData.subscriber.ts b/packages/backend/src/stock/stockLiveData.subscriber.ts index e28d55e2..cfad7af7 100644 --- a/packages/backend/src/stock/stockLiveData.subscriber.ts +++ b/packages/backend/src/stock/stockLiveData.subscriber.ts @@ -7,6 +7,7 @@ import { import { Logger } from 'winston'; import { StockLiveData } from './domain/stockLiveData.entity'; import { StockGateway } from './stock.gateway'; +import { AlarmService } from '@/alarm/alarm.service'; @EventSubscriber() export class StockLiveDataSubscriber @@ -14,6 +15,7 @@ export class StockLiveDataSubscriber { constructor( private readonly stockGateway: StockGateway, + private readonly alarmService: AlarmService, @Inject('winston') private readonly logger: Logger, ) {} @@ -21,6 +23,7 @@ export class StockLiveDataSubscriber return StockLiveData; } + // eslint-disable-next-line max-lines-per-function async afterUpdate(event: UpdateEvent) { try { const updatedStockLiveData = @@ -33,6 +36,13 @@ export class StockLiveDataSubscriber changeRate: change, volume: volume, } = updatedStockLiveData; + + const alarms = await this.alarmService.getMatchingAlarms( + stockId, + price, + volume, + ); + this.stockGateway.onUpdateStock(stockId, price, change, volume); } else { this.logger.error( From a5caa36cf9ce95ebf6bfd8b0592ff245394588cf Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Thu, 21 Nov 2024 20:36:49 +0900 Subject: [PATCH 07/32] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9B=B9=20=ED=91=B8?= =?UTF-8?q?=EC=8B=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/alarm/webPush.service.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/backend/src/alarm/webPush.service.ts diff --git a/packages/backend/src/alarm/webPush.service.ts b/packages/backend/src/alarm/webPush.service.ts new file mode 100644 index 00000000..6bd34c0a --- /dev/null +++ b/packages/backend/src/alarm/webPush.service.ts @@ -0,0 +1,38 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { config } from 'dotenv'; +import * as webPush from 'web-push'; +import { Logger } from 'winston'; +import { PushSubscription } from './domain/subscription.entity'; + +config(); + +@Injectable() +export class PushService { + constructor(@Inject('winston') private readonly logger: Logger) { + webPush.setVapidDetails( + 'mailto:admin@juchum.info', + process.env.VAPID_PUBLIC_KEY!, + process.env.VAPID_PRIVATE_KEY!, + ); + } + + async sendPushNotification(subscription: PushSubscription, payload: object) { + const pushPayload = JSON.stringify(payload); + + try { + await webPush.sendNotification( + { + endpoint: subscription.endpoint, + keys: { + p256dh: subscription.p256dh, + auth: subscription.auth, + }, + }, + pushPayload, + ); + this.logger.info('Push notification sent successfully'); + } catch (error) { + this.logger.warn('Failed to send push notification', error); + } + } +} From 0b7cfba97ccc6f17b2797d481f73bed9bb1c536b Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Thu, 21 Nov 2024 20:51:02 +0900 Subject: [PATCH 08/32] =?UTF-8?q?=F0=9F=9A=9A=20chore:=20web-push=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/package.json | 2 + yarn.lock | 168 ++++++++++++++++++++++++---------- 2 files changed, 122 insertions(+), 48 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index c41b6046..a819c549 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -30,6 +30,7 @@ "@nestjs/swagger": "^8.0.5", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.4.7", + "@types/web-push": "^3.6.4", "axios": "^1.7.7", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -44,6 +45,7 @@ "socket.io": "^4.8.1", "typeorm": "^0.3.20", "unzipper": "^0.12.3", + "web-push": "^3.6.7", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, diff --git a/yarn.lock b/yarn.lock index 10d4e991..6158c2c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1199,11 +1199,6 @@ resolved "https://registry.yarnpkg.com/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== -"@microsoft/tsdoc@^0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz#f29a55df17cb6e87cfbabce33ff6a14a9f85076d" - integrity sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA== - "@mdx-js/react@^3.0.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.1.0.tgz#c4522e335b3897b9a845db1dbdd2f966ae8fb0ed" @@ -1211,6 +1206,11 @@ dependencies: "@types/mdx" "^2.0.0" +"@microsoft/tsdoc@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz#f29a55df17cb6e87cfbabce33ff6a14a9f85076d" + integrity sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA== + "@nestjs/cli@^10.0.0": version "10.4.5" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.4.5.tgz#d6563b87e8ca1d0f256c19a7847dbcc96c76a88e" @@ -2000,7 +2000,6 @@ dependencies: "@types/node" "*" -"@types/estree@1.0.6", "@types/estree@^1.0.5", "@types/estree@^1.0.6": "@types/doctrine@^0.0.9": version "0.0.9" resolved "https://registry.yarnpkg.com/@types/doctrine/-/doctrine-0.0.9.tgz#d86a5f452a15e3e3113b99e39616a9baa0f9863f" @@ -2260,15 +2259,22 @@ dependencies: "@types/node" "*" +"@types/uuid@^9.0.1": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + "@types/validator@^13.11.8": version "13.12.2" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.2.tgz#760329e756e18a4aab82fc502b51ebdfebbe49f5" integrity sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA== -"@types/uuid@^9.0.1": - version "9.0.8" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" - integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== +"@types/web-push@^3.6.4": + version "3.6.4" + resolved "https://registry.yarnpkg.com/@types/web-push/-/web-push-3.6.4.tgz#4c6e10d3963ba51e7b4b8fff185f43612c0d1346" + integrity sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ== + dependencies: + "@types/node" "*" "@types/yargs-parser@*": version "21.0.3" @@ -2643,6 +2649,13 @@ acorn@^8.11.0, acorn@^8.14.0, acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8 resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0" integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA== +agent-base@^7.0.2: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + ajv-formats@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2889,6 +2902,16 @@ asap@^2.0.0: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +asn1.js@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + assertion-error@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" @@ -3022,13 +3045,6 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -better-opn@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817" - integrity sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ== - dependencies: - open "^8.0.4" - base64id@2.0.0, base64id@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" @@ -3039,6 +3055,13 @@ base64url@3.x.x: resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== +better-opn@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-3.0.2.tgz#f96f35deaaf8f34144a4102651babcf00d1d8817" + integrity sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ== + dependencies: + open "^8.0.4" + binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" @@ -3058,6 +3081,11 @@ bluebird@~3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bn.js@^4.0.0: + version "4.12.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.1.tgz#215741fe3c9dba2d7e12c001d0cfdbae43975ba7" + integrity sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg== + body-parser@1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" @@ -3127,6 +3155,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -3780,6 +3813,13 @@ debug@2.6.9: dependencies: ms "2.0.0" +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -3787,13 +3827,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== - dependencies: - ms "^2.1.3" - dedent@0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -3978,6 +4011,13 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + editor@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/editor/-/editor-1.0.0.tgz#60c7f87bd62bcc6a894fa8ccd6afb7823a24f742" @@ -5256,6 +5296,19 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http_ece@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.2.0.tgz#84d5885f052eae8c9b075eee4d2eb5105f114479" + integrity sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA== + +https-proxy-agent@^7.0.0: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -6151,13 +6204,6 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - jsdoc-type-pratt-parser@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz#ff6b4a3f339c34a6c188cbf50a16087858d22113" @@ -6229,6 +6275,23 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + keyv@^4.5.3, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -6592,6 +6655,11 @@ min-indent@^1.0.0, min-indent@^1.0.1: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -6618,7 +6686,7 @@ minimist@1.2.7: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -6734,6 +6802,13 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +nest-winston@^1.9.7: + version "1.9.7" + resolved "https://registry.yarnpkg.com/nest-winston/-/nest-winston-1.9.7.tgz#1ef6eb2459ce595655de37d5beb900d2e75b61d3" + integrity sha512-pTTgImRgv7urojsDvaTlenAjyJNLj7ywamfjzrhWKhLhp80AKLYNwf103dVHeqZWe+nzp/vd9DGRs/UN/YadOQ== + dependencies: + fast-safe-stringify "^2.1.1" + no-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" @@ -6742,13 +6817,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -nest-winston@^1.9.7: - version "1.9.7" - resolved "https://registry.yarnpkg.com/nest-winston/-/nest-winston-1.9.7.tgz#1ef6eb2459ce595655de37d5beb900d2e75b61d3" - integrity sha512-pTTgImRgv7urojsDvaTlenAjyJNLj7ywamfjzrhWKhLhp80AKLYNwf103dVHeqZWe+nzp/vd9DGRs/UN/YadOQ== - dependencies: - fast-safe-stringify "^2.1.1" - node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" @@ -7485,7 +7553,6 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -reflect-metadata@^0.2.0: reflect-metadata@^0.2.0, reflect-metadata@^0.2.1: version "0.2.2" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" @@ -7687,7 +7754,7 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -8705,7 +8772,6 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: util@^0.12.5: version "0.12.5" resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" @@ -8717,16 +8783,11 @@ util@^0.12.5: is-typed-array "^1.1.3" which-typed-array "^1.1.2" -utils-merge@1.0.1: +utils-merge@1.0.1, utils-merge@1.x.x, utils-merge@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - uuid@10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" @@ -8803,6 +8864,17 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-push@^3.6.7: + version "3.6.7" + resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.6.7.tgz#5f5e645951153e37ef90a6ddea5c150ea0f709e1" + integrity sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A== + dependencies: + asn1.js "^5.3.0" + http_ece "1.2.0" + https-proxy-agent "^7.0.0" + jws "^4.0.0" + minimist "^1.2.5" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From 24a7843e0fe4a095fd5016823428896544fe5506 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Thu, 21 Nov 2024 21:33:09 +0900 Subject: [PATCH 09/32] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=9B=B9=20=ED=91=B8?= =?UTF-8?q?=EC=8B=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.controller.ts | 4 +- packages/backend/src/alarm/alarm.module.ts | 6 +- packages/backend/src/alarm/alarm.service.ts | 105 ++++++------------ packages/backend/src/alarm/webPush.service.ts | 14 ++- .../src/stock/stockLiveData.subscriber.ts | 6 - 5 files changed, 50 insertions(+), 85 deletions(-) diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts index 255c9d94..5474a244 100644 --- a/packages/backend/src/alarm/alarm.controller.ts +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -40,12 +40,12 @@ export class AlarmController { } @Get('user/:userId') - async getByUserId(@Param('userId') userId: number): Promise { + async getByUserId(@Param('userId') userId: number) { return await this.alarmService.findByUserId(userId); } @Get('stock/:stockId') - async getByStockId(@Param('stockId') stockId: string): Promise { + async getByStockId(@Param('stockId') stockId: string) { return await this.alarmService.findByStockId(stockId); } } diff --git a/packages/backend/src/alarm/alarm.module.ts b/packages/backend/src/alarm/alarm.module.ts index f6460821..dc943c45 100644 --- a/packages/backend/src/alarm/alarm.module.ts +++ b/packages/backend/src/alarm/alarm.module.ts @@ -3,10 +3,12 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AlarmController } from './alarm.controller'; import { AlarmService } from './alarm.service'; import { Alarm } from './domain/alarm.entity'; +import { PushSubscription } from './domain/subscription.entity'; +import { PushService } from './webPush.service'; @Module({ - imports: [TypeOrmModule.forFeature([Alarm])], + imports: [TypeOrmModule.forFeature([Alarm, PushSubscription])], controllers: [AlarmController], - providers: [AlarmService], + providers: [AlarmService, PushService], }) export class AlarmModule {} diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index 34a64078..d38212af 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -1,8 +1,9 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { DataSource, LessThanOrEqual, Repository } from 'typeorm'; +import { DataSource, Repository } from 'typeorm'; import { Alarm } from './domain/alarm.entity'; import { AlarmRequest } from './dto/alarm.request'; +import { PushService } from './webPush.service'; @Injectable() export class AlarmService { @@ -10,108 +11,74 @@ export class AlarmService { @InjectRepository(Alarm) private readonly alarmRepository: Repository, private readonly dataSource: DataSource, + private readonly pushService: PushService, ) {} async create(alarmData: Partial): Promise { return await this.dataSource.transaction(async (manager) => { const repository = manager.getRepository(Alarm); const newAlarm = repository.create(alarmData); - return repository.save(newAlarm); + const savedAlarm = await repository.save(newAlarm); + return savedAlarm; }); } async findByUserId(userId: number): Promise { - return await this.dataSource.transaction(async (manager) => { - const repository = manager.getRepository(Alarm); - return repository.find({ - where: { user: { id: userId } }, - relations: ['user', 'stock'], - }); + return await this.alarmRepository.find({ + where: { user: { id: userId } }, + relations: ['user', 'stock'], }); } async findByStockId(stockId: string): Promise { - return await this.dataSource.transaction(async (manager) => { - const repository = manager.getRepository(Alarm); - return repository.find({ - where: { stock: { id: stockId } }, - relations: ['user', 'stock'], - }); + return await this.alarmRepository.find({ + where: { stock: { id: stockId } }, + relations: ['user', 'stock'], }); } - async findOne(id: number): Promise { - return await this.dataSource.transaction(async (manager) => { - const repository = manager.getRepository(Alarm); - return repository.findOne({ - where: { id }, - relations: ['user', 'stock'], - }); + async findOne(id: number) { + return await this.alarmRepository.findOne({ + where: { id }, + relations: ['user', 'stock'], }); } - async update(id: number, updateData: AlarmRequest): Promise { - return await this.dataSource.transaction(async (manager) => { - const repository = manager.getRepository(Alarm); - - const alarm = await repository.findOne({ where: { id } }); - if (!alarm) { - throw new NotFoundException(`Alarm with ID ${id} not found`); - } + async update(id: number, updateData: AlarmRequest) { + const alarm = await this.alarmRepository.findOne({ where: { id } }); + if (!alarm) { + throw new NotFoundException(`Alarm with ID ${id} not found`); + } - await repository.update(id, updateData); - return ( - (await this.alarmRepository.findOne({ - where: { id }, - relations: ['user', 'stock'], - })) ?? new Alarm() - ); + await this.alarmRepository.update(id, updateData); + const updatedAlarm = await this.alarmRepository.findOne({ + where: { id }, + relations: ['user', 'stock'], }); + return updatedAlarm!; } - async delete(id: number): Promise { - await this.dataSource.transaction(async (manager) => { - const repository = manager.getRepository(Alarm); - - const alarm = await repository.findOne({ where: { id } }); - if (!alarm) { - throw new NotFoundException(`Alarm with ID ${id} not found`); - } - - await repository.delete(id); - }); - } + async delete(id: number) { + const alarm = await this.alarmRepository.findOne({ where: { id } }); + if (!alarm) { + throw new NotFoundException(`Alarm with ID ${id} not found`); + } - async getMatchingAlarms( - stockId: string, - currentPrice: number, - currentVolume: number, - ): Promise { - return this.alarmRepository.find({ - where: [ - { stock: { id: stockId }, targetPrice: LessThanOrEqual(currentPrice) }, - { - stock: { id: stockId }, - targetVolume: LessThanOrEqual(currentVolume), - }, - ], - }); + await this.alarmRepository.delete(id); } - private async sendAlarmNotification(alarm: Alarm): Promise { + private async sendPushNotification(alarm: Alarm): Promise { const { user, stock, targetPrice, targetVolume } = alarm; const payload = { title: '주식 알림', body: `${stock.name}: ${ targetPrice ? `가격이 ${targetPrice}에 도달했습니다.` : '' - } ${targetVolume ? `거래량이 (${targetVolume}에 도달했습니다.` : ''}`, + } ${targetVolume ? `거래량이 ${targetVolume}에 도달했습니다.` : ''}`, }; - // 웹 푸시 전송 - - await this.handleAlarmAfterNotification(); + for (const subscription of user.subscriptions) { + await this.pushService.sendPushNotification(subscription, payload); + } } - - private async handleAlarmAfterNotification() {} } diff --git a/packages/backend/src/alarm/webPush.service.ts b/packages/backend/src/alarm/webPush.service.ts index 6bd34c0a..305a249b 100644 --- a/packages/backend/src/alarm/webPush.service.ts +++ b/packages/backend/src/alarm/webPush.service.ts @@ -1,11 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; -import { config } from 'dotenv'; import * as webPush from 'web-push'; import { Logger } from 'winston'; import { PushSubscription } from './domain/subscription.entity'; -config(); - @Injectable() export class PushService { constructor(@Inject('winston') private readonly logger: Logger) { @@ -16,7 +13,10 @@ export class PushService { ); } - async sendPushNotification(subscription: PushSubscription, payload: object) { + async sendPushNotification( + subscription: PushSubscription, + payload: object, + ): Promise { const pushPayload = JSON.stringify(payload); try { @@ -30,9 +30,11 @@ export class PushService { }, pushPayload, ); - this.logger.info('Push notification sent successfully'); } catch (error) { - this.logger.warn('Failed to send push notification', error); + this.logger.warn( + `Failed to send push notification to ${subscription.endpoint}`, + error, + ); } } } diff --git a/packages/backend/src/stock/stockLiveData.subscriber.ts b/packages/backend/src/stock/stockLiveData.subscriber.ts index cfad7af7..b03488f9 100644 --- a/packages/backend/src/stock/stockLiveData.subscriber.ts +++ b/packages/backend/src/stock/stockLiveData.subscriber.ts @@ -37,12 +37,6 @@ export class StockLiveDataSubscriber volume: volume, } = updatedStockLiveData; - const alarms = await this.alarmService.getMatchingAlarms( - stockId, - price, - volume, - ); - this.stockGateway.onUpdateStock(stockId, price, change, volume); } else { this.logger.error( From 3429cee4136eb0e2eda41500062e59ed76655445 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Mon, 25 Nov 2024 20:27:52 +0900 Subject: [PATCH 10/32] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/alarm/alarm.module.ts | 2 +- packages/backend/src/alarm/alarm.service.ts | 2 +- .../backend/src/alarm/dto/subscription.ts | 10 +++++++ packages/backend/src/alarm/push.controller.ts | 30 +++++++++++++++++++ .../{webPush.service.ts => push.service.ts} | 27 ++++++++++++++++- 5 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 packages/backend/src/alarm/dto/subscription.ts create mode 100644 packages/backend/src/alarm/push.controller.ts rename packages/backend/src/alarm/{webPush.service.ts => push.service.ts} (54%) diff --git a/packages/backend/src/alarm/alarm.module.ts b/packages/backend/src/alarm/alarm.module.ts index dc943c45..c599a4ae 100644 --- a/packages/backend/src/alarm/alarm.module.ts +++ b/packages/backend/src/alarm/alarm.module.ts @@ -4,7 +4,7 @@ import { AlarmController } from './alarm.controller'; import { AlarmService } from './alarm.service'; import { Alarm } from './domain/alarm.entity'; import { PushSubscription } from './domain/subscription.entity'; -import { PushService } from './webPush.service'; +import { PushService } from './push.service'; @Module({ imports: [TypeOrmModule.forFeature([Alarm, PushSubscription])], diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index d38212af..a063dc33 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { Alarm } from './domain/alarm.entity'; import { AlarmRequest } from './dto/alarm.request'; -import { PushService } from './webPush.service'; +import { PushService } from './push.service'; @Injectable() export class AlarmService { diff --git a/packages/backend/src/alarm/dto/subscription.ts b/packages/backend/src/alarm/dto/subscription.ts new file mode 100644 index 00000000..f94ea13f --- /dev/null +++ b/packages/backend/src/alarm/dto/subscription.ts @@ -0,0 +1,10 @@ +import { User } from '@/user/domain/user.entity'; + +export class SubscriptionData { + user: User; + endpoint: string; + keys: { + p256dh: string; + auth: string; + }; +} diff --git a/packages/backend/src/alarm/push.controller.ts b/packages/backend/src/alarm/push.controller.ts new file mode 100644 index 00000000..2f7201d6 --- /dev/null +++ b/packages/backend/src/alarm/push.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { SubscriptionData } from './dto/subscription'; +import { PushService } from './push.service'; +import SessionGuard from '@/auth/session/session.guard'; +import { GetUser } from '@/common/decorator/user.decorator'; +import { User } from '@/user/domain/user.entity'; + +@Controller('/push') +export class PushController { + constructor(private readonly pushService: PushService) {} + + @Post('subscribe') + @UseGuards(SessionGuard) + async subscribe( + @Body() subscriptionData: SubscriptionData, + @GetUser() user: User, + ) { + const userId = user.id; + + const newSubscription = await this.pushService.createSubscription( + userId, + subscriptionData, + ); + + return { + message: 'Subscription saved.', + subscriptionId: newSubscription.id, + }; + } +} diff --git a/packages/backend/src/alarm/webPush.service.ts b/packages/backend/src/alarm/push.service.ts similarity index 54% rename from packages/backend/src/alarm/webPush.service.ts rename to packages/backend/src/alarm/push.service.ts index 305a249b..e6c51e7c 100644 --- a/packages/backend/src/alarm/webPush.service.ts +++ b/packages/backend/src/alarm/push.service.ts @@ -1,11 +1,17 @@ import { Inject, Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; import * as webPush from 'web-push'; import { Logger } from 'winston'; import { PushSubscription } from './domain/subscription.entity'; +import { SubscriptionData } from './dto/subscription'; +import { User } from '@/user/domain/user.entity'; @Injectable() export class PushService { - constructor(@Inject('winston') private readonly logger: Logger) { + constructor( + @Inject('winston') private readonly logger: Logger, + private readonly dataSource: DataSource, + ) { webPush.setVapidDetails( 'mailto:admin@juchum.info', process.env.VAPID_PUBLIC_KEY!, @@ -37,4 +43,23 @@ export class PushService { ); } } + + async createSubscription( + userId: number, + subscriptionData: SubscriptionData, + ): Promise { + return await this.dataSource.transaction(async (manager) => { + const user = new User(); + user.id = userId; + + const newSubscription = manager.create(PushSubscription, { + user: user, + endpoint: subscriptionData.endpoint, + p256dh: subscriptionData.keys.p256dh, + auth: subscriptionData.keys.auth, + }); + + return await manager.save(newSubscription); + }); + } } From 68120615a98f331e606e2847f2e598a34bcaeb51 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Fri, 29 Nov 2024 02:30:12 +0900 Subject: [PATCH 11/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/package.json | 1 + .../backend/src/stock/domain/kospiStock.entity.ts | 15 --------------- packages/backend/src/stock/domain/stock.entity.ts | 7 ++----- yarn.lock | 1 + 4 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 packages/backend/src/stock/domain/kospiStock.entity.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 6ff5ca61..c81d6fad 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -63,6 +63,7 @@ "@types/passport-local": "^1.0.38", "@types/supertest": "^6.0.0", "@types/unzipper": "^0.10.10", + "@types/web-push": "^3.6.4", "@types/ws": "^8.5.13", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", diff --git a/packages/backend/src/stock/domain/kospiStock.entity.ts b/packages/backend/src/stock/domain/kospiStock.entity.ts deleted file mode 100644 index 8f45a87c..00000000 --- a/packages/backend/src/stock/domain/kospiStock.entity.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; -import { Stock } from './stock.entity'; - -@Entity() -export class KospiStock { - @PrimaryColumn({ name: 'stock_id' }) - id: string; - - @Column({ name: 'is_kospi' }) - isKospi: boolean; - - @OneToOne(() => Stock, (stock) => stock.id) - @JoinColumn({ name: 'stock_id' }) - stock: Stock; -} diff --git a/packages/backend/src/stock/domain/stock.entity.ts b/packages/backend/src/stock/domain/stock.entity.ts index b4d5b438..82af0c2e 100644 --- a/packages/backend/src/stock/domain/stock.entity.ts +++ b/packages/backend/src/stock/domain/stock.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm'; +import { Column, Entity, OneToMany, OneToOne, PrimaryColumn } from 'typeorm'; import { StockDaily, StockMinutely, @@ -7,11 +7,11 @@ import { StockYearly, } from './stockData.entity'; import { StockLiveData } from './stockLiveData.entity'; +import { Alarm } from '@/alarm/domain/alarm.entity'; import { Like } from '@/chat/domain/like.entity'; import { DateEmbedded } from '@/common/dateEmbedded.entity'; import { FluctuationRankStock } from '@/stock/domain/FluctuationRankStock.entity'; import { UserStock } from '@/stock/domain/userStock.entity'; -import { Alarm } from '@/alarm/domain/alarm.entity'; @Entity() export class Stock { @@ -57,9 +57,6 @@ export class Stock { @OneToOne(() => StockLiveData, (stockLiveData) => stockLiveData.stock) stockLive?: StockLiveData; - @OneToOne(() => KospiStock, (kospiStock) => kospiStock.stock) - kospiStock?: KospiStock; - @OneToMany( () => FluctuationRankStock, (fluctuationRankStock) => fluctuationRankStock.stock, diff --git a/yarn.lock b/yarn.lock index 39f39d0e..2ff39d51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2292,6 +2292,7 @@ integrity sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ== dependencies: "@types/node" "*" + "@types/ws@^8.5.13": version "8.5.13" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" From cc3e1c65e4f42eaac9123a99c092f74cb064577b Mon Sep 17 00:00:00 2001 From: sunghwki Date: Fri, 29 Nov 2024 02:33:17 +0900 Subject: [PATCH 12/32] =?UTF-8?q?=F0=9F=92=84=20style:=20discribe=20todo?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/stock/stock.gateway.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/stock/stock.gateway.ts b/packages/backend/src/stock/stock.gateway.ts index 2674690d..ab3510c5 100644 --- a/packages/backend/src/stock/stock.gateway.ts +++ b/packages/backend/src/stock/stock.gateway.ts @@ -39,6 +39,7 @@ export class StockGateway { ) { client.leave(stockId); + //TODO : disconnect 시 discribe client.emit('disconnectionSuccess', { message: `Successfully disconnected to stock room: ${stockId}`, stockId, From a87b719f39a8e6f040323072f5ab6662df565832 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Fri, 29 Nov 2024 03:03:09 +0900 Subject: [PATCH 13/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20token?= =?UTF-8?q?=EC=9D=98=20=EA=B2=BD=EC=9A=B0=20global=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=20=EC=97=86=EC=9C=BC=EB=AF=80=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=BD=94=EB=A0=88=EC=9D=B4=ED=84=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/scraper/openapi/api/openapiToken.api.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend/src/scraper/openapi/api/openapiToken.api.ts b/packages/backend/src/scraper/openapi/api/openapiToken.api.ts index 437283d3..53b72d9c 100644 --- a/packages/backend/src/scraper/openapi/api/openapiToken.api.ts +++ b/packages/backend/src/scraper/openapi/api/openapiToken.api.ts @@ -1,4 +1,4 @@ -import { Global, Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { DataSource } from 'typeorm'; import { Logger } from 'winston'; @@ -7,7 +7,6 @@ import { openApiConfig } from '../config/openapi.config'; import { OpenapiException } from '../util/openapiCustom.error'; import { postOpenApi } from '../util/openapiUtil.api'; -@Global() @Injectable() export class OpenapiTokenApi { private config: (typeof openApiConfig)[] = []; From db5a4814cf8edcf23497c4cc1bcb36d05be63102 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Fri, 29 Nov 2024 03:18:21 +0900 Subject: [PATCH 14/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20alarm=20?= =?UTF-8?q?service=EC=99=80=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/alarm/alarm.module.ts | 1 + packages/backend/src/stock/stock.module.ts | 4 ++++ packages/backend/src/stock/stockLiveData.subscriber.ts | 1 - 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/alarm/alarm.module.ts b/packages/backend/src/alarm/alarm.module.ts index c599a4ae..d7a276cb 100644 --- a/packages/backend/src/alarm/alarm.module.ts +++ b/packages/backend/src/alarm/alarm.module.ts @@ -10,5 +10,6 @@ import { PushService } from './push.service'; imports: [TypeOrmModule.forFeature([Alarm, PushSubscription])], controllers: [AlarmController], providers: [AlarmService, PushService], + exports: [AlarmService], }) export class AlarmModule {} diff --git a/packages/backend/src/stock/stock.module.ts b/packages/backend/src/stock/stock.module.ts index 3d79014f..c0777e63 100644 --- a/packages/backend/src/stock/stock.module.ts +++ b/packages/backend/src/stock/stock.module.ts @@ -24,6 +24,8 @@ import { import { StockDetailService } from './stockDetail.service'; import { StockLiveDataSubscriber } from './stockLiveData.subscriber'; import { StockRateIndexService } from './stockRateIndex.service'; +import { AlarmModule } from '@/alarm/alarm.module'; +import { Alarm } from '@/alarm/domain/alarm.entity'; import { OpenapiLiveData } from '@/scraper/openapi/api/openapiLiveData.api'; import { OpenapiTokenApi } from '@/scraper/openapi/api/openapiToken.api'; import { LiveData } from '@/scraper/openapi/liveData.service'; @@ -40,7 +42,9 @@ import { WebsocketClient } from '@/scraper/openapi/websocket/websocketClient.web StockYearly, StockLiveData, StockDetail, + Alarm, ]), + AlarmModule, ], controllers: [StockController], providers: [ diff --git a/packages/backend/src/stock/stockLiveData.subscriber.ts b/packages/backend/src/stock/stockLiveData.subscriber.ts index b03488f9..ef385fc7 100644 --- a/packages/backend/src/stock/stockLiveData.subscriber.ts +++ b/packages/backend/src/stock/stockLiveData.subscriber.ts @@ -23,7 +23,6 @@ export class StockLiveDataSubscriber return StockLiveData; } - // eslint-disable-next-line max-lines-per-function async afterUpdate(event: UpdateEvent) { try { const updatedStockLiveData = From 5110889b5a24cfe002d3a44b21cfb2107cddb525 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Fri, 29 Nov 2024 03:22:42 +0900 Subject: [PATCH 15/32] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20live=20d?= =?UTF-8?q?ata=20=EB=AA=A8=EB=93=88=EA=B0=84=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/scraper/openapi/openapi-scraper.module.ts | 5 +++++ packages/backend/src/scraper/scraper.module.ts | 1 + packages/backend/src/stock/stock.module.ts | 11 ++--------- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/scraper/openapi/openapi-scraper.module.ts b/packages/backend/src/scraper/openapi/openapi-scraper.module.ts index 607f6a85..d5dc0841 100644 --- a/packages/backend/src/scraper/openapi/openapi-scraper.module.ts +++ b/packages/backend/src/scraper/openapi/openapi-scraper.module.ts @@ -6,7 +6,9 @@ import { OpenapiLiveData } from './api/openapiLiveData.api'; import { OpenapiMinuteData } from './api/openapiMinuteData.api'; import { OpenapiPeriodData } from './api/openapiPeriodData.api'; import { OpenapiTokenApi } from './api/openapiToken.api'; +import { LiveData } from './liveData.service'; import { OpenapiScraperService } from './openapi-scraper.service'; +import { WebsocketClient } from './websocket/websocketClient.websocket'; import { OpenapiFluctuationData } from '@/scraper/openapi/api/openapiFluctuationData.api'; import { OpenapiRankViewApi } from '@/scraper/openapi/api/openapiRankView.api'; import { @@ -52,6 +54,9 @@ import { StockLiveData } from '@/stock/domain/stockLiveData.entity'; OpenapiRankViewApi, OpenapiQueue, OpenapiConsumer, + WebsocketClient, + LiveData, ], + exports: [LiveData], }) export class OpenapiScraperModule {} diff --git a/packages/backend/src/scraper/scraper.module.ts b/packages/backend/src/scraper/scraper.module.ts index 96f25299..9935e7dd 100644 --- a/packages/backend/src/scraper/scraper.module.ts +++ b/packages/backend/src/scraper/scraper.module.ts @@ -6,5 +6,6 @@ import { OpenapiScraperModule } from './openapi/openapi-scraper.module'; imports: [KoreaStockInfoModule, OpenapiScraperModule], controllers: [], providers: [], + exports: [OpenapiScraperModule], }) export class ScraperModule {} diff --git a/packages/backend/src/stock/stock.module.ts b/packages/backend/src/stock/stock.module.ts index c0777e63..2a374e66 100644 --- a/packages/backend/src/stock/stock.module.ts +++ b/packages/backend/src/stock/stock.module.ts @@ -26,11 +26,7 @@ import { StockLiveDataSubscriber } from './stockLiveData.subscriber'; import { StockRateIndexService } from './stockRateIndex.service'; import { AlarmModule } from '@/alarm/alarm.module'; import { Alarm } from '@/alarm/domain/alarm.entity'; -import { OpenapiLiveData } from '@/scraper/openapi/api/openapiLiveData.api'; -import { OpenapiTokenApi } from '@/scraper/openapi/api/openapiToken.api'; -import { LiveData } from '@/scraper/openapi/liveData.service'; -import { WebsocketClient } from '@/scraper/openapi/websocket/websocketClient.websocket'; - +import { ScraperModule } from '@/scraper/scraper.module'; @Module({ imports: [ TypeOrmModule.forFeature([ @@ -45,14 +41,11 @@ import { WebsocketClient } from '@/scraper/openapi/websocket/websocketClient.web Alarm, ]), AlarmModule, + ScraperModule, ], controllers: [StockController], providers: [ StockService, - WebsocketClient, - OpenapiTokenApi, - OpenapiLiveData, - LiveData, StockGateway, StockLiveDataSubscriber, StockDataService, From 43af7e6162cccb44878d5590c29bdc8516689f2d Mon Sep 17 00:00:00 2001 From: sunghwki Date: Fri, 29 Nov 2024 11:44:17 +0900 Subject: [PATCH 16/32] =?UTF-8?q?=F0=9F=92=84=20style:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A3=BC=EC=84=9D=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/scraper/openapi/api/openapiFluctuationData.api.ts | 1 - packages/backend/src/user/user.controller.ts | 2 +- test.js | 7 +++++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 test.js diff --git a/packages/backend/src/scraper/openapi/api/openapiFluctuationData.api.ts b/packages/backend/src/scraper/openapi/api/openapiFluctuationData.api.ts index d6586173..9fbaf952 100644 --- a/packages/backend/src/scraper/openapi/api/openapiFluctuationData.api.ts +++ b/packages/backend/src/scraper/openapi/api/openapiFluctuationData.api.ts @@ -26,7 +26,6 @@ export class OpenapiFluctuationData { setTimeout(() => this.getFluctuationRankStocks(), 1000); } - @Cron('* 9-15 * * 1-5') @Cron('*/1 9-15 * * 1-5') async getFluctuationRankStocks() { await this.getFluctuationRankFromApi(true); diff --git a/packages/backend/src/user/user.controller.ts b/packages/backend/src/user/user.controller.ts index 6abc5b56..a3a22bcb 100644 --- a/packages/backend/src/user/user.controller.ts +++ b/packages/backend/src/user/user.controller.ts @@ -18,9 +18,9 @@ import { ApiParam, ApiResponse, } from '@nestjs/swagger'; +import { Request } from 'express'; import { UpdateUserThemeResponse } from './dto/userTheme.response'; import { UserService } from './user.service'; -import { Request } from 'express'; import { User } from '@/user/domain/user.entity'; import { ChangeNicknameRequest } from '@/user/dto/user.request'; diff --git a/test.js b/test.js new file mode 100644 index 00000000..64891d9e --- /dev/null +++ b/test.js @@ -0,0 +1,7 @@ +const webPush = require('web-push'); + +// VAPID 키 쌍 생성 +const vapidKeys = webPush.generateVAPIDKeys(); + +console.log('Public Key:', vapidKeys.publicKey); +console.log('Private Key:', vapidKeys.privateKey); From 4f54cec10614887f3a7b1020fd1d18ecf096e48d Mon Sep 17 00:00:00 2001 From: sunghwki Date: Fri, 29 Nov 2024 11:45:28 +0900 Subject: [PATCH 17/32] =?UTF-8?q?=F0=9F=92=84=20style:=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test.js | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 test.js diff --git a/test.js b/test.js deleted file mode 100644 index 64891d9e..00000000 --- a/test.js +++ /dev/null @@ -1,7 +0,0 @@ -const webPush = require('web-push'); - -// VAPID 키 쌍 생성 -const vapidKeys = webPush.generateVAPIDKeys(); - -console.log('Public Key:', vapidKeys.publicKey); -console.log('Private Key:', vapidKeys.privateKey); From 39267aad1c09a4040c6ca60519973c46b1adb81c Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sat, 30 Nov 2024 11:49:55 +0900 Subject: [PATCH 18/32] =?UTF-8?q?=E2=9C=A8=20feat:=20push=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=99=84=EC=84=B1=20=EB=B0=8F=20dto=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/alarm/dto/subscribe.request.ts | 26 ++++++++++++++ .../src/alarm/dto/subscribe.response.ts | 9 +++++ packages/backend/src/alarm/push.controller.ts | 23 ++++++------ packages/backend/src/alarm/push.service.ts | 36 ++++++++++--------- 4 files changed, 67 insertions(+), 27 deletions(-) create mode 100644 packages/backend/src/alarm/dto/subscribe.request.ts create mode 100644 packages/backend/src/alarm/dto/subscribe.response.ts diff --git a/packages/backend/src/alarm/dto/subscribe.request.ts b/packages/backend/src/alarm/dto/subscribe.request.ts new file mode 100644 index 00000000..1ed453e0 --- /dev/null +++ b/packages/backend/src/alarm/dto/subscribe.request.ts @@ -0,0 +1,26 @@ +import { ApiProperty } from '@nestjs/swagger'; +//import { User } from '@/user/domain/user.entity'; + +export class SubscriptionData { + //@ApiProperty({ type: () => User, description: '유저 아이디' }) + //user: User; + + @ApiProperty({ + type: 'string', + description: '엔드 포인트 설정', + }) + endpoint: string; + + @ApiProperty({ + type: 'object', + description: 'VAPID 키', + properties: { + p256dh: { type: 'string' }, + auth: { type: 'string' }, + }, + }) + keys: { + p256dh: string; + auth: string; + }; +} diff --git a/packages/backend/src/alarm/dto/subscribe.response.ts b/packages/backend/src/alarm/dto/subscribe.response.ts new file mode 100644 index 00000000..8fba349a --- /dev/null +++ b/packages/backend/src/alarm/dto/subscribe.response.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SubscribeResponse { + @ApiProperty({ example: 1, description: 'User ID' }) + userId: number; + + @ApiProperty({ example: 'success', description: 'Response message' }) + message: string; +} diff --git a/packages/backend/src/alarm/push.controller.ts b/packages/backend/src/alarm/push.controller.ts index 2f7201d6..b7c2e263 100644 --- a/packages/backend/src/alarm/push.controller.ts +++ b/packages/backend/src/alarm/push.controller.ts @@ -1,5 +1,7 @@ import { Body, Controller, Post, UseGuards } from '@nestjs/common'; -import { SubscriptionData } from './dto/subscription'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { SubscribeResponse } from './dto/subscribe.response'; +import { SubscriptionData } from './dto/subscription.request'; import { PushService } from './push.service'; import SessionGuard from '@/auth/session/session.guard'; import { GetUser } from '@/common/decorator/user.decorator'; @@ -10,6 +12,15 @@ export class PushController { constructor(private readonly pushService: PushService) {} @Post('subscribe') + @ApiOperation({ + summary: '알림 서비스 등록', + description: '유저가 생성될 때 알림을 받을 수 있게 초기설정한다.', + }) + @ApiResponse({ + status: 201, + description: '아이디와 동일한 알림 업데이트', + type: SubscribeResponse, + }) @UseGuards(SessionGuard) async subscribe( @Body() subscriptionData: SubscriptionData, @@ -17,14 +28,6 @@ export class PushController { ) { const userId = user.id; - const newSubscription = await this.pushService.createSubscription( - userId, - subscriptionData, - ); - - return { - message: 'Subscription saved.', - subscriptionId: newSubscription.id, - }; + return await this.pushService.createSubscription(userId, subscriptionData); } } diff --git a/packages/backend/src/alarm/push.service.ts b/packages/backend/src/alarm/push.service.ts index e6c51e7c..2202507e 100644 --- a/packages/backend/src/alarm/push.service.ts +++ b/packages/backend/src/alarm/push.service.ts @@ -1,11 +1,13 @@ import { Inject, Injectable } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; import { DataSource } from 'typeorm'; import * as webPush from 'web-push'; import { Logger } from 'winston'; import { PushSubscription } from './domain/subscription.entity'; -import { SubscriptionData } from './dto/subscription'; -import { User } from '@/user/domain/user.entity'; +import { SubscribeResponse } from './dto/subscribe.response'; +import { SubscriptionData } from './dto/subscription.request'; +@ApiTags('Push Notifications') @Injectable() export class PushService { constructor( @@ -13,7 +15,7 @@ export class PushService { private readonly dataSource: DataSource, ) { webPush.setVapidDetails( - 'mailto:admin@juchum.info', + 'mailto:noreply@juchum.info', process.env.VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY!, ); @@ -38,7 +40,7 @@ export class PushService { ); } catch (error) { this.logger.warn( - `Failed to send push notification to ${subscription.endpoint}`, + `Fail to send message user id [${subscription.user.id}] : ${pushPayload}`, error, ); } @@ -47,19 +49,19 @@ export class PushService { async createSubscription( userId: number, subscriptionData: SubscriptionData, - ): Promise { - return await this.dataSource.transaction(async (manager) => { - const user = new User(); - user.id = userId; - - const newSubscription = manager.create(PushSubscription, { - user: user, - endpoint: subscriptionData.endpoint, - p256dh: subscriptionData.keys.p256dh, - auth: subscriptionData.keys.auth, - }); - - return await manager.save(newSubscription); + ): Promise { + const newSubscription = this.dataSource.manager.create(PushSubscription, { + user: { id: userId }, + endpoint: subscriptionData.endpoint, + p256dh: subscriptionData.keys.p256dh, + auth: subscriptionData.keys.auth, }); + + await this.dataSource.manager.save(newSubscription); + const result: SubscribeResponse = { + userId, + message: 'Push subscription success', + }; + return result; } } From cb4a2ce01d9a3498465a1a0298b6d208b3a6ddb8 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sat, 30 Nov 2024 12:06:25 +0900 Subject: [PATCH 19/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20alarm=20request=20?= =?UTF-8?q?swagger=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/dto/alarm.request.ts | 24 +++++++++++++++++++ .../backend/src/alarm/dto/alarm.response.ts | 0 2 files changed, 24 insertions(+) create mode 100644 packages/backend/src/alarm/dto/alarm.response.ts diff --git a/packages/backend/src/alarm/dto/alarm.request.ts b/packages/backend/src/alarm/dto/alarm.request.ts index 72dbd3a5..25244d66 100644 --- a/packages/backend/src/alarm/dto/alarm.request.ts +++ b/packages/backend/src/alarm/dto/alarm.request.ts @@ -1,6 +1,30 @@ +import { ApiProperty } from '@nestjs/swagger'; + export class AlarmRequest { + @ApiProperty({ + description: '주식 아이디', + example: '005930', + }) stock_id: string; + + @ApiProperty({ + description: '목표 가격', + example: 150.0, + required: false, + }) targetPrice?: number; + + @ApiProperty({ + description: '목표 거래량', + example: 1000, + required: false, + }) targetVolum?: number; + + @ApiProperty({ + description: '알림 날짜', + example: '2023-10-01T00:00:00Z', + required: false, + }) alarmDate?: Date; } diff --git a/packages/backend/src/alarm/dto/alarm.response.ts b/packages/backend/src/alarm/dto/alarm.response.ts new file mode 100644 index 00000000..e69de29b From af61d208b92fd600e0488273dacaf362581be87c Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sat, 30 Nov 2024 12:06:54 +0900 Subject: [PATCH 20/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20swagger=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/alarm/dto/alarm.request.ts | 4 ++-- packages/backend/src/alarm/dto/alarm.response.ts | 0 packages/backend/src/alarm/dto/subscription.ts | 10 ---------- 3 files changed, 2 insertions(+), 12 deletions(-) delete mode 100644 packages/backend/src/alarm/dto/alarm.response.ts delete mode 100644 packages/backend/src/alarm/dto/subscription.ts diff --git a/packages/backend/src/alarm/dto/alarm.request.ts b/packages/backend/src/alarm/dto/alarm.request.ts index 25244d66..0dc67277 100644 --- a/packages/backend/src/alarm/dto/alarm.request.ts +++ b/packages/backend/src/alarm/dto/alarm.request.ts @@ -22,8 +22,8 @@ export class AlarmRequest { targetVolum?: number; @ApiProperty({ - description: '알림 날짜', - example: '2023-10-01T00:00:00Z', + description: '알림 종료 날짜', + example: '2024-12-01T00:00:00Z', required: false, }) alarmDate?: Date; diff --git a/packages/backend/src/alarm/dto/alarm.response.ts b/packages/backend/src/alarm/dto/alarm.response.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/backend/src/alarm/dto/subscription.ts b/packages/backend/src/alarm/dto/subscription.ts deleted file mode 100644 index f94ea13f..00000000 --- a/packages/backend/src/alarm/dto/subscription.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { User } from '@/user/domain/user.entity'; - -export class SubscriptionData { - user: User; - endpoint: string; - keys: { - p256dh: string; - auth: string; - }; -} From bebbb0a514ad6c47ba6cc084774f24ef45290ab3 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sat, 30 Nov 2024 12:54:43 +0900 Subject: [PATCH 21/32] =?UTF-8?q?=F0=9F=90=9B=20fix:=20=EC=95=8C=EB=9E=8C?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EC=88=98=EC=A0=95,=20swagger=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.controller.ts | 73 +++++++++++++++++-- packages/backend/src/alarm/alarm.module.ts | 3 +- packages/backend/src/alarm/alarm.service.ts | 14 +++- 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts index 5474a244..efe3751c 100644 --- a/packages/backend/src/alarm/alarm.controller.ts +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -6,45 +6,102 @@ import { Body, Put, Delete, + UseGuards, } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; import { AlarmService } from './alarm.service'; import { Alarm } from './domain/alarm.entity'; import { AlarmRequest } from './dto/alarm.request'; +import SessionGuard from '@/auth/session/session.guard'; @Controller('alarm') export class AlarmController { constructor(private readonly alarmService: AlarmService) {} @Post() - async create(@Body() alarmRequest: AlarmRequest) { + @ApiOperation({ + summary: '알림 생성', + description: '각 정보에 맞는 알림을 생성한다.', + }) + @ApiOkResponse({ + description: '알림 생성 완료', + type: Alarm, + }) + @UseGuards(SessionGuard) + async create(@Body() alarmRequest: AlarmRequest): Promise { return await this.alarmService.create(alarmRequest); } @Get(':id') - async findOne(@Param('id') id: number): Promise { - return this.alarmService.findOne(id); + @ApiOperation({ + summary: '등록된 알림 확인', + description: '등록된 알림을 알림 아이디를 기준으로 찾을 수 있다.', + }) + @ApiOkResponse({ + description: '아이디와 동일한 알림 찾음', + type: Alarm, + }) + @UseGuards(SessionGuard) + async findOne(@Param('alarmId') alarmId: number): Promise { + return this.alarmService.findOne(alarmId); } @Put(':id') + @ApiOperation({ + summary: '등록된 알림 업데이트', + description: '알림 아이디 기준으로 업데이트를 할 수 있다.', + }) + @ApiOkResponse({ + description: '아이디와 동일한 알림 업데이트', + type: Alarm, + }) + @UseGuards(SessionGuard) async update( - @Param('id') id: number, + @Param('alarmId') alarmId: number, @Body() updateData: AlarmRequest, ): Promise { - return this.alarmService.update(id, updateData); + return this.alarmService.update(alarmId, updateData); } @Delete(':id') - async delete(@Param('id') id: number) { - await this.alarmService.delete(id); - return { message: `Alarm with ID ${id} deleted successfully` }; + @ApiOperation({ + summary: '등록된 알림 업데이트', + description: '알림 아이디 기준으로 업데이트를 할 수 있다.', + }) + @ApiOkResponse({ + description: '아이디와 동일한 알림 업데이트', + type: Alarm, + }) + @UseGuards(SessionGuard) + async delete(@Param('alarmId') alarmId: number) { + await this.alarmService.delete(alarmId); + return { message: '알림이 정상적으로 삭제되었습니다.' }; } @Get('user/:userId') + @ApiOperation({ + summary: '사용자별 알림 조회', + description: '사용자 아이디를 기준으로 알림을 조회한다.', + }) + @ApiOkResponse({ + description: '사용자에게 등록되어 있는 알림 조회', + type: [Alarm], + }) + @UseGuards(SessionGuard) async getByUserId(@Param('userId') userId: number) { return await this.alarmService.findByUserId(userId); } @Get('stock/:stockId') + @ApiOperation({ + summary: '주식별 알림 조회', + description: '주식 아이디를 기준으로 알림을 조회한다.', + }) + @ApiOkResponse({ + description: '주식 아이디에 등록되어 있는 알림 조회', + type: [Alarm], + }) + @UseGuards(SessionGuard) async getByStockId(@Param('stockId') stockId: string) { return await this.alarmService.findByStockId(stockId); } diff --git a/packages/backend/src/alarm/alarm.module.ts b/packages/backend/src/alarm/alarm.module.ts index d7a276cb..5cb4d405 100644 --- a/packages/backend/src/alarm/alarm.module.ts +++ b/packages/backend/src/alarm/alarm.module.ts @@ -4,11 +4,12 @@ import { AlarmController } from './alarm.controller'; import { AlarmService } from './alarm.service'; 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], + controllers: [AlarmController, PushController], providers: [AlarmService, PushService], exports: [AlarmService], }) diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index a063dc33..1cf6baca 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -38,16 +38,18 @@ export class AlarmService { } async findOne(id: number) { - return await this.alarmRepository.findOne({ + const result = await this.alarmRepository.findOne({ where: { id }, relations: ['user', 'stock'], }); + if (result) return result; + else throw new NotFoundException('등록된 알림을 찾을 수 없습니다.'); } async update(id: number, updateData: AlarmRequest) { const alarm = await this.alarmRepository.findOne({ where: { id } }); if (!alarm) { - throw new NotFoundException(`Alarm with ID ${id} not found`); + throw new NotFoundException('등록된 알림을 찾을 수 없습니다.'); } await this.alarmRepository.update(id, updateData); @@ -55,13 +57,17 @@ export class AlarmService { where: { id }, relations: ['user', 'stock'], }); - return updatedAlarm!; + if (updatedAlarm) return updatedAlarm; + else + throw new NotFoundException( + `${id} : 업데이트할 알림을 찾을 수 없습니다.`, + ); } async delete(id: number) { const alarm = await this.alarmRepository.findOne({ where: { id } }); if (!alarm) { - throw new NotFoundException(`Alarm with ID ${id} not found`); + throw new NotFoundException(`${id} : 삭제할 알림을 찾을 수 없습니다.`); } await this.alarmRepository.delete(id); From 47ae8b813dce805cf7cf26475e1bfa00934f7af1 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sat, 30 Nov 2024 12:55:02 +0900 Subject: [PATCH 22/32] =?UTF-8?q?=F0=9F=90=9B=20fix:=20push=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20swagger=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/alarm/push.controller.ts | 6 +++--- packages/backend/src/alarm/push.service.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/alarm/push.controller.ts b/packages/backend/src/alarm/push.controller.ts index b7c2e263..7e6c0b38 100644 --- a/packages/backend/src/alarm/push.controller.ts +++ b/packages/backend/src/alarm/push.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Post, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { SubscriptionData } from './dto/subscribe.request'; import { SubscribeResponse } from './dto/subscribe.response'; -import { SubscriptionData } from './dto/subscription.request'; import { PushService } from './push.service'; import SessionGuard from '@/auth/session/session.guard'; import { GetUser } from '@/common/decorator/user.decorator'; @@ -13,12 +13,12 @@ export class PushController { @Post('subscribe') @ApiOperation({ - summary: '알림 서비스 등록', + summary: '알림 서비스 초기 설정', description: '유저가 생성될 때 알림을 받을 수 있게 초기설정한다.', }) @ApiResponse({ status: 201, - description: '아이디와 동일한 알림 업데이트', + description: '알림 초기설정', type: SubscribeResponse, }) @UseGuards(SessionGuard) diff --git a/packages/backend/src/alarm/push.service.ts b/packages/backend/src/alarm/push.service.ts index 2202507e..38d7bc31 100644 --- a/packages/backend/src/alarm/push.service.ts +++ b/packages/backend/src/alarm/push.service.ts @@ -4,8 +4,8 @@ import { DataSource } from 'typeorm'; import * as webPush from 'web-push'; import { Logger } from 'winston'; import { PushSubscription } from './domain/subscription.entity'; +import { SubscriptionData } from './dto/subscribe.request'; import { SubscribeResponse } from './dto/subscribe.response'; -import { SubscriptionData } from './dto/subscription.request'; @ApiTags('Push Notifications') @Injectable() From 480c6e1a7e856af200b218a143cfa3548acba86a Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 17:29:07 +0900 Subject: [PATCH 23/32] =?UTF-8?q?=F0=9F=90=9B=20fix:=20localhost=20cors?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0.=20=EC=9D=B4=EA=B1=B4..?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/main.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/main.ts b/packages/backend/src/main.ts index d9c69898..33b9f13a 100644 --- a/packages/backend/src/main.ts +++ b/packages/backend/src/main.ts @@ -15,7 +15,12 @@ const setCors = (app: INestApplication) => { 'http://localhost:5173', ], methods: '*', - allowedHeaders: '*', + allowedHeaders: [ + 'Content-Type', + 'Authorization', + 'X-Requested-With', + 'Accept', + ], credentials: true, }); }; From d0affc656fd912ec39ceb91c1e8434e64ef1417f Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 17:46:08 +0900 Subject: [PATCH 24/32] =?UTF-8?q?=E2=9C=A8=20feat:=20subscriber=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.subscriber.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 packages/backend/src/alarm/alarm.subscriber.ts diff --git a/packages/backend/src/alarm/alarm.subscriber.ts b/packages/backend/src/alarm/alarm.subscriber.ts new file mode 100644 index 00000000..2bbe5a07 --- /dev/null +++ b/packages/backend/src/alarm/alarm.subscriber.ts @@ -0,0 +1,67 @@ +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 { Stock } from '@/stock/domain/stock.entity'; +import { StockMinutely } from '@/stock/domain/stockData.entity'; + +@Injectable() +@EventSubscriber() +export class AlarmSubscriber + implements EntitySubscriberInterface +{ + constructor( + private readonly datasource: DataSource, + private readonly alarmService: AlarmService, + @Inject('winston') private readonly logger: Logger, + ) { + console.log('eh'); + this.datasource.subscribers.push(this); + setInterval(() => { + this.test(); + }, 10000); + } + + async test() { + const stockMinutelyData = { + close: 54200.0, + low: 53800.0, + high: 55300.0, + open: 55100.0, + volume: 24513531, + startTime: new Date(), + createdAt: new Date('2024-12-01 16:48:37'), + stock: { id: '005930' } as Stock, // Example stock ID, ensure it's valid in your database + }; + console.log('test'); + await this.datasource.manager.save(StockMinutely, stockMinutelyData); + } + + listenTo() { + return StockMinutely; + } + + async afterInsert(event: InsertEvent) { + try { + const stockLiveData = event.entity; + const alarms = await this.datasource.manager.find(Alarm, { + where: { stock: { id: stockLiveData.stock.id } }, + relations: ['user', 'stock'], + }); + console.log('after insert'); + + for (const alarm of alarms) { + console.log('in alarm'); + await this.alarmService.sendPushNotification(alarm); + } + } catch (error) { + this.logger.warn(`Failed to handle alarm afterInsert event : ${error}`); + } + } +} From 00a4b4113edbed55d7888bfab499f512f093c59e Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 17:46:25 +0900 Subject: [PATCH 25/32] =?UTF-8?q?=F0=9F=90=9B=20fix:=20userId=EA=B0=80=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.controller.ts | 25 ++++++++++++++++--- packages/backend/src/alarm/alarm.module.ts | 3 ++- packages/backend/src/alarm/alarm.service.ts | 23 ++++++++++++----- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts index efe3751c..dee3d31b 100644 --- a/packages/backend/src/alarm/alarm.controller.ts +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -8,11 +8,13 @@ import { Delete, UseGuards, } from '@nestjs/common'; -import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; +import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; import { AlarmService } from './alarm.service'; import { Alarm } from './domain/alarm.entity'; import { AlarmRequest } from './dto/alarm.request'; 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 { @@ -28,8 +30,13 @@ export class AlarmController { type: Alarm, }) @UseGuards(SessionGuard) - async create(@Body() alarmRequest: AlarmRequest): Promise { - return await this.alarmService.create(alarmRequest); + async create( + @Body() alarmRequest: AlarmRequest, + @GetUser() user: User, + ): Promise { + const userId = user.id; + + return await this.alarmService.create(alarmRequest, userId); } @Get(':id') @@ -41,6 +48,12 @@ export class AlarmController { description: '아이디와 동일한 알림 찾음', type: Alarm, }) + @ApiParam({ + name: 'id', + type: Number, + description: '알림 아이디', + example: 1, + }) @UseGuards(SessionGuard) async findOne(@Param('alarmId') alarmId: number): Promise { return this.alarmService.findOne(alarmId); @@ -56,6 +69,12 @@ export class AlarmController { type: Alarm, }) @UseGuards(SessionGuard) + @ApiParam({ + name: 'id', + type: Number, + description: '알림 아이디', + example: 1, + }) async update( @Param('alarmId') alarmId: number, @Body() updateData: AlarmRequest, diff --git a/packages/backend/src/alarm/alarm.module.ts b/packages/backend/src/alarm/alarm.module.ts index 5cb4d405..c283febc 100644 --- a/packages/backend/src/alarm/alarm.module.ts +++ b/packages/backend/src/alarm/alarm.module.ts @@ -2,6 +2,7 @@ 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'; @@ -10,7 +11,7 @@ import { PushService } from './push.service'; @Module({ imports: [TypeOrmModule.forFeature([Alarm, PushSubscription])], controllers: [AlarmController, PushController], - providers: [AlarmService, PushService], + providers: [AlarmService, PushService, AlarmSubscriber], exports: [AlarmService], }) export class AlarmModule {} diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index 1cf6baca..bb9d16f7 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -1,9 +1,14 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import { Alarm } from './domain/alarm.entity'; import { AlarmRequest } from './dto/alarm.request'; import { PushService } from './push.service'; +import { User } from '@/user/domain/user.entity'; @Injectable() export class AlarmService { @@ -14,12 +19,18 @@ export class AlarmService { private readonly pushService: PushService, ) {} - async create(alarmData: Partial): Promise { + async create(alarmData: Partial, userId: number): Promise { return await this.dataSource.transaction(async (manager) => { const repository = manager.getRepository(Alarm); - const newAlarm = repository.create(alarmData); - const savedAlarm = await repository.save(newAlarm); - return savedAlarm; + const user = await manager.findOne(User, { where: { id: userId } }); + if (!user) { + throw new ForbiddenException('User not found'); + } + const newAlarm = repository.create({ + ...alarmData, + user, + }); + return await repository.save(newAlarm); }); } @@ -73,7 +84,7 @@ export class AlarmService { await this.alarmRepository.delete(id); } - private async sendPushNotification(alarm: Alarm): Promise { + async sendPushNotification(alarm: Alarm): Promise { const { user, stock, targetPrice, targetVolume } = alarm; const payload = { From a6fc5b888e9196d0fa13dab48ce2d260bd475acf Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 17:54:45 +0900 Subject: [PATCH 26/32] =?UTF-8?q?=F0=9F=90=9B=20fix:=20userId=EB=A5=BC=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=EC=A0=81=EC=9C=BC=EB=A1=9C=EB=B0=9B=EC=A7=80?= =?UTF-8?q?=20=EC=95=8A=EC=9D=8C.=20swagger=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.controller.ts | 19 ++++++++++++------- packages/backend/src/alarm/alarm.service.ts | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts index dee3d31b..a5e2cb9e 100644 --- a/packages/backend/src/alarm/alarm.controller.ts +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -97,17 +97,19 @@ export class AlarmController { return { message: '알림이 정상적으로 삭제되었습니다.' }; } - @Get('user/:userId') + @Get('user') @ApiOperation({ summary: '사용자별 알림 조회', - description: '사용자 아이디를 기준으로 알림을 조회한다.', + description: '사용자 아이디를 기준으로 모든 알림을 조회한다.', }) @ApiOkResponse({ - description: '사용자에게 등록되어 있는 알림 조회', + description: '사용자에게 등록되어 있는 모든 알림 조회', type: [Alarm], }) @UseGuards(SessionGuard) - async getByUserId(@Param('userId') userId: number) { + async getByUserId(@GetUser() user: User) { + const userId = user.id; + return await this.alarmService.findByUserId(userId); } @@ -117,11 +119,14 @@ export class AlarmController { description: '주식 아이디를 기준으로 알림을 조회한다.', }) @ApiOkResponse({ - description: '주식 아이디에 등록되어 있는 알림 조회', + description: + '주식 아이디에 등록되어 있는 알림 중 유저에 해당하는 알림 조회', type: [Alarm], }) @UseGuards(SessionGuard) - async getByStockId(@Param('stockId') stockId: string) { - return await this.alarmService.findByStockId(stockId); + async getByStockId(@Param('stockId') stockId: string, @GetUser() user: User) { + const userId = user.id; + + return await this.alarmService.findByStockId(stockId, userId); } } diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index bb9d16f7..988a4112 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -41,9 +41,9 @@ export class AlarmService { }); } - async findByStockId(stockId: string): Promise { + async findByStockId(stockId: string, userId: number): Promise { return await this.alarmRepository.find({ - where: { stock: { id: stockId } }, + where: { stock: { id: stockId }, user: { id: userId } }, relations: ['user', 'stock'], }); } From 7e3a2b3336ab99c60c604d952dd6b460b20f69bc Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 17:55:13 +0900 Subject: [PATCH 27/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20swagger=20param=20?= =?UTF-8?q?=EC=95=A0=EB=A7=A4=ED=95=9C=20=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/alarm/alarm.controller.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts index a5e2cb9e..b07902b0 100644 --- a/packages/backend/src/alarm/alarm.controller.ts +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -45,7 +45,7 @@ export class AlarmController { description: '등록된 알림을 알림 아이디를 기준으로 찾을 수 있다.', }) @ApiOkResponse({ - description: '아이디와 동일한 알림 찾음', + description: '알림 아이디와 동일한 알림 찾음', type: Alarm, }) @ApiParam({ @@ -68,13 +68,13 @@ export class AlarmController { description: '아이디와 동일한 알림 업데이트', type: Alarm, }) - @UseGuards(SessionGuard) @ApiParam({ name: 'id', type: Number, description: '알림 아이디', example: 1, }) + @UseGuards(SessionGuard) async update( @Param('alarmId') alarmId: number, @Body() updateData: AlarmRequest, @@ -91,6 +91,12 @@ export class AlarmController { description: '아이디와 동일한 알림 업데이트', type: Alarm, }) + @ApiParam({ + name: 'id', + type: Number, + description: '알림 아이디', + example: 1, + }) @UseGuards(SessionGuard) async delete(@Param('alarmId') alarmId: number) { await this.alarmService.delete(alarmId); @@ -123,6 +129,12 @@ export class AlarmController { '주식 아이디에 등록되어 있는 알림 중 유저에 해당하는 알림 조회', type: [Alarm], }) + @ApiParam({ + name: 'id', + type: String, + description: '주식 아이디', + example: '005930', + }) @UseGuards(SessionGuard) async getByStockId(@Param('stockId') stockId: string, @GetUser() user: User) { const userId = user.id; From 1beebb54aca4ff6f4f61446aec36ff7c57539b53 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 18:32:05 +0900 Subject: [PATCH 28/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20live=20data=20logg?= =?UTF-8?q?er=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/stock/stockLiveData.subscriber.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/stock/stockLiveData.subscriber.ts b/packages/backend/src/stock/stockLiveData.subscriber.ts index 3689dc8e..f22315be 100644 --- a/packages/backend/src/stock/stockLiveData.subscriber.ts +++ b/packages/backend/src/stock/stockLiveData.subscriber.ts @@ -9,7 +9,6 @@ import { import { Logger } from 'winston'; import { StockLiveData } from './domain/stockLiveData.entity'; import { StockGateway } from './stock.gateway'; -import { AlarmService } from '@/alarm/alarm.service'; @Injectable() @EventSubscriber() @@ -19,7 +18,6 @@ export class StockLiveDataSubscriber constructor( private readonly datasource: DataSource, private readonly stockGateway: StockGateway, - private readonly alarmService: AlarmService, @Inject('winston') private readonly logger: Logger, ) { this.datasource.subscribers.push(this); @@ -41,7 +39,9 @@ export class StockLiveDataSubscriber this.stockGateway.onUpdateStock(stockId, price, change, volume); } catch (error) { - this.logger.warn(`Failed to handle afterInsert event : ${error}`); + this.logger.warn( + `Failed to handle stock live data afterInsert event : ${error}`, + ); } } @@ -65,7 +65,9 @@ export class StockLiveDataSubscriber ); } } catch (error) { - this.logger.warn(`Failed to handle afterUpdate event : ${error}`); + this.logger.warn( + `Failed to handle stock live data afterUpdate event : ${error}`, + ); } } From 04b411c252d9f1e34317ac33474e561fc4f734fe Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 18:34:14 +0900 Subject: [PATCH 29/32] =?UTF-8?q?=F0=9F=90=9B=20fix:=20alarm=20subscribe?= =?UTF-8?q?=20=EC=B0=BE=EC=A7=80=20=EB=AA=BB=ED=95=B4=20=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EC=A7=80=20=EB=AA=BB=ED=95=98=EB=8A=94=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/alarm/alarm.service.ts | 15 +++++++++++-- .../backend/src/alarm/alarm.subscriber.ts | 22 ------------------- .../backend/src/alarm/dto/alarm.request.ts | 2 +- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index 988a4112..8d9b4438 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -6,6 +6,7 @@ import { 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 { PushService } from './push.service'; import { User } from '@/user/domain/user.entity'; @@ -19,16 +20,18 @@ export class AlarmService { private readonly pushService: PushService, ) {} - async create(alarmData: Partial, userId: number): Promise { + async create(alarmData: AlarmRequest, userId: number): Promise { 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('User not found'); } + const newAlarm = repository.create({ ...alarmData, user, + stock: { id: alarmData.stockId }, }); return await repository.save(newAlarm); }); @@ -92,9 +95,17 @@ export class AlarmService { body: `${stock.name}: ${ targetPrice ? `가격이 ${targetPrice}에 도달했습니다.` : '' } ${targetVolume ? `거래량이 ${targetVolume}에 도달했습니다.` : ''}`, + stockId: stock.id, }; - for (const subscription of user.subscriptions) { + const subscriptions = await this.dataSource.manager.findBy( + PushSubscription, + { + user: { id: user.id }, + }, + ); + + for (const subscription of subscriptions) { await this.pushService.sendPushNotification(subscription, payload); } } diff --git a/packages/backend/src/alarm/alarm.subscriber.ts b/packages/backend/src/alarm/alarm.subscriber.ts index 2bbe5a07..31cd9e7e 100644 --- a/packages/backend/src/alarm/alarm.subscriber.ts +++ b/packages/backend/src/alarm/alarm.subscriber.ts @@ -8,7 +8,6 @@ import { import { Logger } from 'winston'; import { AlarmService } from './alarm.service'; import { Alarm } from './domain/alarm.entity'; -import { Stock } from '@/stock/domain/stock.entity'; import { StockMinutely } from '@/stock/domain/stockData.entity'; @Injectable() @@ -21,26 +20,7 @@ export class AlarmSubscriber private readonly alarmService: AlarmService, @Inject('winston') private readonly logger: Logger, ) { - console.log('eh'); this.datasource.subscribers.push(this); - setInterval(() => { - this.test(); - }, 10000); - } - - async test() { - const stockMinutelyData = { - close: 54200.0, - low: 53800.0, - high: 55300.0, - open: 55100.0, - volume: 24513531, - startTime: new Date(), - createdAt: new Date('2024-12-01 16:48:37'), - stock: { id: '005930' } as Stock, // Example stock ID, ensure it's valid in your database - }; - console.log('test'); - await this.datasource.manager.save(StockMinutely, stockMinutelyData); } listenTo() { @@ -54,10 +34,8 @@ export class AlarmSubscriber where: { stock: { id: stockLiveData.stock.id } }, relations: ['user', 'stock'], }); - console.log('after insert'); for (const alarm of alarms) { - console.log('in alarm'); await this.alarmService.sendPushNotification(alarm); } } catch (error) { diff --git a/packages/backend/src/alarm/dto/alarm.request.ts b/packages/backend/src/alarm/dto/alarm.request.ts index 0dc67277..9d8e5ad0 100644 --- a/packages/backend/src/alarm/dto/alarm.request.ts +++ b/packages/backend/src/alarm/dto/alarm.request.ts @@ -5,7 +5,7 @@ export class AlarmRequest { description: '주식 아이디', example: '005930', }) - stock_id: string; + stockId: string; @ApiProperty({ description: '목표 가격', From 7a802f780df592bae246c3ae9f1df36b4b6812e2 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 19:15:17 +0900 Subject: [PATCH 30/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20alarm=20response?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80,=20subscribe=20response=20=EC=A4=91=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=20=EC=97=86=EB=8A=94=20=EA=B1=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.controller.ts | 7 +-- packages/backend/src/alarm/alarm.service.ts | 17 +++--- .../backend/src/alarm/dto/alarm.response.ts | 53 +++++++++++++++++++ .../src/alarm/dto/subscribe.response.ts | 5 +- packages/backend/src/alarm/push.controller.ts | 2 +- packages/backend/src/alarm/push.service.ts | 1 - 6 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 packages/backend/src/alarm/dto/alarm.response.ts diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts index b07902b0..0bbd64b4 100644 --- a/packages/backend/src/alarm/alarm.controller.ts +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -12,6 +12,7 @@ import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; import { AlarmService } from './alarm.service'; import { Alarm } from './domain/alarm.entity'; import { AlarmRequest } from './dto/alarm.request'; +import { AlarmResponse } 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'; @@ -33,7 +34,7 @@ export class AlarmController { async create( @Body() alarmRequest: AlarmRequest, @GetUser() user: User, - ): Promise { + ): Promise { const userId = user.id; return await this.alarmService.create(alarmRequest, userId); @@ -55,7 +56,7 @@ export class AlarmController { example: 1, }) @UseGuards(SessionGuard) - async findOne(@Param('alarmId') alarmId: number): Promise { + async findOne(@Param('alarmId') alarmId: number): Promise { return this.alarmService.findOne(alarmId); } @@ -78,7 +79,7 @@ export class AlarmController { async update( @Param('alarmId') alarmId: number, @Body() updateData: AlarmRequest, - ): Promise { + ): Promise { return this.alarmService.update(alarmId, updateData); } diff --git a/packages/backend/src/alarm/alarm.service.ts b/packages/backend/src/alarm/alarm.service.ts index 8d9b4438..a2b214ce 100644 --- a/packages/backend/src/alarm/alarm.service.ts +++ b/packages/backend/src/alarm/alarm.service.ts @@ -8,6 +8,7 @@ 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'; @@ -20,12 +21,12 @@ export class AlarmService { private readonly pushService: PushService, ) {} - async create(alarmData: AlarmRequest, userId: number): Promise { + 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('User not found'); + throw new ForbiddenException('유저를 찾을 수 없습니다.'); } const newAlarm = repository.create({ @@ -33,15 +34,17 @@ export class AlarmService { user, stock: { id: alarmData.stockId }, }); - return await repository.save(newAlarm); + const result = await repository.save(newAlarm); + return new AlarmResponse(result); }); } - async findByUserId(userId: number): Promise { - return await this.alarmRepository.find({ + 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 { @@ -56,7 +59,7 @@ export class AlarmService { where: { id }, relations: ['user', 'stock'], }); - if (result) return result; + if (result) return new AlarmResponse(result); else throw new NotFoundException('등록된 알림을 찾을 수 없습니다.'); } @@ -71,7 +74,7 @@ export class AlarmService { where: { id }, relations: ['user', 'stock'], }); - if (updatedAlarm) return updatedAlarm; + if (updatedAlarm) return new AlarmResponse(updatedAlarm); else throw new NotFoundException( `${id} : 업데이트할 알림을 찾을 수 없습니다.`, diff --git a/packages/backend/src/alarm/dto/alarm.response.ts b/packages/backend/src/alarm/dto/alarm.response.ts new file mode 100644 index 00000000..73156d1c --- /dev/null +++ b/packages/backend/src/alarm/dto/alarm.response.ts @@ -0,0 +1,53 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Alarm } from '../domain/alarm.entity'; + +export class AlarmResponse { + @ApiProperty({ + description: '알림 아이디', + example: 10, + }) + alarmId: number; + + @ApiProperty({ + description: '주식 코드', + example: '005930', + }) + stockId: string; + + @ApiProperty({ + description: '목표 주식 가격', + example: 50000, + nullable: true, + }) + targetPrice?: number; + + @ApiProperty({ + description: '목표 주식 거래량', + example: 10, + nullable: true, + }) + targetVolume?: number; + + @ApiProperty({ + description: '알림 만료일', + example: 10, + nullable: true, + }) + alarmDate?: Date; + + constructor(alarm: Alarm) { + this.alarmId = alarm.id; + this.stockId = alarm.stock.id; + this.targetPrice = alarm.targetPrice; + this.targetVolume = alarm.targetVolume; + this.alarmDate = alarm.alarmDate; + } +} + +export class AlarmSuccessResponse { + @ApiProperty({ + description: '성공 메시지', + example: 'success', + }) + message: string; +} diff --git a/packages/backend/src/alarm/dto/subscribe.response.ts b/packages/backend/src/alarm/dto/subscribe.response.ts index 8fba349a..f6a9174d 100644 --- a/packages/backend/src/alarm/dto/subscribe.response.ts +++ b/packages/backend/src/alarm/dto/subscribe.response.ts @@ -1,9 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; export class SubscribeResponse { - @ApiProperty({ example: 1, description: 'User ID' }) - userId: number; - - @ApiProperty({ example: 'success', description: 'Response message' }) + @ApiProperty({ example: 'success', description: '성공 메시지' }) message: string; } diff --git a/packages/backend/src/alarm/push.controller.ts b/packages/backend/src/alarm/push.controller.ts index 7e6c0b38..991e187c 100644 --- a/packages/backend/src/alarm/push.controller.ts +++ b/packages/backend/src/alarm/push.controller.ts @@ -14,7 +14,7 @@ export class PushController { @Post('subscribe') @ApiOperation({ summary: '알림 서비스 초기 설정', - description: '유저가 생성될 때 알림을 받을 수 있게 초기설정한다.', + description: '유저가 로그인할 때 알림을 받을 수 있게 초기설정한다.', }) @ApiResponse({ status: 201, diff --git a/packages/backend/src/alarm/push.service.ts b/packages/backend/src/alarm/push.service.ts index 38d7bc31..ae61b8b5 100644 --- a/packages/backend/src/alarm/push.service.ts +++ b/packages/backend/src/alarm/push.service.ts @@ -59,7 +59,6 @@ export class PushService { await this.dataSource.manager.save(newSubscription); const result: SubscribeResponse = { - userId, message: 'Push subscription success', }; return result; From 6a8ada606ede599cd962972d20156d50c337b720 Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 19:35:48 +0900 Subject: [PATCH 31/32] =?UTF-8?q?=F0=9F=93=9D=20docs:=20swagger=20message?= =?UTF-8?q?=20response=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.controller.ts | 38 +++++++++---------- .../backend/src/alarm/dto/alarm.response.ts | 3 ++ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/backend/src/alarm/alarm.controller.ts b/packages/backend/src/alarm/alarm.controller.ts index 0bbd64b4..15dfc480 100644 --- a/packages/backend/src/alarm/alarm.controller.ts +++ b/packages/backend/src/alarm/alarm.controller.ts @@ -10,9 +10,8 @@ import { } from '@nestjs/common'; import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; import { AlarmService } from './alarm.service'; -import { Alarm } from './domain/alarm.entity'; import { AlarmRequest } from './dto/alarm.request'; -import { AlarmResponse } from './dto/alarm.response'; +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'; @@ -28,7 +27,7 @@ export class AlarmController { }) @ApiOkResponse({ description: '알림 생성 완료', - type: Alarm, + type: AlarmResponse, }) @UseGuards(SessionGuard) async create( @@ -47,7 +46,7 @@ export class AlarmController { }) @ApiOkResponse({ description: '알림 아이디와 동일한 알림 찾음', - type: Alarm, + type: AlarmResponse, }) @ApiParam({ name: 'id', @@ -56,7 +55,7 @@ export class AlarmController { example: 1, }) @UseGuards(SessionGuard) - async findOne(@Param('alarmId') alarmId: number): Promise { + async findOne(@Param('id') alarmId: number): Promise { return this.alarmService.findOne(alarmId); } @@ -67,7 +66,7 @@ export class AlarmController { }) @ApiOkResponse({ description: '아이디와 동일한 알림 업데이트', - type: Alarm, + type: AlarmResponse, }) @ApiParam({ name: 'id', @@ -77,31 +76,32 @@ export class AlarmController { }) @UseGuards(SessionGuard) async update( - @Param('alarmId') alarmId: number, + @Param('id') alarmId: number, @Body() updateData: AlarmRequest, ): Promise { return this.alarmService.update(alarmId, updateData); } @Delete(':id') - @ApiOperation({ - summary: '등록된 알림 업데이트', - description: '알림 아이디 기준으로 업데이트를 할 수 있다.', - }) - @ApiOkResponse({ - description: '아이디와 동일한 알림 업데이트', - type: Alarm, - }) @ApiParam({ name: 'id', type: Number, description: '알림 아이디', example: 1, }) + @ApiOperation({ + summary: '등록된 알림 삭제', + description: '알림 아이디 기준으로 삭제를 할 수 있다.', + }) + @ApiOkResponse({ + description: '아이디와 동일한 알림 삭제', + type: AlarmSuccessResponse, + }) @UseGuards(SessionGuard) - async delete(@Param('alarmId') alarmId: number) { + async delete(@Param('id') alarmId: number) { await this.alarmService.delete(alarmId); - return { message: '알림이 정상적으로 삭제되었습니다.' }; + + return new AlarmSuccessResponse('알림 삭제를 성공했습니다.'); } @Get('user') @@ -111,7 +111,7 @@ export class AlarmController { }) @ApiOkResponse({ description: '사용자에게 등록되어 있는 모든 알림 조회', - type: [Alarm], + type: [AlarmResponse], }) @UseGuards(SessionGuard) async getByUserId(@GetUser() user: User) { @@ -128,7 +128,7 @@ export class AlarmController { @ApiOkResponse({ description: '주식 아이디에 등록되어 있는 알림 중 유저에 해당하는 알림 조회', - type: [Alarm], + type: [AlarmResponse], }) @ApiParam({ name: 'id', diff --git a/packages/backend/src/alarm/dto/alarm.response.ts b/packages/backend/src/alarm/dto/alarm.response.ts index 73156d1c..d2dc21fd 100644 --- a/packages/backend/src/alarm/dto/alarm.response.ts +++ b/packages/backend/src/alarm/dto/alarm.response.ts @@ -50,4 +50,7 @@ export class AlarmSuccessResponse { example: 'success', }) message: string; + constructor(message: string) { + this.message = message; + } } From ec589ea38410c2166299e811ed7fa3d627db21bc Mon Sep 17 00:00:00 2001 From: sunghwki Date: Sun, 1 Dec 2024 19:58:26 +0900 Subject: [PATCH 32/32] =?UTF-8?q?=F0=9F=90=9B=20fix:=20subscriber=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/alarm/alarm.subscriber.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/alarm/alarm.subscriber.ts b/packages/backend/src/alarm/alarm.subscriber.ts index 31cd9e7e..f5e15a6b 100644 --- a/packages/backend/src/alarm/alarm.subscriber.ts +++ b/packages/backend/src/alarm/alarm.subscriber.ts @@ -27,14 +27,31 @@ export class AlarmSubscriber 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; + } + if (alarm.targetVolume && alarm.targetVolume >= entity.volume) { + return true; + } + return false; + } + } + async afterInsert(event: InsertEvent) { try { - const stockLiveData = event.entity; - const alarms = await this.datasource.manager.find(Alarm, { - where: { stock: { id: stockLiveData.stock.id } }, + 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); }