diff --git a/eslint.config.mjs b/eslint.config.mjs index 7f1cc2f..e5aa415 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,17 +4,11 @@ import tseslint from 'typescript-eslint'; import prettierConfig from 'eslint-config-prettier'; import prettierRecommended from 'eslint-plugin-prettier/recommended'; -export default tseslint.config( - { - files: ['**/*.js', '**/*.mjs', '**/*.ts'], - extends: [eslint.configs.recommended, ...tseslint.configs.recommended], - rules: { - 'no-console': 'warn', - }, +export default tseslint.config({ + files: ['**/*.js', '**/*.mjs', '**/*.ts'], + extends: [eslint.configs.recommended, ...tseslint.configs.recommended, prettierRecommended], + rules: { + ...prettierConfig.rules, + 'no-console': 'warn', }, - { - files: ['**/*.js', '**/*.mjs', '**/*.ts'], - extends: [prettierRecommended], - rules: prettierConfig.rules, - }, -); +}); diff --git a/src/domain/activity/study/controller/study.admin.controller.ts b/src/domain/activity/study/controller/study.admin.controller.ts index 90c84c8..54be335 100644 --- a/src/domain/activity/study/controller/study.admin.controller.ts +++ b/src/domain/activity/study/controller/study.admin.controller.ts @@ -1,9 +1,7 @@ import { Body, Controller, Delete, Patch, Put } from '@nestjs/common'; import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; -import { AuthAdminAccount, AuthAdminAccountException, ReqMember } from '@wink/auth/guard'; - -import { Member } from '@wink/member/schema'; +import { AuthAdminAccount, AuthAdminAccountException } from '@wink/auth/guard'; import { CreateCategoryRequestDto, @@ -16,6 +14,7 @@ import { } from '@wink/activity/dto'; import { AlreadyExistsCategoryException, + AlreadyExistsStudyException, CategoryNotFoundException, StudyNotFoundException, } from '@wink/activity/exception'; @@ -65,12 +64,13 @@ export class StudyAdminController { @ApiOperation({ summary: '스터디 생성' }) @ApiProperty({ type: CreateStudyRequestDto }) @ApiCustomResponse(CreateStudyResponseDto) - @ApiCustomErrorResponse([...AuthAdminAccountException, CategoryNotFoundException]) - async createStudy( - @ReqMember() member: Member, - @Body() request: CreateStudyRequestDto, - ): Promise { - return this.studyAdminService.createStudy(member, request); + @ApiCustomErrorResponse([ + ...AuthAdminAccountException, + CategoryNotFoundException, + AlreadyExistsStudyException, + ]) + async createStudy(@Body() request: CreateStudyRequestDto): Promise { + return this.studyAdminService.createStudy(request); } @Delete() diff --git a/src/domain/activity/study/exception/already-exists-study.exception.ts b/src/domain/activity/study/exception/already-exists-study.exception.ts new file mode 100644 index 0000000..c316d0c --- /dev/null +++ b/src/domain/activity/study/exception/already-exists-study.exception.ts @@ -0,0 +1,13 @@ +import { HttpStatus } from '@nestjs/common'; + +import { ApiException } from '@wink/swagger'; + +export class AlreadyExistsStudyException extends ApiException { + constructor() { + super({ + swagger: '스터디가 이미 존재하는 경우', + message: '스터디가 이미 존재합니다.', + code: HttpStatus.CONFLICT, + }); + } +} diff --git a/src/domain/activity/study/exception/index.ts b/src/domain/activity/study/exception/index.ts index 00950df..25a8e11 100644 --- a/src/domain/activity/study/exception/index.ts +++ b/src/domain/activity/study/exception/index.ts @@ -1,4 +1,4 @@ export * from './already-exists-category.exception'; +export * from './already-exists-study.exception'; export * from './category-not-found.exception'; - export * from './study-not-found.exception'; diff --git a/src/domain/activity/study/repository/category.repository.ts b/src/domain/activity/study/repository/category.repository.ts index b54496e..0591803 100644 --- a/src/domain/activity/study/repository/category.repository.ts +++ b/src/domain/activity/study/repository/category.repository.ts @@ -22,10 +22,6 @@ export class CategoryRepository { return this.categoryModel.find().exec(); } - async findById(id: string): Promise { - return this.categoryModel.findById(id).exec(); - } - async findByName(name: string): Promise { return this.categoryModel.findOne({ name }).exec(); } diff --git a/src/domain/activity/study/repository/study.repository.ts b/src/domain/activity/study/repository/study.repository.ts index 26feb0d..051a62f 100644 --- a/src/domain/activity/study/repository/study.repository.ts +++ b/src/domain/activity/study/repository/study.repository.ts @@ -28,10 +28,6 @@ export class StudyRepository { .exec(); } - async findById(id: string): Promise { - return this.studyModel.findById(id).exec(); - } - // Delete async deleteById(id: string): Promise { await this.studyModel.deleteOne({ _id: id }).exec(); @@ -41,4 +37,8 @@ export class StudyRepository { async existsById(id: string): Promise { return !!(await this.studyModel.exists({ _id: id }).exec()); } + + async existsByLink(link: string): Promise { + return !!(await this.studyModel.exists({ link }).exec()); + } } diff --git a/src/domain/activity/study/service/study.admin.service.ts b/src/domain/activity/study/service/study.admin.service.ts index 62026cc..4668ff5 100644 --- a/src/domain/activity/study/service/study.admin.service.ts +++ b/src/domain/activity/study/service/study.admin.service.ts @@ -1,7 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Member } from '@wink/member/schema'; - import { CreateCategoryRequestDto, CreateCategoryResponseDto, @@ -13,6 +11,7 @@ import { } from '@wink/activity/dto'; import { AlreadyExistsCategoryException, + AlreadyExistsStudyException, CategoryNotFoundException, StudyNotFoundException, } from '@wink/activity/exception'; @@ -59,22 +58,24 @@ export class StudyAdminService { await this.categoryRepository.deleteById(categoryId); } - async createStudy( - member: Member, - { link }: CreateStudyRequestDto, - ): Promise { + async createStudy({ link }: CreateStudyRequestDto): Promise { + if (await this.studyRepository.existsByLink(link)) { + throw new AlreadyExistsStudyException(); + } + const { data: html } = await axios.get(link); const $ = cheerio.load(html); const title = $('meta[property="og:title"]').attr('content')!; const content = $('meta[property="og:description"]').attr('content')!; + const author = $('meta[property="og.article.author"]').attr('content')!; const image = $('meta[property="og:image"]').attr('content')!; - const uploadedAt = $('meta[property="article:published_time"]').attr('content')!; + const rawUploadedAt = $('meta[property="article:published_time"]').attr('content')!; + const uploadedAt = new Date(new Date(rawUploadedAt).getTime() + 9 * 60 * 60 * 1000); const entryInfoMatch = html.match(/window\.T\.entryInfo\s*=\s*({[^}]*});/); const entryInfo = entryInfoMatch ? JSON.parse(entryInfoMatch[1]) : null; const categoryLabel = entryInfo['categoryLabel']; - const category = await this.categoryRepository.findByName(categoryLabel); if (!category) { @@ -82,11 +83,11 @@ export class StudyAdminService { } const study: Partial = { - author: member, title, content, + author, image, - uploadedAt: new Date(uploadedAt), + uploadedAt, link, category, }; diff --git a/src/domain/member/constant/Role.ts b/src/domain/member/constant/Role.ts deleted file mode 100644 index 00b7e5b..0000000 --- a/src/domain/member/constant/Role.ts +++ /dev/null @@ -1,34 +0,0 @@ -export enum Role { - PRESIDENT = 'PRESIDENT', - VICE_PRESIDENT = 'VICE_PRESIDENT', - TREASURY_HEAD = 'TREASURY_HEAD', - TREASURY_ASSISTANT = 'TREASURY_ASSISTANT', - PUBLIC_RELATIONS_HEAD = 'PUBLIC_RELATIONS_HEAD', - PUBLIC_RELATIONS_ASSISTANT = 'PUBLIC_RELATIONS_ASSISTANT', - PLANNING_HEAD = 'PLANNING_HEAD', - PLANNING_ASSISTANT = 'PLANNING_ASSISTANT', - MEMBER = 'MEMBER', -} - -const roleHierarchy: { [key: string]: number } = { - [Role.PRESIDENT]: 1, - - [Role.VICE_PRESIDENT]: 2, - - [Role.TREASURY_HEAD]: 3, - [Role.PUBLIC_RELATIONS_HEAD]: 3, - [Role.PLANNING_HEAD]: 3, - - [Role.TREASURY_ASSISTANT]: 4, - [Role.PUBLIC_RELATIONS_ASSISTANT]: 4, - [Role.PLANNING_ASSISTANT]: 4, - - [Role.MEMBER]: 5, -}; - -export const checkRoleHierarchy = (myRole: Role, targetRole: Role): boolean => { - const myRoleIndex = roleHierarchy[myRole]; - const targetRoleIndex = roleHierarchy[targetRole]; - - return myRoleIndex < targetRoleIndex; -}; diff --git a/src/domain/member/constant/index.ts b/src/domain/member/constant/index.ts deleted file mode 100644 index 940d536..0000000 --- a/src/domain/member/constant/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Role'; diff --git a/src/domain/member/repository/member.repository.ts b/src/domain/member/repository/member.repository.ts index d163ad9..92ed673 100644 --- a/src/domain/member/repository/member.repository.ts +++ b/src/domain/member/repository/member.repository.ts @@ -36,27 +36,27 @@ export class MemberRepository { } // Update - async updatePassword(id: string, password: string): Promise { + async updatePasswordById(id: string, password: string): Promise { await this.memberModel.updateOne({ _id: id }, { password }).exec(); } - async updateDescription(id: string, description: string | null): Promise { + async updateDescriptionById(id: string, description: string | null): Promise { await this.memberModel.updateOne({ _id: id }, { description }).exec(); } - async updateGithub(id: string, githubUrl: string | null): Promise { + async updateGithubUrlById(id: string, githubUrl: string | null): Promise { await this.memberModel.updateOne({ _id: id }, { 'link.github': githubUrl }).exec(); } - async updateInstagram(id: string, instagramUrl: string | null): Promise { + async updateInstagramUrlById(id: string, instagramUrl: string | null): Promise { await this.memberModel.updateOne({ _id: id }, { 'link.instagram': instagramUrl }).exec(); } - async updateBlog(id: string, blog: string | null): Promise { + async updateBlogById(id: string, blog: string | null): Promise { await this.memberModel.updateOne({ _id: id }, { 'link.blog': blog }).exec(); } - async updateAvatar(id: string, avatar: string | null): Promise { + async updateAvatarById(id: string, avatar: string | null): Promise { await this.memberModel.updateOne({ _id: id }, { avatar }).exec(); } diff --git a/src/domain/member/service/member.service.ts b/src/domain/member/service/member.service.ts index c489120..c27de16 100644 --- a/src/domain/member/service/member.service.ts +++ b/src/domain/member/service/member.service.ts @@ -48,10 +48,10 @@ export class MemberService { ): Promise { const { _id: id } = member; - await this.memberRepository.updateDescription(id, description); - await this.memberRepository.updateGithub(id, github); - await this.memberRepository.updateInstagram(id, instagram); - await this.memberRepository.updateBlog(id, blog); + await this.memberRepository.updateDescriptionById(id, description); + await this.memberRepository.updateGithubUrlById(id, github); + await this.memberRepository.updateInstagramUrlById(id, instagram); + await this.memberRepository.updateBlogById(id, blog); this.eventEmitter.emit( UpdateMyInfoEvent.EVENT_NAME, @@ -73,7 +73,7 @@ export class MemberService { const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(newPassword, salt); - await this.memberRepository.updatePassword(id, hash); + await this.memberRepository.updatePasswordById(id, hash); this.eventEmitter.emit(UpdateMyPasswordEvent.EVENT_NAME, new UpdateMyPasswordEvent(member)); } @@ -85,7 +85,7 @@ export class MemberService { const { _id: id, avatar: original } = member; const avatar = await this.avatarService.upload(file); - await this.memberRepository.updateAvatar(id, avatar); + await this.memberRepository.updateAvatarById(id, avatar); if (original) { const key = this.avatarService.extractKeyFromUrl(original); @@ -105,7 +105,7 @@ export class MemberService { const key = this.avatarService.extractKeyFromUrl(avatar); await this.avatarService.delete(key); - await this.memberRepository.updateAvatar(id, null); + await this.memberRepository.updateAvatarById(id, null); this.eventEmitter.emit(DeleteMyAvatarEvent.EVENT_NAME, new DeleteMyAvatarEvent(member)); } diff --git a/test/mock/module/auth.mock.ts b/test/mock/module/auth.mock.ts index 64903b5..039e314 100644 --- a/test/mock/module/auth.mock.ts +++ b/test/mock/module/auth.mock.ts @@ -15,7 +15,6 @@ import { AuthService } from '@wink/auth/service'; import { MemberRepository } from '@wink/member/repository'; import { Member } from '@wink/member/schema'; -import { RedisService } from '@wink/redis'; import { MailService } from '@wink/mail'; import { ConfigService } from '@nestjs/config';