-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #288 from boostcampwm-2024/develop
[Merger] 5주차 메인 브랜치 배포
- Loading branch information
Showing
126 changed files
with
2,705 additions
and
835 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ on: | |
branches: | ||
- develop | ||
paths: | ||
- "Backend/src/**" | ||
- "Backend/apps/api/src/**" | ||
|
||
jobs: | ||
deploy: | ||
|
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Injectable, ExecutionContext, UnauthorizedException, Logger } from '@nestjs/common'; | ||
import { AuthGuard } from '@nestjs/passport'; | ||
|
||
@Injectable() | ||
export class JwtAuthGuard extends AuthGuard('jwt') { | ||
private readonly logger = new Logger(JwtAuthGuard.name); | ||
|
||
handleRequest(err: any, user: any, info: any, context: ExecutionContext) { | ||
if (err || !user) { | ||
const request = context.switchToHttp().getRequest(); | ||
this.logger.warn(`Unauthorized access attempt: ${request.method} ${request.url}`); | ||
throw err || new UnauthorizedException('Unauthorized'); | ||
} | ||
return user; | ||
} | ||
} |
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 5 additions & 5 deletions
10
Backend/src/videos/videos.controller.spec.ts → ...ps/api/src/chats/chats.controller.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Body, Controller, Post, Req, UseGuards, ValidationPipe } from '@nestjs/common'; | ||
import { SendChatDto } from './dto/send.chat.dto'; | ||
import { ChatsService } from './chats.service'; | ||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; | ||
import { UserEntity } from '../users/entity/user.entity'; | ||
|
||
@Controller('chats') | ||
export class ChatsController { | ||
constructor(private readonly chatsService: ChatsService) {} | ||
|
||
@Post() | ||
@UseGuards(JwtAuthGuard) | ||
async sendChat(@Body(ValidationPipe) sendChatDto: SendChatDto, @Req() req: Request & { user: UserEntity }) { | ||
this.chatsService.ingestChat({ ...sendChatDto, userId: req.user.id, nickname: req.user.nickname }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { InjectRedis } from '@nestjs-modules/ioredis'; | ||
import { HttpService } from '@nestjs/axios'; | ||
import { Injectable } from '@nestjs/common'; | ||
import { ConfigService } from '@nestjs/config'; | ||
import { UUID } from 'crypto'; | ||
import Redis from 'ioredis'; | ||
import { firstValueFrom } from 'rxjs'; | ||
|
||
@Injectable() | ||
export class ChatsService { | ||
constructor( | ||
@InjectRedis() private readonly redisClient: Redis, | ||
private readonly httpService: HttpService, | ||
private readonly configService: ConfigService, | ||
) {} | ||
|
||
async ingestChat({ | ||
channelId, | ||
message, | ||
userId, | ||
nickname, | ||
}: { | ||
channelId; | ||
message: string; | ||
userId: number; | ||
nickname: string; | ||
}) { | ||
const chatId = crypto.randomUUID(); | ||
const chat = { | ||
content: message, | ||
userId, | ||
nickname, | ||
timestamp: new Date(), | ||
channelId, | ||
chatId, | ||
filteringResult: true, | ||
}; | ||
const chatString = JSON.stringify(chat); | ||
await this.redisClient.multi().publish(`${channelId}:chat`, chatString).rpush('chatQueue', chatId).exec(); | ||
this.clovaFiltering(chat); | ||
} | ||
|
||
async readViewers(channelId: UUID) { | ||
return await this.redisClient.hlen(`${channelId}:viewers`); | ||
} | ||
|
||
async clearChat(channelId: UUID) { | ||
this.redisClient.del(`${channelId}:chats`); | ||
} | ||
|
||
async clovaFiltering(chat) { | ||
const postData = { | ||
messages: [ | ||
{ | ||
role: 'system', | ||
content: this.configService.get<string>('CLOVA_CHAT_FILTERING_SYSTEM_PROMPT'), | ||
}, | ||
{ | ||
role: 'user', | ||
content: `채팅내용 : "${chat.content}"`, | ||
}, | ||
], | ||
maxTokens: this.configService.get<number>('CLOVA_CHAT_FILTERING_MAX_TOKEN') || 10, | ||
topP: 0.8, | ||
topK: 1, | ||
temperature: 0.1, | ||
repeatPenalty: 1.0, | ||
includeAiFilters: true, | ||
seed: 0, | ||
}; | ||
const { data } = await firstValueFrom( | ||
this.httpService.post(this.configService.get<string>('CLOVA_API_URL'), postData, { | ||
headers: { | ||
'X-NCP-CLOVASTUDIO-API-KEY': this.configService.get<string>('CLOVA_API_KEY'), | ||
'X-NCP-APIGW-API-KEY': this.configService.get<string>('CLOVA_API_GATEWAY_KEY'), | ||
'X-NCP-CLOVASTUDIO-REQUEST-ID': this.configService.get<string>('CLOVA_REQUEST_ID'), | ||
}, | ||
}), | ||
); | ||
|
||
chat.filteringResult = data?.result?.message?.content?.includes('true'); | ||
await this.redisClient | ||
.multi() | ||
.publish( | ||
`${chat.channelId}:filter`, | ||
JSON.stringify({ chatId: chat.chatId, filteringResult: chat.filteringResult }), | ||
) | ||
.hset('chatCache', chat.chatId, JSON.stringify(chat)) | ||
.exec(); | ||
this.flushChat(); | ||
} | ||
|
||
async flushChat() { | ||
const lockKey = 'chat:flush:lock'; | ||
const lock = await this.redisClient.set(lockKey, 'lock', 'NX'); | ||
|
||
try { | ||
if (lock) { | ||
while (true) { | ||
const frontChatId = await this.redisClient.lindex('chatQueue', 0); | ||
const chatString = await this.redisClient.hget('chatCache', frontChatId); | ||
if (!chatString) { | ||
break; | ||
} else { | ||
const chat = JSON.parse(chatString); | ||
await this.redisClient.multi().rpush(`${chat.channelId}:chats`, chatString).lpop('chatQueue').exec(); | ||
} | ||
} | ||
} | ||
} catch (err) { | ||
console.log(err); | ||
} finally { | ||
await this.redisClient.del(lockKey); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { IsNotEmpty, IsUUID, IsString } from 'class-validator'; | ||
import { UUID } from 'crypto'; | ||
export class SendChatDto { | ||
@IsUUID() | ||
@IsNotEmpty() | ||
channelId: UUID; | ||
|
||
@IsString() | ||
@IsNotEmpty() | ||
message: string; | ||
} |
45 changes: 45 additions & 0 deletions
45
Backend/apps/api/src/common/filters/http-exception.filter.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { | ||
ExceptionFilter, | ||
Catch, | ||
ArgumentsHost, | ||
HttpException, | ||
HttpStatus, | ||
Inject, | ||
} from '@nestjs/common'; | ||
import { Request, Response } from 'express'; | ||
import { Logger } from 'winston'; | ||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; | ||
|
||
@Catch() | ||
export class HttpExceptionFilter implements ExceptionFilter { | ||
constructor( | ||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, | ||
) {} | ||
|
||
catch(exception: unknown, host: ArgumentsHost) { | ||
const ctx = host.switchToHttp(); | ||
|
||
const request = ctx.getRequest<Request>(); | ||
const response = ctx.getResponse<Response>(); | ||
|
||
const status = | ||
exception instanceof HttpException | ||
? exception.getStatus() | ||
: HttpStatus.INTERNAL_SERVER_ERROR; | ||
|
||
const message = | ||
exception instanceof HttpException | ||
? exception.getResponse() | ||
: exception; | ||
|
||
this.logger.error( | ||
`HTTP Status: ${status} Error Message: ${JSON.stringify(message)}`, | ||
); | ||
|
||
response.status(status).json({ | ||
statusCode: status, | ||
timestamp: new Date().toISOString(), | ||
path: request.url, | ||
}); | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
Backend/apps/api/src/common/interceptors/logging.interceptor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { | ||
CallHandler, | ||
ExecutionContext, | ||
Injectable, | ||
NestInterceptor, | ||
Inject, | ||
} from '@nestjs/common'; | ||
import { Observable } from 'rxjs'; | ||
import { tap } from 'rxjs/operators'; | ||
import { Request, Response } from 'express'; | ||
import { Logger } from 'winston'; | ||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; | ||
|
||
@Injectable() | ||
export class LoggingInterceptor implements NestInterceptor { | ||
constructor( | ||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, | ||
) {} | ||
|
||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { | ||
const now = Date.now(); | ||
|
||
const ctx = context.switchToHttp(); | ||
const request = ctx.getRequest<Request>(); | ||
const response = ctx.getResponse<Response>(); | ||
|
||
const { method, originalUrl } = request; | ||
const headers = { ...request.headers }; | ||
delete headers['authorization']; // Authorization 헤더 제외 - 보안상 | ||
|
||
return next.handle().pipe( | ||
tap(() => { | ||
const statusCode = response.statusCode; | ||
const contentLength = response.get('content-length') || '0'; | ||
|
||
this.logger.info( | ||
`${method} ${originalUrl} ${statusCode} ${contentLength} - ${ | ||
Date.now() - now | ||
}ms`, | ||
{ headers }, // 제외한 헤더를 로그에 포함 | ||
); | ||
}), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import * as winston from 'winston'; | ||
import 'winston-daily-rotate-file'; | ||
|
||
const logLevel = 'warn'; | ||
const logDir = './logs'; | ||
|
||
export const winstonConfig: winston.LoggerOptions = { | ||
level: logLevel, | ||
format: winston.format.combine( | ||
winston.format.timestamp(), | ||
winston.format.printf(({ timestamp, level, message }) => { | ||
return `${timestamp} [${level.toUpperCase()}]: ${message}`; | ||
}), | ||
), | ||
transports: [ | ||
new winston.transports.Console(), | ||
new winston.transports.DailyRotateFile({ | ||
dirname: logDir, | ||
filename: 'application-%DATE%.log', | ||
datePattern: 'YYYY-MM-DD', | ||
zippedArchive: true, | ||
maxSize: '20m', | ||
maxFiles: '14d', | ||
}), | ||
], | ||
}; |
File renamed without changes.
File renamed without changes.
File renamed without changes.
Oops, something went wrong.