diff --git a/.gitignore b/.gitignore index 22f55ad..ed3ae97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# .env +.env + # compiled output /dist /node_modules diff --git a/package-lock.json b/package-lock.json index 8047516..7aff050 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.1.0", "@nestjs/mongoose": "^10.0.1", @@ -1470,6 +1471,21 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.0.0.tgz", + "integrity": "sha512-fzASk1Uv6AjdE6uA1na8zpqRCXAhRpcfgpCVv3SAKlgJ3VR3bEjcI4G17WHLgLBsmPzI1ofdkSI451WLD1F1Rw==", + "dependencies": { + "dotenv": "16.1.4", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13" + } + }, "node_modules/@nestjs/core": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.1.2.tgz", @@ -3522,6 +3538,25 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.1.4.tgz", + "integrity": "sha512-m55RtE8AsPeJBpOIFKihEmqUcoVncQIwo7x9U8ZwLEZw9ZpXboz2c+rvog+jUaJvVrZ5kBOeYQBX5+8Aa/OZQw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "engines": { + "node": ">=12" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -8095,6 +8130,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index f5a147c..e837df5 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.0.0", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.1.0", "@nestjs/mongoose": "^10.0.1", diff --git a/src/app.module.ts b/src/app.module.ts index cad6434..bb71ee7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,21 +5,32 @@ import { UsersModule } from './users/users.module'; import { AuthMiddleware } from './common/auth.middleware'; import { BabblesController } from './babbles/babbles.controller'; import { JwtService } from '@nestjs/jwt'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { TrialsModule } from './trials/trials.module'; + @Module({ imports: [ - MongooseModule.forRoot( - '', - ), + ConfigModule.forRoot(), + MongooseModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (config: ConfigService) => { + console.log(config.get('MONGO_DB_URI')); + return ({ + + uri: config.get('MONGO_DB_URI'), // Loaded from .ENV + })} + }), BabblesModule, UsersModule, + TrialsModule ], - providers : [JwtService] + providers: [JwtService], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { - consumer.apply(AuthMiddleware).forRoutes(BabblesController); + consumer.apply(AuthMiddleware).forRoutes(BabblesController); } - } diff --git a/src/babbles/babbles.controller.ts b/src/babbles/babbles.controller.ts index 9969b43..fc0424c 100644 --- a/src/babbles/babbles.controller.ts +++ b/src/babbles/babbles.controller.ts @@ -97,6 +97,7 @@ export class BabblesController { await this.babblesService.validateBabbles(id, user); const data = await this.babblesService.getHistoryById(id); + data.messages.shift(); console.log(data); return res.status(200).json(data); } diff --git a/src/babbles/babbles.service.ts b/src/babbles/babbles.service.ts index 88dabfd..c281134 100644 --- a/src/babbles/babbles.service.ts +++ b/src/babbles/babbles.service.ts @@ -27,7 +27,7 @@ export class BabblesService { messages: [ { role: 'system', - content: `너는 ${createBabbleDto.personality} 역할이야. 답변은 말하듯 짧게 1~3 문장으로 해줘. 잘 부탁해.`, + content: `너는 ${createBabbleDto.personality} 역할이야. 답변은 말하듯 짧게 1~3 문장으로 해줘. 너무 길면 불편하거든. 잘 부탁해.`, }, ], }; diff --git a/src/babbles/interfaces/message.interface.ts b/src/babbles/interfaces/message.interface.ts deleted file mode 100644 index 3ac7b90..0000000 --- a/src/babbles/interfaces/message.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -// import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; - -// @Schema() -// export class Message { - -// @Prop() -// role : string; -// @Prop() -// content : string; -// } - -// export const MessageSchema = SchemaFactory.createForClass(Message) \ No newline at end of file diff --git a/src/common/openai.service.ts b/src/common/openai.service.ts index 3079f64..87f0a65 100644 --- a/src/common/openai.service.ts +++ b/src/common/openai.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; - +import { ConfigService } from '@nestjs/config/dist'; import { ChatCompletionRequestMessage, ChatCompletionResponseMessage, @@ -7,17 +7,18 @@ import { OpenAIApi, } from 'openai'; -const configuration = new Configuration({ - organization: 'org-jPO32mgFLsFXUbX0VhM5CsXo', - apiKey: '', -}); -const openai = new OpenAIApi(configuration); - @Injectable() export class OpenAIService { async createAIbabble( messages?: ChatCompletionRequestMessage[] | any, ): Promise { + const configuration = new Configuration({ + organization: 'org-jPO32mgFLsFXUbX0VhM5CsXo', + apiKey: process.env.OPEN_AI_KEY, + }); + + const openai = new OpenAIApi(configuration); + const response = await openai.createChatCompletion({ model: 'gpt-3.5-turbo', messages: messages, diff --git a/src/trials/dto/create-trial.dto.ts b/src/trials/dto/create-trial.dto.ts new file mode 100644 index 0000000..53145e8 --- /dev/null +++ b/src/trials/dto/create-trial.dto.ts @@ -0,0 +1,14 @@ +import { IsNotEmpty } from "class-validator"; +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateTrialDto { + + @ApiProperty() + @IsNotEmpty() + readonly deviceId: string; + + @ApiProperty() + @IsNotEmpty() + readonly personality : string; + +} \ No newline at end of file diff --git a/src/trials/dto/make-trial-message.dto.ts b/src/trials/dto/make-trial-message.dto.ts new file mode 100644 index 0000000..ba897c1 --- /dev/null +++ b/src/trials/dto/make-trial-message.dto.ts @@ -0,0 +1,8 @@ +import { IsNotEmpty } from "class-validator"; +import { ApiProperty } from '@nestjs/swagger'; + +export class MakeTrialMessageDTO { + @IsNotEmpty() + @ApiProperty() + readonly content : string; +} \ No newline at end of file diff --git a/src/trials/interfaces/trials.interface.ts b/src/trials/interfaces/trials.interface.ts new file mode 100644 index 0000000..8f444bc --- /dev/null +++ b/src/trials/interfaces/trials.interface.ts @@ -0,0 +1,29 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import mongoose from 'mongoose'; + +class Message { + @Prop() + role : string; + @Prop() + content : string; +} + +@Schema() +export class Trial { + @Prop({ default: new mongoose.Types.ObjectId }) + _id: string; + + @Prop() + talker: string; + @Prop() + created: Date; + @Prop() + modified: Date; + @Prop() + personality: string; + + @Prop() + messages: Message[]; +} + +export const TrialSchema = SchemaFactory.createForClass(Trial); diff --git a/src/trials/trials.controller.ts b/src/trials/trials.controller.ts new file mode 100644 index 0000000..f387954 --- /dev/null +++ b/src/trials/trials.controller.ts @@ -0,0 +1,103 @@ +import { Body, Controller, Res, Req, Get, Post, Param } from '@nestjs/common'; +import { TrialsService } from './trials.service'; +import { CreateTrialDto } from './dto/create-trial.dto'; +import { MakeTrialMessageDTO } from './dto/make-trial-message.dto'; +import { OpenAIService } from 'src/common/openai.service'; +import { Response } from 'express'; + +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; + +@Controller('api/trials') +@ApiTags('[체험판 관련]') +export class TrialsController { + constructor( + private readonly trialsService: TrialsService, + private readonly openaiService: OpenAIService, + ) {} + + @Post() + @ApiOperation({ + summary: '체험판 생성', + description: + '부여된 성격으로 대화 상대를 설정 하여 방을 생성.(deviceId 필요)', + }) + @ApiResponse({ + status: 201, + description: '대화방 생성 성공시 대화방의 id = (deviceId) 가 반환.', + }) + @ApiResponse({ + status: 400, + description: '이미 체험판을 이용한적이 있다면 에러응답.', + }) + async handleTrialRequest( + @Body() createTrialDto: CreateTrialDto, + @Res() res: Response, + ) { + const created = await this.trialsService.create(createTrialDto); + return res + .status(201) + .json({ message: 'success', trialId: createTrialDto.deviceId }); + } + + + @Get(':deviceId') + @ApiOperation({ + summary: '체험판 기록 확인하기', + description: '특정 디바이스의 체험판 기록을 응답으로 반환.', + }) + @ApiResponse({ + status: 400, + description: + '체험판 신청한적이 없다면 에러 응답 반환.', + }) + async handleTrialHistoryRequest( + @Res() res: Response, + @Param('deviceId') deviceId: string, + ) { + await this.trialsService.validateTrials(deviceId); + + const log = await this.trialsService.getHistoryByDeviceId(deviceId); + log.messages.shift(); + return res.status(200).json(log); + } + + + @Post(':deviceId') + @ApiOperation({ + summary: '체험판 대화하기', + description: '요청자의 메세지를 처리하여 AI의 메세지를 응답으로 반환.', + }) + @ApiResponse({ + status: 200, + description: '{role:string , content : string } 형태의 객체로 응답 반환', + }) + @ApiResponse({ + status: 400, + description: + '체험판 ID가 잘못되었거나, 이미 3번 이상 사용했을때 에러 응답 반환.', + }) + async handleTrialConversationRequest( + @Res() res: Response, + @Param('deviceId') deviceId: string, + @Body() makeTrialMessageDto: MakeTrialMessageDTO, + ) { + await this.trialsService.validateTrials(deviceId); + + const updated = await this.trialsService.pushMessage( + deviceId, + 'user', + makeTrialMessageDto.content, + ); + + // console.log(updated); + const ais = await this.openaiService.createAIbabble( + updated.messages.map((one) => ({ role: one.role, content: one.content })), + ); + + await this.trialsService.pushMessage(deviceId, 'assistant', ais.content); + + return res.status(200).json(ais); + } + + +} diff --git a/src/trials/trials.module.ts b/src/trials/trials.module.ts new file mode 100644 index 0000000..af3fe59 --- /dev/null +++ b/src/trials/trials.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TrialsController } from './trials.controller'; +import { TrialsService } from './trials.service'; +import { OpenAIService } from 'src/common/openai.service'; + +import { MongooseModule } from '@nestjs/mongoose'; +import { TrialSchema } from './interfaces/trials.interface'; + +@Module({ + imports: [ + MongooseModule.forFeature([{ name: 'Trial', schema: TrialSchema }]), + ], + controllers: [TrialsController], + providers: [TrialsService, OpenAIService], +}) +export class TrialsModule {} diff --git a/src/trials/trials.service.ts b/src/trials/trials.service.ts new file mode 100644 index 0000000..5a4fb26 --- /dev/null +++ b/src/trials/trials.service.ts @@ -0,0 +1,72 @@ +import { + Injectable, + BadRequestException, + NotFoundException, +} from '@nestjs/common'; +import { CreateTrialDto } from './dto/create-trial.dto'; +import { Trial } from './interfaces/trials.interface'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; + +@Injectable() +export class TrialsService { + constructor( + @InjectModel('Trial') private readonly trialModel: Model, + ) {} + + async create(CreateTrialDto: CreateTrialDto): Promise { + const found = await this.trialModel + .findOne({ talker: CreateTrialDto.deviceId }) + .exec(); + if (found) { + throw new BadRequestException('Already used the trial version.'); + } + + const trial = { + talker: CreateTrialDto.deviceId, + created: new Date(), + personality: CreateTrialDto.personality, + modified: new Date(), + messages: [ + { + role: 'system', + content: `너는 ${CreateTrialDto.personality} 역할이야. 답변은 말하듯 짧게 1~2 문장으로 해줘. 너무 길면 불편하거든. 잘 부탁해.`, + }, + ], + }; + return await this.trialModel.create(trial); + } + + async validateTrials(deviceId : string): Promise { + const found = await this.trialModel.findOne({talker : deviceId}).exec(); + if (!found) { + throw new NotFoundException('Have not applied for a trial version'); + } + } + + async pushMessage(deviceId: string, role: string, content: string): Promise { + const found = await this.trialModel.findOne({talker : deviceId}).exec(); + + if (found.messages.filter((e) => e.role === 'assistant').length >= 3) { + throw new BadRequestException( + 'Used up all the trial usage opportunities.', + ); + } + + const result = await this.trialModel + .findOneAndUpdate( + {talker : deviceId}, + { + $push: { messages: { role: role, content: content } }, + $set: { modified: new Date() }, + }, + { returnDocument: 'after' }, + ) + .exec(); + return result; + } + + async getHistoryByDeviceId(deviceId: string): Promise { + return await this.trialModel.findOne({talker : deviceId}).exec(); + } +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index eaa09fe..bfaef7f 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -7,6 +7,7 @@ import { Query, Res, UnauthorizedException, + BadRequestException, } from '@nestjs/common'; import { SignUpDto } from './dto/sign-up.dto'; import { UsersService } from './users.service'; @@ -14,12 +15,11 @@ import { Response } from 'express'; import { SignInDto } from './dto/sign-in.dto'; import { JwtService } from '@nestjs/jwt'; -import { ApiTags, ApiOperation, ApiResponse} from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { User } from './interfaces/user.interface'; - @Controller('api/users') -@ApiTags("[사용자 관련]") +@ApiTags('[사용자 관련]') export class UsersController { constructor( private readonly usersService: UsersService, @@ -27,18 +27,28 @@ export class UsersController { ) {} @Post('/new') - @ApiOperation({ summary: '신규 유저 등록', description: '전송된 데이터를 토대로 신규 유저 정보를 등록한다.' }) - @ApiResponse({ status: 201 }) - @ApiResponse({ status: 400, description: '부정확한 정보를 포함하고 있거나 기타 장애로 생성 실패시' }) + @ApiOperation({ + summary: '신규 유저 등록', + description: '전송된 데이터를 토대로 신규 유저 정보를 등록한다.', + }) + @ApiResponse({ status: 201 }) + @ApiResponse({ + status: 400, + description: '부정확한 정보를 포함하고 있거나 기타 장애로 생성 실패시', + }) async signupHandle(@Body() signupDto: SignUpDto, @Res() res: Response) { const created = await this.usersService.create(signupDto); res.status(201).json(created); } @Get('/auth') - @ApiOperation({ summary: '유저 확인', description: '전송된 데이터를 토대로 유저 정보를 확인하여 억세스토큰을 발급한다.' }) - @ApiResponse({ status: 200}) - @ApiResponse({ status: 400, description: '사용자 검색에 실패시' }) + @ApiOperation({ + summary: '유저 확인', + description: + '전송된 데이터를 토대로 유저 정보를 확인하여 억세스토큰을 발급한다.', + }) + @ApiResponse({ status: 200 }) + @ApiResponse({ status: 400, description: '사용자 검색에 실패시' }) async signInHandle(@Query() signinDto: SignInDto, @Res() res: Response) { await this.usersService.authenticate(signinDto); @@ -55,15 +65,34 @@ export class UsersController { } @Get('/private') - async myInfoHandle(@Headers('authorization') token: string) { + async myInfoHandle( + @Headers('authorization') token: string, + @Res() res: Response, + ) { // console.log(token); - try { - const result = await this.jwtService.verifyAsync(token, { - secret: 'ah7T4hbC0sNlM99XnRWI11vlA9FdSPR9', - }); - } catch (e) { - // console.log(e); - throw new UnauthorizedException('invalid token'); + if (token) { + if (!token.startsWith('Bearer') || token.split(' ').length !== 2) { + return res + .status(401) + .json({ statusCode: 401, message: 'Invalid token' }); + } + const extrat = token.split(' ')[1]; + try { + const decoded = await this.jwtService.verifyAsync(extrat, { + secret: 'ah7T4hbC0sNlM99XnRWI11vlA9FdSPR9', + }); + res.status(200).json(decoded); + } catch (error) { + // 토큰이 유효하지 않은 경우 또는 오류가 발생한 경우 처리 + return res + .status(401) + .json({ statusCode: 401, message: 'Invalid token' }); + } + } else { + // 토큰이 없는 경우 처리 + return res + .status(401) + .json({ statusCode: 401, message: 'Token not provided' }); } } }