diff --git a/src/auth/oauth.service.ts b/src/auth/oauth.service.ts index 4d7b33c..08ae704 100644 --- a/src/auth/oauth.service.ts +++ b/src/auth/oauth.service.ts @@ -21,6 +21,7 @@ import { RedisService } from '../infra/redis/redis.service'; import { REFRESH_TOKEN_KEY } from './constants'; import { KakaoLoginRequest } from './dtos/request/kakao-login.request.dto'; import { KakaoLoginDto } from './dtos/kakao-login.dto'; +import { PROVIDER } from '../users/constant/provider.constant'; @Injectable() export class OAuthService { @@ -97,61 +98,56 @@ export class OAuthService { throw new BadRequestException('Please Agree to share your email'); } - let user = await this.userRepository.findOneByEmail(email); + const user = await this.userRepository.findOneByEmail(email); + if (user) { + return this.oauthLogin(user.email); + } // 회원가입인 경우 기본 카테고리 생성 작업 진행 - if (!user) { - user = new User(); - user.email = email; - user.name = userInfo.kakao_account.profile.nickname; - user.profileImage = userInfo.kakao_account.profile?.profile_image_url; - user.password = this.encodePasswordFromEmail( - email, - process.env.KAKAO_JS_KEY, - ); + const newUser = User.of({ + email, + name: userInfo.kakao_account.profile.nickname, + profileImage: userInfo.kakao_account.profile?.profile_image_url, + password: this.encodePasswordFromEmail(email, process.env.KAKAO_JS_KEY), + provider: PROVIDER.KAKAO, + }); - await this.userRepository.createOne(user); - await this.categoryRepository.createDefaultCategories(user); - } + await this.userRepository.createOne(newUser); + await this.categoryRepository.createDefaultCategories(newUser); - return this.oauthLogin(user.email); + return this.oauthLogin(newUser.email); } catch (e) { throw e; } } async createOneWithKakao({ authorizationToken }: KakaoLoginDto) { - try { - const { userInfo } = await this.oauthUtil.getKakaoUserInfo( - authorizationToken, - ); + const { userInfo } = + await this.oauthUtil.getKakaoUserInfo(authorizationToken); - const email = userInfo.kakao_account.email; - if (!email) { - throw new BadRequestException('Please Agree to share your email'); - } - - let user = await this.userRepository.findOneByEmail(email); - - // 회원가입인 경우 기본 카테고리 생성 작업 진행 - if (!user) { - user = new User(); - user.email = email; - user.name = userInfo.kakao_account.profile.nickname; - user.profileImage = userInfo.kakao_account.profile?.profile_image_url; - user.password = this.encodePasswordFromEmail( - email, - process.env.KAKAO_JS_KEY, - ); + const email = userInfo.kakao_account.email; + if (!email) { + throw new BadRequestException('Please Agree to share your email'); + } - await this.userRepository.createOne(user); - await this.categoryRepository.createDefaultCategories(user); - } + const user = await this.userRepository.findOneByEmail(email); + if (user) { return this.oauthLogin(user.email); - } catch (e) { - throw e; } + + // 회원가입인 경우 기본 카테고리 생성 작업 진행 + const newUser = User.of({ + email, + name: userInfo.kakao_account.profile.nickname, + profileImage: userInfo.kakao_account.profile?.profile_image_url, + password: this.encodePasswordFromEmail(email, process.env.KAKAO_JS_KEY), + provider: PROVIDER.KAKAO, + }); + + await this.userRepository.createOne(newUser); + await this.categoryRepository.createDefaultCategories(newUser); + return this.oauthLogin(newUser.email); } // Login with Google account info @@ -160,28 +156,28 @@ export class OAuthService { name, picture, }: googleUserInfo): Promise { - try { - let user = await this.userRepository.findOneByEmail(email); - - // 회원가입인 경우 기본 카테고리 생성 작업 진행 - if (!user) { - user = new User(); - user.email = email; - user.name = name; - user.profileImage = picture; - user.password = this.encodePasswordFromEmail( - email, - process.env.GOOGLE_CLIENT_ID, - ); - - await this.userRepository.createOne(user); - await this.categoryRepository.createDefaultCategories(user); - } + const user = await this.userRepository.findOneByEmail(email); + if (user) { return this.oauthLogin(user.email); - } catch (e) { - throw e; } + + // 회원가입인 경우 기본 카테고리 생성 작업 진행 + const newUser = User.of({ + email, + name, + profileImage: picture, + password: this.encodePasswordFromEmail( + email, + process.env.GOOGLE_CLIENT_ID, + ), + provider: PROVIDER.GOOGLE, + }); + + await this.userRepository.createOne(newUser); + await this.categoryRepository.createDefaultCategories(newUser); + + return this.oauthLogin(newUser.email); } private encodePasswordFromEmail(email: string, key?: string): string { @@ -229,21 +225,25 @@ export class OAuthService { const { sub: id, email } = this.jwtService.decode(data.id_token); - let user = await this.userRepository.findOneByEmail(email); + const user = await this.userRepository.findOneByEmail(email); - if (!user) { - user = new User(); - user.email = email; - user.name = email.split('@')[0]; - user.password = this.encodePasswordFromEmail( + if (user) { + return this.oauthLogin(user.email); + } + + const newUser = User.of({ + email, + name: email.split('@')[0], + password: this.encodePasswordFromEmail( email, process.env.APPLE_CLIENT_ID, - ); + ), + provider: PROVIDER.APPLE, + }); - await this.userRepository.createOne(user); - await this.categoryRepository.createDefaultCategories(user); - } + await this.userRepository.createOne(newUser); + await this.categoryRepository.createDefaultCategories(newUser); - return this.oauthLogin(user.email); + return this.oauthLogin(newUser.email); } } diff --git a/src/categories/category.controller.ts b/src/categories/category.controller.ts index 1c0f6f2..055ecda 100644 --- a/src/categories/category.controller.ts +++ b/src/categories/category.controller.ts @@ -157,19 +157,4 @@ export class CategoryController { ): Promise { return this.categoryService.loadFrequentCategories(user); } - - @ApiOperation({ - summary: '아티클 카테고리 자동 지정', - description: - '아티클에 적절한 카테고리를 유저의 카테고리 목록에서 찾는 메서드', - }) - @ApiBearerAuth('Authorization') - @UseGuards(JwtAuthGuard) - @Get('auto-categorize') - async autoCategorize( - @AuthUser() user: User, - @Query() { link }: AutoCategorizeRequest, - ): Promise { - return this.categoryService.autoCategorize(user, link); - } } diff --git a/src/categories/category.service.ts b/src/categories/category.service.ts index 0990023..f18d365 100644 --- a/src/categories/category.service.ts +++ b/src/categories/category.service.ts @@ -414,79 +414,6 @@ export class CategoryService { } } - async autoCategorize( - user: User, - link: string, - ): Promise { - try { - const userInDb = await this.userRepository.findOneWithCategories(user.id); - if (!userInDb) { - throw new NotFoundException('User not found'); - } - - if (!userInDb.categories) { - throw new NotFoundException('Categories not found'); - } - const categories: string[] = []; - userInDb.categories.forEach((category) => { - if (!category.parentId) { - categories.push(category.name); - } - }); - const { title, siteName, description } = await getLinkInfo(link); - - const content = await getLinkContent(link); - - const questionLines = [ - "You are a machine tasked with auto-categorizing articles based on information obtained through web scraping. You can only answer a single category name. Here is the article's information:", - ]; - - if (title) { - questionLines.push( - `The article in question is titled "${title.trim()}"`, - ); - } - - if (content) { - const contentLength = content.length / 2; - questionLines.push( - `The 150 characters of the article is, "${content - .replace(/\s/g, '') - .slice(contentLength - 150, contentLength + 150) - .trim()}"`, - ); - } - - if (description) { - questionLines.push(`The description is ${description.trim()}"`); - } - - if (siteName) { - questionLines.push(`The site's name is "${siteName.trim()}"`); - } - - // Add the category options to the end of the list - questionLines.push( - `Please provide the most suitable category among the following. Here is Category options: [${categories.join( - ', ', - )}, None]`, - ); - - // Join all lines together into a single string - const question = questionLines.join(' '); - console.log(question); - - const response = await this.openaiService.createChatCompletion({ - question, - temperature: 0, - }); - - return { category: response.choices[0].message?.content || 'None' }; - } catch (e) { - throw e; - } - } - async autoCategorizeWithId(user: User, link: string) { const _categories = await this.categoryRepository.findByUserId(user.id); if (_categories.length === 0) { @@ -539,6 +466,7 @@ Given the categories below, please provide suitable category for the article fol Present your reply options in JSON format below. +- If there's no suitable category, must provide reply with "None". \`\`\`json { "id": id, @@ -569,66 +497,4 @@ Present your reply options in JSON format below. throw e; } } - - async autoCategorizeForTest( - autoCategorizeBody: AutoCategorizeBodyDto, - ): Promise { - try { - const { link, categories } = autoCategorizeBody; - const { title, siteName, description } = await getLinkInfo(link); - - /** - * TODO: 본문 크롤링 개선 필요 - * 현재 p 태그만 크롤링하는데, 불필요한 내용이 포함되는 경우가 많음 - * 그러나 하나하나 예외 처리하는 방법을 제외하곤 방법을 못 찾은 상황 - */ - const content = await getLinkContent(link); - - const questionLines = [ - "You are a machine tasked with auto-categorizing articles based on information obtained through web scraping. You can only answer a single category name. Here is the article's information:", - ]; - - if (title) { - questionLines.push( - `The article in question is titled "${title.trim()}"`, - ); - } - - if (content) { - const contentLength = content.length / 2; - questionLines.push( - `The 150 characters of the article is, "${content - .replace(/\s/g, '') - .slice(contentLength - 150, contentLength + 150) - .trim()}"`, - ); - } - - if (description) { - questionLines.push(`The description is ${description.trim()}"`); - } - - if (siteName) { - questionLines.push(`The site's name is "${siteName.trim()}"`); - } - - // Add the category options to the end of the list - questionLines.push( - `Please provide the most suitable category among the following. Here is Category options: [${categories.join( - ', ', - )}, None]`, - ); - - // Join all lines together into a single string - const question = questionLines.join(' '); - - const response = await this.openaiService.createChatCompletion({ - question, - }); - - return { category: response.choices[0].message?.content || 'None' }; - } catch (e) { - throw e; - } - } } diff --git a/src/test/test.controller.ts b/src/test/test.controller.ts index fef8537..73ed5b8 100644 --- a/src/test/test.controller.ts +++ b/src/test/test.controller.ts @@ -7,10 +7,6 @@ import { } from '@nestjs/swagger'; import { ErrorOutput } from '../common/dtos/output.dto'; import { ContentsService } from '../contents/contents.service'; -import { - AutoCategorizeBodyDto, - AutoCategorizeOutput, -} from '../categories/dtos/category.dto'; import { SummarizeContentOutput, SummarizeContentBodyDto, @@ -43,15 +39,4 @@ export class TestController { ): Promise { return this.contentsService.testSummarizeContent(content); } - - @ApiOperation({ - summary: '아티클 카테고리 자동 지정 (테스트용)', - description: 'url을 넘기면 적절한 아티클 카테고리를 반환하는 메서드', - }) - @Post('auto-categorize') - async autoCategorize( - @Body() autoCategorizeBody: AutoCategorizeBodyDto, - ): Promise { - return this.categoryService.autoCategorizeForTest(autoCategorizeBody); - } } diff --git a/src/users/constant/provider.constant.ts b/src/users/constant/provider.constant.ts new file mode 100644 index 0000000..4d9d2af --- /dev/null +++ b/src/users/constant/provider.constant.ts @@ -0,0 +1,7 @@ +export const PROVIDER = { + GOOGLE: 'google', + KAKAO: 'kakao', + APPLE: 'apple', +} as const; + +export type PROVIDER = (typeof PROVIDER)[keyof typeof PROVIDER]; diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts index c321e14..de03870 100644 --- a/src/users/entities/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -15,6 +15,7 @@ import { Category } from '../../categories/category.entity'; import { Collection } from '../../collections/entities/collection.entity'; import { CoreEntity } from '../../common/entities/core.entity'; import { PaidPlan } from './paid-plan.entity'; +import { PROVIDER } from '../constant/provider.constant'; export enum UserRole { Client = 'Client', @@ -26,12 +27,12 @@ export class User extends CoreEntity { @ApiProperty({ example: 'tester', description: 'User Name' }) @Column() @IsString() - name!: string; + name: string; @ApiProperty({ example: 'ex@g.com', description: 'User Email' }) @Column({ unique: true }) @IsEmail() - email!: string; + email: string; @ApiProperty({ example: 'https://ex.com', description: 'User Profile Image' }) @Column({ nullable: true }) @@ -44,7 +45,7 @@ export class User extends CoreEntity { @Matches(/^(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/, { message: 'Password must be at least 8 characters long, contain 1 number', }) - password!: string; + password: string; @ApiProperty({ example: 'Client', @@ -53,12 +54,15 @@ export class User extends CoreEntity { }) @Column({ type: 'enum', enum: UserRole, default: UserRole.Client }) @IsEnum(UserRole) - role!: UserRole; + role: UserRole; + + @Column({ type: 'enum', enum: PROVIDER }) + provider: PROVIDER; @ApiProperty({ description: 'User Verified' }) @Column({ default: false }) @IsBoolean() - verified!: boolean; + verified: boolean; @ApiProperty({ description: 'User Content List', @@ -121,4 +125,27 @@ export class User extends CoreEntity { throw new InternalServerErrorException(); } } + + static of({ + email, + name, + profileImage, + password, + provider, + }: { + email: string; + name: string; + profileImage?: string; + password: string; + provider: PROVIDER; + }): User { + const user = new User(); + user.email = email; + user.name = name; + user.profileImage = profileImage; + user.password = password; + user.provider = provider; + + return user; + } }