Skip to content

Commit

Permalink
Feat/gpt recommendation (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
chanwoonglee authored Sep 26, 2024
1 parent 1293654 commit 57613c9
Show file tree
Hide file tree
Showing 13 changed files with 664 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org vitaminc --project korrk-back ./dist && sentry-cli sourcemaps upload --org vitaminc --project korrk-back ./dist"
},
"dependencies": {
"@azure/openai": "2.0.0-beta.1",
"@mikro-orm/core": "^6.2.9",
"@mikro-orm/migrations": "^6.2.9",
"@mikro-orm/nestjs": "^6.0.2",
Expand Down
21 changes: 21 additions & 0 deletions src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,24 @@ export const SEARCH_KEYWORD_MAX_LENGTH = 10;
export const INVITE_LINK_EXPIRATION_DAYS = 7;
export const INVITE_LINK_PREVIEW_LENGTH = 6;
export const DEFAULT_CATEGORY_ICON_CODE = 100;

export const GPT_USAGE_MAX_LIMIT = 3;

export const RESTAURANT_CATEGORY_LIST = [
'일식',
'고기',
'생선',
'호프',
'양식',
'치킨',
'중식',
'아시안',
'백반',
'분식',
'카페',
'피자',
'삼겹살',
'회',
'패스트푸드',
'베이커리',
];
39 changes: 39 additions & 0 deletions src/entities/gpt-usage.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
Entity,
EntityRepositoryType,
ManyToOne,
PrimaryKey,
Property,
Unique,
} from '@mikro-orm/core';

import { GPT_USAGE_MAX_LIMIT } from 'src/common/constants';
import { GptUsageRepository } from 'src/entities/gpt-usage.repository';
import { User } from 'src/entities/user.entity';

@Unique({ properties: ['user', 'usageYear', 'usageMonth', 'usageDay'] })
@Entity({ repository: () => GptUsageRepository })
export class GptUsage {
@PrimaryKey({ autoincrement: true })
id: number;

@ManyToOne(() => User)
user: User;

@Property()
usageYear: number;

@Property()
usageMonth: number;

@Property()
usageDay: number;

@Property({ default: 1 })
usageCount!: number;

@Property({ default: GPT_USAGE_MAX_LIMIT })
maxLimit!: number;

[EntityRepositoryType]?: GptUsageRepository;
}
4 changes: 4 additions & 0 deletions src/entities/gpt-usage.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { ExtendedEntityRepository } from 'src/common/helper/extended-repository.helper';
import { GptUsage } from 'src/entities/gpt-usage.entity';

export class GptUsageRepository extends ExtendedEntityRepository<GptUsage> {}
5 changes: 5 additions & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CategoryIconMapping } from 'src/entities/category-icon-mapping.entity';
import { GptUsage } from 'src/entities/gpt-usage.entity';
import { Tag } from 'src/entities/tag.entity';

import { GroupMap } from './group-map.entity';
Expand Down Expand Up @@ -40,6 +41,9 @@ export * from './tag-icon.repository';
export * from './category-icon-mapping.entity';
export * from './category-icon-mapping.repository';

export * from './gpt-usage.entity';
export * from './gpt-usage.repository';

export const entities = [
User,
GroupMap,
Expand All @@ -51,4 +55,5 @@ export const entities = [
TagIcon,
Tag,
CategoryIconMapping,
GptUsage,
];
5 changes: 5 additions & 0 deletions src/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@ export class PlaceNotMineException extends ExceptionOf.USER(
HttpStatus.FORBIDDEN,
'내가 등록한 장소만 삭제할 수 있습니다.' as const,
) {}

export class GptUsageLimitExceededException extends ExceptionOf.USER(
HttpStatus.TOO_MANY_REQUESTS,
'GPT 사용량을 초과했습니다.' as const,
) {}
35 changes: 35 additions & 0 deletions src/gpt/dto/gpt-usage-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ApiProperty } from '@nestjs/swagger';

import { IsNotEmpty } from 'class-validator';

import { GptUsage, User } from 'src/entities';

