Skip to content

Commit

Permalink
Feature/#365 알림 response 변경, 알림 검증 로직 추가, 중복 함수 리팩토링 (#366)
Browse files Browse the repository at this point in the history
* 💄 style: eslint 깨지는 파일 수정

* 📝 docs: stockId 잘못 들어 있던 데코레이터 수정

* 📝 docs: response alarmresponse 로 수정

* 🐛 fix: bigint가 런타임때는 string으로 작동, decimal 15,2로 변경

* 🐛 fix: stock id를 이상하게 호출하는 문제 해결, express 와 동일한 구조로 되어있어 라우터 등록 순서에 영향 받는 다는 것을 간과함

* 🐛 fix: put 에러 수정, alarmDate -\> alarmExpiredDate로 명확화

* 📝 docs: reqeust 업데이트

* 🐛 fix: alarmDate 업데이트

* 🐛 fix: alarmExpiredDate로 변환, 이에 맞춰 docs도 업데이트

* ✨ feat: 알림 서비스 검증 로직 구현

* 📝 docs: 알림 docs 에러 수정

* 🐛 fix: expired date 논리 오류 수정

* 📝 docs: 원 추가
  • Loading branch information
swkim12345 authored Dec 4, 2024
1 parent c035e0e commit b48f975
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 25 deletions.
26 changes: 24 additions & 2 deletions packages/backend/src/alarm/alarm.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
UseGuards,
} from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
Expand All @@ -35,6 +36,17 @@ export class AlarmController {
description: '알림 생성 완료',
type: AlarmResponse,
})
@ApiBadRequestResponse({
description: '유효하지 않은 알람 입력값으로 인해 예외가 발생했습니다.',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 400 },
message: { type: 'string', example: '알람 조건을 다시 확인해주세요.' },
error: { type: 'string', example: 'Bad Request' },
},
},
})
@UseGuards(SessionGuard)
async create(
@Body() alarmRequest: AlarmRequest,
Expand Down Expand Up @@ -109,8 +121,7 @@ export class AlarmController {
summary: '등록된 알림 업데이트',
description: '알림 아이디 기준으로 업데이트를 할 수 있다.',
})
@ApiResponse({
status: 201,
@ApiOkResponse({
description: '아이디와 동일한 알림 업데이트',
type: AlarmResponse,
})
Expand All @@ -120,6 +131,17 @@ export class AlarmController {
description: '알림 아이디',
example: 1,
})
@ApiBadRequestResponse({
description: '유효하지 않은 알람 입력값으로 인해 예외가 발생했습니다.',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 400 },
message: { type: 'string', example: '알람 조건을 다시 확인해주세요.' },
error: { type: 'string', example: 'Bad Request' },
},
},
})
@UseGuards(SessionGuard)
async update(
@Param('id') alarmId: number,
Expand Down
126 changes: 123 additions & 3 deletions packages/backend/src/alarm/alarm.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BadRequestException,
ForbiddenException,
Injectable,
NotFoundException,
Expand All @@ -10,6 +11,8 @@ import { PushSubscription } from './domain/subscription.entity';
import { AlarmRequest } from './dto/alarm.request';
import { AlarmResponse } from './dto/alarm.response';
import { PushService } from './push.service';
import { StockMinutely } from '@/stock/domain/stockData.entity';
import { StockLiveData } from '@/stock/domain/stockLiveData.entity';
import { User } from '@/user/domain/user.entity';

@Injectable()
Expand All @@ -21,9 +24,113 @@ export class AlarmService {
private readonly pushService: PushService,
) {}

private isAlarmNotExpired(
expiredDate: Date,
recent: StockMinutely | StockLiveData,
): boolean {
const updatedAt =
(recent as StockLiveData).updatedAt ||
(recent as StockMinutely).createdAt;
return updatedAt && expiredDate >= updatedAt;
}

private isTargetPriceMet(
targetPrice: number,
recent: StockMinutely | StockLiveData,
): boolean {
return targetPrice <= recent.open;
}

private isTargetVolumeMet(
targetVolume: number,
recent: StockMinutely | StockLiveData,
): boolean {
return targetVolume <= recent.volume;
}

isValidAlarmCompareEntity(
alarm: Partial<Alarm>,
recent: StockMinutely | StockLiveData,
): boolean {
if (
alarm.alarmExpiredDate &&
this.isAlarmNotExpired(alarm.alarmExpiredDate, recent)
) {
return true;
}
if (alarm.targetPrice && this.isTargetPriceMet(alarm.targetPrice, recent)) {
return true;
}
if (
alarm.targetVolume &&
this.isTargetVolumeMet(alarm.targetVolume, recent)
) {
return true;
}
return false;
}

private validAlarmThrow(
alarm: Partial<Alarm>,
recent: StockMinutely | StockLiveData,
) {
if (
alarm.alarmExpiredDate &&
!this.isAlarmNotExpired(alarm.alarmExpiredDate, recent)
)
throw new BadRequestException(
`${alarm.alarmExpiredDate}는 잘못된 날짜입니다. 다시 입력해주세요.`,
);

if (alarm.targetPrice && this.isTargetPriceMet(alarm.targetPrice, recent))
throw new BadRequestException(
`${alarm.targetPrice}원은 최근 가격보다 낮습니다. 다시 입력해주세요.`,
);

if (
alarm.targetVolume &&
this.isTargetVolumeMet(alarm.targetVolume, recent)
)
throw new BadRequestException(
`${alarm.targetVolume}은 최근 거래량보다 낮습니다. 다시 입력해주세요.`,
);
}

async validAlarmThrowException(
alarmData: AlarmRequest,
stockId: string = alarmData.stockId,
) {
const recentLiveData = await this.dataSource.manager.findOne(
StockLiveData,
{
where: { stock: { id: stockId } },
},
);

if (recentLiveData) {
this.validAlarmThrow(alarmData, recentLiveData);
}

const recentMinuteData = await this.dataSource.manager.findOne(
StockMinutely,
{
where: { stock: { id: stockId } },
order: { startTime: 'DESC' },
},
);

if (recentMinuteData) {
this.validAlarmThrow(alarmData, recentMinuteData);
}

return true;
}

async create(alarmData: AlarmRequest, userId: number) {
await this.validAlarmThrowException(alarmData);
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('유저를 찾을 수 없습니다.');
Expand All @@ -34,7 +141,9 @@ export class AlarmService {
user,
stock: { id: alarmData.stockId },
});

const result = await repository.save(newAlarm);

return new AlarmResponse(result);
});
}
Expand All @@ -44,6 +153,7 @@ export class AlarmService {
where: { user: { id: userId } },
relations: ['user', 'stock'],
});

