From dc8bffb2c491f6973471735e82264517b9ecbc1d Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Sat, 10 Dec 2022 16:17:01 +0900 Subject: [PATCH 01/31] =?UTF-8?q?feat(BE):=20=EC=97=98=EB=9D=BC=EC=8A=A4?= =?UTF-8?q?=ED=8B=B1=20=EC=84=9C=EC=B9=98=20=EC=97=B0=EA=B2=B0=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/package-lock.json | 122 ++++++++++++++++++ server/package.json | 2 + server/src/domain/post/post.module.ts | 18 ++- server/src/domain/post/post.service.ts | 59 ++++++++- .../post/types/post-search-body.interface.ts | 18 +++ 5 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 server/src/domain/post/types/post-search-body.interface.ts diff --git a/server/package-lock.json b/server/package-lock.json index 316b890e..088c101b 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,10 +9,12 @@ "version": "0.1.0", "license": "UNLICENSED", "dependencies": { + "@elastic/elasticsearch": "^8.5.0", "@nestjs/axios": "^1.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", + "@nestjs/elasticsearch": "^9.0.0", "@nestjs/jwt": "^9.0.0", "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", @@ -827,6 +829,39 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@elastic/elasticsearch": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.5.0.tgz", + "integrity": "sha512-iOgr/3zQi84WmPhAplnK2W13R89VXD2oc6WhlQmH3bARQwmI+De23ZJKBEn7bvuG/AHMAqasPXX7uJIiJa2MqQ==", + "dependencies": { + "@elastic/transport": "^8.2.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@elastic/transport": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.2.0.tgz", + "integrity": "sha512-H/HmefMNQfLiBSVTmNExu2lYs5EzwipUnQB53WLr17RCTDaQX0oOLHcWpDsbKQSRhDAMPPzp5YZsZMJxuxPh7A==", + "dependencies": { + "debug": "^4.3.4", + "hpagent": "^1.0.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0", + "tslib": "^2.4.0", + "undici": "^5.1.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@elastic/transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -1616,6 +1651,16 @@ } } }, + "node_modules/@nestjs/elasticsearch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/elasticsearch/-/elasticsearch-9.0.0.tgz", + "integrity": "sha512-+NhXg0od/luNuObrCoouO8IGJ5d8esd2LbqOyX4wqM3PtzoVgQC5uDoxHu0ZyWkp/Coj7OiT0osYN9rgctEPbg==", + "peerDependencies": { + "@elastic/elasticsearch": "^7.4.0 || ^8.0.0", + "@nestjs/common": "^8.0.0 || ^9.0.0", + "rxjs": "^7.2.0" + } + }, "node_modules/@nestjs/jwt": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-9.0.0.tgz", @@ -5362,6 +5407,14 @@ "node": "*" } }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "engines": { + "node": ">=14" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8903,6 +8956,11 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/secure-json-parse": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.6.0.tgz", + "integrity": "sha512-B9osKohb6L+EZ6Kve3wHKfsAClzOC/iISA2vSuCe5Jx5NAKiwitfxx8ZKYapHXr0sYRj7UZInT7pLb3rp2Yx6A==" + }, "node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -10142,6 +10200,17 @@ "node": ">=4.2.0" } }, + "node_modules/undici": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz", + "integrity": "sha512-UDZKtwb2k7KRsK4SdXWG7ErXiL7yTGgLWvk2AXO1JMjgjh404nFo6tWSCM2xMpJwMPx3J8i/vfqEh1zOqvj82Q==", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=12.18" + } + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -11211,6 +11280,35 @@ } } }, + "@elastic/elasticsearch": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.5.0.tgz", + "integrity": "sha512-iOgr/3zQi84WmPhAplnK2W13R89VXD2oc6WhlQmH3bARQwmI+De23ZJKBEn7bvuG/AHMAqasPXX7uJIiJa2MqQ==", + "requires": { + "@elastic/transport": "^8.2.0", + "tslib": "^2.4.0" + } + }, + "@elastic/transport": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.2.0.tgz", + "integrity": "sha512-H/HmefMNQfLiBSVTmNExu2lYs5EzwipUnQB53WLr17RCTDaQX0oOLHcWpDsbKQSRhDAMPPzp5YZsZMJxuxPh7A==", + "requires": { + "debug": "^4.3.4", + "hpagent": "^1.0.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0", + "tslib": "^2.4.0", + "undici": "^5.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -11803,6 +11901,12 @@ "uuid": "9.0.0" } }, + "@nestjs/elasticsearch": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/elasticsearch/-/elasticsearch-9.0.0.tgz", + "integrity": "sha512-+NhXg0od/luNuObrCoouO8IGJ5d8esd2LbqOyX4wqM3PtzoVgQC5uDoxHu0ZyWkp/Coj7OiT0osYN9rgctEPbg==", + "requires": {} + }, "@nestjs/jwt": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-9.0.0.tgz", @@ -14668,6 +14772,11 @@ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" }, + "hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==" + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -17290,6 +17399,11 @@ } } }, + "secure-json-parse": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.6.0.tgz", + "integrity": "sha512-B9osKohb6L+EZ6Kve3wHKfsAClzOC/iISA2vSuCe5Jx5NAKiwitfxx8ZKYapHXr0sYRj7UZInT7pLb3rp2Yx6A==" + }, "semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -18119,6 +18233,14 @@ "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "devOptional": true }, + "undici": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.13.0.tgz", + "integrity": "sha512-UDZKtwb2k7KRsK4SdXWG7ErXiL7yTGgLWvk2AXO1JMjgjh404nFo6tWSCM2xMpJwMPx3J8i/vfqEh1zOqvj82Q==", + "requires": { + "busboy": "^1.6.0" + } + }, "unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", diff --git a/server/package.json b/server/package.json index 270ff1b6..ba63f3b1 100644 --- a/server/package.json +++ b/server/package.json @@ -22,10 +22,12 @@ "prepare": "chmod ug+x .husky/* && cd .. && husky install server/.husky" }, "dependencies": { + "@elastic/elasticsearch": "^8.5.0", "@nestjs/axios": "^1.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", + "@nestjs/elasticsearch": "^9.0.0", "@nestjs/jwt": "^9.0.0", "@nestjs/passport": "^9.0.0", "@nestjs/platform-express": "^9.0.0", diff --git a/server/src/domain/post/post.module.ts b/server/src/domain/post/post.module.ts index 6d13d401..68c534bc 100644 --- a/server/src/domain/post/post.module.ts +++ b/server/src/domain/post/post.module.ts @@ -15,6 +15,7 @@ import { UserService } from '../user/user.service'; import { SearchHistoryMongoRepository } from '../search/search-history.mongo.repository'; import { BookmarkService } from '../bookmark/bookmark.service'; import { BookmarkRepository } from '../bookmark/bookmark.repository'; +import { ElasticsearchModule } from '@nestjs/elasticsearch'; @Module({ controllers: [PostController], @@ -33,7 +34,20 @@ import { BookmarkRepository } from '../bookmark/bookmark.repository'; SearchHistoryMongoRepository, PostSubscriber, ], - imports: [HttpModule, JwtModule.register({})], // TODO App으로 올릴지 이야기하기 - exports: [PostService, PostRepository], + imports: [ + HttpModule, + JwtModule.register({}), + ElasticsearchModule.registerAsync({ + useFactory: () => ({ + // TODO env로 이동 + node: 'http://localhost:9200', + maxRetries: 10, + requestTimeout: 60000, + pingTimeout: 60000, + sniffOnStart: true, + }), + }), + ], // TODO App으로 올릴지 이야기하기 + exports: [PostService, PostRepository, ElasticsearchModule], }) export class PostModule {} diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index 7feb3e72..d6429667 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -13,6 +13,7 @@ import { UserNotFoundException } from 'src/exception/user-not-found.exception'; import { UserRepository } from '../user/user.repository'; import { UserNotSameException } from '../../exception/user-not-same.exception'; import { PostNotFoundException } from '../../exception/post-not-found.exception'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; @Injectable() export class PostService { @@ -21,6 +22,7 @@ export class PostService { private readonly postToTagRepository: PostToTagRepository, private readonly tagRepository: TagRepository, private readonly userRepository: UserRepository, + private readonly esService: ElasticsearchService, ) {} async write( @@ -42,7 +44,7 @@ export class PostService { }); const postToTagEntities = await this.toPostToTagEntities(tags); - const postEntity = new Post(); + let postEntity: Post = new Post(); postEntity.title = title; postEntity.content = content; postEntity.code = code; @@ -51,7 +53,21 @@ export class PostService { postEntity.user = userEntity; postEntity.images = imageEntities; postEntity.postToTags = postToTagEntities; - await this.postRepository.save(postEntity); + + postEntity = await this.postRepository.save(postEntity); + + const x = await this.esService.index({ + index: 'test', //TODO 이름 임시 + body: { + id: postEntity.id, + title: postEntity.title, + content: postEntity.content, + language: postEntity.language, + authorId: postEntity.user.id, + authorNickname: postEntity.user.nickname, + tags: tags.join(' '), + }, + }); return postEntity.id; } @@ -87,6 +103,45 @@ export class PostService { loadPostListRequestDto; let isLast = true; + // tags 직렬화 + // details 직렬화 + // TODO 조건이 비어있을 때 사용 안하는 방법 있을까. 없을거같은데 + const body = await this.esService.search({ + index: 'test', // TODO 최신순 3개 + body: { + query: { + bool: { + filter: { + bool: { + must: [ + { + match: { + tags: tags.join(' '), + }, + }, + { + multi_match: { + query: details.join(' '), + fields: [ + 'title', + 'content', + 'language', + 'authorNickname', + 'tags', + ], + }, + }, + ], + }, + }, + }, + }, + }, + }); + + const hits = body.hits.hits; + console.log('hits', hits); + const postIdsFiltered = await this.filter( tags, reviewCount, diff --git a/server/src/domain/post/types/post-search-body.interface.ts b/server/src/domain/post/types/post-search-body.interface.ts new file mode 100644 index 00000000..a207aa96 --- /dev/null +++ b/server/src/domain/post/types/post-search-body.interface.ts @@ -0,0 +1,18 @@ +interface PostSearchBody { + id: number; + title: string; + content: string; + language: string; + authorId: number; + authorNickname: string; + tags: string; +} + +interface PostSearchResult { + hits: { + total: number; + hits: Array<{ + _source: PostSearchBody; + }>; + }; +} From 9ae8e3529f4580f5c9174fd08fa01bc1c83b5096 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Sat, 10 Dec 2022 23:00:05 +0900 Subject: [PATCH 02/31] =?UTF-8?q?feat(BE):=20=EC=9E=85=EB=A0=A5=EB=B0=9B?= =?UTF-8?q?=EC=9D=80=20=EB=AC=B8=EC=9E=90=EB=93=A4=EC=9D=84=20=ED=86=B5?= =?UTF-8?q?=ED=95=9C=20elastic=20=EA=B2=80=EC=83=89=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/post/post-search.service.ts | 104 +++++++++++ server/src/domain/post/post.module.ts | 2 + server/src/domain/post/post.service.ts | 172 ++---------------- .../post/types/post-search-body.interface.ts | 8 +- 4 files changed, 126 insertions(+), 160 deletions(-) create mode 100644 server/src/domain/post/post-search.service.ts diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts new file mode 100644 index 00000000..10032459 --- /dev/null +++ b/server/src/domain/post/post-search.service.ts @@ -0,0 +1,104 @@ +import { Injectable } from '@nestjs/common'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { Post } from './post.entity'; +import { LoadPostListRequestDto } from './dto/service-request.dto'; +import { SEND_POST_CNT } from './post.controller'; + +@Injectable() +export class PostSearchService { + constructor(private readonly esService: ElasticsearchService) {} + + async indexPost(post: Post, tags: any) { + // 태그를 정렬해 넣는다. + let tagValue = ''; + if (tags) { + tags.sort(); + tagValue = tags.join(' '); + } + const x = await this.esService.index({ + index: 'test', //TODO 이름 임시 + body: { + id: post.id, + title: post.title, + content: post.content, + code: post.code, + language: post.language, + createdAt: post.createdAt, + updatedAt: post.updatedAt, + authorId: post.user.id, + authorNickname: post.user.nickname, + tags: tagValue, + lineCount: post.lineCount, + }, + }); + console.log('indexPost ', x); + } + + async search(loadPostListRequestDto: LoadPostListRequestDto) { + const { lastId, tags, reviewCount, likeCount, details } = + loadPostListRequestDto; + const searchFilter: any = { + sort: [ + { + createdAt: { + order: 'desc', + }, + }, + ], + size: SEND_POST_CNT + 1, + index: 'test', // TODO 최신순 3개 + body: { + query: { + bool: { + filter: { + bool: { + must: [], + }, + }, + }, + }, + }, + }; + if (lastId !== -1) { + searchFilter.from = lastId; + } + + if (details && details.length > 0) { + // const q = details.length == 1 ? details : details.join(' '); + // console.log(q); + for (const detail of details) { + searchFilter.body.query.bool.filter.bool.must.push({ + multi_match: { + query: detail, + fields: [ + 'title', + 'content', + 'code', + 'language', + 'authorNickname', + 'tags', + ], + }, + }); + } + } + if (tags && tags.length > 0) { + const q = tags.length == 1 ? tags : tags.join(' '); + searchFilter.body.query.bool.filter.bool.must.push({ + match: { + tags: { + query: q, + operator: 'AND', + }, + }, + }); + } + + const body = await this.esService.search(searchFilter); + // TODO 이따 searchFilter로 내용 갈아끼기 + const result = body.hits.hits; + console.log('hits', result); //결과 + + return result; + } +} diff --git a/server/src/domain/post/post.module.ts b/server/src/domain/post/post.module.ts index 68c534bc..2a180e5f 100644 --- a/server/src/domain/post/post.module.ts +++ b/server/src/domain/post/post.module.ts @@ -16,6 +16,7 @@ import { SearchHistoryMongoRepository } from '../search/search-history.mongo.rep import { BookmarkService } from '../bookmark/bookmark.service'; import { BookmarkRepository } from '../bookmark/bookmark.repository'; import { ElasticsearchModule } from '@nestjs/elasticsearch'; +import { PostSearchService } from './post-search.service'; @Module({ controllers: [PostController], @@ -25,6 +26,7 @@ import { ElasticsearchModule } from '@nestjs/elasticsearch'; AuthService, UserService, BookmarkService, + PostSearchService, PostRepository, TagRepository, PostToTagRepository, diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index d6429667..8f005fad 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -13,7 +13,7 @@ import { UserNotFoundException } from 'src/exception/user-not-found.exception'; import { UserRepository } from '../user/user.repository'; import { UserNotSameException } from '../../exception/user-not-same.exception'; import { PostNotFoundException } from '../../exception/post-not-found.exception'; -import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { PostSearchService } from './post-search.service'; @Injectable() export class PostService { @@ -22,7 +22,7 @@ export class PostService { private readonly postToTagRepository: PostToTagRepository, private readonly tagRepository: TagRepository, private readonly userRepository: UserRepository, - private readonly esService: ElasticsearchService, + private readonly postSearchService: PostSearchService, ) {} async write( @@ -55,19 +55,7 @@ export class PostService { postEntity.postToTags = postToTagEntities; postEntity = await this.postRepository.save(postEntity); - - const x = await this.esService.index({ - index: 'test', //TODO 이름 임시 - body: { - id: postEntity.id, - title: postEntity.title, - content: postEntity.content, - language: postEntity.language, - authorId: postEntity.user.id, - authorNickname: postEntity.user.nickname, - tags: tags.join(' '), - }, - }); + this.postSearchService.indexPost(postEntity, tags); return postEntity.id; } @@ -93,117 +81,6 @@ export class PostService { return Promise.all(postToTagEntityPromises); } - /** - * 게시물을 조회한다 - */ - async loadPostList( - loadPostListRequestDto: LoadPostListRequestDto, - ): Promise { - const { lastId, tags, reviewCount, likeCount, details } = - loadPostListRequestDto; - let isLast = true; - - // tags 직렬화 - // details 직렬화 - // TODO 조건이 비어있을 때 사용 안하는 방법 있을까. 없을거같은데 - const body = await this.esService.search({ - index: 'test', // TODO 최신순 3개 - body: { - query: { - bool: { - filter: { - bool: { - must: [ - { - match: { - tags: tags.join(' '), - }, - }, - { - multi_match: { - query: details.join(' '), - fields: [ - 'title', - 'content', - 'language', - 'authorNickname', - 'tags', - ], - }, - }, - ], - }, - }, - }, - }, - }, - }); - - const hits = body.hits.hits; - console.log('hits', hits); - - const postIdsFiltered = await this.filter( - tags, - reviewCount, - likeCount, - details, - ); - const result = await this.postRepository.findByIdWithFilterResult( - lastId, - postIdsFiltered, - ); - - if (this.canGetNextPost(result.length)) { - result.pop(); - isLast = false; - } - return new LoadPostListResponseDto(result, isLast); - } - - private canGetNextPost(resultCnt: number) { - return resultCnt === SEND_POST_CNT + 1; - } - - private async filter( - tags: string[], - reviewCount: number, - likeCount: number, - details: string[], - ) { - const postsThatPassEachFilter = await Promise.all([ - this.postRepository.findByIdLikesCntGreaterThanOrEqual(likeCount), - this.postToTagRepository.findByContainingTags(tags), - this.postRepository.findByReviewCntGreaterThanOrEqual(reviewCount), - this.filterUsingDetails(details), - ]); - return this.mergeFilterResult(postsThatPassEachFilter); - } - - /** - * 사용된 검색 조건이 한개도 없으면 -> null을 반환 - * 조건을 만족시키는 사용자가 한 명도 없으면 -> 비어있는 배열 []을 반환 - * 배열 안에 값이 있다면 -> 조건을 만족하는 사용자들의 id 리스트를 반환 - */ - public mergeFilterResult(postInfos: any[]) { - let result; - for (const postInfo of postInfos) { - if (postInfo === null) { - continue; - } - if (result === undefined) { - result = postInfo.map((obj) => obj.postId); - } else { - result = postInfo - .map((obj) => obj.postId) - .filter((each) => result.includes(each)); - } - } - if (!result) { - return null; - } - return result; - } - async delete(userId: number, postId: number) { const post = await this.postRepository.findOne({ where: { @@ -233,42 +110,19 @@ export class PostService { return new LoadPostListResponseDto([post], true); } - private async filterUsingDetails(details: string[]) { - if (!details || details.length < 1) { - return null; - } - const postsFilteringEachDetail = await Promise.all( - details.map((detail) => this.postRepository.filterUsingDetail(detail)), - ); + async loadPostList(loadPostListRequestDto: LoadPostListRequestDto) { + let isLast = true; + const result = await this.postSearchService.search(loadPostListRequestDto); - const temp = []; - for (const posts of postsFilteringEachDetail) { - temp.push(posts.map((post) => post.postId)); + if (this.canGetNextPost(result.length)) { + result.pop(); + isLast = false; } - return this.findIntersection(temp); + // result + image와 Author 정보를 붙인다 + return new LoadPostListResponseDto([], isLast); // TODO elastic에서 가져온 값 넣기 } - private findIntersection(bundles: any[][]) { - const result = []; - let smallestBundle = bundles[0]; - for (const post of bundles) { - if (smallestBundle.length > post.length) { - smallestBundle = post; - } - } - - for (const postId of smallestBundle) { - let isIntersect = true; - for (const bundle of bundles) { - if (!bundle.includes(postId)) { - isIntersect = false; - break; - } - } - if (isIntersect) { - result.push({ postId: postId }); - } - } - return result; + private canGetNextPost(resultCnt: number) { + return resultCnt === SEND_POST_CNT + 1; } } diff --git a/server/src/domain/post/types/post-search-body.interface.ts b/server/src/domain/post/types/post-search-body.interface.ts index a207aa96..8c66a6b2 100644 --- a/server/src/domain/post/types/post-search-body.interface.ts +++ b/server/src/domain/post/types/post-search-body.interface.ts @@ -2,10 +2,16 @@ interface PostSearchBody { id: number; title: string; content: string; + code: string; language: string; - authorId: number; + // TODO 이미지 url은 필요없음. 가져올거임 + createdAt: Date; + updatedAt: Date; + authorId: number; // TODO Author 관해서도 가져올거임 authorNickname: string; tags: string; + // likesCount: number; + lineCount: number; } interface PostSearchResult { From f6eccf08965ca269bca88cfbeb880264725ed37c Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Mon, 12 Dec 2022 11:02:36 +0900 Subject: [PATCH 03/31] =?UTF-8?q?refactor(BE):=20=EB=B3=91=ED=95=A9?= =?UTF-8?q?=EC=8B=9C=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/image/image.entity.ts | 3 + server/src/domain/image/image.repository.ts | 10 +++ .../post/dto/controller-response.dto.ts | 79 +++++++++++-------- server/src/domain/post/post-search.service.ts | 35 +++++--- server/src/domain/post/post.controller.ts | 34 +++----- server/src/domain/post/post.entity.ts | 12 +++ server/src/domain/post/post.module.ts | 2 + server/src/domain/post/post.service.ts | 45 +++++++++-- .../post/types/post-search-body.interface.ts | 2 +- 9 files changed, 147 insertions(+), 75 deletions(-) create mode 100644 server/src/domain/image/image.repository.ts diff --git a/server/src/domain/image/image.entity.ts b/server/src/domain/image/image.entity.ts index 6929f20c..d3cbd215 100644 --- a/server/src/domain/image/image.entity.ts +++ b/server/src/domain/image/image.entity.ts @@ -10,6 +10,9 @@ export class Image extends BaseTimeEntity { @ManyToOne(() => Post, (post) => post.images) post: Post; + @Column() + postId: number; + @Column({ length: 2000 }) src!: string; } diff --git a/server/src/domain/image/image.repository.ts b/server/src/domain/image/image.repository.ts new file mode 100644 index 00000000..560d1ead --- /dev/null +++ b/server/src/domain/image/image.repository.ts @@ -0,0 +1,10 @@ +import { DataSource, Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { Image } from './image.entity'; + +@Injectable() +export class ImageRepository extends Repository { + constructor(private dataSource: DataSource) { + super(Image, dataSource.createEntityManager()); + } +} diff --git a/server/src/domain/post/dto/controller-response.dto.ts b/server/src/domain/post/dto/controller-response.dto.ts index 2f0a6428..b1a30adf 100644 --- a/server/src/domain/post/dto/controller-response.dto.ts +++ b/server/src/domain/post/dto/controller-response.dto.ts @@ -1,44 +1,59 @@ -export class InquiryUsingFilterDto { - posts: PostDtoUsingInquiryUsingFilter[]; +import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import { AuthorDto, EachImageResponseDto } from './service-response.dto'; +import { Image } from '../../image/image.entity'; +import { User } from '../../user/user.entity'; + +export class SearchResponseDto { + posts: EachSearchResponseDto[]; lastId: number; isLast: boolean; + + constructor( + posts: SearchHit[], + authors: User[], + imagesList: Image[][], + isLast: boolean, + ) { + this.posts = []; + for (let i = 0; i < posts.length; i++) { + this.posts.push( + new EachSearchResponseDto(posts[i], authors[i], imagesList[i]), + ); + } + + this.lastId = posts.length == 0 ? -1 : Number(posts.slice(-1)[0]._id); + this.isLast = isLast; + } } -export class PostDtoUsingInquiryUsingFilter { +export class EachSearchResponseDto { id: number; title: string; content: string; code: string; language: string; - images: ImageDtoUsingInquiryUsingFilter[]; - updatedAt: string; - author: AuthorDtoUsingInquiryUsingFilter[]; + images: EachImageResponseDto[]; + updatedAt: Date; + author: AuthorDto; tags: string[]; - reviews: ReviewDtoUsingInquiryUsingFilter[]; -} - -export class ImageDtoUsingInquiryUsingFilter { - src: string; - name: string; -} - -export class AuthorDtoUsingInquiryUsingFilter { - id: number; - nickname: string; - profileUrl: string; - email: string; -} + isLiked: boolean; + isBookmarked: boolean; + likesCount: number; + lineCount: number; -export class ReviewDtoUsingInquiryUsingFilter { - id: number; - reviewers: ReviewerDtoUsingInquiryUsingFilter[]; - content: string; - updatedAt: string; -} - -export class ReviewerDtoUsingInquiryUsingFilter { - id: number; - nickname: string; - profileUrl: string; - email: string; + constructor(post: any, author, images) { + this.id = Number(post._id); + this.title = post._source.title; + this.content = post._source.content; + this.code = post._source.code; + this.language = post._source.language; + this.updatedAt = post._source.updatedat; + this.tags = JSON.parse(post._source.tags); + this.isLiked = false; + this.isBookmarked = false; + this.likesCount = post._source.likecount; + this.lineCount = post._source.linecount; + this.author = new AuthorDto(author); + this.images = images.map((image) => new EachImageResponseDto(image)); + } } diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index 10032459..bae6d685 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -15,8 +15,10 @@ export class PostSearchService { tags.sort(); tagValue = tags.join(' '); } - const x = await this.esService.index({ + + this.esService.index({ index: 'test', //TODO 이름 임시 + id: String(post.id), body: { id: post.id, title: post.title, @@ -29,9 +31,9 @@ export class PostSearchService { authorNickname: post.user.nickname, tags: tagValue, lineCount: post.lineCount, + likeCount: post.likeCount, }, }); - console.log('indexPost ', x); } async search(loadPostListRequestDto: LoadPostListRequestDto) { @@ -40,7 +42,7 @@ export class PostSearchService { const searchFilter: any = { sort: [ { - createdAt: { + id: { order: 'desc', }, }, @@ -64,8 +66,6 @@ export class PostSearchService { } if (details && details.length > 0) { - // const q = details.length == 1 ? details : details.join(' '); - // console.log(q); for (const detail of details) { searchFilter.body.query.bool.filter.bool.must.push({ multi_match: { @@ -93,12 +93,25 @@ export class PostSearchService { }, }); } - + if (reviewCount && reviewCount >= 1) { + searchFilter.body.query.bool.filter.bool.must.push({ + range: { + reviewCount: { + gte: reviewCount, + }, + }, + }); + } + if (likeCount && likeCount >= 1) { + searchFilter.body.query.bool.filter.bool.must.push({ + range: { + likeCount: { + gte: likeCount, + }, + }, + }); + } const body = await this.esService.search(searchFilter); - // TODO 이따 searchFilter로 내용 갈아끼기 - const result = body.hits.hits; - console.log('hits', result); //결과 - - return result; + return body.hits.hits; } } diff --git a/server/src/domain/post/post.controller.ts b/server/src/domain/post/post.controller.ts index 0362b5a8..0c78f342 100644 --- a/server/src/domain/post/post.controller.ts +++ b/server/src/domain/post/post.controller.ts @@ -17,10 +17,6 @@ import { } from '@nestjs/common'; import { Request } from 'express'; import { PostService } from './post.service'; -import { - EachPostResponseDto, - LoadPostListResponseDto, -} from './dto/service-response.dto'; import { InquiryDto, InquiryPostDto, @@ -47,6 +43,7 @@ import { UserService } from '../user/user.service'; import { BookmarkService } from '../bookmark/bookmark.service'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; +import { SearchResponseDto } from './dto/controller-response.dto'; import { BookmarkCreateRequestDto } from '../bookmark/dto/controller-request.dto'; export const SEND_POST_CNT = 3; @@ -75,14 +72,18 @@ export class PostController { async search( @Query() inquiryDto: InquiryDto, @Headers() headers, - ): Promise { + ): Promise { const { tags, lastId, reviewCount, likeCount, details } = inquiryDto; const returnValue = await this.postService.loadPostList( new LoadPostListRequestDto(lastId, tags, reviewCount, likeCount, details), ); - await this.addLikesCntColumnEveryPosts(returnValue); + + const posts = returnValue.posts; + + // TODO 로그인 시 해당 작업 잘 동작하나 검사하기 await this.addLikesToPostIfLogin(headers['authorization'], returnValue); await this.addBookmarksToPostIfLogin(headers['authorization'], returnValue); + await this.addSearchHistory(headers['authorization'], inquiryDto); await this.addSearchHistory(headers['authorization'], inquiryDto); @@ -91,9 +92,10 @@ export class PostController { return returnValue; } - private async addLikesToPostIfLogin(token, result: LoadPostListResponseDto) { + private async addLikesToPostIfLogin(token, result: SearchResponseDto) { if (token) { const userId = this.authService.authenticate(token); + if (userId) { const postIdsYouLike = await this.likesService.findPostIdsByUserId( userId, @@ -107,10 +109,7 @@ export class PostController { } } - private async addBookmarksToPostIfLogin( - token, - result: LoadPostListResponseDto, - ) { + private async addBookmarksToPostIfLogin(token, result: SearchResponseDto) { if (token) { const userId = this.authService.authenticate(token); @@ -127,17 +126,6 @@ export class PostController { } } - private async addLikesCntColumnEveryPosts(result: LoadPostListResponseDto) { - const ary = []; - for (const post of result.posts) { - ary.push(this.likesService.countLikesCntByPostId(post.id)); - } - const likesCntStore = await Promise.all(ary); - for (let i = 0; i < result.posts.length; i++) { - result.posts[i].likesCount = likesCntStore[i]; - } - } - private async addSearchHistory(token, inquiryDto: InquiryDto) { if (!token) { return; @@ -225,7 +213,7 @@ export class PostController { async inquiryPost( @Param('postId') postId: number, @Headers() header, - @Query() requestDto: InquiryPostDto, + @Query() requestDto: InquiryPostDto, //TODO 살펴보기 ) { try { const returnValue = await this.postService.inquiryPost(postId); diff --git a/server/src/domain/post/post.entity.ts b/server/src/domain/post/post.entity.ts index f30a6716..17951a56 100644 --- a/server/src/domain/post/post.entity.ts +++ b/server/src/domain/post/post.entity.ts @@ -40,4 +40,16 @@ export class Post extends BaseTimeEntity { cascade: ['insert'], }) postToTags: PostToTag[]; + + @Column({ default: 0 }) + likeCount: number; + + @Column({ default: 0 }) + commentCount: number; + + @Column({ length: 255, default: '[]' }) + tags: string; + + @Column({ length: 24, default: '' }) + userNickname: string; } diff --git a/server/src/domain/post/post.module.ts b/server/src/domain/post/post.module.ts index 2a180e5f..d49ddf35 100644 --- a/server/src/domain/post/post.module.ts +++ b/server/src/domain/post/post.module.ts @@ -17,6 +17,7 @@ import { BookmarkService } from '../bookmark/bookmark.service'; import { BookmarkRepository } from '../bookmark/bookmark.repository'; import { ElasticsearchModule } from '@nestjs/elasticsearch'; import { PostSearchService } from './post-search.service'; +import { ImageRepository } from '../image/image.repository'; @Module({ controllers: [PostController], @@ -35,6 +36,7 @@ import { PostSearchService } from './post-search.service'; LikesRepository, SearchHistoryMongoRepository, PostSubscriber, + ImageRepository, ], imports: [ HttpModule, diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index 8f005fad..e394da4c 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -14,6 +14,8 @@ import { UserRepository } from '../user/user.repository'; import { UserNotSameException } from '../../exception/user-not-same.exception'; import { PostNotFoundException } from '../../exception/post-not-found.exception'; import { PostSearchService } from './post-search.service'; +import { SearchResponseDto } from './dto/controller-response.dto'; +import { ImageRepository } from '../image/image.repository'; @Injectable() export class PostService { @@ -23,12 +25,15 @@ export class PostService { private readonly tagRepository: TagRepository, private readonly userRepository: UserRepository, private readonly postSearchService: PostSearchService, + private readonly imageRepository: ImageRepository, ) {} async write( userId: number, { title, content, code, language, lineCount, images, tags }, ) { + tags.sort(); + const userEntity = await this.userRepository.findOneBy({ id: userId, }); @@ -53,9 +58,11 @@ export class PostService { postEntity.user = userEntity; postEntity.images = imageEntities; postEntity.postToTags = postToTagEntities; + postEntity.tags = JSON.stringify(tags); + postEntity.userNickname = userEntity.nickname; postEntity = await this.postRepository.save(postEntity); - this.postSearchService.indexPost(postEntity, tags); + this.postSearchService.indexPost(postEntity, tags); // TODO 데이터 더미로 넣을때 주석처리. 배치로 넣는게 더 빠름 return postEntity.id; } @@ -110,16 +117,38 @@ export class PostService { return new LoadPostListResponseDto([post], true); } - async loadPostList(loadPostListRequestDto: LoadPostListRequestDto) { + async loadPostList( + loadPostListRequestDto: LoadPostListRequestDto, + ): Promise { let isLast = true; - const result = await this.postSearchService.search(loadPostListRequestDto); - - if (this.canGetNextPost(result.length)) { - result.pop(); + const results = await this.postSearchService.search(loadPostListRequestDto); + results.reverse(); + if (this.canGetNextPost(results.length)) { + results.pop(); isLast = false; } - // result + image와 Author 정보를 붙인다 - return new LoadPostListResponseDto([], isLast); // TODO elastic에서 가져온 값 넣기 + + const authors = []; + const images = []; + + for (const result of results) { + authors.push( + this.userRepository.findOneBy({ + id: result._source['authorId'], + }), + ); + images.push( + this.imageRepository.findBy({ + postId: Number(result._id), + }), + ); + } + return new SearchResponseDto( + results, + await Promise.all(authors), + await Promise.all(images), + isLast, + ); } private canGetNextPost(resultCnt: number) { diff --git a/server/src/domain/post/types/post-search-body.interface.ts b/server/src/domain/post/types/post-search-body.interface.ts index 8c66a6b2..0021290b 100644 --- a/server/src/domain/post/types/post-search-body.interface.ts +++ b/server/src/domain/post/types/post-search-body.interface.ts @@ -10,7 +10,7 @@ interface PostSearchBody { authorId: number; // TODO Author 관해서도 가져올거임 authorNickname: string; tags: string; - // likesCount: number; + likeCount: number; lineCount: number; } From f222f171878181446a786e3b68e8fb25d9fa7a15 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Sun, 11 Dec 2022 22:42:00 +0900 Subject: [PATCH 04/31] =?UTF-8?q?refactor(BE):=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94,=20=EB=8C=93=EA=B8=80=EA=B3=BC=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EB=AC=BC=20=EA=B0=84=20=EC=A0=95=ED=95=A9=EC=84=B1=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=94=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/likes/likes.service.ts | 10 +++++++ .../post/dto/controller-response.dto.ts | 12 ++++++++- server/src/domain/post/post-search.service.ts | 1 + server/src/domain/post/post.controller.ts | 2 -- server/src/domain/post/post.entity.ts | 2 +- server/src/domain/post/post.repository.ts | 27 +++++++++++++++++++ .../post/types/post-search-body.interface.ts | 1 + server/src/domain/review/review.service.ts | 1 + 8 files changed, 52 insertions(+), 4 deletions(-) diff --git a/server/src/domain/likes/likes.service.ts b/server/src/domain/likes/likes.service.ts index 4f03af1b..c2458c5f 100644 --- a/server/src/domain/likes/likes.service.ts +++ b/server/src/domain/likes/likes.service.ts @@ -29,13 +29,23 @@ export class LikesService { likes.user = user; likes.post = post; await this.likesRepository.save(likes); + this.postRepository.increaseLikeCount(post); } async cancelLikes(userId: number, postId: number) { + const [user, post] = await Promise.all([ + this.userRepository.findOneBy({ id: userId }), + this.postRepository.findOneBy({ id: postId }), + ]); + if (post === null || user === null) { + return; + } + await this.likesRepository.delete({ userId: userId, postId: postId, }); + this.postRepository.decreaseLikeCount(post); } async findPostIdsByUserId(userId: number) { diff --git a/server/src/domain/post/dto/controller-response.dto.ts b/server/src/domain/post/dto/controller-response.dto.ts index b1a30adf..bddf250e 100644 --- a/server/src/domain/post/dto/controller-response.dto.ts +++ b/server/src/domain/post/dto/controller-response.dto.ts @@ -42,13 +42,23 @@ export class EachSearchResponseDto { lineCount: number; constructor(post: any, author, images) { + let tags; + try { + tags = JSON.parse(post._source.tags); + } catch (e) { + if (post._source.tags.length > 0) { + tags = [post._source.tags]; + } else { + tags = []; + } + } this.id = Number(post._id); this.title = post._source.title; this.content = post._source.content; this.code = post._source.code; this.language = post._source.language; this.updatedAt = post._source.updatedat; - this.tags = JSON.parse(post._source.tags); + this.tags = tags; this.isLiked = false; this.isBookmarked = false; this.likesCount = post._source.likecount; diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index bae6d685..c38caab3 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -31,6 +31,7 @@ export class PostSearchService { authorNickname: post.user.nickname, tags: tagValue, lineCount: post.lineCount, + reviewCount: post.reviewCount, likeCount: post.likeCount, }, }); diff --git a/server/src/domain/post/post.controller.ts b/server/src/domain/post/post.controller.ts index 0c78f342..7b3f8c59 100644 --- a/server/src/domain/post/post.controller.ts +++ b/server/src/domain/post/post.controller.ts @@ -78,8 +78,6 @@ export class PostController { new LoadPostListRequestDto(lastId, tags, reviewCount, likeCount, details), ); - const posts = returnValue.posts; - // TODO 로그인 시 해당 작업 잘 동작하나 검사하기 await this.addLikesToPostIfLogin(headers['authorization'], returnValue); await this.addBookmarksToPostIfLogin(headers['authorization'], returnValue); diff --git a/server/src/domain/post/post.entity.ts b/server/src/domain/post/post.entity.ts index 17951a56..ab6733f0 100644 --- a/server/src/domain/post/post.entity.ts +++ b/server/src/domain/post/post.entity.ts @@ -45,7 +45,7 @@ export class Post extends BaseTimeEntity { likeCount: number; @Column({ default: 0 }) - commentCount: number; + reviewCount: number; @Column({ length: 255, default: '[]' }) tags: string; diff --git a/server/src/domain/post/post.repository.ts b/server/src/domain/post/post.repository.ts index 720c5049..a9c6b09f 100644 --- a/server/src/domain/post/post.repository.ts +++ b/server/src/domain/post/post.repository.ts @@ -119,4 +119,31 @@ export class PostRepository extends Repository { .where('post.id = :postId', { postId: postId }) .getOne(); } + + increaseLikeCount(post: Post) { + this.createQueryBuilder() + .update(Post) + .set({ likeCount: () => 'likeCount + 1' }) + .where('id=:id', { id: post.id }) + .execute(); + } + + decreaseLikeCount(post: Post) { + if (post.likeCount <= 0) { + return; + } + this.createQueryBuilder() + .update(Post) + .set({ likeCount: () => 'likeCount - 1' }) + .where('id=:id', { id: post.id }) + .execute(); + } + + increaseReviewCount(post: Post) { + this.createQueryBuilder() + .update(Post) + .set({ reviewCount: () => 'reviewCount + 1' }) + .where('id=:id', { id: post.id }) + .execute(); + } } diff --git a/server/src/domain/post/types/post-search-body.interface.ts b/server/src/domain/post/types/post-search-body.interface.ts index 0021290b..87943ee0 100644 --- a/server/src/domain/post/types/post-search-body.interface.ts +++ b/server/src/domain/post/types/post-search-body.interface.ts @@ -11,6 +11,7 @@ interface PostSearchBody { authorNickname: string; tags: string; likeCount: number; + reviewCount: number; lineCount: number; } diff --git a/server/src/domain/review/review.service.ts b/server/src/domain/review/review.service.ts index 510e93f0..8f4215af 100644 --- a/server/src/domain/review/review.service.ts +++ b/server/src/domain/review/review.service.ts @@ -50,6 +50,7 @@ export class ReviewService { reviewEntity.content = content; await this.reviewRepository.insert(reviewEntity); + this.postRepository.increaseReviewCount(postEntity); } async getReviewsOfPost(postId: number, { lastId }: ReviewGetAllRequestDto) { From 1b8ce339fda05589a6ca15ce81c2fe614b5950c0 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Sun, 11 Dec 2022 23:31:23 +0900 Subject: [PATCH 05/31] =?UTF-8?q?fix(BE):=20=ED=83=9C=EA=B7=B8=EA=B0=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/post/post-search.service.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index c38caab3..a916cd3f 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -49,7 +49,7 @@ export class PostSearchService { }, ], size: SEND_POST_CNT + 1, - index: 'test', // TODO 최신순 3개 + index: 'test', // TODO 이름 임시 body: { query: { bool: { @@ -71,24 +71,23 @@ export class PostSearchService { searchFilter.body.query.bool.filter.bool.must.push({ multi_match: { query: detail, - fields: [ - 'title', - 'content', - 'code', - 'language', - 'authorNickname', - 'tags', - ], + fields: ['title', 'content', 'code', 'language', 'authorNickname'], }, }); } } + if (tags && tags.length > 0) { - const q = tags.length == 1 ? tags : tags.join(' '); + const q = + tags.length == 1 + ? `/"${tags}/"` + : tags.map((tag) => `/"${tag}/"`).join(' '); + console.log(q); searchFilter.body.query.bool.filter.bool.must.push({ match: { tags: { query: q, + operator: 'AND', }, }, @@ -113,6 +112,7 @@ export class PostSearchService { }); } const body = await this.esService.search(searchFilter); + console.log(body.hits.hits); return body.hits.hits; } } From a0cf0f9599785f9177b351c8e72065f0f8b80872 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Mon, 12 Dec 2022 04:23:54 +0900 Subject: [PATCH 06/31] =?UTF-8?q?fix(BE):=2010=5F000=EA=B0=9C=EA=B0=80=20?= =?UTF-8?q?=EB=84=98=EC=96=B4=EA=B0=80=EB=A9=B4=20=EC=97=98=EB=9D=BC?= =?UTF-8?q?=EC=8A=A4=ED=8B=B1=EC=84=9C=EC=B9=98=EA=B0=80=20=ED=84=B0?= =?UTF-8?q?=EC=A7=80=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/post/dto/controller-response.dto.ts | 11 +++++++++-- server/src/domain/post/post-search.service.ts | 4 +--- server/src/domain/post/post.service.ts | 3 +-- .../domain/post/types/post-search-body.interface.ts | 3 +-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/server/src/domain/post/dto/controller-response.dto.ts b/server/src/domain/post/dto/controller-response.dto.ts index bddf250e..282b58bf 100644 --- a/server/src/domain/post/dto/controller-response.dto.ts +++ b/server/src/domain/post/dto/controller-response.dto.ts @@ -2,6 +2,9 @@ import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { AuthorDto, EachImageResponseDto } from './service-response.dto'; import { Image } from '../../image/image.entity'; import { User } from '../../user/user.entity'; +import { SEND_POST_CNT } from '../post.controller'; + +const SORT_BY_ID = 0; export class SearchResponseDto { posts: EachSearchResponseDto[]; @@ -21,9 +24,13 @@ export class SearchResponseDto { ); } - this.lastId = posts.length == 0 ? -1 : Number(posts.slice(-1)[0]._id); + this.lastId = posts.length == 0 ? -1 : this.getLastId(posts); this.isLast = isLast; } + + private getLastId(posts: SearchHit[]) { + return posts[SEND_POST_CNT - 1].sort[SORT_BY_ID]; + } } export class EachSearchResponseDto { @@ -52,7 +59,7 @@ export class EachSearchResponseDto { tags = []; } } - this.id = Number(post._id); + this.id = post._source.id; this.title = post._source.title; this.content = post._source.content; this.code = post._source.code; diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index a916cd3f..54070d28 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -18,7 +18,6 @@ export class PostSearchService { this.esService.index({ index: 'test', //TODO 이름 임시 - id: String(post.id), body: { id: post.id, title: post.title, @@ -63,7 +62,7 @@ export class PostSearchService { }, }; if (lastId !== -1) { - searchFilter.from = lastId; + searchFilter.search_after = [lastId]; } if (details && details.length > 0) { @@ -82,7 +81,6 @@ export class PostSearchService { tags.length == 1 ? `/"${tags}/"` : tags.map((tag) => `/"${tag}/"`).join(' '); - console.log(q); searchFilter.body.query.bool.filter.bool.must.push({ match: { tags: { diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index e394da4c..4bae1118 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -122,7 +122,6 @@ export class PostService { ): Promise { let isLast = true; const results = await this.postSearchService.search(loadPostListRequestDto); - results.reverse(); if (this.canGetNextPost(results.length)) { results.pop(); isLast = false; @@ -139,7 +138,7 @@ export class PostService { ); images.push( this.imageRepository.findBy({ - postId: Number(result._id), + postId: Number(result._source['id']), }), ); } diff --git a/server/src/domain/post/types/post-search-body.interface.ts b/server/src/domain/post/types/post-search-body.interface.ts index 87943ee0..8eead70f 100644 --- a/server/src/domain/post/types/post-search-body.interface.ts +++ b/server/src/domain/post/types/post-search-body.interface.ts @@ -4,10 +4,9 @@ interface PostSearchBody { content: string; code: string; language: string; - // TODO 이미지 url은 필요없음. 가져올거임 createdAt: Date; updatedAt: Date; - authorId: number; // TODO Author 관해서도 가져올거임 + authorId: number; authorNickname: string; tags: string; likeCount: number; From ae772a0c633291fdee949d8748e5da7c44f7c351 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Mon, 12 Dec 2022 14:11:46 +0900 Subject: [PATCH 07/31] =?UTF-8?q?refactor(BE):=20=EC=97=98=EB=9D=BC?= =?UTF-8?q?=EC=8A=A4=ED=8B=B1=EC=84=9C=EC=B9=98=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/dto/controller-response.dto.ts | 8 ++++--- server/src/domain/post/post-search.service.ts | 16 +++++++++----- server/src/domain/post/post.module.ts | 22 +++++++++++++------ server/src/domain/post/post.service.ts | 2 +- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/server/src/domain/post/dto/controller-response.dto.ts b/server/src/domain/post/dto/controller-response.dto.ts index 282b58bf..7a550965 100644 --- a/server/src/domain/post/dto/controller-response.dto.ts +++ b/server/src/domain/post/dto/controller-response.dto.ts @@ -2,8 +2,10 @@ import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; import { AuthorDto, EachImageResponseDto } from './service-response.dto'; import { Image } from '../../image/image.entity'; import { User } from '../../user/user.entity'; -import { SEND_POST_CNT } from '../post.controller'; +/** + * 정렬 기준을 한개만 사용하기 때문에(id desc) 0번째 인덱스를 사용하면 정렬된 값의 id를 꺼낼 수 있다 + */ const SORT_BY_ID = 0; export class SearchResponseDto { @@ -29,11 +31,11 @@ export class SearchResponseDto { } private getLastId(posts: SearchHit[]) { - return posts[SEND_POST_CNT - 1].sort[SORT_BY_ID]; + return posts[posts.length - 1].sort[SORT_BY_ID]; } } -export class EachSearchResponseDto { +class EachSearchResponseDto { id: number; title: string; content: string; diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index 54070d28..f8d80d6b 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -3,13 +3,16 @@ import { ElasticsearchService } from '@nestjs/elasticsearch'; import { Post } from './post.entity'; import { LoadPostListRequestDto } from './dto/service-request.dto'; import { SEND_POST_CNT } from './post.controller'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class PostSearchService { - constructor(private readonly esService: ElasticsearchService) {} + constructor( + private readonly esService: ElasticsearchService, + private readonly configService: ConfigService, + ) {} async indexPost(post: Post, tags: any) { - // 태그를 정렬해 넣는다. let tagValue = ''; if (tags) { tags.sort(); @@ -17,7 +20,8 @@ export class PostSearchService { } this.esService.index({ - index: 'test', //TODO 이름 임시 + index: this.configService.get('ELASTICSEARCH_INDEX'), + id: String(post.id), body: { id: post.id, title: post.title, @@ -43,12 +47,12 @@ export class PostSearchService { sort: [ { id: { - order: 'desc', + order: 'DESC', }, }, ], size: SEND_POST_CNT + 1, - index: 'test', // TODO 이름 임시 + index: this.configService.get('ELASTICSEARCH_INDEX'), body: { query: { bool: { @@ -109,8 +113,8 @@ export class PostSearchService { }, }); } + const body = await this.esService.search(searchFilter); - console.log(body.hits.hits); return body.hits.hits; } } diff --git a/server/src/domain/post/post.module.ts b/server/src/domain/post/post.module.ts index d49ddf35..458e20ca 100644 --- a/server/src/domain/post/post.module.ts +++ b/server/src/domain/post/post.module.ts @@ -18,6 +18,7 @@ import { BookmarkRepository } from '../bookmark/bookmark.repository'; import { ElasticsearchModule } from '@nestjs/elasticsearch'; import { PostSearchService } from './post-search.service'; import { ImageRepository } from '../image/image.repository'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ controllers: [PostController], @@ -41,17 +42,24 @@ import { ImageRepository } from '../image/image.repository'; imports: [ HttpModule, JwtModule.register({}), + ConfigModule, ElasticsearchModule.registerAsync({ - useFactory: () => ({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ // TODO env로 이동 - node: 'http://localhost:9200', - maxRetries: 10, - requestTimeout: 60000, - pingTimeout: 60000, - sniffOnStart: true, + node: configService.get('ELASTICSEARCH_URL'), + auth: { + username: configService.get('ELASTICSEARCH_USERNAME'), //TODO username, password 넣기 + password: configService.get('ELASTICSEARCH_PASSWORD'), + }, + maxRetries: configService.get('ELASTICSEARCH_MAX_RETRIES'), + requestTimeout: configService.get('ELASTICSEARCH_REQUEST_TIMEOUT'), + pingTimeout: configService.get('ELASTICSEARCH_PING_TIMEOUT'), + sniffOnStart: configService.get('ELASTICSEARCH_SNIFF_ON_START'), }), + inject: [ConfigService], }), - ], // TODO App으로 올릴지 이야기하기 + ], exports: [PostService, PostRepository, ElasticsearchModule], }) export class PostModule {} diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index 4bae1118..3bd31ebd 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -62,7 +62,7 @@ export class PostService { postEntity.userNickname = userEntity.nickname; postEntity = await this.postRepository.save(postEntity); - this.postSearchService.indexPost(postEntity, tags); // TODO 데이터 더미로 넣을때 주석처리. 배치로 넣는게 더 빠름 + this.postSearchService.indexPost(postEntity, tags); return postEntity.id; } From 34d662d96e94594d3b7adf164fb72bb43bb4f296 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Mon, 12 Dec 2022 20:38:51 +0900 Subject: [PATCH 08/31] =?UTF-8?q?refactor(BE):=20tag=EB=A1=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=ED=95=A0=20=EB=95=8C=20=20=EC=8C=8D=EB=94=B0=EC=98=B4?= =?UTF-8?q?=ED=91=9C=EB=A5=BC=20=EB=B6=99=EC=97=AC=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/post/post-search.service.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index f8d80d6b..7b320f91 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -82,9 +82,10 @@ export class PostSearchService { if (tags && tags.length > 0) { const q = - tags.length == 1 - ? `/"${tags}/"` - : tags.map((tag) => `/"${tag}/"`).join(' '); + typeof tags === 'string' + ? `${tags}` + : tags.map((tag) => `${tag}`).join(' '); + searchFilter.body.query.bool.filter.bool.must.push({ match: { tags: { From 28fbf4e64e1b4e05118bcf939cbae0e730f13a90 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Tue, 13 Dec 2022 01:05:14 +0900 Subject: [PATCH 09/31] =?UTF-8?q?test(BE):=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=EC=84=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=ED=95=A0=20=EC=83=81=ED=99=A9=20=EC=A7=80?= =?UTF-8?q?=EC=A0=95=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/post/dto/service-request.dto.ts | 1 + .../domain/post/post-search.service.spec.ts | 204 +++++++ server/src/domain/post/post-search.service.ts | 1 + server/src/domain/post/post.service.spec.ts | 521 +----------------- 4 files changed, 234 insertions(+), 493 deletions(-) create mode 100644 server/src/domain/post/post-search.service.spec.ts diff --git a/server/src/domain/post/dto/service-request.dto.ts b/server/src/domain/post/dto/service-request.dto.ts index 85f0ed59..e9cfb953 100644 --- a/server/src/domain/post/dto/service-request.dto.ts +++ b/server/src/domain/post/dto/service-request.dto.ts @@ -1,6 +1,7 @@ import { IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator'; export class LoadPostListRequestDto { + //TODO 이름 변경. SearchCondition.. constructor( lastId: number, tags: string[], diff --git a/server/src/domain/post/post-search.service.spec.ts b/server/src/domain/post/post-search.service.spec.ts new file mode 100644 index 00000000..0c015781 --- /dev/null +++ b/server/src/domain/post/post-search.service.spec.ts @@ -0,0 +1,204 @@ +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PostSearchService } from './post-search.service'; +import { LoadPostListRequestDto } from './dto/service-request.dto'; + +const mockElasticSearchService = { + index: jest.fn(), + search: jest.fn(), +}; +const mockConfigService = { + get: jest.fn(), +}; + +describe('PostSearchService', () => { + let service: PostSearchService; + let esService: ElasticsearchService; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PostSearchService, + ElasticsearchService, + ConfigService, + { + provide: ElasticsearchService, + useValue: mockElasticSearchService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + service = module.get(PostSearchService); + esService = module.get(ElasticsearchService); + configService = module.get(ConfigService); + }); + + it('should be defined', () => { + expect(esService).toBeDefined(); + }); + + describe('저장', () => { + it('게시물이 들어오지 않으면 예외를 반환한다', () => { + // + }); + + it('게시물이 1개 들어오면 정상적으로 동작한다', () => { + // + }); + + it('게시물이 1개 넘게 들어오면 예외를 반환한다', () => { + // + }); + + it('태그가 0개 들어오면 정상 동작한다', () => { + // + }); + + it('태그가 1개 들어오면 정상 동작한다', () => { + // + }); + + it('태그가 5개 들어오면 정상 동작한다', () => { + // + }); + + it('태그가 6개 들어오면 예외를 반환한다', () => { + // + }); + }); + + describe('검색', () => { + let searchCondition: LoadPostListRequestDto; + beforeEach(() => { + searchCondition = { + lastId: 1, + tags: [], + reviewCount: 1, + likeCount: 1, + details: [], + }; + }); + + describe('입력값 검증', () => { + it('lastId가 없어도, 정상적으로 동작한다', async () => { + // + }); + + it('tags가 비어 있을 때, 정상 출력한다', async () => { + // + }); + + it('tags가 한 개 들어왔을 때, 정상 출력한다', async () => { + // + }); + + it('tags가 다섯 개 들어왔을 때, 정상 출력한다', async () => { + // + }); + + it('tags가 여섯 개 들어왔을 때, 예외를 반환한다', async () => { + // + }); + + it('tags가 여섯 개 들어왔을 때, 예외를 반환한다', async () => { + // + }); + it('details가 비어있을 때, 정상 출력한다', async () => { + // + }); + + it('details가 1개일 때, 정상 출력한다', async () => { + // + }); + + it('details가 1개를 넘어가면, 예외를 반환한다', async () => { + // + }); + + it('reviewCount가 음수면, 예외를 반환한다', async () => { + // + }); + + it('reviewCount가 20보다 크면, 예외를 반환한다', async () => { + // + }); + + it('likesCount가 음수면, 예외를 반환한다', async () => { + // + }); + + it('likesCount가 20보다 크면, 예외를 반환한다', async () => { + // + }); + }); + describe('결과값 검증', () => { + it('검색결과가 id로 오름차순 정렬되었을 때, 정상 출력한다', async () => { + //given + + //when + const result = await service.search(searchCondition); + + //then + }); + + it('검색결과가 id로 오름차순 정렬되지 않았을 때, 예외를 반환한다', async () => { + //given + + //when + const result = await service.search(searchCondition); + + //then + }); + + it('N+1개의 결과가 오면, posts를 3개 반환하고 isLast를 false로 반환한다', async () => { + //given + + //when + const result = await service.search(searchCondition); + + //then + }); + + it('N개의 결과가 오면, posts를 N개 반환하고 isLast를 true로 반환한다', async () => { + //given + + //when + const result = await service.search(searchCondition); + + //then + }); + + it('0개의 결과가 오면, posts를 0개 반환하고 isLast를 true로 반환한다', async () => { + //given + + //when + const result = await service.search(searchCondition); + + //then + }); + + it('N+1개 보다 많은 결과가 오면, 예외를 반환한다', async () => { + //given + + //when + const result = await service.search(searchCondition); + + //then + }); + + it('결과값에 중복되는 아이디가 있으면 예외를 반환한다', async () => { + //given + + //when + const result = await service.search(searchCondition); + + //then + }); + }); + }); +}); diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index 7b320f91..31f42784 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -116,6 +116,7 @@ export class PostSearchService { } const body = await this.esService.search(searchFilter); + console.log(body.hits.hits[0]); return body.hits.hits; } } diff --git a/server/src/domain/post/post.service.spec.ts b/server/src/domain/post/post.service.spec.ts index 0f9a3f0b..ef26794d 100644 --- a/server/src/domain/post/post.service.spec.ts +++ b/server/src/domain/post/post.service.spec.ts @@ -3,7 +3,6 @@ import { WriteDto } from '../post/dto/controller-request.dto'; import { PostService } from './post.service'; import { PostRepository } from './post.repository'; import { PostToTagRepository } from '../post-to-tag/post-to-tag.repository'; -import { LoadPostListRequestDto } from './dto/service-request.dto'; import { TagRepository } from '../tag/tag.repository'; import { UserNotFoundException } from '../../exception/user-not-found.exception'; import { UserRepository } from '../user/user.repository'; @@ -14,13 +13,19 @@ import { UserNotSameException } from '../../exception/user-not-same.exception'; import { Post } from './post.entity'; import { Tag } from '../tag/tag.entity'; import { PostToTag } from '../post-to-tag/post-to-tag.entity'; +import { ImageRepository } from '../image/image.repository'; +import { PostSearchService } from './post-search.service'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import { ConfigService } from '@nestjs/config'; describe('PostService', () => { let service: PostService; - let postRepository; - let postToTagRepository; - let tagRepository; - let userRepository; + let postRepository: PostRepository; + let postToTagRepository: PostToTagRepository; + let tagRepository: TagRepository; + let userRepository: UserRepository; + let imageRepository: ImageRepository; + let searchService: PostSearchService; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -30,12 +35,27 @@ describe('PostService', () => { PostToTagRepository, UserRepository, TagRepository, + ImageRepository, + PostSearchService, { provide: DataSource, useValue: { createEntityManager: () => jest.fn(), }, }, + { + provide: ElasticsearchService, + useValue: { + index: () => jest.fn(), + search: () => jest.fn(), + }, + }, + { + provide: ConfigService, + useValue: { + get: () => jest.fn(), + }, + }, ], }).compile(); @@ -44,499 +64,14 @@ describe('PostService', () => { postToTagRepository = module.get(PostToTagRepository); tagRepository = module.get(TagRepository); userRepository = module.get(UserRepository); + imageRepository = module.get(ImageRepository); + searchService = module.get(PostSearchService); }); it('should be defined', () => { expect(service).toBeDefined(); }); - describe('게시물 조회', () => { - const searchCondition: LoadPostListRequestDto = { - lastId: 1, - tags: [], - reviewCount: 1, - likeCount: 1, - }; - const resultFilteringLikesCnt = [ - { postId: 1, likesCnt: '1' }, - { postId: 2, likesCnt: '2' }, - ]; - const resultFilteringTag = [{ postId: 2 }, { postId: 3 }]; - - const postListThatHasOnePost = [ - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 6, - title: 'bubble sort 이렇게 해도 되나요?', - content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - code: "console.log('코드가 작성되니다');", - language: 'java', - user: { - isDeleted: false, - createdAt: '2022-11-17T08:01:08.967Z', - updatedAt: '2022-11-17T08:01:08.967Z', - id: 1, - nickname: 'taehoon1229', - profileUrl: 'https://avatars.githubusercontent.com/u/67636607?v=4', - email: 'kimth9981@naver.com', - }, - postToTags: [], - images: [ - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 1, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - }, - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 2, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - }, - ], - }, - ]; - - const postListThatHasFourPost = [ - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 6, - title: 'bubble sort 이렇게 해도 되나요?', - content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - code: "console.log('코드가 작성되니다');", - language: 'java', - user: { - isDeleted: false, - createdAt: '2022-11-17T08:01:08.967Z', - updatedAt: '2022-11-17T08:01:08.967Z', - id: 1, - nickname: 'taehoon1229', - profileUrl: 'https://avatars.githubusercontent.com/u/67636607?v=4', - email: 'kimth9981@naver.com', - }, - postToTags: [], - images: [ - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 1, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - }, - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 2, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - }, - ], - }, - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 5, - title: 'bubble sort 이렇게 해도 되나요?', - content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - code: "console.log('코드가 작성되니다');", - language: 'java', - user: { - isDeleted: false, - createdAt: '2022-11-17T08:01:08.967Z', - updatedAt: '2022-11-17T08:01:08.967Z', - id: 1, - nickname: 'taehoon1229', - profileUrl: 'https://avatars.githubusercontent.com/u/67636607?v=4', - email: 'kimth9981@naver.com', - }, - postToTags: [], - images: [ - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 1, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - }, - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 2, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - }, - ], - }, - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 4, - title: 'bubble sort 이렇게 해도 되나요?', - content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - code: "console.log('코드가 작성되니다');", - language: 'java', - user: { - isDeleted: false, - createdAt: '2022-11-17T08:01:08.967Z', - updatedAt: '2022-11-17T08:01:08.967Z', - id: 1, - nickname: 'taehoon1229', - profileUrl: 'https://avatars.githubusercontent.com/u/67636607?v=4', - email: 'kimth9981@naver.com', - }, - postToTags: [], - images: [ - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 1, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - }, - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 2, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - }, - ], - }, - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 3, - title: 'bubble sort 이렇게 해도 되나요?', - content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - code: "console.log('코드가 작성되니다');", - language: 'java', - user: { - isDeleted: false, - createdAt: '2022-11-17T08:01:08.967Z', - updatedAt: '2022-11-17T08:01:08.967Z', - id: 1, - nickname: 'taehoon1229', - profileUrl: 'https://avatars.githubusercontent.com/u/67636607?v=4', - email: 'kimth9981@naver.com', - }, - postToTags: [], - images: [ - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 1, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - }, - { - isDeleted: false, - createdAt: '2022-11-17T11:41:34.568Z', - updatedAt: '2022-11-17T11:41:34.568Z', - id: 2, - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - }, - ], - }, - ]; - - const inquiryResult = { - posts: [ - { - id: 6, - title: 'bubble sort 이렇게 해도 되나요?', - content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - code: "console.log('코드가 작성되니다');", - language: 'java', - images: [ - { - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - name: 'js-error', - }, - { - src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - name: 'suggestions', - }, - ], - updatedAt: '2022-11-17T11:41:34.568Z', - author: { - id: 1, - nickname: 'taehoon1229', - profileUrl: 'https://avatars.githubusercontent.com/u/67636607?v=4', - email: 'kimth9981@naver.com', - }, - tags: [], - isLiked: false, - }, - ], - lastId: 6, - isLast: true, - }; - - beforeEach(async () => { - // given - jest - .spyOn(postRepository, 'findByIdLikesCntGreaterThanOrEqual') - .mockResolvedValue(resultFilteringLikesCnt); - jest - .spyOn(postToTagRepository, 'findByContainingTags') - .mockResolvedValue(resultFilteringTag); - jest - .spyOn(postRepository, 'filterUsingDetail') - .mockResolvedValue(resultFilteringTag); - jest - .spyOn(postRepository, 'findByReviewCntGreaterThanOrEqual') - .mockResolvedValue(resultFilteringTag); - jest.spyOn(tagRepository, 'findById').mockResolvedValue({ name: 'java' }); - }); - - it('마지막 결과값을 포함할 때 isLast는 true가 된다', async () => { - //given - jest - .spyOn(postRepository, 'findByIdWithFilterResult') - .mockResolvedValue(postListThatHasOnePost); - - //when - const result = await service.loadPostList(searchCondition); - - //then - // expect(result).toEqual({ - // posts: [ - // { - // id: 6, - // title: 'bubble sort 이렇게 해도 되나요?', - // content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - // code: "console.log('코드가 작성되니다');", - // language: 'java', - // images: [ - // { - // src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - // name: 'js-error', - // }, - // { - // src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - // name: 'suggestions', - // }, - // ], - // updatedAt: '2022-11-17T11:41:34.568Z', - // author: { - // id: 1, - // nickname: 'taehoon1229', - // profileUrl: - // 'https://avatars.githubusercontent.com/u/67636607?v=4', - // email: 'kimth9981@naver.com', - // }, - // tags: [], - // isLiked: false, - // }, - // ], - // lastId: 6, - // isLast: true, - // }); - }); - - it('마지막 결과가 아닐 때 isLast는 false가 된다', async () => { - //given - jest - .spyOn(postRepository, 'findByIdWithFilterResult') - .mockResolvedValue(postListThatHasFourPost); - - // when - const result = await service.loadPostList(searchCondition); - // then - // expect(result).toEqual({ - // posts: [ - // { - // id: 6, - // title: 'bubble sort 이렇게 해도 되나요?', - // content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - // code: "console.log('코드가 작성되니다');", - // language: 'java', - // images: [ - // { - // src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - // name: 'js-error', - // }, - // { - // src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - // name: 'suggestions', - // }, - // ], - // updatedAt: '2022-11-17T11:41:34.568Z', - // author: { - // id: 1, - // nickname: 'taehoon1229', - // profileUrl: - // 'https://avatars.githubusercontent.com/u/67636607?v=4', - // email: 'kimth9981@naver.com', - // }, - // tags: [], - // isLiked: false, - // }, - // { - // id: 5, - // title: 'bubble sort 이렇게 해도 되나요?', - // content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - // code: "console.log('코드가 작성되니다');", - // language: 'java', - // images: [ - // { - // src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - // name: 'js-error', - // }, - // { - // src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - // name: 'suggestions', - // }, - // ], - // updatedAt: '2022-11-17T11:41:34.568Z', - // author: { - // id: 1, - // nickname: 'taehoon1229', - // profileUrl: - // 'https://avatars.githubusercontent.com/u/67636607?v=4', - // email: 'kimth9981@naver.com', - // }, - // tags: [], - // isLiked: false, - // }, - // { - // id: 4, - // title: 'bubble sort 이렇게 해도 되나요?', - // content: '이게 맞나 모르겠어요 ㅠㅠㅠ', - // code: "console.log('코드가 작성되니다');", - // language: 'java', - // images: [ - // { - // src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/js-error.png', - // name: 'js-error', - // }, - // { - // src: 'https://code.visualstudio.com/assets/docs/nodejs/reactjs/suggestions.png', - // name: 'suggestions', - // }, - // ], - // updatedAt: '2022-11-17T11:41:34.568Z', - // author: { - // id: 1, - // nickname: 'taehoon1229', - // profileUrl: - // 'https://avatars.githubusercontent.com/u/67636607?v=4', - // email: 'kimth9981@naver.com', - // }, - // tags: [], - // isLiked: false, - // }, - // ], - // lastId: 4, - // isLast: false, - // }); - }); - - it('검색 결과가 없을 때, post=[], lastId -1 isLast: true 를 반환한다', async () => { - // given - jest - .spyOn(postRepository, 'findByIdWithFilterResult') - .mockResolvedValue([]); - - // when - const result = await service.loadPostList(searchCondition); - - // then - expect(result).toEqual({ - posts: [], - lastId: -1, - isLast: true, - }); - }); - - it('이미지가 있을때, 결과를 정상적으로 반환한다', async () => { - // TODO - }); - - it('태그들이 있을 때, 정상적으로 결과를 반환한다', async () => { - // TODO - }); - - describe('returnPostIdByAllConditionPass 테스트: 여러 조건(좋아요, 태그, 검색어)로 post를 필터링한다', () => { - it('모든 조건을 다 사용하는 상황 ', () => { - const resultFilteringLikesCnt = [ - { postId: 1, likesCnt: '1' }, - { postId: 2, likesCnt: '2' }, - { postId: 3, likesCnt: '2' }, - ]; - const resultFilteringTag = [{ postId: 2 }, { postId: 3 }]; - const resultFilteringSearchWord = [{ postId: 2 }, { postId: 3 }]; - - const result = service.mergeFilterResult([ - resultFilteringLikesCnt, - resultFilteringTag, - resultFilteringSearchWord, - ]); - - expect(result).toEqual([2, 3]); - }); - - it('하나의 조건을 만족하는 Post가 한 개도 없는 경우', () => { - const resultFilteringLikesCnt = [ - { postId: 1, likesCnt: '1' }, - { postId: 2, likesCnt: '2' }, - { postId: 3, likesCnt: '2' }, - ]; - const resultFilteringTag = []; - const resultFilteringSearchWord = [{ postId: 2 }, { postId: 3 }]; - - const result = service.mergeFilterResult([ - resultFilteringLikesCnt, - resultFilteringTag, - resultFilteringSearchWord, - ]); - - expect(result).toEqual([]); - }); - - it('하나의 조건을 사용하지 않는 경우(null을 리턴)', () => { - const resultFilteringLikesCnt = [ - { postId: 1, likesCnt: '1' }, - { postId: 2, likesCnt: '2' }, - { postId: 3, likesCnt: '2' }, - ]; - const resultFilteringTag = [{ postId: 2 }, { postId: 3 }]; - const resultFilteringSearchWord = null; - - const result = service.mergeFilterResult([ - resultFilteringLikesCnt, - resultFilteringTag, - ]); - - expect(result).toEqual([2, 3]); - }); - - it('모든 조건을 사용하지 않는 경우', () => { - const resultFilteringLikesCnt = null; - const resultFilteringTag = null; - const resultFilteringSearchWord = null; - - const result = service.mergeFilterResult([ - resultFilteringLikesCnt, - resultFilteringTag, - resultFilteringSearchWord, - ]); - - expect(result).toBeNull(); - }); - }); - }); - describe('글 작성', () => { const writeDto: WriteDto = { title: '제목', @@ -559,7 +94,7 @@ describe('PostService', () => { }); it('글 작성 성공', async () => { - userRepository.findOneBy = jest.fn(() => new User()); + userRepository.findOneBy = jest.fn().mockResolvedValue(() => new User()); tagRepository.findOneBy = jest.fn( () => new Promise((resolve) => resolve(new Tag())), ); From 1e1dab7a14fdea51df2f30187cd70d75ba1c6357 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Tue, 13 Dec 2022 03:11:07 +0900 Subject: [PATCH 10/31] =?UTF-8?q?test(BE):=20=EA=B2=80=EC=83=89=EC=8B=9C?= =?UTF-8?q?=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=9E=91=EC=84=B1=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/post/post-search.service.spec.ts | 334 +++++++++++++----- server/src/domain/post/post.service.ts | 1 + .../exception/post-input-invalid.exception.ts | 5 + .../search-param-invalid.exception.ts | 5 + 4 files changed, 253 insertions(+), 92 deletions(-) create mode 100644 server/src/exception/post-input-invalid.exception.ts create mode 100644 server/src/exception/search-param-invalid.exception.ts diff --git a/server/src/domain/post/post-search.service.spec.ts b/server/src/domain/post/post-search.service.spec.ts index 0c015781..007d9ffc 100644 --- a/server/src/domain/post/post-search.service.spec.ts +++ b/server/src/domain/post/post-search.service.spec.ts @@ -3,13 +3,24 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { PostSearchService } from './post-search.service'; import { LoadPostListRequestDto } from './dto/service-request.dto'; +import { Post } from './post.entity'; +import { User } from '../user/user.entity'; +import { PostNotFoundException } from '../../exception/post-not-found.exception'; +import { PostInputInvalidException } from '../../exception/post-input-invalid.exception'; +import { SEND_POST_CNT } from './post.controller'; +import { SearchParamInvalidException } from '../../exception/search-param-invalid.exception'; const mockElasticSearchService = { + //TODO 위로 옮겨가도 테스트 할때마다 초기화가 될까? index: jest.fn(), - search: jest.fn(), + search: jest.fn().mockResolvedValue({ + hits: { + hits: {}, + }, + }), }; const mockConfigService = { - get: jest.fn(), + get: jest.fn(() => '엘라스틱_인덱스명'), }; describe('PostSearchService', () => { @@ -17,6 +28,8 @@ describe('PostSearchService', () => { let esService: ElasticsearchService; let configService: ConfigService; + let post: Post; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -36,6 +49,23 @@ describe('PostSearchService', () => { service = module.get(PostSearchService); esService = module.get(ElasticsearchService); configService = module.get(ConfigService); + + const user = new User(); + user.id = 10; + user.nickname = '사용자이름'; + + post = new Post(); + post.id = 1; + post.title = '제목'; + post.content = 'content'; + post.code = 'System.out.println("zz")'; + post.language = 'java'; + post.createdAt = new Date(); + post.updatedAt = new Date(); + post.user = user; + post.lineCount = 1; + post.reviewCount = 0; + post.likeCount = 0; }); it('should be defined', () => { @@ -44,160 +74,280 @@ describe('PostSearchService', () => { describe('저장', () => { it('게시물이 들어오지 않으면 예외를 반환한다', () => { - // + expect(() => { + service.indexPost(null, ['greedy', 'sort']); + }).toThrow(PostNotFoundException); }); it('게시물이 1개 들어오면 정상적으로 동작한다', () => { - // - }); - - it('게시물이 1개 넘게 들어오면 예외를 반환한다', () => { - // + service.indexPost(post, ['greedy', 'sort']); + expect(esService.index).toBeCalled(); + // TODO 내부 값 테스트 }); it('태그가 0개 들어오면 정상 동작한다', () => { - // + service.indexPost(post, []); + expect(esService.index).toBeCalled(); }); it('태그가 1개 들어오면 정상 동작한다', () => { - // + service.indexPost(post, ['one']); + expect(esService.index).toBeCalled(); }); it('태그가 5개 들어오면 정상 동작한다', () => { - // + service.indexPost(post, ['one', 'two', 'three', 'four', 'five']); + expect(esService.index).toBeCalled(); + }); + + it('중복된 태그가 들어오면 예외를 반환한다', () => { + expect(() => + service.indexPost(post, ['one', 'two', 'three', 'two', 'five']), + ).toThrow(PostInputInvalidException); }); it('태그가 6개 들어오면 예외를 반환한다', () => { - // + expect(() => + service.indexPost(post, ['one', 'two', 'three', 'four', 'five', 'six']), + ).toThrow(PostInputInvalidException); + }); + + // TODO 태그를 객체로 분리하기 + it('태그 중 10글자까지는 정상 동작한다', () => { + service.indexPost(post, ['가나다라마바사아자카']); + expect(esService.index).toBeCalled(); + }); + + it('공백만 들어간 태그가 있으면 예외를 반환한다', () => { + expect(() => + service.indexPost(post, ['one', ' ', 'three', 'four', 'five']), + ).toThrow(PostInputInvalidException); + }); + + it('한글, 영대소문자, 숫자, 언더바 이외의 문자(#로 테스트)가 들어간 태그가 있으면 예외를 반환한다', () => { + expect(() => + service.indexPost(post, ['one', 'two', '#three', 'four']), + ).toThrow(PostInputInvalidException); + }); + + it('한글, 영대소문자, 숫자, 언더바 이외의 문자가 들어간 태그(공백으로 테스트)가 있으면 예외를 반환한다', () => { + expect(() => + service.indexPost(post, [' one', 'two', 'thre@e', 'four']), + ).toThrow(PostInputInvalidException); + }); + + it('각 태그에 한글, 영대소문자, 숫자, 언더바만 들어가면 정상 동작한다', () => { + expect(() => + service.indexPost(post, ['한글', '한rD', 'THR22', 'F_ou_r', 'five']), + ).toThrow(PostInputInvalidException); + }); + + it('각 태그의 시작에 언더바가 있다면 예외를 반환한다', () => { + expect(() => + service.indexPost(post, ['_한글', '한rD', 'THR22', 'F_ou_r', 'five']), + ).toThrow(PostInputInvalidException); + }); + + it('각 태그의 끝에 언더바가 있다면 예외를 반환한다', () => { + expect(() => + service.indexPost(post, ['한글_', '한rD', 'THR22', 'F_ou_r', 'five']), + ).toThrow(PostInputInvalidException); + }); + + // 언더바가 연속으로 들어간 경우 -> 예외 + + it('태그 중 10글자를 넘는 값이 있다면 예외를 반환한다', () => { + expect(() => service.indexPost(post, ['12345678901'])).toThrow( + PostInputInvalidException, + ); }); }); describe('검색', () => { let searchCondition: LoadPostListRequestDto; + let searchConditionUsingES; + beforeEach(() => { + searchConditionUsingES = { + sort: [ + { + id: { + order: 'DESC', + }, + }, + ], + size: SEND_POST_CNT + 1, + index: configService.get(''), + body: { + query: { + bool: { + filter: { + bool: { + must: [], + }, + }, + }, + }, + }, + }; + searchCondition = { - lastId: 1, + lastId: -1, tags: [], - reviewCount: 1, - likeCount: 1, + reviewCount: 0, + likeCount: 0, details: [], }; }); describe('입력값 검증', () => { it('lastId가 없어도, 정상적으로 동작한다', async () => { - // - }); + delete searchCondition.lastId; - it('tags가 비어 있을 때, 정상 출력한다', async () => { - // - }); + await service.search(searchCondition); - it('tags가 한 개 들어왔을 때, 정상 출력한다', async () => { - // + expect(esService.search).toBeCalled(); + expect(esService.search).toBeCalledWith(searchConditionUsingES); }); - it('tags가 다섯 개 들어왔을 때, 정상 출력한다', async () => { - // - }); + it('필터링 조건이 하나도 들어가지 않을 때 정상적으로 동작한다', async () => { + await service.search(searchCondition); - it('tags가 여섯 개 들어왔을 때, 예외를 반환한다', async () => { - // + expect(esService.search).toBeCalled(); + expect(esService.search).toBeCalledWith(searchConditionUsingES); }); - it('tags가 여섯 개 들어왔을 때, 예외를 반환한다', async () => { - // - }); - it('details가 비어있을 때, 정상 출력한다', async () => { - // - }); - - it('details가 1개일 때, 정상 출력한다', async () => { - // - }); - - it('details가 1개를 넘어가면, 예외를 반환한다', async () => { - // - }); - - it('reviewCount가 음수면, 예외를 반환한다', async () => { - // - }); - - it('reviewCount가 20보다 크면, 예외를 반환한다', async () => { - // - }); - - it('likesCount가 음수면, 예외를 반환한다', async () => { - // - }); - - it('likesCount가 20보다 크면, 예외를 반환한다', async () => { - // - }); - }); - describe('결과값 검증', () => { - it('검색결과가 id로 오름차순 정렬되었을 때, 정상 출력한다', async () => { + it('tags가 한 개 들어왔을 때, 정상 출력한다', async () => { //given + searchCondition.tags = ['one']; + searchConditionUsingES.body.query.bool.filter.bool.must.push({ + match: { + tags: { + query: 'one', + operator: 'AND', + }, + }, + }); //when - const result = await service.search(searchCondition); + await service.search(searchCondition); - //then + expect(esService.search).toBeCalled(); + expect(esService.search).toBeCalledWith(searchConditionUsingES); }); - it('검색결과가 id로 오름차순 정렬되지 않았을 때, 예외를 반환한다', async () => { + it('tags가 다섯 개 들어왔을 때, 정상 출력한다', async () => { //given + searchCondition.tags = ['one', 'two', 'three', 'four', 'five']; + searchConditionUsingES.body.query.bool.filter.bool.must.push({ + match: { + tags: { + query: ['one', 'two', 'three', 'four', 'five'].join(' '), + operator: 'AND', + }, + }, + }); //when - const result = await service.search(searchCondition); + await service.search(searchCondition); - //then + expect(esService.search).toBeCalled(); + expect(esService.search).toBeCalledWith(searchConditionUsingES); }); - it('N+1개의 결과가 오면, posts를 3개 반환하고 isLast를 false로 반환한다', async () => { - //given - - //when - const result = await service.search(searchCondition); - - //then + it('tags가 여섯 개 들어왔을 때, 예외를 반환한다', async () => { + try { + //given + searchCondition.tags = ['one', 'two', 'three', 'four', 'five', 'six']; + + //when + await service.search(searchCondition); + } catch (err) { + //then + expect(err).toBeInstanceOf(SearchParamInvalidException); + } }); - it('N개의 결과가 오면, posts를 N개 반환하고 isLast를 true로 반환한다', async () => { - //given - - //when - const result = await service.search(searchCondition); - - //then + it('details에 공백이 넘어올 때, 정상 출력한다', async () => { + //TODO 놓친 케이스. 프론트에서 막아줄건지, 백엔드에서 무시할건지 이야기 }); - it('0개의 결과가 오면, posts를 0개 반환하고 isLast를 true로 반환한다', async () => { + it('details가 1개일 때, 정상 출력한다', async () => { //given + searchCondition.details = ['hello my name is taehoon kim']; + searchConditionUsingES.body.query.bool.filter.bool.must.push({ + multi_match: { + query: searchCondition.details, + fields: ['title', 'content', 'code', 'language', 'authorNickname'], + }, + }); //when - const result = await service.search(searchCondition); + await service.search(searchCondition); - //then + expect(esService.search).toBeCalled(); + expect(esService.search).toBeCalledWith(searchConditionUsingES); }); - it('N+1개 보다 많은 결과가 오면, 예외를 반환한다', async () => { - //given + // TODO 검색어 길이 제한 - //when - const result = await service.search(searchCondition); + it('details가 1개를 넘어가면, 예외를 반환한다', async () => { + try { + //given + searchCondition.details = [ + 'hello my name is taehoon kim', + 'second value', + ]; + //when + await service.search(searchCondition); + } catch (err) { + expect(err).toBeInstanceOf(SearchParamInvalidException); + } + }); - //then + //ddd + it('reviewCount가 음수면, 예외를 반환한다', async () => { + try { + //given + searchCondition.reviewCount = -1; + //when + await service.search(searchCondition); + } catch (err) { + expect(err).toBeInstanceOf(SearchParamInvalidException); + } }); - it('결과값에 중복되는 아이디가 있으면 예외를 반환한다', async () => { - //given + it('reviewCount가 20보다 크면, 예외를 반환한다', async () => { + try { + //given + searchCondition.reviewCount = 21; + //when + await service.search(searchCondition); + } catch (err) { + expect(err).toBeInstanceOf(SearchParamInvalidException); + } + }); - //when - const result = await service.search(searchCondition); + it('likeCount가 음수면, 예외를 반환한다', async () => { + try { + //given + searchCondition.likeCount = -1; + //when + await service.search(searchCondition); + } catch (err) { + expect(err).toBeInstanceOf(SearchParamInvalidException); + } + }); - //then + it('likeCount가 20보다 크면, 예외를 반환한다', async () => { + try { + //given + searchCondition.likeCount = 21; + //when + await service.search(searchCondition); + } catch (err) { + expect(err).toBeInstanceOf(SearchParamInvalidException); + } }); }); }); diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index 3bd31ebd..aa421137 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -32,6 +32,7 @@ export class PostService { userId: number, { title, content, code, language, lineCount, images, tags }, ) { + // TODO 입력값 검증 tags.sort(); const userEntity = await this.userRepository.findOneBy({ diff --git a/server/src/exception/post-input-invalid.exception.ts b/server/src/exception/post-input-invalid.exception.ts new file mode 100644 index 00000000..2acfae7c --- /dev/null +++ b/server/src/exception/post-input-invalid.exception.ts @@ -0,0 +1,5 @@ +export class PostInputInvalidException extends Error { + constructor(msg: string) { + super(msg); + } +} diff --git a/server/src/exception/search-param-invalid.exception.ts b/server/src/exception/search-param-invalid.exception.ts new file mode 100644 index 00000000..a8270b21 --- /dev/null +++ b/server/src/exception/search-param-invalid.exception.ts @@ -0,0 +1,5 @@ +export class SearchParamInvalidException extends Error { + constructor(msg: string) { + super(msg); + } +} From d9c54441c7e80f44c0bb6b9f940c65550f4977b5 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Tue, 13 Dec 2022 12:04:58 +0900 Subject: [PATCH 11/31] =?UTF-8?q?refactor(BE):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=EB=A5=BC=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/post/post-search.service.spec.ts | 364 ++++++++++++++---- server/src/domain/post/post-search.service.ts | 118 +++++- server/src/domain/post/post.controller.ts | 7 +- server/src/domain/post/post.service.spec.ts | 21 +- server/src/domain/post/post.service.ts | 3 + .../search-response-invalid.exception.ts | 5 + server/src/exception/tag-invalid.exception.ts | 5 + 7 files changed, 423 insertions(+), 100 deletions(-) create mode 100644 server/src/exception/search-response-invalid.exception.ts create mode 100644 server/src/exception/tag-invalid.exception.ts diff --git a/server/src/domain/post/post-search.service.spec.ts b/server/src/domain/post/post-search.service.spec.ts index 007d9ffc..24a4169c 100644 --- a/server/src/domain/post/post-search.service.spec.ts +++ b/server/src/domain/post/post-search.service.spec.ts @@ -9,13 +9,14 @@ import { PostNotFoundException } from '../../exception/post-not-found.exception' import { PostInputInvalidException } from '../../exception/post-input-invalid.exception'; import { SEND_POST_CNT } from './post.controller'; import { SearchParamInvalidException } from '../../exception/search-param-invalid.exception'; +import { SearchResponseInvalidException } from '../../exception/search-response-invalid.exception'; +import { TagInvalidException } from '../../exception/tag-invalid.exception'; const mockElasticSearchService = { - //TODO 위로 옮겨가도 테스트 할때마다 초기화가 될까? index: jest.fn(), search: jest.fn().mockResolvedValue({ hits: { - hits: {}, + hits: [], }, }), }; @@ -28,8 +29,6 @@ describe('PostSearchService', () => { let esService: ElasticsearchService; let configService: ConfigService; - let post: Post; - beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -49,23 +48,6 @@ describe('PostSearchService', () => { service = module.get(PostSearchService); esService = module.get(ElasticsearchService); configService = module.get(ConfigService); - - const user = new User(); - user.id = 10; - user.nickname = '사용자이름'; - - post = new Post(); - post.id = 1; - post.title = '제목'; - post.content = 'content'; - post.code = 'System.out.println("zz")'; - post.language = 'java'; - post.createdAt = new Date(); - post.updatedAt = new Date(); - post.user = user; - post.lineCount = 1; - post.reviewCount = 0; - post.likeCount = 0; }); it('should be defined', () => { @@ -73,6 +55,47 @@ describe('PostSearchService', () => { }); describe('저장', () => { + let post: Post; + let indexParameter; + + beforeEach(async () => { + const user = new User(); + user.id = 10; + user.nickname = '사용자이름'; + + post = new Post(); + post.id = 1; + post.title = '제목'; + post.content = 'content'; + post.code = 'System.out.println("zz")'; + post.language = 'java'; + post.createdAt = new Date(); + post.updatedAt = new Date(); + post.user = user; + post.lineCount = 1; + post.reviewCount = 0; + post.likeCount = 0; + + indexParameter = { + index: configService.get(''), + id: String(post.id), + body: { + id: post.id, + title: post.title, + content: post.content, + code: post.code, + language: post.language, + createdAt: post.createdAt, + updatedAt: post.updatedAt, + authorId: post.user.id, + authorNickname: post.user.nickname, + // tags: tagValue, + lineCount: post.lineCount, + reviewCount: post.reviewCount, + likeCount: post.likeCount, + }, + }; + }); it('게시물이 들어오지 않으면 예외를 반환한다', () => { expect(() => { service.indexPost(null, ['greedy', 'sort']); @@ -80,30 +103,56 @@ describe('PostSearchService', () => { }); it('게시물이 1개 들어오면 정상적으로 동작한다', () => { - service.indexPost(post, ['greedy', 'sort']); + //given + const tags = ['greedy', 'sort']; + + //when + service.indexPost(post, tags); + indexParameter.body.tags = tags.join(' '); + expect(esService.index).toBeCalled(); - // TODO 내부 값 테스트 + expect(esService.index).toBeCalledWith(indexParameter); }); it('태그가 0개 들어오면 정상 동작한다', () => { service.indexPost(post, []); + indexParameter.body.tags = ''; // 비어있는 값일 때 ''가 들어간다 + expect(esService.index).toBeCalled(); + expect(esService.index).toBeCalledWith(indexParameter); }); it('태그가 1개 들어오면 정상 동작한다', () => { - service.indexPost(post, ['one']); + //given + const tags = ['one']; + indexParameter.body.tags = tags[0]; + //when + service.indexPost(post, tags); + expect(esService.index).toBeCalled(); + expect(esService.index).toBeCalledWith(indexParameter); }); it('태그가 5개 들어오면 정상 동작한다', () => { - service.indexPost(post, ['one', 'two', 'three', 'four', 'five']); + const tags = ['one', 'two', 'three', 'four', 'five']; + indexParameter.body.tags = tags.join(' '); + + service.indexPost(post, tags); + expect(esService.index).toBeCalled(); + expect(esService.index).toBeCalledWith(indexParameter); }); - it('중복된 태그가 들어오면 예외를 반환한다', () => { - expect(() => - service.indexPost(post, ['one', 'two', 'three', 'two', 'five']), - ).toThrow(PostInputInvalidException); + it('중복된 태그가 들어오면 중복을 제거하고 저장한다', () => { + //given + const tagsRemovedDuplicate = Array.from( + new Set(['one', 'two', 'three', 'two', 'five']), + ); + indexParameter.body.tags = tagsRemovedDuplicate.join(' '); + + //when + service.indexPost(post, ['one', 'two', 'three', 'two', 'five']); + expect(esService.index).toBeCalledWith(indexParameter); }); it('태그가 6개 들어오면 예외를 반환한다', () => { @@ -112,54 +161,35 @@ describe('PostSearchService', () => { ).toThrow(PostInputInvalidException); }); - // TODO 태그를 객체로 분리하기 - it('태그 중 10글자까지는 정상 동작한다', () => { - service.indexPost(post, ['가나다라마바사아자카']); + it('태그 하나의 글자수가 30개면 정상 동작한다', () => { + service.indexPost(post, [ + '부스트캠프부스트캠프부스트캠프부스트캠프부스트캠프부스트캠프', + ]); expect(esService.index).toBeCalled(); }); - it('공백만 들어간 태그가 있으면 예외를 반환한다', () => { + it('태그 하나의 글자수가 31개면 예외를 반환한다', () => { expect(() => - service.indexPost(post, ['one', ' ', 'three', 'four', 'five']), - ).toThrow(PostInputInvalidException); + service.indexPost(post, [ + 'one', + '부스트캠프부스트캠프부스트캠프부스트캠프부스트캠프부스트캠프1', + 'three', + 'four', + 'five', + ]), + ).toThrow(TagInvalidException); }); - it('한글, 영대소문자, 숫자, 언더바 이외의 문자(#로 테스트)가 들어간 태그가 있으면 예외를 반환한다', () => { + it('아무 값도 들어있지 않은 태그가 있으면 예외를 반환한다', () => { expect(() => - service.indexPost(post, ['one', 'two', '#three', 'four']), - ).toThrow(PostInputInvalidException); - }); - - it('한글, 영대소문자, 숫자, 언더바 이외의 문자가 들어간 태그(공백으로 테스트)가 있으면 예외를 반환한다', () => { - expect(() => - service.indexPost(post, [' one', 'two', 'thre@e', 'four']), - ).toThrow(PostInputInvalidException); - }); - - it('각 태그에 한글, 영대소문자, 숫자, 언더바만 들어가면 정상 동작한다', () => { - expect(() => - service.indexPost(post, ['한글', '한rD', 'THR22', 'F_ou_r', 'five']), - ).toThrow(PostInputInvalidException); - }); - - it('각 태그의 시작에 언더바가 있다면 예외를 반환한다', () => { - expect(() => - service.indexPost(post, ['_한글', '한rD', 'THR22', 'F_ou_r', 'five']), - ).toThrow(PostInputInvalidException); + service.indexPost(post, ['one', ' ', 'three', 'four', 'five']), + ).toThrow(TagInvalidException); }); - it('각 태그의 끝에 언더바가 있다면 예외를 반환한다', () => { + it('공백이 포함된 태그가 있으면 예외를 반환한다', () => { expect(() => - service.indexPost(post, ['한글_', '한rD', 'THR22', 'F_ou_r', 'five']), - ).toThrow(PostInputInvalidException); - }); - - // 언더바가 연속으로 들어간 경우 -> 예외 - - it('태그 중 10글자를 넘는 값이 있다면 예외를 반환한다', () => { - expect(() => service.indexPost(post, ['12345678901'])).toThrow( - PostInputInvalidException, - ); + service.indexPost(post, [' one', 'two', 'thre e', 'four']), + ).toThrow(TagInvalidException); }); }); @@ -201,22 +231,6 @@ describe('PostSearchService', () => { }); describe('입력값 검증', () => { - it('lastId가 없어도, 정상적으로 동작한다', async () => { - delete searchCondition.lastId; - - await service.search(searchCondition); - - expect(esService.search).toBeCalled(); - expect(esService.search).toBeCalledWith(searchConditionUsingES); - }); - - it('필터링 조건이 하나도 들어가지 않을 때 정상적으로 동작한다', async () => { - await service.search(searchCondition); - - expect(esService.search).toBeCalled(); - expect(esService.search).toBeCalledWith(searchConditionUsingES); - }); - it('tags가 한 개 들어왔을 때, 정상 출력한다', async () => { //given searchCondition.tags = ['one']; @@ -262,12 +276,70 @@ describe('PostSearchService', () => { //when await service.search(searchCondition); + throw new Error(); } catch (err) { //then expect(err).toBeInstanceOf(SearchParamInvalidException); } }); + it('태그 하나의 글자수가 30개면 정상 동작한다', async () => { + //given + searchCondition.tags = [ + '부스트캠프부스트캠프부스트캠프부스트캠프부스트캠프부스트캠프', + ]; + + //when + await service.search(searchCondition); + + expect(esService.search).toBeCalled(); + }); + + it('태그 하나의 글자수가 31개면 예외를 반환한다', async () => { + try { + //given + searchCondition.tags = [ + '부스트캠프부스트캠프부스트캠프부스트캠프부스트캠프부스트캠프1', + ]; + + //when + await service.search(searchCondition); + console.log('rerererer', searchCondition); + throw new Error(); + } catch (err) { + //then + expect(err).toBeInstanceOf(TagInvalidException); + } + }); + + it('아무 값도 들어있지 않은 태그가 있으면 예외를 반환한다', async () => { + try { + //given + searchCondition.tags = ['one', ' ', 'three', 'four', 'five']; + + //when + await service.search(searchCondition); + throw new Error(); + } catch (err) { + //then + expect(err).toBeInstanceOf(TagInvalidException); + } + }); + + it('공백이 포함된 태그가 있으면 예외를 반환한다', async () => { + try { + //given + searchCondition.tags = [' one', 'two', 'thre e', 'four']; + + //when + await service.search(searchCondition); + throw new Error(); + } catch (err) { + //then + expect(err).toBeInstanceOf(TagInvalidException); + } + }); + it('details에 공백이 넘어올 때, 정상 출력한다', async () => { //TODO 놓친 케이스. 프론트에서 막아줄건지, 백엔드에서 무시할건지 이야기 }); @@ -277,7 +349,7 @@ describe('PostSearchService', () => { searchCondition.details = ['hello my name is taehoon kim']; searchConditionUsingES.body.query.bool.filter.bool.must.push({ multi_match: { - query: searchCondition.details, + query: searchCondition.details[0], fields: ['title', 'content', 'code', 'language', 'authorNickname'], }, }); @@ -300,6 +372,7 @@ describe('PostSearchService', () => { ]; //when await service.search(searchCondition); + throw new Error(); } catch (err) { expect(err).toBeInstanceOf(SearchParamInvalidException); } @@ -312,6 +385,7 @@ describe('PostSearchService', () => { searchCondition.reviewCount = -1; //when await service.search(searchCondition); + throw new Error(); } catch (err) { expect(err).toBeInstanceOf(SearchParamInvalidException); } @@ -323,17 +397,55 @@ describe('PostSearchService', () => { searchCondition.reviewCount = 21; //when await service.search(searchCondition); + throw new Error(); } catch (err) { expect(err).toBeInstanceOf(SearchParamInvalidException); } }); + it('reviewCount가 1개일 때 정상 출력한다', async () => { + //given + searchCondition.reviewCount = 1; + searchConditionUsingES.body.query.bool.filter.bool.must.push({ + range: { + reviewCount: { + gte: searchCondition.reviewCount, + }, + }, + }); + + //when + await service.search(searchCondition); + + expect(esService.search).toBeCalled(); + expect(esService.search).toBeCalledWith(searchConditionUsingES); + }); + + it('likeCount 1개일 때 정상 출력한다', async () => { + //given + searchCondition.likeCount = 1; + searchConditionUsingES.body.query.bool.filter.bool.must.push({ + range: { + likeCount: { + gte: searchCondition.likeCount, + }, + }, + }); + + //when + await service.search(searchCondition); + + expect(esService.search).toBeCalled(); + expect(esService.search).toBeCalledWith(searchConditionUsingES); + }); + it('likeCount가 음수면, 예외를 반환한다', async () => { try { //given searchCondition.likeCount = -1; //when await service.search(searchCondition); + throw new Error(); } catch (err) { expect(err).toBeInstanceOf(SearchParamInvalidException); } @@ -345,10 +457,94 @@ describe('PostSearchService', () => { searchCondition.likeCount = 21; //when await service.search(searchCondition); + throw new Error(); } catch (err) { expect(err).toBeInstanceOf(SearchParamInvalidException); } }); }); + + describe('결과값 검증', () => { + it('검색결과가 id로 오름차순 정렬되었을 때, 정상 출력한다', async () => { + //given + mockElasticSearchService.search = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { _source: { id: 1 } }, + { _source: { id: 2 } }, + { _source: { id: 3 } }, + ], + }, + }); + //when + const result = await service.search(searchCondition); + + //then + expect(result).toStrictEqual([ + { _source: { id: 1 } }, + { _source: { id: 2 } }, + { _source: { id: 3 } }, + ]); + }); + + it('검색결과가 id로 오름차순 정렬되지 않았을 때, 예외를 반환한다', async () => { + try { + mockElasticSearchService.search = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { _source: { id: 4 } }, + { _source: { id: 2 } }, + { _source: { id: 3 } }, + ], + }, + }); + //when + await service.search(searchCondition); + throw new Error(); + } catch (err) { + expect(err).toBeInstanceOf(SearchResponseInvalidException); + } + }); + + it('N+1개 보다 많은 결과가 오면, 예외를 반환한다', async () => { + try { + mockElasticSearchService.search = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { _source: { id: 1 } }, + { _source: { id: 2 } }, + { _source: { id: 3 } }, + { _source: { id: 4 } }, + { _source: { id: 5 } }, + ], + }, + }); + //when + await service.search(searchCondition); + throw new Error(); + } catch (err) { + expect(err).toBeInstanceOf(SearchResponseInvalidException); + } + }); + + it('결과값에 중복되는 아이디가 있으면 예외를 반환한다', async () => { + try { + mockElasticSearchService.search = jest.fn().mockResolvedValue({ + hits: { + hits: [ + { _source: { id: 1 } }, + { _source: { id: 1 } }, + { _source: { id: 3 } }, + ], + }, + }); + //when + await service.search(searchCondition); + throw new Error(); + } catch (err) { + expect(err).toBeInstanceOf(SearchResponseInvalidException); + } + }); + }); }); }); diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index 31f42784..347c81af 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -4,6 +4,21 @@ import { Post } from './post.entity'; import { LoadPostListRequestDto } from './dto/service-request.dto'; import { SEND_POST_CNT } from './post.controller'; import { ConfigService } from '@nestjs/config'; +import { PostNotFoundException } from '../../exception/post-not-found.exception'; +import { PostInputInvalidException } from '../../exception/post-input-invalid.exception'; +import { SearchParamInvalidException } from '../../exception/search-param-invalid.exception'; +import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import { SearchResponseInvalidException } from '../../exception/search-response-invalid.exception'; +import { TagInvalidException } from '../../exception/tag-invalid.exception'; + +const MAX_TAG_COUNT = 5; // TODO 상수 클래스 지정 + +const MIN_LIKES_FILTERING_COUNT = 0; +const MIN_REVIEW_FILTERING_COUNT = 0; +const MAX_LIKES_FILTERING_COUNT = 20; +const MAX_REVIEW_FILTERING_COUNT = 20; + +const MAX_TAG_LENGTH = 30; @Injectable() export class PostSearchService { @@ -12,11 +27,19 @@ export class PostSearchService { private readonly configService: ConfigService, ) {} - async indexPost(post: Post, tags: any) { + indexPost(post: Post, tags: string[]) { + if (!post) { + throw new PostNotFoundException(); + } + + if (tags.length > MAX_TAG_COUNT) { + throw new PostInputInvalidException('태그의 개수가 적절하지 않습니다'); + } + tags.map((tag) => tag.trim()).forEach((tag) => this.validateTag(tag)); + let tagValue = ''; if (tags) { - tags.sort(); - tagValue = tags.join(' '); + tagValue = Array.from(new Set(tags)).join(' '); } this.esService.index({ @@ -43,6 +66,8 @@ export class PostSearchService { async search(loadPostListRequestDto: LoadPostListRequestDto) { const { lastId, tags, reviewCount, likeCount, details } = loadPostListRequestDto; + this.validateParam(loadPostListRequestDto); + const searchFilter: any = { sort: [ { @@ -65,19 +90,17 @@ export class PostSearchService { }, }, }; - if (lastId !== -1) { + if (!(lastId === null || lastId === undefined || lastId === -1)) { searchFilter.search_after = [lastId]; } - if (details && details.length > 0) { - for (const detail of details) { - searchFilter.body.query.bool.filter.bool.must.push({ - multi_match: { - query: detail, - fields: ['title', 'content', 'code', 'language', 'authorNickname'], - }, - }); - } + if (details.length === 1) { + searchFilter.body.query.bool.filter.bool.must.push({ + multi_match: { + query: details[0], + fields: ['title', 'content', 'code', 'language', 'authorNickname'], + }, + }); } if (tags && tags.length > 0) { @@ -116,7 +139,74 @@ export class PostSearchService { } const body = await this.esService.search(searchFilter); - console.log(body.hits.hits[0]); + this.validateReturnValue(body.hits.hits); return body.hits.hits; } + + private validateReturnValue(hits: SearchHit[]) { + if (hits.length > SEND_POST_CNT + 1) { + throw new SearchResponseInvalidException( + '너무 많은 검색 결과가 반환되었습니다', + ); + } + const ids = hits.map((each) => each._source['id']); + if (new Set(ids).size !== ids.length) { + throw new SearchResponseInvalidException( + '중복되는 결과가 반환되었습니다', + ); + } + let min = 0; + for (const id of ids) { + if (min >= id) { + throw new SearchResponseInvalidException( + '결과가 오름차순 정렬되지 않았습니다', + ); + } + min = id; + } + } + + private validateParam(loadPostListRequestDto: LoadPostListRequestDto) { + const { tags, reviewCount, likeCount, details } = loadPostListRequestDto; + + if (tags.length > MAX_TAG_COUNT) { + throw new SearchParamInvalidException('태그의 개수가 적절하지 않습니다'); + } + + tags.forEach((tag) => this.validateTag(tag)); + if ( + reviewCount < MIN_REVIEW_FILTERING_COUNT || + MAX_REVIEW_FILTERING_COUNT < reviewCount + ) { + throw new SearchParamInvalidException( + '선택한 리뷰 개수는 적절하지 않습니다', + ); + } + if ( + likeCount < MIN_LIKES_FILTERING_COUNT || + MAX_LIKES_FILTERING_COUNT < likeCount + ) { + throw new SearchParamInvalidException( + '선택한 리뷰 개수는 적절하지 않습니다', + ); + } + if (details.length > 1) { + throw new SearchParamInvalidException( + '검색어의 개수가 적절하지 않습니다', + ); + } + } + + private validateTag(tag: string) { + if (tag.includes(' ')) { + throw new TagInvalidException('태그 이름은 공백이 올 수 없습니다'); + } + + if (tag.length === 0) { + throw new TagInvalidException('태그는 공백이 올 수 없습니다'); + } + if (tag.length > MAX_TAG_LENGTH) { + throw new TagInvalidException('태그의 길이가 너무 깁니다'); + } + } } diff --git a/server/src/domain/post/post.controller.ts b/server/src/domain/post/post.controller.ts index 7b3f8c59..908a2107 100644 --- a/server/src/domain/post/post.controller.ts +++ b/server/src/domain/post/post.controller.ts @@ -73,7 +73,12 @@ export class PostController { @Query() inquiryDto: InquiryDto, @Headers() headers, ): Promise { - const { tags, lastId, reviewCount, likeCount, details } = inquiryDto; + const { lastId, reviewCount, likeCount, details } = inquiryDto; + let { tags } = inquiryDto; + // TODO @Transform을 쓰는게 맞을지, 이게 맞을지 고민하기 + if (typeof tags === 'string') { + tags = [tags]; + } const returnValue = await this.postService.loadPostList( new LoadPostListRequestDto(lastId, tags, reviewCount, likeCount, details), ); diff --git a/server/src/domain/post/post.service.spec.ts b/server/src/domain/post/post.service.spec.ts index ef26794d..4cd269ea 100644 --- a/server/src/domain/post/post.service.spec.ts +++ b/server/src/domain/post/post.service.spec.ts @@ -36,7 +36,22 @@ describe('PostService', () => { UserRepository, TagRepository, ImageRepository, - PostSearchService, + { + provide: PostSearchService, + useValue: { + indexPost: () => jest.fn(), + search: () => + jest.fn().mockResolvedValue({ + hits: { + hits: [ + { _source: { id: 1 } }, + { _source: { id: 2 } }, + { _source: { id: 3 } }, + ], + }, + }), + }, + }, { provide: DataSource, useValue: { @@ -72,6 +87,7 @@ describe('PostService', () => { expect(service).toBeDefined(); }); + // TODO PostService에서 글 작성시, 중복된 태그 입력하면 예외 처리하기 describe('글 작성', () => { const writeDto: WriteDto = { title: '제목', @@ -98,6 +114,9 @@ describe('PostService', () => { tagRepository.findOneBy = jest.fn( () => new Promise((resolve) => resolve(new Tag())), ); + const post = new Post(); + post.id = 1; + postRepository.save = jest.fn().mockResolvedValue(post); await service.write(1, writeDto); expect(postRepository.save).toBeCalledTimes(1); diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index aa421137..35488218 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -123,6 +123,9 @@ export class PostService { ): Promise { let isLast = true; const results = await this.postSearchService.search(loadPostListRequestDto); + if (results.length > SEND_POST_CNT + 1) { + throw new Error('너무 많은 검색 결과가 반환되었습니다'); + } if (this.canGetNextPost(results.length)) { results.pop(); isLast = false; diff --git a/server/src/exception/search-response-invalid.exception.ts b/server/src/exception/search-response-invalid.exception.ts new file mode 100644 index 00000000..e9a608b6 --- /dev/null +++ b/server/src/exception/search-response-invalid.exception.ts @@ -0,0 +1,5 @@ +export class SearchResponseInvalidException extends Error { + constructor(msg) { + super(msg); + } +} diff --git a/server/src/exception/tag-invalid.exception.ts b/server/src/exception/tag-invalid.exception.ts new file mode 100644 index 00000000..f7cb9cbe --- /dev/null +++ b/server/src/exception/tag-invalid.exception.ts @@ -0,0 +1,5 @@ +export class TagInvalidException extends Error { + constructor(msg) { + super(msg); + } +} From 3cbf6465fff4a98b54bfd3c9019bc8327db8daa7 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Tue, 13 Dec 2022 12:25:00 +0900 Subject: [PATCH 12/31] =?UTF-8?q?fix(BE):=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EC=A4=80=EC=9D=84=20id=20=20?= =?UTF-8?q?=EB=82=B4=EB=A6=BC=EC=B0=A8=EC=88=9C=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/post/post-search.service.spec.ts | 22 +++++++++---------- server/src/domain/post/post-search.service.ts | 6 ++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/server/src/domain/post/post-search.service.spec.ts b/server/src/domain/post/post-search.service.spec.ts index 24a4169c..c959f185 100644 --- a/server/src/domain/post/post-search.service.spec.ts +++ b/server/src/domain/post/post-search.service.spec.ts @@ -465,14 +465,14 @@ describe('PostSearchService', () => { }); describe('결과값 검증', () => { - it('검색결과가 id로 오름차순 정렬되었을 때, 정상 출력한다', async () => { + it('검색결과가 id로 내림차순 정렬되었을 때, 정상 출력한다', async () => { //given mockElasticSearchService.search = jest.fn().mockResolvedValue({ hits: { hits: [ - { _source: { id: 1 } }, - { _source: { id: 2 } }, { _source: { id: 3 } }, + { _source: { id: 2 } }, + { _source: { id: 1 } }, ], }, }); @@ -481,13 +481,13 @@ describe('PostSearchService', () => { //then expect(result).toStrictEqual([ - { _source: { id: 1 } }, - { _source: { id: 2 } }, { _source: { id: 3 } }, + { _source: { id: 2 } }, + { _source: { id: 1 } }, ]); }); - it('검색결과가 id로 오름차순 정렬되지 않았을 때, 예외를 반환한다', async () => { + it('검색결과가 id로 내림차순 정렬되지 않았을 때, 예외를 반환한다', async () => { try { mockElasticSearchService.search = jest.fn().mockResolvedValue({ hits: { @@ -511,11 +511,11 @@ describe('PostSearchService', () => { mockElasticSearchService.search = jest.fn().mockResolvedValue({ hits: { hits: [ - { _source: { id: 1 } }, - { _source: { id: 2 } }, - { _source: { id: 3 } }, - { _source: { id: 4 } }, { _source: { id: 5 } }, + { _source: { id: 4 } }, + { _source: { id: 3 } }, + { _source: { id: 2 } }, + { _source: { id: 1 } }, ], }, }); @@ -532,9 +532,9 @@ describe('PostSearchService', () => { mockElasticSearchService.search = jest.fn().mockResolvedValue({ hits: { hits: [ + { _source: { id: 3 } }, { _source: { id: 1 } }, { _source: { id: 1 } }, - { _source: { id: 3 } }, ], }, }); diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index 347c81af..5a1babfc 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -155,11 +155,11 @@ export class PostSearchService { '중복되는 결과가 반환되었습니다', ); } - let min = 0; + let min = 21000000; for (const id of ids) { - if (min >= id) { + if (min < id) { throw new SearchResponseInvalidException( - '결과가 오름차순 정렬되지 않았습니다', + '결과가 내림차순 정렬되지 않았습니다', ); } min = id; From 59de77835070d5a07aa385d354484d1d45c43d8b Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Tue, 13 Dec 2022 15:54:48 +0900 Subject: [PATCH 13/31] =?UTF-8?q?feat(FE):=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=8D=94=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Post/PostImageSlider/PostImageSlider.scss | 29 ++++++++--- .../Post/PostImageSlider/PostImageSlider.tsx | 49 ++++++++++++++----- .../SlideButton/SlideButton.scss | 40 +++++++++++++++ .../SlideButton/SlideButton.tsx | 38 ++++++++++++++ client/src/constants/style.ts | 1 + client/src/mocks/datasource/mockDataSource.ts | 2 +- client/src/utils/style.ts | 6 +++ 7 files changed, 147 insertions(+), 18 deletions(-) create mode 100644 client/src/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton.scss create mode 100644 client/src/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton.tsx create mode 100644 client/src/constants/style.ts create mode 100644 client/src/utils/style.ts diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss index 82ea6e17..ec30029e 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss @@ -1,14 +1,31 @@ .post__image-slider { + display: flex; flex-shrink: 0; - width: 100%; - height: 51rem; - overflow: hidden; - background-color: black; + height: 100%; + + &--relative { + position: relative; + } + + &__wrapper { + width: 51rem; + min-width: 51rem; + height: 51rem; + overflow: hidden; + background-color: black; + } &--image { - width: inherit; - height: inherit; + width: auto; + min-width: 51rem; + height: auto; cursor: pointer; object-fit: cover; // 가로-세로 비율 맞춰서 꽉 차게 } + + &--button { + position: absolute; + top: 50%; + width: 100%; + } } diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx index 8ea621de..3446c893 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx @@ -1,32 +1,59 @@ -import React, { useContext } from "react"; +import React, { useContext, useState } from "react"; import { PostContext } from "@/components/main/PostScroll/Post/Post"; import ProgressiveImage from "@/components/commons/ProgressiveImage/ProgressiveImage"; import codePlaceholder from "@/assets/progressive-image.jpg"; import useCommonModalStore from "@/store/useCommonModalStore"; import ReviewModal from "@/components/main/Modal/ReviewModal/ReviewModal"; +import SlideButton from "@/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton"; +import { getNextImageStyle } from "@/utils/style"; import "./PostImageSlider.scss"; const PostImageSlider = (): JSX.Element => { const { images, id: postId, code, language } = useContext(PostContext); const [openModal] = useCommonModalStore((state) => [state.openModal]); + const [currentImgIndex, setCurrentImgIndex] = useState(0); + const [style, setStyle] = useState(getNextImageStyle(currentImgIndex)); + + const handleNextImage = (): void => { + setCurrentImgIndex(currentImgIndex + 1); + setStyle(getNextImageStyle(currentImgIndex + 1)); + }; + + const handlePrevSlide = (): void => { + setCurrentImgIndex(currentImgIndex - 1); + setStyle(getNextImageStyle(currentImgIndex - 1)); + }; const handleClickImage = (): void => { openModal(); }; return ( -
- +
+
+
+ {images.map((image) => ( + + ))} +
+ +
); }; diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton.scss b/client/src/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton.scss new file mode 100644 index 00000000..5d304825 --- /dev/null +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton.scss @@ -0,0 +1,40 @@ +@import "@/styles/theme"; +@import "@/styles/mixin"; + +.prev-button, +.next-button { + @include flex-center; + position: absolute; + top: 50%; + width: 2rem; + height: 2rem; + padding: 0; + text-align: center; + cursor: pointer; + background-color: $code-color; + border: none; + border-radius: $radius-circle; + + & > svg { + width: 1.2rem; + height: 1.2rem; + margin-bottom: 0.5px; + color: $codeblock-color; + } +} + +.prev-button { + left: 1%; + + & > svg { + margin-right: 1.2px; + } +} + +.next-button { + right: 1%; + + & > svg { + margin-left: 2px; + } +} diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton.tsx b/client/src/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton.tsx new file mode 100644 index 00000000..41762a8c --- /dev/null +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton.tsx @@ -0,0 +1,38 @@ +import React, { FC } from "react"; +import ArrowBackIosOutlinedIcon from "@mui/icons-material/ArrowBackIosNewOutlined"; +import ArrowForwardIosOutlinedIcon from "@mui/icons-material/ArrowForwardIosOutlined"; + +import { ARROW_ICON_STYLE } from "@/constants/style"; + +import "./SlideButton.scss"; + +interface SlideButtonProps { + isFirst: boolean; + isLast: boolean; + handlePrevImage: () => void; + handleNextImage: () => void; +} + +const SlideButton: FC = ({ + isFirst, + isLast, + handlePrevImage, + handleNextImage, +}) => { + return ( + <> + {!isFirst && ( + + )} + {!isLast && ( + + )} + + ); +}; + +export default SlideButton; diff --git a/client/src/constants/style.ts b/client/src/constants/style.ts new file mode 100644 index 00000000..a27e3fbc --- /dev/null +++ b/client/src/constants/style.ts @@ -0,0 +1 @@ +export const ARROW_ICON_STYLE = { stroke: "#292c33", strokeWidth: 0.5 }; diff --git a/client/src/mocks/datasource/mockDataSource.ts b/client/src/mocks/datasource/mockDataSource.ts index dc729e5e..52794fe4 100644 --- a/client/src/mocks/datasource/mockDataSource.ts +++ b/client/src/mocks/datasource/mockDataSource.ts @@ -24,7 +24,7 @@ posts = Array.from(Array(1024).keys()).map((id) => ({ name: "image1", }, { - src: "http://placeimg.com/640/640/animals", + src: "http://placeimg.com/640/640/people", name: "image1", }, ], diff --git a/client/src/utils/style.ts b/client/src/utils/style.ts new file mode 100644 index 00000000..6b86b2da --- /dev/null +++ b/client/src/utils/style.ts @@ -0,0 +1,6 @@ +export const getNextImageStyle = ( + index: number +): { transform: string; transition: string } => ({ + transform: `translateX(-${index}00%)`, + transition: `all 0.4s ease-in-out`, +}); From 96857533e1af4f75635a1e45b22b37b07f2295ff Mon Sep 17 00:00:00 2001 From: WOOSERK <55542546+WOOSERK@users.noreply.github.com> Date: Tue, 13 Dec 2022 16:00:13 +0900 Subject: [PATCH 14/31] =?UTF-8?q?feat(BE):=2024=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EB=8F=99=EC=9E=91=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=EC=97=90=20=EC=8B=9C=EA=B0=84=20=EA=B0=80=EC=A4=91?= =?UTF-8?q?=EC=B9=98=20=EC=A0=81=EC=9A=A9=20(#392)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ranking/ranking.controller.ts | 2 +- .../src/ranking/ranking.service.ts | 127 +++++++++--------- 2 files changed, 63 insertions(+), 66 deletions(-) diff --git a/scheduler-server/src/ranking/ranking.controller.ts b/scheduler-server/src/ranking/ranking.controller.ts index 6d0db36d..0eddd94a 100644 --- a/scheduler-server/src/ranking/ranking.controller.ts +++ b/scheduler-server/src/ranking/ranking.controller.ts @@ -19,7 +19,7 @@ import { ApiTags, } from '@nestjs/swagger'; -export const MAX_TAG_COUNT = 10; +export const MAX_TAG_COUNT = 5; export const TAG_NAME_REGEX = /^[0-9|a-z|A-Z|ㄱ-ㅎ|가-힣]+$/; @Controller() diff --git a/scheduler-server/src/ranking/ranking.service.ts b/scheduler-server/src/ranking/ranking.service.ts index 7537409d..d1b2db29 100644 --- a/scheduler-server/src/ranking/ranking.service.ts +++ b/scheduler-server/src/ranking/ranking.service.ts @@ -1,23 +1,23 @@ import { Injectable } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; +import { Interval } from '@nestjs/schedule'; + +export const QUEUE_CYCLE = 15; +export const ROTATION_PER_MINUTE = 60 / QUEUE_CYCLE; // 15초마다 1번 +export const ARRAY_SIZE = ROTATION_PER_MINUTE * 60 * 24; // 1분에 4번 * 60(분) * 24(시간) -export const MINUTES_PER_HOUR = 60; -export const QUEUE_CYCLE_EXPRESSED_SECOND = 15; export const RANKING_COUNT = 10; export const TAG_NAME_INDEX = 0; export const TAG_COUNT_INDEX = 1; @Injectable() export class RankingService { - private readonly tagsCountsCircularQueue: any[]; - private tagCountBuffer; + private readonly tagCountsCircularQueue: any[]; + private index: number; private ranking: any[]; constructor() { - this.tagsCountsCircularQueue = new Array( - (MINUTES_PER_HOUR / QUEUE_CYCLE_EXPRESSED_SECOND) * MINUTES_PER_HOUR * 24, - ); - this.tagCountBuffer = {}; + this.tagCountsCircularQueue = new Array(ARRAY_SIZE); + this.index = 0; this.ranking = []; // TODO DB를 연동해 초기데이터 넣어주기 } @@ -27,54 +27,37 @@ export class RankingService { } async saveSearchedTags(tags: string[]) { + const tagCounts = this.tagCountsCircularQueue[this.index]; + tags.map((tag) => { - if (this.tagCountBuffer[tag]) { - this.tagCountBuffer[tag] += 1; + if (tagCounts[tag]) { + tagCounts[tag] += 1; } else { - this.tagCountBuffer[tag] = 1; + tagCounts[tag] = 1; } }); } - @Cron('0/' + QUEUE_CYCLE_EXPRESSED_SECOND + ' * * * * *') - async updateRanking() { - this.putValueInQueue(this.tagCountBuffer); - this.tagCountBuffer = {}; - const curRanking = this.getTopRankTagNames(); - this.ranking = this.addUpAndDownInfo(curRanking); - } - - /** - * 일정 시간마다 tagCountBuffer를 tagsCountsCircularQueue에 넣는다 - * 이후 tagCountBuffer를 초기화한다 - */ - private putValueInQueue(tagCountBuffer) { - const index = this.makeIndexUsingTimeStamp(new Date()); - this.tagsCountsCircularQueue[index] = Object.assign({}, tagCountBuffer); - } + @Interval(4000) + updateRanking() { + const tagCounts = this.countAllTags(); + const newRanking = this.getTopRankTagNames(tagCounts); + this.ranking = this.addPrevInfo(newRanking); - /** - * 시간을 사용해 Queue의 인덱스를 만든다 - * 일정 시간이 지나면 덮어쓸 큐의 index를 구하기 위해 사용한다 - */ - private makeIndexUsingTimeStamp(date: Date) { - return ( - date.getMinutes() * (MINUTES_PER_HOUR / QUEUE_CYCLE_EXPRESSED_SECOND) + - Math.floor(date.getSeconds() / QUEUE_CYCLE_EXPRESSED_SECOND) - ); + this.index = (this.index + 1) % this.tagCountsCircularQueue.length; + this.tagCountsCircularQueue[this.index] = {}; } /** * 가장 검색이 자주 된 최상위 10개 태그의 이름을 반환한다 * 만약 데이터가 10개가 되지 않으면, 존재하는 만큼만 반환한다 */ - private getTopRankTagNames() { - const tagsCount = this.countForEachTags(); + private getTopRankTagNames(tagCounts) { + const result = Object.keys(tagCounts).reduce((arr, tag) => { + arr.push([tag, tagCounts[tag]]); + return arr; + }, []); - const result = []; - for (const tagName of Object.keys(tagsCount)) { - result.push([tagName, tagsCount[tagName]]); - } result.sort((prev, next) => next[TAG_COUNT_INDEX] - prev[TAG_COUNT_INDEX]); return result @@ -82,35 +65,49 @@ export class RankingService { .map((each) => each[TAG_NAME_INDEX]); } - private countForEachTags() { - const tagsCount = {}; - for (const tagsCountsSearchedAtTime of this.tagsCountsCircularQueue) { - if (!tagsCountsSearchedAtTime) { - continue; + /** + * 시간 가중치를 적용한 태그 개수들을 반환한다 + */ + private countAllTags() { + const totalTagCounts = {}; + this.tagCountsCircularQueue.forEach((tagCounts, idx) => { + if (!tagCounts) { + return; } - for (const tagName of Object.keys(tagsCountsSearchedAtTime)) { - if (tagsCount[tagName]) { - tagsCount[tagName] += tagsCountsSearchedAtTime[tagName]; + + const timeWaste = this.calcTimeWaste(idx); + for (const tag in tagCounts) { + if (totalTagCounts[tag]) { + totalTagCounts[tag] += tagCounts[tag] * timeWaste; continue; } - tagsCount[tagName] = tagsCountsSearchedAtTime[tagName]; + + totalTagCounts[tag] = tagCounts[tag] * timeWaste; } - } - return tagsCount; + }); + + return totalTagCounts; } - private addUpAndDownInfo(curRanking: any[]) { - const rankingInfo = []; - for (let i = 0; i < curRanking.length; i++) { - const tagName = curRanking[i]; - if (!this.ranking) { - rankingInfo.push({ name: tagName, prev: 0 }); - continue; - } + /** + * 시간 가중치는 현재 큐의 인덱스와 배열의 크기에 따라 정해진다 + */ + private calcTimeWaste(idx: number): number { + const offset = idx - this.index + ARRAY_SIZE - 1; + const timeWaste = ((offset % ARRAY_SIZE) + 1) / ARRAY_SIZE; + + return timeWaste; + } + + private addPrevInfo(newRanking: any[]) { + const rankingInfo = newRanking.reduce((arr, tag) => { const prevIndex = - this.ranking.map((obj) => obj.name).indexOf(tagName) + 1; - rankingInfo.push({ name: tagName, prev: prevIndex }); - } + this.ranking.findIndex((tagInfo) => tagInfo.name === tag) + 1; + + arr.push({ name: tag, prev: prevIndex }); + return arr; + }, []); + return rankingInfo; } } From 27e63654d31cac8a5dd1e3bba1bf444a71cdc047 Mon Sep 17 00:00:00 2001 From: WOOSERK <55542546+WOOSERK@users.noreply.github.com> Date: Tue, 13 Dec 2022 16:28:30 +0900 Subject: [PATCH 15/31] =?UTF-8?q?feat(BE):=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EA=B0=84=EA=B2=A9=204=EC=B4=88=EC=97=90=EC=84=9C=2015=EC=B4=88?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#392)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scheduler-server/src/ranking/ranking.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scheduler-server/src/ranking/ranking.service.ts b/scheduler-server/src/ranking/ranking.service.ts index d1b2db29..e3e86a5f 100644 --- a/scheduler-server/src/ranking/ranking.service.ts +++ b/scheduler-server/src/ranking/ranking.service.ts @@ -38,7 +38,7 @@ export class RankingService { }); } - @Interval(4000) + @Interval(15000) updateRanking() { const tagCounts = this.countAllTags(); const newRanking = this.getTopRankTagNames(tagCounts); From 60cdeec25063c7e9916475fe8d37c17b6fe553b2 Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Tue, 13 Dec 2022 16:59:42 +0900 Subject: [PATCH 16/31] =?UTF-8?q?design(FE):=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=ED=95=98=EB=8B=A8=20=EB=AA=87=EB=B2=88=EC=A8=B0=20=EC=8A=AC?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=8D=98=EC=A7=80=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A3=BC=EB=8A=94=20=EC=A0=90=20=EC=B6=94=EA=B0=80=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Post/PostImageSlider/PostImageSlider.scss | 29 +++++++++++++++++++ .../Post/PostImageSlider/PostImageSlider.tsx | 14 +++++++++ 2 files changed, 43 insertions(+) diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss index ec30029e..6f4234f4 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss @@ -1,3 +1,5 @@ +@import "@/styles/theme"; + .post__image-slider { display: flex; flex-shrink: 0; @@ -28,4 +30,31 @@ top: 50%; width: 100%; } + + &__dots { + position: absolute; + bottom: 0; + display: flex; + justify-content: center; + width: 100%; + pointer-events: none; + + &--other, + &--now { + width: 0.8rem; + height: 0.8rem; + margin: 0.8rem 0.2rem; + border-radius: $radius-circle; + transition: all 0.2s ease-in-out; + } + + &--other { + background-color: $weview-off-white; + opacity: 0.5; + } + + &--now { + background-color: $weview-off-white; + } + } } diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx index 3446c893..daf522c9 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx @@ -53,6 +53,20 @@ const PostImageSlider = (): JSX.Element => { handlePrevImage={handlePrevSlide} handleNextImage={handleNextImage} /> + {images.length >= 1 && ( +
+ {images.map((image, idx) => { + return ( +
+ ); + })} +
+ )}
); From aa54d58ed49972de2324e9eead1aed14e61a992b Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Tue, 13 Dec 2022 18:18:46 +0900 Subject: [PATCH 17/31] =?UTF-8?q?fix(FE):=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EA=B0=80=20=EB=91=90=20=EC=9E=A5=20=EC=9D=B4=EC=83=81=EC=9D=BC?= =?UTF-8?q?=20=EB=95=8C=EB=A7=8C=20dots=EC=9D=B4=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx index daf522c9..9c44d79e 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx @@ -53,7 +53,7 @@ const PostImageSlider = (): JSX.Element => { handlePrevImage={handlePrevSlide} handleNextImage={handleNextImage} /> - {images.length >= 1 && ( + {images.length > 1 && (
{images.map((image, idx) => { return ( From 14b7d7e4367dee3d3a8794e0c42b986789dcdd3a Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Tue, 13 Dec 2022 18:40:34 +0900 Subject: [PATCH 18/31] =?UTF-8?q?design(FE):=20=ED=88=AC=EB=AA=85=ED=95=98?= =?UTF-8?q?=EB=8B=A4=EA=B0=80=20wrapper=20=ED=98=B8=EB=B2=84=EC=8B=9C=20do?= =?UTF-8?q?ts=20=EC=84=A0=EB=AA=85=ED=95=B4=EC=A7=80=EB=8A=94=20=EC=8A=AC?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=8D=94=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Post/PostImageSlider/PostImageSlider.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss index 6f4234f4..abc01f80 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.scss @@ -5,6 +5,10 @@ flex-shrink: 0; height: 100%; + &:hover .post__image-slider__dots { + opacity: 1; + } + &--relative { position: relative; } @@ -15,6 +19,11 @@ height: 51rem; overflow: hidden; background-color: black; + + &:hover .post__image-slider__dots { + opacity: 1; + transition: all 0.2s ease-in-out; + } } &--image { @@ -38,6 +47,7 @@ justify-content: center; width: 100%; pointer-events: none; + opacity: 0.4; &--other, &--now { From 4b7eec44d01ca1ec01a5b47bc22c8e6c218b51e0 Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Tue, 13 Dec 2022 18:54:16 +0900 Subject: [PATCH 19/31] =?UTF-8?q?refactor(FE):=20hanldePrevSlide=20->=20ha?= =?UTF-8?q?ndlePrevImage=20=ED=95=A8=EC=88=98=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Post/PostImageSlider/PostImageSlider.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx index 9c44d79e..55400861 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx @@ -16,16 +16,16 @@ const PostImageSlider = (): JSX.Element => { const [currentImgIndex, setCurrentImgIndex] = useState(0); const [style, setStyle] = useState(getNextImageStyle(currentImgIndex)); + const handlePrevImage = (): void => { + setCurrentImgIndex(currentImgIndex - 1); + setStyle(getNextImageStyle(currentImgIndex - 1)); + }; + const handleNextImage = (): void => { setCurrentImgIndex(currentImgIndex + 1); setStyle(getNextImageStyle(currentImgIndex + 1)); }; - const handlePrevSlide = (): void => { - setCurrentImgIndex(currentImgIndex - 1); - setStyle(getNextImageStyle(currentImgIndex - 1)); - }; - const handleClickImage = (): void => { openModal(); }; @@ -50,7 +50,7 @@ const PostImageSlider = (): JSX.Element => { {images.length > 1 && ( From b9c534c78335accafeadf2509d9ec3177b96168a Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Tue, 13 Dec 2022 20:32:43 +0900 Subject: [PATCH 20/31] =?UTF-8?q?feat(FE):=20=EB=AA=A8=EB=B0=94=EC=9D=BC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=94=A9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=8A=AC=EB=9D=BC=EC=9D=B4=EB=8D=94=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EA=B5=AC=ED=98=84=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Post/PostImageSlider/PostImageSlider.tsx | 49 +++++++++++++++++-- client/src/utils/style.ts | 16 ++++-- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx index 55400861..090a3708 100644 --- a/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx +++ b/client/src/components/main/PostScroll/Post/PostImageSlider/PostImageSlider.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React, { TouchEvent, useContext, useRef, useState } from "react"; import { PostContext } from "@/components/main/PostScroll/Post/Post"; import ProgressiveImage from "@/components/commons/ProgressiveImage/ProgressiveImage"; @@ -6,7 +6,7 @@ import codePlaceholder from "@/assets/progressive-image.jpg"; import useCommonModalStore from "@/store/useCommonModalStore"; import ReviewModal from "@/components/main/Modal/ReviewModal/ReviewModal"; import SlideButton from "@/components/main/PostScroll/Post/PostImageSlider/SlideButton/SlideButton"; -import { getNextImageStyle } from "@/utils/style"; +import { get3dImageTransformStyle, getNextImageStyle } from "@/utils/style"; import "./PostImageSlider.scss"; @@ -14,14 +14,24 @@ const PostImageSlider = (): JSX.Element => { const { images, id: postId, code, language } = useContext(PostContext); const [openModal] = useCommonModalStore((state) => [state.openModal]); const [currentImgIndex, setCurrentImgIndex] = useState(0); + const imageRef = useRef(null); const [style, setStyle] = useState(getNextImageStyle(currentImgIndex)); + const [touchedX, setTouchedX] = useState({ start: 0, end: 0 }); const handlePrevImage = (): void => { + if (currentImgIndex === 0) { + setStyle(getNextImageStyle(currentImgIndex)); + return; + } setCurrentImgIndex(currentImgIndex - 1); setStyle(getNextImageStyle(currentImgIndex - 1)); }; const handleNextImage = (): void => { + if (currentImgIndex === images.length - 1) { + setStyle(getNextImageStyle(currentImgIndex)); + return; + } setCurrentImgIndex(currentImgIndex + 1); setStyle(getNextImageStyle(currentImgIndex + 1)); }; @@ -30,10 +40,41 @@ const PostImageSlider = (): JSX.Element => { openModal(); }; + const handleTouchStartWrapper = (e: TouchEvent): void => { + setTouchedX({ + ...touchedX, + start: e.touches[0].pageX, + }); + }; + + const handleMoveWrapper = (e: TouchEvent): void => { + if (imageRef?.current != null) { + const current = imageRef.current.clientWidth * currentImgIndex; + // e.targetTouches[0] 은 처음 터치한 표면 정보 즉 마우스 이동한만큼 이미지 이동 + const result = -current + (e.targetTouches[0].pageX - touchedX.start); + setStyle(get3dImageTransformStyle(result)); + } + }; + + const handleTouchEndWrapper = (e: TouchEvent): void => { + const end = e.changedTouches[0].pageX; + const isMoveRight = touchedX.start > end; + isMoveRight ? handleNextImage() : handlePrevImage(); + setTouchedX({ + ...touchedX, + end, + }); + }; + return (
-
-
+
+
{images.map((image) => ( ({ +interface ImageSlidingStyle { + transform: string; + transition: string; +} + +export const getNextImageStyle = (index: number): ImageSlidingStyle => ({ transform: `translateX(-${index}00%)`, transition: `all 0.4s ease-in-out`, }); + +export const get3dImageTransformStyle = ( + result: number +): ImageSlidingStyle => ({ + transform: `translate3d(${result}px, 0px, 0px)`, + transition: "0ms", +}); From e06800502a8d766bbe5deb5448ab74910edcac14 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Tue, 13 Dec 2022 20:45:11 +0900 Subject: [PATCH 21/31] =?UTF-8?q?fix(BE):=20=EA=B2=B0=EA=B3=BC=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EB=82=B4=EB=B6=80=EC=97=90=EC=84=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=ED=95=B4=20=EA=B2=B0=EA=B3=BC=EC=97=90=20=EA=B4=80?= =?UTF-8?q?=EA=B3=84=EC=97=86=EC=9D=B4=20=EC=A0=95=EB=A0=AC=EB=90=9C=20?= =?UTF-8?q?=EA=B0=92=20=EB=B0=98=ED=99=98=20(#426)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/post/post-search.service.spec.ts | 25 +++-------------- server/src/domain/post/post-search.service.ts | 27 ++++++++----------- .../post/types/post-search-body.interface.ts | 14 +++++----- 3 files changed, 21 insertions(+), 45 deletions(-) diff --git a/server/src/domain/post/post-search.service.spec.ts b/server/src/domain/post/post-search.service.spec.ts index c959f185..a6cde18e 100644 --- a/server/src/domain/post/post-search.service.spec.ts +++ b/server/src/domain/post/post-search.service.spec.ts @@ -465,14 +465,14 @@ describe('PostSearchService', () => { }); describe('결과값 검증', () => { - it('검색결과가 id로 내림차순 정렬되었을 때, 정상 출력한다', async () => { + it('결과값은 내림차순으로 정렬되어진다', async () => { //given mockElasticSearchService.search = jest.fn().mockResolvedValue({ hits: { hits: [ { _source: { id: 3 } }, { _source: { id: 2 } }, - { _source: { id: 1 } }, + { _source: { id: 10 } }, ], }, }); @@ -481,31 +481,12 @@ describe('PostSearchService', () => { //then expect(result).toStrictEqual([ + { _source: { id: 10 } }, { _source: { id: 3 } }, { _source: { id: 2 } }, - { _source: { id: 1 } }, ]); }); - it('검색결과가 id로 내림차순 정렬되지 않았을 때, 예외를 반환한다', async () => { - try { - mockElasticSearchService.search = jest.fn().mockResolvedValue({ - hits: { - hits: [ - { _source: { id: 4 } }, - { _source: { id: 2 } }, - { _source: { id: 3 } }, - ], - }, - }); - //when - await service.search(searchCondition); - throw new Error(); - } catch (err) { - expect(err).toBeInstanceOf(SearchResponseInvalidException); - } - }); - it('N+1개 보다 많은 결과가 오면, 예외를 반환한다', async () => { try { mockElasticSearchService.search = jest.fn().mockResolvedValue({ diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index 5a1babfc..d2074ef7 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -51,14 +51,14 @@ export class PostSearchService { content: post.content, code: post.code, language: post.language, - createdAt: post.createdAt, - updatedAt: post.updatedAt, - authorId: post.user.id, - authorNickname: post.user.nickname, + createdat: post.createdAt, + updatedat: post.updatedAt, + authorid: post.user.id, + authornickname: post.user.nickname, tags: tagValue, - lineCount: post.lineCount, - reviewCount: post.reviewCount, - likeCount: post.likeCount, + linecount: post.lineCount, + reviewcount: post.reviewCount, + likecount: post.likeCount, }, }); } @@ -140,6 +140,10 @@ export class PostSearchService { const body = await this.esService.search(searchFilter); this.validateReturnValue(body.hits.hits); + + body.hits.hits.sort( + (prev, next) => Number(next._source['id']) - Number(prev._source['id']), + ); return body.hits.hits; } @@ -155,15 +159,6 @@ export class PostSearchService { '중복되는 결과가 반환되었습니다', ); } - let min = 21000000; - for (const id of ids) { - if (min < id) { - throw new SearchResponseInvalidException( - '결과가 내림차순 정렬되지 않았습니다', - ); - } - min = id; - } } private validateParam(loadPostListRequestDto: LoadPostListRequestDto) { diff --git a/server/src/domain/post/types/post-search-body.interface.ts b/server/src/domain/post/types/post-search-body.interface.ts index 8eead70f..2c27dd02 100644 --- a/server/src/domain/post/types/post-search-body.interface.ts +++ b/server/src/domain/post/types/post-search-body.interface.ts @@ -4,14 +4,14 @@ interface PostSearchBody { content: string; code: string; language: string; - createdAt: Date; - updatedAt: Date; - authorId: number; - authorNickname: string; + createdat: Date; + updatedat: Date; + authorid: number; + authornickname: string; tags: string; - likeCount: number; - reviewCount: number; - lineCount: number; + likecount: number; + reviewcount: number; + linecount: number; } interface PostSearchResult { From c3ec9248b613216c312d8fa529267a1296cd30af Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Tue, 13 Dec 2022 20:56:08 +0900 Subject: [PATCH 22/31] =?UTF-8?q?test(BE):=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=9D=BC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#426)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/post/post-search.service.spec.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/server/src/domain/post/post-search.service.spec.ts b/server/src/domain/post/post-search.service.spec.ts index a6cde18e..aa1075fb 100644 --- a/server/src/domain/post/post-search.service.spec.ts +++ b/server/src/domain/post/post-search.service.spec.ts @@ -85,14 +85,13 @@ describe('PostSearchService', () => { content: post.content, code: post.code, language: post.language, - createdAt: post.createdAt, - updatedAt: post.updatedAt, - authorId: post.user.id, - authorNickname: post.user.nickname, - // tags: tagValue, - lineCount: post.lineCount, - reviewCount: post.reviewCount, - likeCount: post.likeCount, + createdat: post.createdAt, + updatedat: post.updatedAt, + authorid: post.user.id, + authornickname: post.user.nickname, + linecount: post.lineCount, + reviewcount: post.reviewCount, + likecount: post.likeCount, }, }; }); From 167408955d605141a7a69afe739cfa59e5044c06 Mon Sep 17 00:00:00 2001 From: WOOSERK <55542546+WOOSERK@users.noreply.github.com> Date: Wed, 14 Dec 2022 03:37:40 +0900 Subject: [PATCH 23/31] =?UTF-8?q?ci(DO):=20CI/CD=20=ED=8C=8C=EC=9D=BC=20pa?= =?UTF-8?q?ths-filter=20=EC=95=A1=EC=85=98=EC=9C=BC=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20(#3?= =?UTF-8?q?82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/deployment-dev-api-server.yml | 22 ------ .github/workflows/deployment-dev-client.yml | 25 ------ .../deployment-dev-scheduler-server.yml | 22 ------ .github/workflows/deployment-dev.yml | 77 +++++++++++++++++++ .../workflows/deployment-main-api-server.yml | 22 ------ .github/workflows/deployment-main-client.yml | 25 ------ .../deployment-main-scheduler-server.yml | 22 ------ .github/workflows/deployment-main.yml | 76 ++++++++++++++++++ .github/workflows/integration-api-server.yml | 13 ---- .github/workflows/integration-client.yml | 13 ---- .../integration-scheduler-server.yml | 13 ---- .github/workflows/integration.yml | 49 ++++++++++++ 12 files changed, 202 insertions(+), 177 deletions(-) delete mode 100644 .github/workflows/deployment-dev-api-server.yml delete mode 100644 .github/workflows/deployment-dev-client.yml delete mode 100644 .github/workflows/deployment-dev-scheduler-server.yml create mode 100644 .github/workflows/deployment-dev.yml delete mode 100644 .github/workflows/deployment-main-api-server.yml delete mode 100644 .github/workflows/deployment-main-client.yml delete mode 100644 .github/workflows/deployment-main-scheduler-server.yml create mode 100644 .github/workflows/deployment-main.yml delete mode 100644 .github/workflows/integration-api-server.yml delete mode 100644 .github/workflows/integration-client.yml delete mode 100644 .github/workflows/integration-scheduler-server.yml create mode 100644 .github/workflows/integration.yml diff --git a/.github/workflows/deployment-dev-api-server.yml b/.github/workflows/deployment-dev-api-server.yml deleted file mode 100644 index 4cb997c2..00000000 --- a/.github/workflows/deployment-dev-api-server.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: "[CD] DEV API 서버" -on: - push: - branches: - - dev - paths: - - "server/**" - workflow_dispatch: - -jobs: - reusable: - uses: ./.github/workflows/reusable-deployment-server.yml - with: - docker-context: server - docker-image-name: weview-dev - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} - NCLOUD_HOST: ${{ secrets.DEV_NCLOUD_HOST }} - NCLOUD_USERNAME: ${{ secrets.DEV_NCLOUD_USERNAME }} - NCLOUD_PASSWORD: ${{ secrets.DEV_NCLOUD_PASSWORD }} - NCLOUD_PORT: ${{ secrets.DEV_NCLOUD_PORT }} diff --git a/.github/workflows/deployment-dev-client.yml b/.github/workflows/deployment-dev-client.yml deleted file mode 100644 index 05d76bf5..00000000 --- a/.github/workflows/deployment-dev-client.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: "[CD] DEV 클라이언트" -on: - push: - branches: - - dev - paths: - - "client/**" - workflow_dispatch: - -jobs: - reusable: - uses: ./.github/workflows/reusable-deployment-client.yml - with: - bucket-name: weview-dev - secrets: - VITE_SERVER_URL: ${{ secrets.DEV_VITE_SERVER_URL }} - VITE_LOCAL_URL: ${{ secrets.DEV_VITE_LOCAL_URL }} - VITE_GITHUB_AUTH_SERVER_URL: ${{ secrets.DEV_VITE_GITHUB_AUTH_SERVER_URL }} - VITE_API_MODE: ${{ secrets.DEV_VITE_API_MODE }} - NCLOUD_BUCKET_ACCESS_KEY: ${{ secrets.DEV_NCLOUD_BUCKET_ACCESS_KEY }} - NCLOUD_BUCKET_SECRET_KEY: ${{ secrets.DEV_NCLOUD_BUCKET_SECRET_KEY }} - NCLOUD_HOST: ${{ secrets.DEV_NCLOUD_HOST }} - NCLOUD_USERNAME: ${{ secrets.DEV_NCLOUD_USERNAME }} - NCLOUD_PASSWORD: ${{ secrets.DEV_NCLOUD_PASSWORD }} - NCLOUD_PORT: ${{ secrets.DEV_NCLOUD_PORT }} diff --git a/.github/workflows/deployment-dev-scheduler-server.yml b/.github/workflows/deployment-dev-scheduler-server.yml deleted file mode 100644 index 8b256a3d..00000000 --- a/.github/workflows/deployment-dev-scheduler-server.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: "[CD] DEV 스케쥴러 서버" -on: - push: - branches: - - dev - paths: - - "scheduler-server/**" - workflow_dispatch: - -jobs: - reusable: - uses: ./.github/workflows/reusable-deployment-server.yml - with: - docker-context: scheduler-server - docker-image-name: weview-scheduler-dev - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} - NCLOUD_HOST: ${{ secrets.DEV_NCLOUD_HOST }} - NCLOUD_USERNAME: ${{ secrets.DEV_NCLOUD_USERNAME }} - NCLOUD_PASSWORD: ${{ secrets.DEV_NCLOUD_PASSWORD }} - NCLOUD_PORT: ${{ secrets.DEV_NCLOUD_PORT }} diff --git a/.github/workflows/deployment-dev.yml b/.github/workflows/deployment-dev.yml new file mode 100644 index 00000000..fd4c2fd1 --- /dev/null +++ b/.github/workflows/deployment-dev.yml @@ -0,0 +1,77 @@ +name: "[CD] DEV" + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + path-check: + runs-on: ubuntu-20.04 + outputs: + client: ${{ steps.filter.outputs.client }} + server: ${{ steps.filter.outputs.server }} + scheduler-server: ${{ steps.filter.outputs.scheduler-server }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + client: + - 'client/**' + server: + - 'server/**' + scheduler-server: + - 'scheduler-server/**' + + client: + needs: path-check + if: ${{ needs.path-check.outputs.client == 'true' }} + uses: ./.github/workflows/reusable-deployment-client.yml + with: + bucket-name: weview-dev + secrets: + VITE_SERVER_URL: ${{ secrets.DEV_VITE_SERVER_URL }} + VITE_LOCAL_URL: ${{ secrets.DEV_VITE_LOCAL_URL }} + VITE_GITHUB_AUTH_SERVER_URL: ${{ secrets.DEV_VITE_GITHUB_AUTH_SERVER_URL }} + VITE_API_MODE: ${{ secrets.DEV_VITE_API_MODE }} + NCLOUD_BUCKET_ACCESS_KEY: ${{ secrets.DEV_NCLOUD_BUCKET_ACCESS_KEY }} + NCLOUD_BUCKET_SECRET_KEY: ${{ secrets.DEV_NCLOUD_BUCKET_SECRET_KEY }} + NCLOUD_HOST: ${{ secrets.DEV_NCLOUD_HOST }} + NCLOUD_USERNAME: ${{ secrets.DEV_NCLOUD_USERNAME }} + NCLOUD_PASSWORD: ${{ secrets.DEV_NCLOUD_PASSWORD }} + NCLOUD_PORT: ${{ secrets.DEV_NCLOUD_PORT }} + + server: + needs: path-check + if: ${{ needs.path-check.outputs.server == 'true' }} + uses: ./.github/workflows/reusable-deployment-server.yml + with: + docker-context: server + docker-image-name: weview-dev + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} + NCLOUD_HOST: ${{ secrets.DEV_NCLOUD_HOST }} + NCLOUD_USERNAME: ${{ secrets.DEV_NCLOUD_USERNAME }} + NCLOUD_PASSWORD: ${{ secrets.DEV_NCLOUD_PASSWORD }} + NCLOUD_PORT: ${{ secrets.DEV_NCLOUD_PORT }} + + scheduler-server: + needs: path-check + if: ${{ needs.path-check.outputs.scheduler-server == 'true' }} + uses: ./.github/workflows/reusable-deployment-server.yml + with: + docker-context: scheduler-server + docker-image-name: weview-scheduler-dev + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} + NCLOUD_HOST: ${{ secrets.DEV_NCLOUD_HOST }} + NCLOUD_USERNAME: ${{ secrets.DEV_NCLOUD_USERNAME }} + NCLOUD_PASSWORD: ${{ secrets.DEV_NCLOUD_PASSWORD }} + NCLOUD_PORT: ${{ secrets.DEV_NCLOUD_PORT }} diff --git a/.github/workflows/deployment-main-api-server.yml b/.github/workflows/deployment-main-api-server.yml deleted file mode 100644 index 54baf7d7..00000000 --- a/.github/workflows/deployment-main-api-server.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: "[CD] MAIN API 서버" -on: - push: - tags: - - "v*" - paths: - - "server/**" - workflow_dispatch: - -jobs: - reusable: - uses: ./.github/workflows/reusable-deployment-server.yml - with: - docker-context: server - docker-image-name: weview-main - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} - NCLOUD_HOST: ${{ secrets.MAIN_NCLOUD_HOST }} - NCLOUD_USERNAME: ${{ secrets.MAIN_NCLOUD_USERNAME }} - NCLOUD_PASSWORD: ${{ secrets.MAIN_NCLOUD_PASSWORD }} - NCLOUD_PORT: ${{ secrets.MAIN_NCLOUD_PORT }} diff --git a/.github/workflows/deployment-main-client.yml b/.github/workflows/deployment-main-client.yml deleted file mode 100644 index 02432e91..00000000 --- a/.github/workflows/deployment-main-client.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: "[CD] MAIN 클라이언트" -on: - push: - tags: - - "v*" - paths: - - "client/**" - workflow_dispatch: - -jobs: - reusable: - uses: ./.github/workflows/reusable-deployment-client.yml - with: - bucket-name: weview - secrets: - VITE_SERVER_URL: ${{ secrets.MAIN_VITE_SERVER_URL }} - VITE_LOCAL_URL: ${{ secrets.MAIN_VITE_LOCAL_URL }} - VITE_GITHUB_AUTH_SERVER_URL: ${{ secrets.MAIN_VITE_GITHUB_AUTH_SERVER_URL }} - VITE_API_MODE: ${{ secrets.MAIN_VITE_API_MODE }} - NCLOUD_BUCKET_ACCESS_KEY: ${{ secrets.MAIN_NCLOUD_BUCKET_ACCESS_KEY }} - NCLOUD_BUCKET_SECRET_KEY: ${{ secrets.MAIN_NCLOUD_BUCKET_SECRET_KEY }} - NCLOUD_HOST: ${{ secrets.MAIN_NCLOUD_HOST }} - NCLOUD_USERNAME: ${{ secrets.MAIN_NCLOUD_USERNAME }} - NCLOUD_PASSWORD: ${{ secrets.MAIN_NCLOUD_PASSWORD }} - NCLOUD_PORT: ${{ secrets.MAIN_NCLOUD_PORT }} diff --git a/.github/workflows/deployment-main-scheduler-server.yml b/.github/workflows/deployment-main-scheduler-server.yml deleted file mode 100644 index 2196ca37..00000000 --- a/.github/workflows/deployment-main-scheduler-server.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: "[CD] MAIN 스케쥴러 서버" -on: - push: - tags: - - "v*" - paths: - - "scheduler-server/**" - workflow_dispatch: - -jobs: - reusable: - uses: ./.github/workflows/reusable-deployment-server.yml - with: - docker-context: scheduler-server - docker-image-name: weview-scheduler-main - secrets: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} - NCLOUD_HOST: ${{ secrets.MAIN_NCLOUD_HOST }} - NCLOUD_USERNAME: ${{ secrets.MAIN_NCLOUD_USERNAME }} - NCLOUD_PASSWORD: ${{ secrets.MAIN_NCLOUD_PASSWORD }} - NCLOUD_PORT: ${{ secrets.MAIN_NCLOUD_PORT }} diff --git a/.github/workflows/deployment-main.yml b/.github/workflows/deployment-main.yml new file mode 100644 index 00000000..e803857c --- /dev/null +++ b/.github/workflows/deployment-main.yml @@ -0,0 +1,76 @@ +name: "[CD] MAIN" +on: + push: + tags: + - "v*" + workflow_dispatch: + +jobs: + path-check: + runs-on: ubuntu-20.04 + outputs: + client: ${{ steps.filter.outputs.client }} + server: ${{ steps.filter.outputs.server }} + scheduler-server: ${{ steps.filter.outputs.scheduler-server }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + client: + - 'client/**' + server: + - 'server/**' + scheduler-server: + - 'scheduler-server/**' + + client: + needs: path-check + if: ${{ needs.path-check.outputs.client == 'true' }} + uses: ./.github/workflows/reusable-deployment-client.yml + with: + bucket-name: weview + secrets: + VITE_SERVER_URL: ${{ secrets.MAIN_VITE_SERVER_URL }} + VITE_LOCAL_URL: ${{ secrets.MAIN_VITE_LOCAL_URL }} + VITE_GITHUB_AUTH_SERVER_URL: ${{ secrets.MAIN_VITE_GITHUB_AUTH_SERVER_URL }} + VITE_API_MODE: ${{ secrets.MAIN_VITE_API_MODE }} + NCLOUD_BUCKET_ACCESS_KEY: ${{ secrets.MAIN_NCLOUD_BUCKET_ACCESS_KEY }} + NCLOUD_BUCKET_SECRET_KEY: ${{ secrets.MAIN_NCLOUD_BUCKET_SECRET_KEY }} + NCLOUD_HOST: ${{ secrets.MAIN_NCLOUD_HOST }} + NCLOUD_USERNAME: ${{ secrets.MAIN_NCLOUD_USERNAME }} + NCLOUD_PASSWORD: ${{ secrets.MAIN_NCLOUD_PASSWORD }} + NCLOUD_PORT: ${{ secrets.MAIN_NCLOUD_PORT }} + + server: + needs: path-check + if: ${{ needs.path-check.outputs.server == 'true' }} + uses: ./.github/workflows/reusable-deployment-server.yml + with: + docker-context: server + docker-image-name: weview-main + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} + NCLOUD_HOST: ${{ secrets.MAIN_NCLOUD_HOST }} + NCLOUD_USERNAME: ${{ secrets.MAIN_NCLOUD_USERNAME }} + NCLOUD_PASSWORD: ${{ secrets.MAIN_NCLOUD_PASSWORD }} + NCLOUD_PORT: ${{ secrets.MAIN_NCLOUD_PORT }} + + scheduler-server: + needs: path-check + if: ${{ needs.path-check.outputs.scheduler-server == 'true' }} + uses: ./.github/workflows/reusable-deployment-server.yml + with: + docker-context: scheduler-server + docker-image-name: weview-scheduler-main + secrets: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_ACCESS_TOKEN: ${{ secrets.DOCKERHUB_ACCESS_TOKEN }} + NCLOUD_HOST: ${{ secrets.MAIN_NCLOUD_HOST }} + NCLOUD_USERNAME: ${{ secrets.MAIN_NCLOUD_USERNAME }} + NCLOUD_PASSWORD: ${{ secrets.MAIN_NCLOUD_PASSWORD }} + NCLOUD_PORT: ${{ secrets.MAIN_NCLOUD_PORT }} diff --git a/.github/workflows/integration-api-server.yml b/.github/workflows/integration-api-server.yml deleted file mode 100644 index f7c9b65d..00000000 --- a/.github/workflows/integration-api-server.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "[CI] API 서버(Pull Request)" -on: - pull_request: - branches: ["main", "dev"] - paths: - - "server/**" - workflow_dispatch: - -jobs: - reusable: - uses: ./.github/workflows/reusable-integration.yml - with: - working-directory: server diff --git a/.github/workflows/integration-client.yml b/.github/workflows/integration-client.yml deleted file mode 100644 index 5d97654b..00000000 --- a/.github/workflows/integration-client.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "[CI] 클라이언트(Pull Request)" -on: - pull_request: - branches: ["main", "dev"] - paths: - - "client/**" - workflow_dispatch: - -jobs: - reusable: - uses: ./.github/workflows/reusable-integration.yml - with: - working-directory: client diff --git a/.github/workflows/integration-scheduler-server.yml b/.github/workflows/integration-scheduler-server.yml deleted file mode 100644 index ee251fbd..00000000 --- a/.github/workflows/integration-scheduler-server.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "[CI] 스케쥴러 서버(Pull Request)" -on: - pull_request: - branches: ["main", "dev"] - paths: - - "scheduler-server/**" - workflow_dispatch: - -jobs: - reusable: - uses: ./.github/workflows/reusable-integration.yml - with: - working-directory: scheduler-server diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 00000000..9f2f2581 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,49 @@ +name: CI + +on: + pull_request: + branches: ["main", "dev"] + workflow_dispatch: + +jobs: + path-check: + runs-on: ubuntu-20.04 + outputs: + client: ${{ steps.filter.outputs.client }} + server: ${{ steps.filter.outputs.server }} + scheduler-server: ${{ steps.filter.outputs.scheduler-server }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + client: + - 'client/**' + server: + - 'server/**' + scheduler-server: + - 'scheduler-server/**' + + client: + needs: path-check + if: ${{ needs.path-check.outputs.client == 'true' }} + uses: ./.github/workflows/reusable-integration.yml + with: + working-directory: client + + server: + needs: path-check + if: ${{ needs.path-check.outputs.server == 'true' }} + uses: ./.github/workflows/reusable-integration.yml + with: + working-directory: server + + scheduler-server: + needs: path-check + if: ${{ needs.path-check.outputs.scheduler-server == 'true' }} + uses: ./.github/workflows/reusable-integration.yml + with: + working-directory: scheduler-server From 9af857603239b6d117a1a1e47a42c53cabd4686f Mon Sep 17 00:00:00 2001 From: WOOSERK <55542546+WOOSERK@users.noreply.github.com> Date: Wed, 14 Dec 2022 10:40:39 +0900 Subject: [PATCH 24/31] =?UTF-8?q?fix(DO):=20yaml=20=EB=AC=B8=EB=B2=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#382)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deployment-main.yml | 36 +++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/deployment-main.yml b/.github/workflows/deployment-main.yml index e803857c..e93b6dae 100644 --- a/.github/workflows/deployment-main.yml +++ b/.github/workflows/deployment-main.yml @@ -7,25 +7,25 @@ on: jobs: path-check: - runs-on: ubuntu-20.04 - outputs: - client: ${{ steps.filter.outputs.client }} - server: ${{ steps.filter.outputs.server }} - scheduler-server: ${{ steps.filter.outputs.scheduler-server }} - steps: - - name: Checkout - uses: actions/checkout@v3 + runs-on: ubuntu-20.04 + outputs: + client: ${{ steps.filter.outputs.client }} + server: ${{ steps.filter.outputs.server }} + scheduler-server: ${{ steps.filter.outputs.scheduler-server }} + steps: + - name: Checkout + uses: actions/checkout@v3 - - uses: dorny/paths-filter@v2 - id: filter - with: - filters: | - client: - - 'client/**' - server: - - 'server/**' - scheduler-server: - - 'scheduler-server/**' + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + client: + - 'client/**' + server: + - 'server/**' + scheduler-server: + - 'scheduler-server/**' client: needs: path-check From 0aac56854601bac9983cc880e3951a78a9ebc090 Mon Sep 17 00:00:00 2001 From: WOOSERK <55542546+WOOSERK@users.noreply.github.com> Date: Wed, 14 Dec 2022 11:07:42 +0900 Subject: [PATCH 25/31] =?UTF-8?q?fix(DO):=20paths=20filter=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=20=EB=8C=80=EC=83=81=EC=9D=B4=20=EC=9D=B4=EC=83=81?= =?UTF-8?q?=ED=95=9C=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20(#382)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deployment-dev.yml | 2 ++ .github/workflows/deployment-main.yml | 2 ++ .github/workflows/integration.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/.github/workflows/deployment-dev.yml b/.github/workflows/deployment-dev.yml index fd4c2fd1..3e0eaf1d 100644 --- a/.github/workflows/deployment-dev.yml +++ b/.github/workflows/deployment-dev.yml @@ -20,6 +20,8 @@ jobs: - uses: dorny/paths-filter@v2 id: filter with: + base: ${{ github.ref }} + ref: ${{ github.action_ref }} filters: | client: - 'client/**' diff --git a/.github/workflows/deployment-main.yml b/.github/workflows/deployment-main.yml index e93b6dae..db484884 100644 --- a/.github/workflows/deployment-main.yml +++ b/.github/workflows/deployment-main.yml @@ -19,6 +19,8 @@ jobs: - uses: dorny/paths-filter@v2 id: filter with: + base: ${{ github.ref }} + ref: ${{ github.action_ref }} filters: | client: - 'client/**' diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 9f2f2581..1b5604b9 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -19,6 +19,8 @@ jobs: - uses: dorny/paths-filter@v2 id: filter with: + base: ${{ github.ref }} + ref: ${{ github.action_ref }} filters: | client: - 'client/**' From c2a5a7f0d16f6ffa4ffaa813643be63ce4e09a40 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Wed, 14 Dec 2022 12:52:15 +0900 Subject: [PATCH 26/31] =?UTF-8?q?fix(BE):=20=EC=97=98=EB=9D=BC=EC=8A=A4?= =?UTF-8?q?=ED=8B=B1=20=EC=84=9C=EC=B9=98=20=EA=B2=80=EC=83=89=EC=9D=B4=20?= =?UTF-8?q?=EC=9D=B4=EC=83=81=ED=95=98=EA=B2=8C=20=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=EB=93=A4=20=EC=88=98=EC=A0=95=20(#432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/post/post-search.service.ts | 32 +++++++++---------- server/src/domain/post/post.service.ts | 2 +- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/server/src/domain/post/post-search.service.ts b/server/src/domain/post/post-search.service.ts index d2074ef7..f852f661 100644 --- a/server/src/domain/post/post-search.service.ts +++ b/server/src/domain/post/post-search.service.ts @@ -41,7 +41,6 @@ export class PostSearchService { if (tags) { tagValue = Array.from(new Set(tags)).join(' '); } - this.esService.index({ index: this.configService.get('ELASTICSEARCH_INDEX'), id: String(post.id), @@ -64,9 +63,15 @@ export class PostSearchService { } async search(loadPostListRequestDto: LoadPostListRequestDto) { - const { lastId, tags, reviewCount, likeCount, details } = - loadPostListRequestDto; + const { lastId, reviewCount, likeCount } = loadPostListRequestDto; + let { tags, details } = loadPostListRequestDto; this.validateParam(loadPostListRequestDto); + if (typeof details === 'string') { + details = [details]; + } + if (typeof tags === 'string') { + tags = [tags]; + } const searchFilter: any = { sort: [ @@ -104,20 +109,13 @@ export class PostSearchService { } if (tags && tags.length > 0) { - const q = - typeof tags === 'string' - ? `${tags}` - : tags.map((tag) => `${tag}`).join(' '); - - searchFilter.body.query.bool.filter.bool.must.push({ - match: { - tags: { - query: q, - - operator: 'AND', + for (const tag of tags) { + searchFilter.body.query.bool.filter.bool.must.push({ + match: { + tags: tag, }, - }, - }); + }); + } } if (reviewCount && reviewCount >= 1) { searchFilter.body.query.bool.filter.bool.must.push({ @@ -185,7 +183,7 @@ export class PostSearchService { '선택한 리뷰 개수는 적절하지 않습니다', ); } - if (details.length > 1) { + if (Array.isArray(details) && details.length > 1) { throw new SearchParamInvalidException( '검색어의 개수가 적절하지 않습니다', ); diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index 35488218..390cbf18 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -137,7 +137,7 @@ export class PostService { for (const result of results) { authors.push( this.userRepository.findOneBy({ - id: result._source['authorId'], + id: result._source['authorid'], }), ); images.push( From 53b33216f7199cff3e7edb59d56c4375acc50782 Mon Sep 17 00:00:00 2001 From: WOOSERK <55542546+WOOSERK@users.noreply.github.com> Date: Wed, 14 Dec 2022 12:59:38 +0900 Subject: [PATCH 27/31] =?UTF-8?q?fix(DO):=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20?= =?UTF-8?q?=EB=B9=84=EA=B5=90=20=EB=8C=80=EC=83=81=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#382)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deployment-dev.yml | 2 +- .github/workflows/deployment-main.yml | 2 +- .github/workflows/integration.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deployment-dev.yml b/.github/workflows/deployment-dev.yml index 3e0eaf1d..e2c103fa 100644 --- a/.github/workflows/deployment-dev.yml +++ b/.github/workflows/deployment-dev.yml @@ -21,7 +21,7 @@ jobs: id: filter with: base: ${{ github.ref }} - ref: ${{ github.action_ref }} + ref: ${{ github.head_ref }} filters: | client: - 'client/**' diff --git a/.github/workflows/deployment-main.yml b/.github/workflows/deployment-main.yml index db484884..f7c0beb6 100644 --- a/.github/workflows/deployment-main.yml +++ b/.github/workflows/deployment-main.yml @@ -20,7 +20,7 @@ jobs: id: filter with: base: ${{ github.ref }} - ref: ${{ github.action_ref }} + ref: ${{ github.head_ref }} filters: | client: - 'client/**' diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 1b5604b9..40ad8158 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -20,7 +20,7 @@ jobs: id: filter with: base: ${{ github.ref }} - ref: ${{ github.action_ref }} + ref: ${{ github.head_ref }} filters: | client: - 'client/**' From fdda44da08c683c12ee146754ee8971c8f9231e6 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Wed, 14 Dec 2022 13:07:52 +0900 Subject: [PATCH 28/31] =?UTF-8?q?fix(BE):=20=ED=83=9C=EA=B7=B8=EA=B0=80=20?= =?UTF-8?q?=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EA=B2=80=EC=83=89=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20(#432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/post/post.entity.ts | 3 +++ server/src/domain/post/post.service.ts | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/domain/post/post.entity.ts b/server/src/domain/post/post.entity.ts index ab6733f0..17764f44 100644 --- a/server/src/domain/post/post.entity.ts +++ b/server/src/domain/post/post.entity.ts @@ -52,4 +52,7 @@ export class Post extends BaseTimeEntity { @Column({ length: 24, default: '' }) userNickname: string; + + @Column() + userId!: number; } diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index 390cbf18..b98046e3 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -133,7 +133,6 @@ export class PostService { const authors = []; const images = []; - for (const result of results) { authors.push( this.userRepository.findOneBy({ From ce8eff33909c602e8cf28bb8b8b0a8490167eb4e Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Wed, 14 Dec 2022 13:16:34 +0900 Subject: [PATCH 29/31] =?UTF-8?q?test(BE):=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95=20(#432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/post/post-search.service.spec.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/server/src/domain/post/post-search.service.spec.ts b/server/src/domain/post/post-search.service.spec.ts index aa1075fb..f8ca2323 100644 --- a/server/src/domain/post/post-search.service.spec.ts +++ b/server/src/domain/post/post-search.service.spec.ts @@ -235,10 +235,7 @@ describe('PostSearchService', () => { searchCondition.tags = ['one']; searchConditionUsingES.body.query.bool.filter.bool.must.push({ match: { - tags: { - query: 'one', - operator: 'AND', - }, + tags: 'one', }, }); @@ -252,14 +249,13 @@ describe('PostSearchService', () => { it('tags가 다섯 개 들어왔을 때, 정상 출력한다', async () => { //given searchCondition.tags = ['one', 'two', 'three', 'four', 'five']; - searchConditionUsingES.body.query.bool.filter.bool.must.push({ - match: { - tags: { - query: ['one', 'two', 'three', 'four', 'five'].join(' '), - operator: 'AND', + for (const tag of searchCondition.tags) { + searchConditionUsingES.body.query.bool.filter.bool.must.push({ + match: { + tags: tag, }, - }, - }); + }); + } //when await service.search(searchCondition); From 1214ae13f4d30e48e9322eeef512654074edc94c Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Wed, 14 Dec 2022 15:56:13 +0900 Subject: [PATCH 30/31] =?UTF-8?q?fix(BE):=20=EA=B2=80=EC=83=89=20=EC=9D=B4?= =?UTF-8?q?=EC=83=81=ED=95=98=EA=B2=8C=20=EB=90=98=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=EB=A5=BC=20=EC=88=98=EC=A0=95-2=EC=B0=A8=20(#432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/post/dto/controller-response.dto.ts | 2 +- server/src/domain/post/post.controller.ts | 2 -- server/src/domain/post/post.service.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/server/src/domain/post/dto/controller-response.dto.ts b/server/src/domain/post/dto/controller-response.dto.ts index 7a550965..c60942cf 100644 --- a/server/src/domain/post/dto/controller-response.dto.ts +++ b/server/src/domain/post/dto/controller-response.dto.ts @@ -26,7 +26,7 @@ export class SearchResponseDto { ); } - this.lastId = posts.length == 0 ? -1 : this.getLastId(posts); + this.lastId = posts.length == 0 ? -1 : Number(this.getLastId(posts)); this.isLast = isLast; } diff --git a/server/src/domain/post/post.controller.ts b/server/src/domain/post/post.controller.ts index 908a2107..0404c6c0 100644 --- a/server/src/domain/post/post.controller.ts +++ b/server/src/domain/post/post.controller.ts @@ -88,8 +88,6 @@ export class PostController { await this.addBookmarksToPostIfLogin(headers['authorization'], returnValue); await this.addSearchHistory(headers['authorization'], inquiryDto); - await this.addSearchHistory(headers['authorization'], inquiryDto); - this.applyTags(tags, lastId); return returnValue; diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index b98046e3..b451418a 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -136,7 +136,7 @@ export class PostService { for (const result of results) { authors.push( this.userRepository.findOneBy({ - id: result._source['authorid'], + id: result._source['userid'], }), ); images.push( From 723aa5372434821d57e54358f524edbc70d55603 Mon Sep 17 00:00:00 2001 From: kimtaehoonDev Date: Wed, 14 Dec 2022 16:54:41 +0900 Subject: [PATCH 31/31] =?UTF-8?q?fix(BE):=20=EA=B8=80=EC=9D=84=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=EB=92=A4=20=EC=83=88=EB=A1=9C=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8=EC=9D=B4=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20(#432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/domain/post/post.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/domain/post/post.service.ts b/server/src/domain/post/post.service.ts index b451418a..712afc43 100644 --- a/server/src/domain/post/post.service.ts +++ b/server/src/domain/post/post.service.ts @@ -63,7 +63,7 @@ export class PostService { postEntity.userNickname = userEntity.nickname; postEntity = await this.postRepository.save(postEntity); - this.postSearchService.indexPost(postEntity, tags); + await this.postSearchService.indexPost(postEntity, tags); return postEntity.id; }