export class GptUsageResponseDto {
@ApiProperty({ type: Number })
@IsNotEmpty()
usageCount: number;

@ApiProperty({ type: Number })
@IsNotEmpty()
maxLimit: number;

@ApiProperty({ type: Number })
@IsNotEmpty()
usageYear: number;

@ApiProperty({ type: Number })
@IsNotEmpty()
usageMonth: number;

@ApiProperty({ type: Number })
@IsNotEmpty()
usageDay: number;

constructor(gptUsage: GptUsage) {
this.usageCount = gptUsage.usageCount;
this.maxLimit = gptUsage.maxLimit;
this.usageYear = gptUsage.usageYear;
this.usageMonth = gptUsage.usageMonth;
this.usageDay = gptUsage.usageDay;
}
}
51 changes: 44 additions & 7 deletions src/gpt/gpt.controller.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,56 @@
import { Controller, Get, Injectable, Param, Query, Sse } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Controller, Get, Param, Query, Sse } from '@nestjs/common';
import { ApiBearerAuth, ApiResponse, ApiTags } from '@nestjs/swagger';

import { Observable } from 'rxjs';

import { UseAuthGuard } from 'src/common/decorators/auth-guard.decorator';
import { CurrentUser } from 'src/common/decorators/user.decorator';
import { User, UserRole } from 'src/entities';
import { GptUsageLimitExceededException } from 'src/exceptions';
import { testPlaces } from 'src/gpt/__fixtures__/stream-test-places';
import { GptUsageResponseDto } from 'src/gpt/dto/gpt-usage-response.dto';

import { GptService } from './gpt.service';

@Injectable()
@ApiTags('gpt')
@ApiBearerAuth()
@Controller('gpt')
export class GptController {
constructor(private readonly gptService: GptService) {}

@UseAuthGuard([UserRole.USER])
@ApiResponse({ type: GptUsageResponseDto })
@Get('usage')
async getGptUsage(@CurrentUser() user: User) {
return await this.gptService.getGptUsageByUser(user);
}

@Get(':word')
@UseAuthGuard(['ADMIN'])
checkIfIsBadWordx(@Param('word') word: string) {
return this.gptService.checkIfIsBadwordWithGpt(word);
return this.gptService.checkIfIsBadWordWithGpt(word);
}

@UseAuthGuard([UserRole.USER])
@Sse('restaurants/recommend')
async recommendRestaurants(
@CurrentUser() user: User,
@Query('question') question: string,
// default x,y 강남역으로 해놨음
@Query('x') x: string = '127.027926',
@Query('y') y: string = '37.497175',
): Promise<Observable<MessageEvent>> {
console.time('Execution Time');
if (await this.gptService.isGptUsageLimitExceeded(user)) {
throw new GptUsageLimitExceededException();
}
const result = await this.gptService.recommendRestaurants(
user,
question,
x,
y,
);
console.timeEnd('Execution Time');
return result;
}

@UseAuthGuard([UserRole.USER])
Expand All @@ -38,14 +68,21 @@ export class GptController {
'테스트테스트테스트줄넘기\n테스트테스트테스트테스트테스트테스트테스트🤩테스트테스트'.split(
'',
);

for (const char of textStream) {
await new Promise((resolve) => setTimeout(resolve, 100));
// @ts-ignore
observer.next({ type: 'text', data: char });
observer.next({
type: 'text',
data: char,
});
}
testPlaces.forEach((testPlace) => {
// @ts-ignore
observer.next({ data: testPlace, type: 'json' });
observer.next({
data: testPlace,
type: 'json',
});
});
observer.complete();
};
Expand Down
7 changes: 6 additions & 1 deletion src/gpt/gpt.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Module } from '@nestjs/common';

import { MikroOrmModule } from '@mikro-orm/nestjs';

import { GptUsage } from 'src/entities/gpt-usage.entity';
import { SearchModule } from 'src/search/search.module';

import { GptController } from './gpt.controller';
import { GptService } from './gpt.service';

@Module({
imports: [],
imports: [SearchModule, MikroOrmModule.forFeature([GptUsage])],
controllers: [GptController],
providers: [GptService],
exports: [GptService],
Expand Down
Loading

0 comments on commit 57613c9

Please sign in to comment.