return result.map((val) => new AlarmResponse(val));
}

Expand All @@ -52,29 +162,38 @@ export class AlarmService {
where: { stock: { id: stockId }, user: { id: userId } },
relations: ['user', 'stock'],
});

return result.map((val) => new AlarmResponse(val));
}

async findOne(id: number) {
const result = await this.alarmRepository.findOne({
where: { id },
relations: ['user', 'stock'],
relations: ['stock'],
});

if (result) return new AlarmResponse(result);
else throw new NotFoundException('등록된 알림을 찾을 수 없습니다.');
}

async update(id: number, updateData: AlarmRequest) {
await this.validAlarmThrowException(updateData);
const alarm = await this.alarmRepository.findOne({ where: { id } });
if (!alarm) {
throw new NotFoundException('등록된 알림을 찾을 수 없습니다.');
}

await this.alarmRepository.update(id, updateData);
await this.alarmRepository.update(id, {
stock: { id: updateData.stockId },
targetVolume: updateData.targetVolume,
targetPrice: updateData.targetPrice,
alarmExpiredDate: updateData.alarmExpiredDate,
});
const updatedAlarm = await this.alarmRepository.findOne({
where: { id },
relations: ['user', 'stock'],
relations: ['stock'],
});

if (updatedAlarm) return new AlarmResponse(updatedAlarm);
else
throw new NotFoundException(
Expand All @@ -84,6 +203,7 @@ export class AlarmService {

async delete(id: number) {
const alarm = await this.alarmRepository.findOne({ where: { id } });

if (!alarm) {
throw new NotFoundException(`${id} : 삭제할 알림을 찾을 수 없습니다.`);
}
Expand Down
16 changes: 1 addition & 15 deletions packages/backend/src/alarm/alarm.subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,6 @@ 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<StockMinutely>) {
try {
const stockMinutely = event.entity;
Expand All @@ -49,7 +35,7 @@ export class AlarmSubscriber
relations: ['user', 'stock'],
});
const alarms = rawAlarms.filter((val) =>
this.isValidAlarm(val, stockMinutely),
this.alarmService.isValidAlarmCompareEntity(val, stockMinutely),
);
for (const alarm of alarms) {
await this.alarmService.sendPushNotification(alarm);
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/alarm/domain/alarm.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class Alarm {
targetVolume?: number;

@Column({ type: 'timestamp', name: 'alarm_date', nullable: true })
alarmDate?: Date;
alarmExpiredDate?: Date;

@CreateDateColumn({ type: 'timestamp', name: 'created_at' })
createdAt: Date;
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/alarm/dto/alarm.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export class AlarmRequest {

@ApiProperty({
description: '알림 종료 날짜',
example: '2024-12-01T00:00:00Z',
example: '2026-12-01T00:00:00Z',
required: false,
})
alarmDate?: Date;
alarmExpiredDate?: Date;
}
4 changes: 2 additions & 2 deletions packages/backend/src/alarm/dto/alarm.response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ export class AlarmResponse {
example: 10,
nullable: true,
})
alarmDate?: Date;
alarmExpiredDate?: 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;
this.alarmExpiredDate = alarm.alarmExpiredDate;
}
}

Expand Down

0 comments on commit b48f975

Please sign in to comment.