Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

상품 조회 오류 수정, 푸시 알림 shop 정보 추가, URL 검증 api 분리 #246

Merged
merged 4 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ export const REGEX_SHOP = {
NaverBrand: /http[s]?:\/\/(?:www\.|m\.)?brand\.naver\.com\/(?:[a-zA-Z0-9_-]+)\/products\/([a-zA-Z0-9_-]+)/,
} as const;
export const BROWSER_VERSION_20 = 20;
export const API_VERSION_NEUTRAL = 0;
export const API_VERSION_1 = 1;
26 changes: 9 additions & 17 deletions backend/src/cron/cron.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class CronService {
const recentProductInfo = await Promise.all(
totalProducts.map(async ({ productCode, id, shop }) => {
const productInfo = await getProductInfo(shop, productCode);
return { ...productInfo, id };
return { ...productInfo, productId: id };
sickbirdd marked this conversation as resolved.
Show resolved Hide resolved
}),
);
const productList = recentProductInfo.map((data) => `product:${data.productId}`);
Expand Down Expand Up @@ -119,47 +119,39 @@ export class CronService {
}

async findMatchedProducts(trackingList: TrackingProduct[], product: ProductInfoDto) {
const { productPrice, productCode, productName, imageUrl } = product;
const notifications = [];
const matchedProducts = [];

for (const trackingProduct of trackingList) {
const { userId, targetPrice, isFirst, isAlert } = trackingProduct;
if (!isFirst && targetPrice < productPrice) {
if (!isFirst && targetPrice < product.productPrice) {
trackingProduct.isFirst = true;
await this.trackingProductRepository.save(trackingProduct);
} else if (targetPrice >= productPrice && isFirst && isAlert) {
} else if (targetPrice >= product.productPrice && isFirst && isAlert) {
const firebaseToken = await this.redis.get(`firebaseToken:${userId}`);
if (firebaseToken) {
notifications.push(
this.getMessage(productCode, productName, productPrice, imageUrl, firebaseToken),
);
notifications.push(this.getMessage(product, firebaseToken));
sickbirdd marked this conversation as resolved.
Show resolved Hide resolved
matchedProducts.push(trackingProduct);
}
}
}
return { notifications, matchedProducts };
}

private getMessage(
productCode: string,
productName: string,
productPrice: number,
imageUrl: string,
token: string,
): Message {
private getMessage(product: ProductInfoDto, token: string): Message {
return {
notification: {
title: '목표 가격 이하로 내려갔습니다!',
body: `${productName}의 현재 가격은 ${productPrice}원 입니다.`,
body: `${product.productName}의 현재 가격은 ${product.productPrice}원 입니다.`,
},
data: {
productCode,
shop: product.shop,
productCode: product.productCode,
},
android: {
notification: {
channelId: CHANNEL_ID,
imageUrl,
imageUrl: product.imageUrl,
sickbirdd marked this conversation as resolved.
Show resolved Hide resolved
},
},
token,
Expand Down
29 changes: 27 additions & 2 deletions backend/src/product/product.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { User } from 'src/entities/user.entity';
import { AuthGuard } from '@nestjs/passport';
import { HttpExceptionFilter } from 'src/exceptions/http.exception.filter';
import { ExpiredTokenError } from 'src/dto/auth.swagger.dto';
import { API_VERSION_1, API_VERSION_NEUTRAL } from 'src/constants';

@ApiBearerAuth()
@ApiHeader({
Expand All @@ -71,8 +72,32 @@ export class ProductController {
@ApiBadRequestResponse({ type: UrlError, description: '유효하지 않은 링크' })
@Post('/verify')
async verifyUrl(@Body() productUrlDto: ProductUrlDto): Promise<VerifyUrlSuccess> {
const { productName, productCode, productPrice, shop, imageUrl } =
await this.productService.verifyUrl(productUrlDto);
const { productName, productCode, productPrice, shop, imageUrl } = await this.productService.verifyUrl(
productUrlDto,
API_VERSION_NEUTRAL,
);
return {
statusCode: HttpStatus.OK,
message: '상품 URL 검증 성공',
productCode,
productName,
productPrice,
shop,
imageUrl,
};
}

@ApiOperation({ summary: 'SmartStore가 추가된 상품 URL 검증 API', description: '상품 URL을 검증한다' })
@ApiBody({ type: ProductUrlDto })
@ApiOkResponse({ type: VerifyUrlSuccess, description: '상품 URL 검증 성공' })
@ApiBadRequestResponse({ type: UrlError, description: '유효하지 않은 링크' })
@Post('/verify')
@Version('1')
async verifyUrlV1(@Body() productUrlDto: ProductUrlDto): Promise<VerifyUrlSuccess> {
const { productName, productCode, productPrice, shop, imageUrl } = await this.productService.verifyUrl(
productUrlDto,
API_VERSION_1,
);
return {
statusCode: HttpStatus.OK,
message: '상품 URL 검증 성공',
Expand Down
4 changes: 2 additions & 2 deletions backend/src/product/product.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export class ProductService {
private cacheService: CacheService,
) {}

async verifyUrl(productUrlDto: ProductUrlDto): Promise<ProductInfoDto> {
async verifyUrl(productUrlDto: ProductUrlDto, apiVersion: number): Promise<ProductInfoDto> {
const { productUrl } = productUrlDto;
const { shop, productCode } = identifyProductByUrl(productUrl);
const { shop, productCode } = identifyProductByUrl(productUrl, apiVersion);
return await getProductInfo(shop, productCode);
}

Expand Down
25 changes: 17 additions & 8 deletions backend/src/utils/product.info.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { ProductInfoDto } from 'src/dto/product.info.dto';
import { BASE_URL_11ST, BROWSER_VERSION_20, OPEN_API_KEY_11ST, REGEX_SHOP } from 'src/constants';
import {
API_VERSION_1,
API_VERSION_NEUTRAL,
BASE_URL_11ST,
BROWSER_VERSION_20,
OPEN_API_KEY_11ST,
REGEX_SHOP,
} from 'src/constants';
import { JSDOM } from 'jsdom';
import * as convert from 'xml-js';
import * as iconv from 'iconv-lite';
Expand Down Expand Up @@ -124,16 +131,18 @@ export function createUrl(shop: string, productCode: string) {
}
}

export function identifyProductByUrl(productUrl: string): ProductIdentifierDto {
export function identifyProductByUrl(productUrl: string, version: number): ProductIdentifierDto {
let matchList = null;
if ((matchList = productUrl.match(REGEX_SHOP['11ST']))) {
if (version === API_VERSION_NEUTRAL && (matchList = productUrl.match(REGEX_SHOP['11ST']))) {
return { shop: '11번가', productCode: matchList[1] };
}
if (
(matchList = productUrl.match(REGEX_SHOP.NaverBrand)) ||
(matchList = productUrl.match(REGEX_SHOP.NaverSmartStore))
) {
return { shop: 'SmartStore', productCode: matchList[1] };
if (version >= API_VERSION_1) {
if (
(matchList = productUrl.match(REGEX_SHOP.NaverBrand)) ||
(matchList = productUrl.match(REGEX_SHOP.NaverSmartStore))
) {
return { shop: 'SmartStore', productCode: matchList[1] };
}
}
throw new HttpException('URL이 유효하지 않습니다.', HttpStatus.BAD_REQUEST);
}